diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index ba2a8ee2d..a94d7b771 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -15,6 +15,7 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) { // For --help we skip telemetry entirely. import { defineCommand, runMain } from "citty"; +import type { ArgsDef, CommandDef } from "citty"; const isHelp = process.argv.includes("--help") || process.argv.includes("-h"); @@ -91,4 +92,13 @@ process.on("exit", () => { _flushSync?.(); }); -runMain(main); +// Lazy-load help renderer — avoids allocating help data on non-help invocations +async function showUsage( + cmd: CommandDef, + parent?: CommandDef, +): Promise { + const { showUsage: impl } = await import("./help.js"); + return impl(cmd as CommandDef, parent as CommandDef | undefined); +} + +runMain(main, { showUsage }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 471ca89bb..aed2cd4b6 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -428,13 +428,7 @@ async function scaffoldProject( export default defineCommand({ meta: { name: "init", - description: `Scaffold a new composition project - -Examples: - hyperframes init my-video # interactive wizard - hyperframes init my-video --template warm-grain # pick a template - hyperframes init my-video --video video.mp4 # with video file - hyperframes init my-video --non-interactive # skip prompts (CI/agents)`, + description: "Scaffold a new composition project", }, args: { name: { type: "positional", description: "Project name", required: false }, diff --git a/packages/cli/src/commands/install-skills.ts b/packages/cli/src/commands/install-skills.ts index 89daf7699..377664435 100644 --- a/packages/cli/src/commands/install-skills.ts +++ b/packages/cli/src/commands/install-skills.ts @@ -364,13 +364,7 @@ async function runInstall({ args }: { args: Record }): Promise< export default defineCommand({ meta: { name: "skills", - description: `Install HyperFrames and GSAP skills for AI coding tools - -Examples: - hyperframes skills # install to Claude, Gemini, Codex - hyperframes skills --claude # install to Claude Code only - hyperframes skills --cursor # install to Cursor (project-level) - hyperframes skills --claude --gemini # install to specific tools`, + description: "Install HyperFrames and GSAP skills for AI coding tools", }, args: { claude: { type: "boolean", description: "Install to Claude Code (~/.claude/skills/)" }, diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 4bc73211c..67acf563f 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -27,13 +27,7 @@ function defaultWorkerCount(): number { export default defineCommand({ meta: { name: "render", - description: `Render a composition to MP4 or WebM - -Examples: - hyperframes render --output output.mp4 - hyperframes render --format webm --output overlay.webm # transparent WebM - hyperframes render --fps 60 --quality high --output hd.mp4 - hyperframes render --docker --output deterministic.mp4`, + description: "Render a composition to MP4 or WebM", }, args: { dir: { diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts new file mode 100644 index 000000000..a2440e845 --- /dev/null +++ b/packages/cli/src/help.ts @@ -0,0 +1,216 @@ +/** + * Custom help renderer for the hyperframes CLI. + * + * Root-level: grouped command categories + examples. + * Subcommands: citty's standard USAGE/ARGUMENTS/OPTIONS + appended examples. + */ +import { renderUsage } from "citty"; +import type { CommandDef } from "citty"; +import { c } from "./ui/colors.js"; +import { VERSION } from "./version.js"; + +// ── Root-level command groups ────────────────────────────────────────────── +interface Group { + title: string; + commands: [name: string, description: string][]; +} + +const GROUPS: Group[] = [ + { + title: "Getting Started", + commands: [ + ["init", "Scaffold a new composition project"], + ["preview", "Start the studio for previewing compositions"], + ["render", "Render a composition to MP4 or WebM"], + ], + }, + { + title: "Project", + commands: [ + ["lint", "Validate a composition for common mistakes"], + ["info", "Print project metadata"], + ["compositions", "List all compositions in a project"], + ["docs", "View inline documentation in the terminal"], + ], + }, + { + title: "Tooling", + commands: [ + [ + "benchmark", + "Render with preset fps/quality/worker configs and compare speed and file size", + ], + ["browser", "Manage the Chrome browser used for rendering"], + ["doctor", "Check system dependencies and environment"], + ["upgrade", "Check for updates and show upgrade instructions"], + ], + }, + { + title: "AI & Integrations", + commands: [ + ["skills", "Install HyperFrames and GSAP skills for AI coding tools"], + [ + "transcribe", + "Transcribe audio/video to word-level timestamps, or import an existing transcript", + ], + ], + }, + { + title: "Settings", + commands: [["telemetry", "Manage anonymous usage telemetry"]], + }, +]; + +// ── Root-level examples ──────────────────────────────────────────────────── +type Example = [comment: string, command: string]; + +const ROOT_EXAMPLES: Example[] = [ + ["Create a new project", "hyperframes init my-video"], + ["Start the live preview studio", "hyperframes preview"], + ["Render to MP4", "hyperframes render -o out.mp4"], + ["Transparent WebM overlay", "hyperframes render --format webm -o out.webm"], + ["Validate your composition", "hyperframes lint"], + ["Check system dependencies", "hyperframes doctor"], +]; + +// ── Per-command examples (comment + command style: comment + command) ──────────────── +const COMMAND_EXAMPLES: Record = { + init: [ + ["Create a project with the interactive wizard", "hyperframes init my-video"], + ["Pick a starter template", "hyperframes init my-video --template 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"], + ], + preview: [ + ["Preview the current project", "hyperframes preview"], + ["Preview a specific project directory", "hyperframes preview ./my-video"], + ["Use a custom port", "hyperframes preview --port 8080"], + ], + render: [ + ["Render to MP4", "hyperframes render --output output.mp4"], + ["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"], + ["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"], + ["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"], + ["Parallel rendering with 4 workers", "hyperframes render --workers 4 --output fast.mp4"], + ], + lint: [ + ["Lint the current project", "hyperframes lint"], + ["Lint a specific directory", "hyperframes lint ./my-video"], + ["Output findings as JSON", "hyperframes lint --json"], + ["Include info-level findings", "hyperframes lint --verbose"], + ], + info: [ + ["Show project metadata", "hyperframes info"], + ["Output as JSON", "hyperframes info --json"], + ], + compositions: [ + ["List compositions in the current project", "hyperframes compositions"], + ["Output as JSON", "hyperframes compositions --json"], + ], + benchmark: [ + ["Run benchmarks with default settings (3 runs)", "hyperframes benchmark"], + ["Run 5 iterations per config", "hyperframes benchmark --runs 5"], + ["Output results as JSON", "hyperframes benchmark --json"], + ], + browser: [ + ["Find or download Chrome for rendering", "hyperframes browser ensure"], + ["Print the Chrome executable path", "hyperframes browser path"], + ["Remove cached Chrome download", "hyperframes browser clear"], + ], + skills: [ + ["Install skills to all supported AI tools", "hyperframes skills"], + ["Install to Claude Code only", "hyperframes skills --claude"], + ["Install to Cursor (project-level)", "hyperframes skills --cursor"], + ["Install to specific tools", "hyperframes skills --claude --gemini"], + ], + transcribe: [ + ["Transcribe an audio file", "hyperframes transcribe audio.mp3"], + ["Transcribe a video file", "hyperframes transcribe video.mp4"], + [ + "Use a larger model for better accuracy", + "hyperframes transcribe audio.mp3 --model medium.en", + ], + ["Set language to filter non-target speech", "hyperframes transcribe audio.mp3 --language en"], + ["Import an existing SRT file", "hyperframes transcribe subtitles.srt"], + ["Import an OpenAI Whisper JSON response", "hyperframes transcribe response.json"], + ], + docs: [ + ["List all available topics", "hyperframes docs"], + ["Read about data attributes", "hyperframes docs data-attributes"], + ["Read about rendering", "hyperframes docs rendering"], + ["Read about GSAP integration", "hyperframes docs gsap"], + ], + doctor: [["Check system dependencies", "hyperframes doctor"]], + upgrade: [ + ["Check for updates interactively", "hyperframes upgrade"], + ["Check for updates without prompting", "hyperframes upgrade --check"], + ["Show upgrade commands directly", "hyperframes upgrade --yes"], + ], + telemetry: [ + ["Check current telemetry status", "hyperframes telemetry status"], + ["Disable telemetry", "hyperframes telemetry disable"], + ["Enable telemetry", "hyperframes telemetry enable"], + ], +}; + +// ── Render root help ─────────────────────────────────────────────────────── +function renderRootHelp(): string { + const NAME_COL = 16; + const CMD_COL = 46; + const lines: string[] = []; + + lines.push( + `${c.bold("hyperframes")} ${c.dim(`v${VERSION}`)} — Create and render HTML video compositions`, + ); + lines.push(""); + lines.push(`${c.bold("Usage:")} hyperframes ${c.cyan("")} [options]`); + lines.push(""); + + for (const group of GROUPS) { + lines.push(c.bold(`${group.title}:`)); + for (const [name, desc] of group.commands) { + lines.push(` ${c.cyan(name.padEnd(NAME_COL))}${desc}`); + } + lines.push(""); + } + + lines.push(c.bold("Examples:")); + for (const [comment, command] of ROOT_EXAMPLES) { + lines.push(` ${c.dim("$")} ${command.padEnd(CMD_COL)} ${c.dim(comment)}`); + } + lines.push(""); + + lines.push(`Run ${c.cyan("hyperframes --help")} for more information about a command.`); + + return lines.join("\n"); +} + +// ── Format examples section (comment + command style) ──────────────────────────────── +function formatExamples(examples: Example[]): string { + const lines: string[] = []; + lines.push(c.bold("Examples:")); + for (const [comment, command] of examples) { + lines.push(` ${c.gray(`# ${comment}`)}`); + lines.push(` ${command}`); + lines.push(""); + } + return lines.join("\n"); +} + +// ── Main showUsage override ──────────────────────────────────────────────── +export async function showUsage(cmd: CommandDef, parent?: CommandDef): Promise { + if (!parent) { + console.log(renderRootHelp() + "\n"); + return; + } + + const meta = await (typeof cmd.meta === "function" ? cmd.meta() : cmd.meta); + const usage = await renderUsage(cmd, parent); + console.log(usage + "\n"); + + const name = meta?.name; + if (name && COMMAND_EXAMPLES[name]) { + console.log(formatExamples(COMMAND_EXAMPLES[name]) + "\n"); + } +} diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index 7e1bdc870..719d25ab2 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -17,6 +17,8 @@ export const c = { dim: wrap(pc.dim), bold: wrap(pc.bold), accent: wrap(teal), + cyan: wrap(pc.cyan), + gray: wrap(pc.gray), progress: wrap(pc.magenta), reset: isColorSupported ? pc.reset : (s: string) => s, };