Files
hyperframes/packages/cli
miga-heygenandMiguel Ángel 1de8b5ae33 fix(cli): reject a swallowed --json when catalog's string flags have no value (#3992)
* 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>
2026-10-09 21:55:42 +00:00
..
2026-10-08 23:28:49 +00:00

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