feat(cli): add remote template fetching via giget (#162)

* feat(cli): add remote template fetching via giget

* fix: update remote.ts to use templates/ instead of examples/

* fix(cli): validate template ID against manifest before downloading

Fails fast with available template list instead of downloading
an empty directory for nonexistent templates.

* refactor(cli): simplify to single --template flag with dynamic validation

- Remove --example flag (--template handles bundled + remote)
- Remove static ALL_TEMPLATE_IDS list (validates against GitHub manifest)
- No CLI release needed to add new templates — just add to templates/ and templates.json
- scaffoldProject auto-detects bundled vs remote

* chore: update lockfiles for giget dependency

* fix(cli): remove undefined isAudioOnly reference
This commit is contained in:
James Russo
2026-03-31 13:41:01 -07:00
committed by GitHub
parent b43b6fb1dc
commit b866a28545
6 changed files with 229 additions and 57 deletions
+65 -28
View File
@@ -14,7 +14,12 @@ import { execFileSync, spawn } from "node:child_process";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
import { printBanner } from "../ui/banner.js";
import { TEMPLATES, type TemplateId } from "../templates/generators.js";
import {
BUNDLED_TEMPLATES,
resolveTemplateList,
type TemplateOption,
} from "../templates/generators.js";
import { fetchRemoteTemplate } from "../templates/remote.js";
import { trackInitTemplate } from "../telemetry/events.js";
import { hasFFmpeg } from "../whisper/manager.js";
@@ -80,8 +85,6 @@ async function installSkills(interactive: boolean): Promise<void> {
}
}
const ALL_TEMPLATE_IDS = TEMPLATES.map((t) => t.id);
interface VideoMeta {
durationSeconds: number;
width: number;
@@ -357,17 +360,22 @@ async function handleVideoFile(
// scaffoldProject — copy template, patch video refs, write meta.json
// ---------------------------------------------------------------------------
function scaffoldProject(
async function scaffoldProject(
destDir: string,
name: string,
templateId: TemplateId,
templateId: string,
localVideoName: string | undefined,
durationSeconds?: number,
): void {
): Promise<void> {
mkdirSync(destDir, { recursive: true });
// Use bundled template if available, otherwise fetch from GitHub
const templateDir = getStaticTemplateDir(templateId);
cpSync(templateDir, destDir, { recursive: true });
if (existsSync(templateDir)) {
cpSync(templateDir, destDir, { recursive: true });
} else {
await fetchRemoteTemplate(templateId, destDir);
}
patchVideoSrc(destDir, localVideoName, durationSeconds);
writeFileSync(
@@ -430,7 +438,7 @@ Examples:
name: { type: "positional", description: "Project name", required: false },
template: {
type: "string",
description: `Template (${ALL_TEMPLATE_IDS.join(", ")})`,
description: "Template name (e.g. warm-grain, swiss-grid, blank)",
alias: "t",
},
video: {
@@ -481,13 +489,7 @@ Examples:
// Non-interactive mode — all inputs from flags, defaults where missing
// -----------------------------------------------------------------------
if (!interactive) {
const resolvedTemplate = templateFlag ?? "blank";
if (!ALL_TEMPLATE_IDS.includes(resolvedTemplate as TemplateId)) {
console.error(c.error(`Unknown template: ${resolvedTemplate}`));
console.error(`Available: ${ALL_TEMPLATE_IDS.join(", ")}`);
process.exit(1);
}
const templateId = resolvedTemplate as TemplateId;
const templateId = templateFlag ?? "blank";
const name = args.name ?? "my-video";
const destDir = resolve(name);
@@ -557,7 +559,23 @@ Examples:
}
// Scaffold
scaffoldProject(destDir, basename(destDir), templateId, localVideoName, videoDuration);
try {
await scaffoldProject(
destDir,
basename(destDir),
templateId,
localVideoName,
videoDuration,
);
} catch (err) {
console.error(
c.error(
`Failed to scaffold template "${templateId}": ${err instanceof Error ? err.message : err}`,
),
);
console.error(c.dim("Use --template blank for offline use."));
process.exit(1);
}
trackInitTemplate(templateId);
const transcriptFile = resolve(destDir, "transcript.json");
if (existsSync(transcriptFile)) {
@@ -708,21 +726,22 @@ Examples:
}
// 3. Pick template — skip prompt if --template was provided
let templateId: TemplateId;
if (templateFlag && ALL_TEMPLATE_IDS.includes(templateFlag as TemplateId)) {
templateId = templateFlag as TemplateId;
let templateId: string;
if (templateFlag) {
templateId = templateFlag;
} else {
if (templateFlag) {
clack.log.warn(`Unknown template "${templateFlag}" — pick from the list below`);
}
// Resolve full template list (bundled + remote)
const allTemplates = await resolveTemplateList();
const defaultTemplate = "blank";
const templateResult = await clack.select({
message: "Pick a template",
options: TEMPLATES.map((t) => ({
options: allTemplates.map((t: TemplateOption) => ({
value: t.id,
label: t.label,
hint: t.hint,
hint: t.source === "remote" ? `${t.hint} (download)` : t.hint,
})),
initialValue: "blank" as TemplateId,
initialValue: defaultTemplate,
});
if (clack.isCancel(templateResult)) {
clack.cancel("Setup cancelled.");
@@ -731,14 +750,32 @@ Examples:
templateId = templateResult;
}
// 4. Copy template and patch
scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
// 4. Scaffold project (bundled templates are instant, remote templates download from GitHub)
const spin = clack.spinner();
const isBundled = BUNDLED_TEMPLATES.some((t) => t.id === templateId);
if (!isBundled) {
spin.start(`Downloading template ${c.accent(templateId)}...`);
}
try {
await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration);
if (!isBundled) {
spin.stop(c.success(`Downloaded ${templateId}`));
}
} catch (err) {
if (!isBundled) {
spin.stop(c.error("Download failed"));
}
clack.log.error(
`${err instanceof Error ? err.message : err}\n${c.dim("Use --template blank for offline use.")}`,
);
process.exit(1);
}
trackInitTemplate(templateId);
// 4b. Patch captions with transcript if available
const transcriptFile = resolve(destDir, "transcript.json");
if (existsSync(transcriptFile)) {
patchTranscript(destDir, transcriptFile);
await patchTranscript(destDir, transcriptFile);
}
// 5. Install AI coding skills