diff --git a/CLAUDE.md b/CLAUDE.md index 5fafb696a..d7305e066 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,8 +87,8 @@ Always run both on changed files before committing. The lefthook pre-commit hook When adding a new CLI command: 1. Define the command in `packages/cli/src/commands/.ts` using `defineCommand` from citty -2. Register it in `packages/cli/src/cli.ts` under `subCommands` (lazy-loaded) -3. **Add examples to `packages/cli/src/help.ts`** in the `COMMAND_EXAMPLES` record — every command must have `--help` examples +2. **Export `examples`** in the same file — `export const examples: Example[] = [...]` (import `Example` from `./_examples.js`). These are displayed by `--help`. +3. Register it in `packages/cli/src/cli.ts` under `subCommands` (lazy-loaded) 4. Validate by running `npx tsx packages/cli/src/cli.ts --help` and verifying the examples section appears ## Key Concepts diff --git a/packages/cli/src/commands/_examples.ts b/packages/cli/src/commands/_examples.ts new file mode 100644 index 000000000..ae07cf2b5 --- /dev/null +++ b/packages/cli/src/commands/_examples.ts @@ -0,0 +1,6 @@ +/** + * Shared type for CLI command examples. + * Each command file exports `examples` using this type. + * help.ts dynamically imports them at --help time. + */ +export type Example = [comment: string, command: string]; diff --git a/packages/cli/src/commands/benchmark.ts b/packages/cli/src/commands/benchmark.ts index b6b8edb40..c18f714c8 100644 --- a/packages/cli/src/commands/benchmark.ts +++ b/packages/cli/src/commands/benchmark.ts @@ -1,5 +1,12 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { existsSync, statSync } from "node:fs"; + +export const examples: Example[] = [ + ["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"], +]; import { resolve, join } from "node:path"; import { resolveProject } from "../utils/project.js"; import { loadProducer } from "../utils/producer.js"; diff --git a/packages/cli/src/commands/browser.ts b/packages/cli/src/commands/browser.ts index f5e390ad5..45fb2027a 100644 --- a/packages/cli/src/commands/browser.ts +++ b/packages/cli/src/commands/browser.ts @@ -1,6 +1,13 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; + +export const examples: Example[] = [ + ["Find or download Chrome for rendering", "hyperframes browser ensure"], + ["Print the Chrome executable path", "hyperframes browser path"], + ["Remove cached Chrome download", "hyperframes browser clear"], +]; import { formatBytes } from "../ui/format.js"; import { ensureBrowser, diff --git a/packages/cli/src/commands/compositions.ts b/packages/cli/src/commands/compositions.ts index d4a32384f..55220658b 100644 --- a/packages/cli/src/commands/compositions.ts +++ b/packages/cli/src/commands/compositions.ts @@ -1,5 +1,11 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { readFileSync } from "node:fs"; + +export const examples: Example[] = [ + ["List compositions in the current project", "hyperframes compositions"], + ["Output as JSON", "hyperframes compositions --json"], +]; import { c } from "../ui/colors.js"; import { ensureDOMParser } from "../utils/dom.js"; import { resolveProject } from "../utils/project.js"; diff --git a/packages/cli/src/commands/docs.ts b/packages/cli/src/commands/docs.ts index 1a6c7bafa..29e462bea 100644 --- a/packages/cli/src/commands/docs.ts +++ b/packages/cli/src/commands/docs.ts @@ -1,5 +1,13 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { readFileSync, existsSync } from "node:fs"; + +export const examples: Example[] = [ + ["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"], +]; import { resolve, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { c } from "../ui/colors.js"; diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index b9fb313a1..ccf332be8 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -1,5 +1,8 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { execSync } from "node:child_process"; + +export const examples: Example[] = [["Check system dependencies", "hyperframes doctor"]]; import { freemem, platform } from "node:os"; import { c } from "../ui/colors.js"; import { findBrowser } from "../browser/manager.js"; diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index ac4719985..e0f85cc4f 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -1,5 +1,11 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { readFileSync, readdirSync, statSync } from "node:fs"; + +export const examples: Example[] = [ + ["Show project metadata", "hyperframes info"], + ["Output as JSON", "hyperframes info --json"], +]; import { join } from "node:path"; import { parseHtml } from "@hyperframes/core"; import { c } from "../ui/colors.js"; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 40d2056e0..b486f6deb 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,4 +1,13 @@ import { defineCommand, runCommand } from "citty"; +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"], + ["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"], +]; import { existsSync, mkdirSync, diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 3871d0afb..ea833113b 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -1,5 +1,13 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { c } from "../ui/colors.js"; + +export const examples: Example[] = [ + ["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"], +]; import { formatLintFindings } from "../utils/lintFormat.js"; import { lintProject } from "../utils/lintProject.js"; import { resolveProject } from "../utils/project.js"; diff --git a/packages/cli/src/commands/preview.ts b/packages/cli/src/commands/preview.ts index 21691eb9b..a5c04b5b5 100644 --- a/packages/cli/src/commands/preview.ts +++ b/packages/cli/src/commands/preview.ts @@ -1,5 +1,12 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { spawn } from "node:child_process"; + +export const examples: Example[] = [ + ["Preview the current project", "hyperframes preview"], + ["Preview a specific project directory", "hyperframes preview ./my-video"], + ["Use a custom port", "hyperframes preview --port 8080"], +]; import { existsSync, lstatSync, symlinkSync, unlinkSync, readlinkSync, mkdirSync } from "node:fs"; import { resolve, dirname, basename, join } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 67acf563f..1e2e65f7f 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -1,5 +1,14 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { existsSync, mkdirSync, statSync } from "node:fs"; + +export const examples: Example[] = [ + ["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"], +]; import { cpus, freemem } from "node:os"; import { resolve, dirname, join } from "node:path"; import { resolveProject } from "../utils/project.js"; diff --git a/packages/cli/src/commands/telemetry.ts b/packages/cli/src/commands/telemetry.ts index a61d0fb0a..1d41d562f 100644 --- a/packages/cli/src/commands/telemetry.ts +++ b/packages/cli/src/commands/telemetry.ts @@ -1,5 +1,12 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { c } from "../ui/colors.js"; + +export const examples: Example[] = [ + ["Check current telemetry status", "hyperframes telemetry status"], + ["Disable telemetry", "hyperframes telemetry disable"], + ["Enable telemetry", "hyperframes telemetry enable"], +]; import { readConfig, writeConfig, CONFIG_PATH } from "../telemetry/config.js"; function runEnable(): void { diff --git a/packages/cli/src/commands/transcribe.ts b/packages/cli/src/commands/transcribe.ts index ff12f15cb..3d38f801c 100644 --- a/packages/cli/src/commands/transcribe.ts +++ b/packages/cli/src/commands/transcribe.ts @@ -1,5 +1,15 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { existsSync, writeFileSync } from "node:fs"; + +export const examples: Example[] = [ + ["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"], +]; import { resolve, join, extname } from "node:path"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; diff --git a/packages/cli/src/commands/tts.ts b/packages/cli/src/commands/tts.ts index 8c55395d4..d64f38721 100644 --- a/packages/cli/src/commands/tts.ts +++ b/packages/cli/src/commands/tts.ts @@ -1,5 +1,15 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import { existsSync, readFileSync } from "node:fs"; + +export const examples: Example[] = [ + ["Generate speech from text", 'hyperframes tts "Welcome to HyperFrames"'], + ["Choose a voice", 'hyperframes tts "Hello world" --voice am_adam'], + ["Save to a specific file", 'hyperframes tts "Intro" --voice bf_emma --output narration.wav'], + ["Adjust speech speed", 'hyperframes tts "Slow and clear" --speed 0.8'], + ["Read text from a file", "hyperframes tts script.txt"], + ["List available voices", "hyperframes tts --list"], +]; import { resolve, extname } from "node:path"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index e3420788b..ce6c66fc9 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -1,6 +1,13 @@ import { defineCommand } from "citty"; +import type { Example } from "./_examples.js"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; + +export const examples: Example[] = [ + ["Check for updates interactively", "hyperframes upgrade"], + ["Check for updates without prompting", "hyperframes upgrade --check"], + ["Show upgrade commands directly", "hyperframes upgrade --yes"], +]; import { VERSION } from "../version.js"; import { checkForUpdate, withMeta } from "../utils/updateCheck.js"; diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts index af83f0003..bd30f5c48 100644 --- a/packages/cli/src/help.ts +++ b/packages/cli/src/help.ts @@ -62,7 +62,7 @@ const GROUPS: Group[] = [ ]; // ── Root-level examples ──────────────────────────────────────────────────── -type Example = [comment: string, command: string]; +import type { Example } from "./commands/_examples.js"; const ROOT_EXAMPLES: Example[] = [ ["Create a new project", "hyperframes init my-video"], @@ -73,93 +73,26 @@ const ROOT_EXAMPLES: Example[] = [ ["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"], - ], +// ── Per-command examples loaded from command files ──────────────────────── +// Each command file exports `examples: Example[]`. This function dynamically +// imports them so examples live next to the command they document. +async function loadExamples(name: string): Promise { + try { + const mod = await import(`./commands/${name}.js`); + return mod.examples; + } catch { + return undefined; + } +} + +// Commands without their own file (e.g. listed in help but not yet a real command) +const STATIC_EXAMPLES: Record = { 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"], ], - tts: [ - ["Generate speech from text", 'hyperframes tts "Welcome to HyperFrames"'], - ["Choose a voice", 'hyperframes tts "Hello world" --voice am_adam'], - ["Save to a specific file", 'hyperframes tts "Intro" --voice bf_emma --output narration.wav'], - ["Adjust speech speed", 'hyperframes tts "Slow and clear" --speed 0.8'], - ["Read text from a file", "hyperframes tts script.txt"], - ["List available voices", "hyperframes tts --list"], - ], - 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 ─────────────────────────────────────────────────────── @@ -218,7 +151,10 @@ export async function showUsage(cmd: CommandDef, parent?: CommandDef): Promise