diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 2c5c343d3..bafb5bee4 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -34,7 +34,7 @@ npx hyperframes The CLI is **non-interactive by default** — designed so AI agents (Claude Code, Gemini CLI, Codex, Cursor) can drive every command without prompts or interactive UI. -- All inputs are passed via flags (e.g., `--template`, `--video`, `--output`) +- All inputs are passed via flags (e.g., `--example`, `--video`, `--output`) - Missing required flags fail fast with a clear error and usage example - Output is plain text suitable for parsing - No interactive prompts, spinners, or selection menus @@ -45,7 +45,7 @@ Add `--human-friendly` to any command to enable the interactive terminal UI with ```bash # Fully non-interactive — all inputs from flags - npx hyperframes init my-video --template blank --video video.mp4 + npx hyperframes init my-video --example blank --video video.mp4 npx hyperframes render --output output.mp4 --fps 30 --quality standard npx hyperframes upgrade --check --json ``` @@ -94,11 +94,11 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ Scaffold a new composition from a template: ```bash - npx hyperframes init --template warm-grain + npx hyperframes init --example warm-grain ``` You will be prompted for a project name, or pass it as an argument: ```bash - npx hyperframes init my-video --template warm-grain + npx hyperframes init my-video --example warm-grain ``` See [Templates](/templates) for all available templates. @@ -142,8 +142,8 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ Create a new composition project from a template: ```bash - # Agent mode (default) — --template is required - npx hyperframes init my-video --template blank --video video.mp4 + # Agent mode (default) — --example is required + npx hyperframes init my-video --example blank --video video.mp4 # Human mode — interactive prompts npx hyperframes init --human-friendly @@ -151,7 +151,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | Flag | Description | |------|-------------| - | `--template, -t` | Template to use (required in default mode, interactive in `--human-friendly`) | + | `--example, -e` | Example to scaffold (required in default mode, interactive in `--human-friendly`) | | `--video, -V` | Path to a video file (MP4, WebM, MOV) | | `--audio, -a` | Path to an audio file (MP3, WAV, M4A) | | `--skip-skills` | Skip AI coding skills installation | @@ -168,7 +168,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_ | `swiss-grid` | Structured grid layout | | `vignelli` | Bold typography with red accents | - In default (agent) mode, `--template` is required — the CLI errors with a usage example if missing. In `--human-friendly` mode, you choose interactively. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable). + In default (agent) mode, `--example` is required — the CLI errors with a usage example if missing. In `--human-friendly` mode, you choose interactively. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable). After scaffolding, the CLI installs AI coding skills for Claude Code, Gemini CLI, and Codex CLI (use `--skip-skills` to disable). See [`skills`](#skills) command. diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 13b2f6143..e48d8162d 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -70,7 +70,7 @@ A 1920x1080 video with an animated title that fades in from above — rendered t This starts an interactive wizard that walks you through template selection and media import. To skip prompts (e.g. in CI or from an agent), use `--non-interactive`: ```bash - npx hyperframes init my-video --non-interactive --template blank + npx hyperframes init my-video --non-interactive --example blank ``` See [Templates](/templates) for all available templates. @@ -101,7 +101,7 @@ A 1920x1080 video with an animated title that fades in from above — rendered t If you have a source video, pass it with `--video` for automatic transcription and captions: ```bash - npx hyperframes init my-video --template warm-grain --video ./intro.mp4 + npx hyperframes init my-video --example warm-grain --video ./intro.mp4 ``` diff --git a/docs/templates.mdx b/docs/templates.mdx index 2cdba614b..6420e8a1f 100644 --- a/docs/templates.mdx +++ b/docs/templates.mdx @@ -6,7 +6,7 @@ description: "Built-in templates for common video patterns. Hover to preview ani Hyperframes includes starter templates to help you scaffold compositions quickly. Each template gives you a working project with the correct [composition structure](/concepts/compositions), [data attributes](/concepts/data-attributes), and a [GSAP timeline](/guides/gsap-animation) already wired up. ```bash Terminal -npx hyperframes init my-video --template +npx hyperframes init my-video --example ``` ## Landscape Templates @@ -55,7 +55,7 @@ npx hyperframes init my-video --template Looking for a minimal starting point? Use **blank** — it gives you an empty composition with just the scaffolding, no visual design. ```bash Terminal - npx hyperframes init my-video --template blank + npx hyperframes init my-video --example blank ``` @@ -217,7 +217,7 @@ npx hyperframes init my-video --template ## Passing a Source Video ```bash Terminal -npx hyperframes init my-video --template warm-grain --video ./my-clip.mp4 +npx hyperframes init my-video --example warm-grain --video ./my-clip.mp4 ``` The CLI will probe the video for duration, resolution, and codec. If the video uses an incompatible codec, it will be automatically transcoded to H.264 MP4 if FFmpeg is available. diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts new file mode 100644 index 000000000..198e3de04 --- /dev/null +++ b/packages/cli/src/commands/init.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts"); + +// Spawns `bun` directly because the CLI entry is a .ts file that needs a +// TypeScript-aware runtime. vitest runs under node, so `process.execPath` +// would be node and couldn't load the entry. This repo hard-depends on bun +// (package.json scripts), so assuming it's on PATH is safe. +function runInit(args: string[]): { status: number; stdout: string; stderr: string } { + const res = spawnSync("bun", ["run", cliEntry, "init", ...args], { + encoding: "utf-8", + timeout: 30_000, + }); + return { + status: res.status ?? -1, + stdout: res.stdout, + stderr: res.stderr, + }; +} + +describe("hyperframes init flag rename", () => { + it("--example blank scaffolds a bundled project", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); + const target = join(dir, "proj"); + try { + const res = runInit([target, "--example", "blank", "--non-interactive", "--skip-skills"]); + expect(res.status).toBe(0); + expect(existsSync(join(target, "index.html"))).toBe(true); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("--template prints a rename hint and exits non-zero", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); + const target = join(dir, "proj"); + try { + const res = runInit([target, "--template", "blank", "--non-interactive", "--skip-skills"]); + expect(res.status).not.toBe(0); + expect(res.stderr).toContain("--template flag was renamed to --example"); + expect(res.stderr).toContain(`--example "blank"`); + expect(existsSync(target)).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1b02f4086..595c631bc 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -3,7 +3,7 @@ import type { Example } from "./_examples.js"; export const examples: Example[] = [ ["Create a project with the interactive wizard", "hyperframes init my-video"], - ["Pick a starter template", "hyperframes init my-video --template warm-grain"], + ["Pick a starter example", "hyperframes init my-video --example warm-grain"], ["Start from an existing video file", "hyperframes init my-video --video clip.mp4"], ["Start from an audio file", "hyperframes init my-video --audio track.mp3"], ["Non-interactive mode (for CI or AI agents)", "hyperframes init my-video --non-interactive"], @@ -362,10 +362,20 @@ export default defineCommand({ }, args: { name: { type: "positional", description: "Project name", required: false }, + example: { + type: "string", + description: "Example name (e.g. warm-grain, swiss-grid, blank)", + alias: "e", + }, + // Accepted-but-errored so users who still type the old flag get a clear + // message rather than citty silently ignoring it and producing a blank + // project. The actual behavior is gone — this exists purely for the + // diagnostic. `hidden` keeps it out of --help output so new users aren't + // taught about a flag that's already gone. template: { type: "string", - description: "Template name (e.g. warm-grain, swiss-grid, blank)", - alias: "t", + description: "[renamed] Use --example instead.", + hidden: true, }, video: { type: "string", @@ -401,7 +411,17 @@ export default defineCommand({ }, }, async run({ args }) { - const templateFlag = args.template; + if (args.template !== undefined) { + // Quote the value in case it looks flag-like — keeps the suggested + // command copy-pasteable. + console.error( + c.error( + `The --template flag was renamed to --example. Example:\n npx hyperframes init ${args.name ?? "my-video"} --example "${args.template}"`, + ), + ); + process.exit(1); + } + const exampleFlag = args.example; const videoFlag = args.video; const audioFlag = args.audio; const skipTranscribe = args["skip-transcribe"] === true; @@ -415,7 +435,7 @@ export default defineCommand({ // Non-interactive mode — all inputs from flags, defaults where missing // ----------------------------------------------------------------------- if (!interactive) { - const templateId = templateFlag ?? "blank"; + const templateId = exampleFlag ?? "blank"; const name = args.name ?? "my-video"; const destDir = resolve(name); @@ -496,10 +516,10 @@ export default defineCommand({ } catch (err) { console.error( c.error( - `Failed to scaffold template "${templateId}": ${err instanceof Error ? err.message : err}`, + `Failed to scaffold example "${templateId}": ${err instanceof Error ? err.message : err}`, ), ); - console.error(c.dim("Use --template blank for offline use.")); + console.error(c.dim("Use --example blank for offline use.")); process.exit(1); } trackInitTemplate(templateId); @@ -646,17 +666,17 @@ export default defineCommand({ } } - // 3. Pick template — skip prompt if --template was provided + // 3. Pick example — skip prompt if --example was provided let templateId: string; - if (templateFlag) { - templateId = templateFlag; + if (exampleFlag) { + templateId = exampleFlag; } else { // Resolve full template list (bundled + remote) const allTemplates = await resolveTemplateList(); const defaultTemplate = "blank"; const templateResult = await clack.select({ - message: "Pick a template", + message: "Pick an example", options: allTemplates.map((t: TemplateOption) => ({ value: t.id, label: t.label, @@ -675,7 +695,7 @@ export default defineCommand({ const spin = clack.spinner(); const isBundled = BUNDLED_TEMPLATES.some((t) => t.id === templateId); if (!isBundled) { - spin.start(`Downloading template ${c.accent(templateId)}...`); + spin.start(`Downloading example ${c.accent(templateId)}...`); } try { await scaffoldProject(destDir, name, templateId, localVideoName, videoDuration); @@ -687,7 +707,7 @@ export default defineCommand({ spin.stop(c.error("Download failed")); } clack.log.error( - `${err instanceof Error ? err.message : err}\n${c.dim("Use --template blank for offline use.")}`, + `${err instanceof Error ? err.message : err}\n${c.dim("Use --example blank for offline use.")}`, ); process.exit(1); } diff --git a/packages/cli/src/docs/templates.md b/packages/cli/src/docs/templates.md index cb08513e4..7e8ec2c32 100644 --- a/packages/cli/src/docs/templates.md +++ b/packages/cli/src/docs/templates.md @@ -1,6 +1,6 @@ # Templates -Built-in templates available via `npx hyperframes init --template `. +Built-in templates available via `npx hyperframes init --example `. ## blank diff --git a/packages/cli/src/registry/resolver.test.ts b/packages/cli/src/registry/resolver.test.ts index 8792668fc..fec6f316b 100644 --- a/packages/cli/src/registry/resolver.test.ts +++ b/packages/cli/src/registry/resolver.test.ts @@ -106,12 +106,12 @@ describe("registry resolver", () => { it("skips items whose manifest fails to load (warning, not failure)", async () => { mockFetch({ missing: ["beta"] }); const baseUrl = uniqueBaseUrl(); - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnings: string[] = []; const entries = await listRegistryItems(undefined, { baseUrl }); - const items = await loadAllItems(entries, { baseUrl }); + const items = await loadAllItems(entries, { baseUrl, onWarn: (m) => warnings.push(m) }); expect(items.map((i) => i.name).sort()).toEqual(["alpha", "gamma"]); - expect(warn).toHaveBeenCalled(); - warn.mockRestore(); + expect(warnings.length).toBeGreaterThan(0); + expect(warnings.some((w) => w.includes("beta"))).toBe(true); }); });