Files
hyperframes/packages/cli/src/help.ts
T
WaterrrForeverandMiguel Angel Simon Sierra f2358315e5 feat(skills): re-land reverse any video on the HyperFrames plan, with clearer credit wording (#6833)
* fix(cli,skills): say a step's credits are its total, and keep cost out of retries

An out-of-credits refusal named what the step needs, and an agent read
that as the shortfall. It now says the number is the step's total.

After its last try, an agent told the person what was spent and what
another try would cost. The HeyGen reference now says not to.

* fix(cli): plan avatar credits from the HyperFrames price rows

An avatar's credits came from its old API price, about five times what
the plan charges, so --max-credits refused calls that fit and a top-up
asked for too much. They now use the Avatar IV photo row plus the
general avatar video row, 950 credits a minute.

* fix(cli): pin the avatar price test and keep pricing under the comment share

* fix(cli): plan avatar seconds on the Avatar IV row alone

The general avatar row is only checked at admission and billed on
seconds left after Avatar IV, so adding it to every second made a 15 s
script plan at 238 instead of 200 and a 220 limit refuse a call that fits.

* fix(cli): the credits link opens the web plan page, which is live

* test(cli): pin the production credits link too

* chore(skills): regenerate the skills manifest after merging main

* docs(cli): keep the person measure's doc comments to four lines

* docs(cli): keep the measure doc comments to four lines

* docs(cli): keep the measure comment lines to 120 characters

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-31 00:47:54 +00:00

245 lines
9.7 KiB
TypeScript

/**
* 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 { stripVTControlCharacters } from "node:util";
import { c, isColorSupported } 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"],
["add", "Install a block or component from the registry"],
["capture", "Capture a website for video production"],
["catalog", "Browse and install blocks and components"],
["preview", "Start the studio for previewing compositions"],
["present", "Open a slideshow deck in presenter mode (with audience sync)"],
["publish", "Upload a project to a stable URL (private by default)"],
["render", "Render a composition to MP4 or WebM"],
["open", "Open a project in the HyperFrames desktop app"],
["catch-up", "See what was done in the desktop app since you last looked"],
],
},
{
title: "Project",
commands: [
["lint", "Validate a composition for common mistakes"],
["check", "Run lint, runtime validation, and layout inspection as one gate"],
[
"validate",
"Runtime-validate a composition in headless Chrome (JS errors, missing assets, contrast)",
],
["beats", "Detect beats in the music track and write beats/<audio>.json"],
["audio", "Measure where sounds land: onsets, hits, loudness, silence, in film time"],
["inspect", "Inspect rendered visual layout across the timeline"],
["keyframes", "Inspect keyframes and render onion-shot diagnostics"],
["snapshot", "Capture key frames as PNG screenshots for visual verification"],
[
"media-treatment",
"Discover, apply, or clear deterministic media treatments on media elements",
],
[
"normalize-audio",
"Match one authored audio clip's loudness to another using integrated LUFS",
],
[
"grade-compare",
"Render candidate color grades onto a reference frame as one labeled comparison PNG",
],
["compare", "Render composition variants into one labeled comparison sheet"],
["diff", "Score a render or a project against the original video, frame by frame"],
[
"measure",
"Measure a video file: clock, cuts, events, frames, motion, crops, boxes, eases, look",
],
["billing", "Open the HyperFrames plans or credits page and wait until it is done"],
["info", "Print project metadata"],
["compositions", "List all compositions in a project"],
["timeline", "Print the project's tracks and clips"],
["history", "List, undo and restore the project's recorded changes"],
["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"],
["clean", "Find and remove what HyperFrames left on disk"],
["upgrade", "Check for updates and show upgrade instructions"],
],
},
{
title: "Deploy",
commands: [
["cloud", "Render compositions on HeyGen's cloud (no local Chrome/ffmpeg)"],
["lambda", "Deploy and drive distributed renders on AWS Lambda"],
["cloudrun", "Deploy and drive distributed renders on Google Cloud Run"],
],
},
{
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",
],
["models", "List or download on-device models (models list, models install parakeet)"],
["tts", "Generate speech audio from text using a local AI model (Kokoro-82M)"],
["remove-background", "Remove background from a video or image to produce transparent media"],
],
},
{
title: "Account",
commands: [["auth", "Sign in to HeyGen and manage credentials"]],
},
{
title: "Settings",
commands: [
["feedback", "Submit feedback about your experience"],
["telemetry", "Manage usage telemetry"],
],
},
];
// ── Root-level examples ────────────────────────────────────────────────────
import type { Example } from "./commands/_examples.js";
const ROOT_EXAMPLES: Example[] = [
["Create a new project", "hyperframes init my-video"],
["Start the live preview studio", "hyperframes preview"],
["Publish to hyperframes.studio", "hyperframes publish"],
["Render to MP4", "hyperframes render -o out.mp4"],
["Transparent WebM overlay", "hyperframes render --format webm -o out.webm"],
["Validate your composition", "hyperframes lint"],
["Inspect visual layout", "hyperframes inspect"],
["Check system dependencies", "hyperframes doctor"],
];
// ── 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.
//
// For nested subverbs (e.g. `cloud render`), try the parent-scoped path
// first (`commands/cloud/render.js`) so we don't collide with the
// top-level command of the same name (`commands/render.js`).
// fallow-ignore-next-line complexity
async function loadExamples(name: string, parentName?: string): Promise<Example[] | undefined> {
// Skip the parent-scoped lookup for the root command — `parentName`
// is `'hyperframes'` for every top-level subcommand and no
// `./commands/hyperframes/<name>.js` directory will ever exist.
if (parentName && parentName !== "hyperframes") {
const examples = await tryLoadExamples(`./commands/${parentName}/${name}.js`);
if (examples) return examples;
}
return await tryLoadExamples(`./commands/${name}.js`);
}
async function tryLoadExamples(modulePath: string): Promise<Example[] | undefined> {
try {
const mod = await import(modulePath);
return mod.examples;
} catch (err) {
// Only swallow "file doesn't exist" — re-throw real load errors
// (syntax error, broken import, init-time throw) so a developer
// sees the diagnostic instead of getting silently wrong help.
if ((err as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") return undefined;
throw err;
}
}
// Commands without their own file (e.g. listed in help but not yet a real command)
const STATIC_EXAMPLES: Record<string, Example[]> = {
skills: [["Install all skills to all supported AI tools", "hyperframes skills"]],
};
// ── Render root help ───────────────────────────────────────────────────────
function renderRootHelp(): string {
const NAME_COL = 19;
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("<command>")} [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 <command>")} (or ${c.cyan("<command> --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 ────────────────────────────────────────────────
// fallow-ignore-next-line complexity
export async function showUsage(
cmd: CommandDef,
parent?: CommandDef,
print: (text: string) => void = console.log,
): Promise<void> {
if (!parent) {
print(renderRootHelp() + "\n");
return;
}
const meta = await (typeof cmd.meta === "function" ? cmd.meta() : cmd.meta);
const usage = await renderUsage(cmd, parent);
print((isColorSupported ? usage : stripVTControlCharacters(usage)) + "\n");
const name = meta?.name;
if (name) {
const parentMeta = await (typeof parent.meta === "function" ? parent.meta() : parent.meta);
const parentName = parentMeta?.name;
const examples = STATIC_EXAMPLES[name] ?? (await loadExamples(name, parentName));
if (examples) {
print(formatExamples(examples) + "\n");
}
}
}