* fix(cli): reject a swallowed --json when catalog's string flags have no value catalog --query --json (missing --query value) parsed as query="--json", json=false, since citty's parser (node:util.parseArgs, strict: false) unconditionally consumes the next token as a string arg's value with no guard against it being another flag -- JSON-mode consumers silently got human-readable stdout instead of a clear error. Added an opt-in rejectSwallowedFlagValues() check (packages/cli/src/utils/ reject-unknown-flags.ts), called from catalog's own run() for its query/type/tag string args. Kept it opt-in rather than wiring it into the global assertKnownFlags pre-parse gate every command goes through: check.ts's --frame-check is a string flag that's documented and tested to work bare or immediately followed by another flag, handled by that command's own raw-args preprocessing before citty parses it -- a blanket pre-parse check can't tell that case apart from this bug, so review caught it as a real regression before it shipped. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): unify swallowed-flag-value handling into one raw-args gate catalog --query --json (round 1's fix) turned out to be one instance of a codebase-wide gap: any command with a type:"string"|"enum" flag followed by --json (or any other declared flag) has its value swallowed by citty's parser (node:util.parseArgs, strict: false unconditionally consumes the next token), confirmed still reproducing on check --layout --json. Round 1's post-parse, catalog-only check also duplicated check.ts's own existing --frame-check raw-args mechanism instead of replacing it, and couldn't distinguish the always-legitimate --query=--json (equals form) from the actual --query --json swallow, since node:util.parseArgs parses both to the same object. Replaced with a single raw-args scan (guardSwallowedFlagValues), wired into the global wrapCommand gate next to assertKnownFlags so every command is covered. Default: throw CliUsageError. Two narrow per-command opt-outs where a command already has its own bespoke handling for this exact bug shape: check's --frame-check (rewritten to --flag=, folding in and removing check.ts's own normalizeFrameCheckRawArgs/swallowedOptionError) and upgrade's --project (left untouched, since it already recovers the swallowed value itself). A third, previously-undiscovered collision surfaced during testing: init.ts had its own bespoke --example dash-value guard that the new global gate now preempts; folded that in too and updated its one affected test's expectation. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli): stop the swallowed-flag usage error from leaking to stdout Round 2's guardSwallowedFlagValues threw CliUsageError without presented:true, the first non-presented CliUsageError construction in the CLI -- executeCli's dead stdout-usage-dump branch (every other CliUsageError goes through failUsage(), which always sets presented:true) fired for the first time, leaking 1.4-5.7KB of ANSI usage text onto stdout on top of the correct stderr message, breaking --json callers' clean-stdout contract on exactly the class of error this ticket exists to fix. Split into two functions instead of one, since the two throw sites have genuinely different reach: throwSwallowedFlagError (used by guardSwallowedFlagValues, thrown before any command's own run() executes) prints the message itself and sets presented:true, matching every other print-then-failUsage() usage-error site in this codebase. swallowedFlagUsageError (used by check.ts's parseFrameCheck) is a plain, side-effect-free CliUsageError factory, because that throw is always caught by check.ts's own run() try/catch, which already prints and presents every error uniformly -- using the printing variant there instead produced a real, verified regression (a doubled stderr line in non-JSON mode, an extra stderr line even in --json mode), caught by adversarial review before it shipped. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * test(cli): assert literal stdout bytes stay empty for the swallowed-flag error The prior test for this only asserted the internal presented:true flag, with a comment noting the actual 0-byte-stdout behavior was checked by hand against the built CLI. Added an automated spawn-based test (same pattern as init.test.ts's runInit) that runs the real CLI and asserts stdout is literally empty, not just that the presented mechanism is engaged. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * refactor(cli): split the swallow guard into small helpers to clear the audit gate Moves the leaf flag checks out of the wrapCommand run arrow, drops ignored args from the owner map, and pulls the per-token decision out of the guard loop. Trims two comments to keep check.ts under its comment share. No behaviour change. * fix(cli): swallow guard hints a value and lets --no- negations through * fix(cli): swallow guard looks past --no- negations that citty drops --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
hyperframes
CLI for creating, previewing, and rendering HTML video compositions.
Install
npm install -g hyperframes
Or use directly with npx:
npx hyperframes <command>
Requirements: Node.js >= 22, FFmpeg
Commands
init
Scaffold a new Hyperframes project from a template:
npx hyperframes init my-video
cd my-video
preview
Start the live preview studio in your browser:
npx hyperframes preview
# Studio: http://localhost:3002/#project/my-video
# Server: http://localhost:3002
npx hyperframes preview --port 4567
In an interactive terminal, the preview stays attached until you press
Ctrl+C. In a non-interactive shell such as a coding-agent session, the same
command starts a managed preview that survives after the command returns. Use
--background or --foreground to choose explicitly, and manage persistent
previews with --status, --stop, --list, and --kill-all. Add --json to
managed lifecycle commands for machine-readable output. --foreground --json
prints the ready-session envelope once, then remains attached until stopped.
normalize-audio
Measure two local authored audio clips with integrated LUFS and match the target
to the unchanged reference. The command is a dry run unless --write is passed:
npx hyperframes normalize-audio --reference target-audio --target user-audio
npx hyperframes normalize-audio --reference target-audio --target user-audio --write
It updates only the target element's data-volume and refuses unsafe boosts
that exceed Studio's +12 dB ceiling or would clip.
render
Render a composition to MP4. Run from the project directory; the positional
argument is the project directory (not a file), so render the project's
index.html directly, or point at a specific composition file with -c:
npx hyperframes render -o output.mp4
npx hyperframes render -c ./my-composition.html -o output.mp4
Set HYPERFRAMES_RENDER_DETACHED=1 when starting a render with nohup or
disown so it keeps running after the invoking shell exits.
publish
Upload a project directory and get a hosted URL that keeps working after the CLI exits. Published projects are private by default:
npx hyperframes publish
npx hyperframes publish ./my-video
npx hyperframes publish --public
npx hyperframes publish --yes
Signed-out publishing returns an authentication-required claim URL; opening it
lets someone sign in and claim the project. Sign in first with
npx hyperframes auth login to publish an owned project you can update. Use
--public to make the claimed project visible to anyone. --yes only skips the
confirmation prompt and does not change visibility.
Signed-in publishers can use --update <url-or-id> to target an existing project
or --space <space-id> to publish into a shared team space. If the requested
project is missing or inaccessible, publishing can create a new project instead;
check the printed URL and status.
See the publish reference for all options, including video proxy settings.
lint
Validate your Hyperframes HTML:
npx hyperframes lint ./my-composition
npx hyperframes lint ./my-composition --json # JSON output for CI/tooling
npx hyperframes lint ./my-composition --verbose # Include info-level findings
By default only errors and warnings are shown. Use --verbose to also display informational findings (e.g., external script dependency notices). Use --json for machine-readable output with errorCount, warningCount, infoCount, and a findings array.
compositions
List compositions found in the current project:
npx hyperframes compositions
benchmark
Run rendering benchmarks:
npx hyperframes benchmark ./my-composition.html
doctor
Check your environment for required dependencies (Chrome, FFmpeg, Node.js):
npx hyperframes doctor
browser
Manage the bundled Chrome/Chromium installation:
npx hyperframes browser
info
Print version and environment info:
npx hyperframes info
docs
Open the documentation in your browser:
npx hyperframes docs
upgrade
Check for updates and show upgrade instructions:
npx hyperframes upgrade
npx hyperframes upgrade --check --json # machine-readable for agents
Documentation
Full documentation: hyperframes.heygen.com/packages/cli
Related packages
@hyperframes/core— types, parsers, frame adapters@hyperframes/engine— rendering engine@hyperframes/producer— render pipeline@hyperframes/studio— composition editor UI