mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
24edb15095d2343fcd20c0468d2591b075810507
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
24edb15095 |
fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional (#1830)
* fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional The #2 render failure bucket ("Composition has zero duration") accounts for ~27K errors / ~7K affected users over 30 days (PostHog project 356858). Root cause: only GSAP timelines got their duration auto-detected — CSS, WAAPI, and Lottie compositions had no source of truth for total duration unless the author remembered to set data-duration on the root element, and the render engine hard-failed capture when neither was present. Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime adapters (packages/core/src/runtime/adapters/*.ts) — each reports the longest finite end time it can discover from its own animations (CSS: computed timing offset by data-start; WAAPI: effect.getComputedTiming().endTime; Lottie: totalFrames/frameRate or the player's own duration). Infinite/ unbounded animations correctly return null and still require data-duration. Wires this into the runtime's existing duration-floor resolution (resolveAdapterDurationFloorSeconds in runtime/init.ts), alongside the existing media-duration and authored-composition floors, so window.__hf.duration becomes positive without any author action for finite-duration non-GSAP compositions. Three.js is unchanged — no AnimationClip/AnimationMixer inspection exists in that adapter, so data-duration remains required there. Tightens frameCapture.ts's zero-duration fast-fail gate to also check hf.duration directly (not just the two authored signals), so a composition mid-inference isn't fast-failed before its adapter-derived duration lands. Adds a new lint rule (root_composition_missing_duration_source) that errors only on genuinely non-inferable cases: no animation signal at all, Three.js without data-duration, or an infinite/unbounded CSS or WAAPI animation without data-duration. Deliberately silent on finite CSS/WAAPI/Lottie animations, since the runtime now infers those — an autofix that "inserts the inferred value" was considered and rejected: every case the rule flags has no derivable value (an infinite spinner has no finite end time; a duration-less Three.js scene has nothing to measure), so any autofix would have to fabricate a placeholder, trading a loud correct failure for a silent wrong-length render. Updates the CSS/WAAPI/Lottie/Three adapter skill docs and the hyperframes-core determinism-rules/data-attributes references to document the new optionality and the runtime mechanism backing it. Verified end-to-end against the real render pipeline (not just unit tests): a CSS-only composition with a finite 3s animation, no GSAP timeline, and no data-duration now renders a correct 3.000s MP4 via `hyperframes render` (previously: "Composition has zero duration" failure). The infinite-CSS negative control still fails fast with a clear diagnostic, matching the new lint rule. Adds a file-level fallow health exemption for lottie.ts's pre-existing `seek` handler — unrelated to this change, but its line numbers shifted when new functions were added earlier in the file, tripping fallow's inherited-finding fingerprint (documented pattern already used elsewhere in .fallowrc.jsonc for the same reason). Known limitation: the static WAAPI usage detector in the lint rule (/\.animate\(\s*[\[$A-Za-z_]/) can miss unusual call shapes; it only affects whether the "no signal at all" branch fires, and errs toward NOT flagging (reducing false positives) rather than over-flagging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(lint): close 3 correctness gaps in root_composition_missing_duration_source - Strip JS/CSS comments before scanning for GSAP/WAAPI/Three/Lottie/CSS animation signals, so a commented-out `.animate()` call or a commented `animation: ... infinite` rule can no longer satisfy the "has a duration source" check and mask a real zero-duration render failure. - Broaden the WAAPI detection regex to also match the object-literal (PropertyIndexedKeyframes) form of `.animate()`, e.g. `el.animate({ opacity: [0,1] }, { duration: 2000 })`, which the previous character class silently missed. Corrected the adjacent comment that incorrectly claimed this shape "can't be a false negative". - Fix hasInfiniteCssAnimation to stop false-positiving on animation NAMEs that merely contain the substring "infinite" (e.g. `infinite-spin`) by anchoring the `infinite` keyword with hyphen-aware boundaries instead of a bare `\b`. Also makes the longhand `animation-name` + separately declared `animation-iteration-count: infinite` pattern detected consistently. Adds targeted unit tests for each fixed false-positive/false-negative. * fix(runtime): keep finite duration signal when an unbounded animation coexists getInferredDurationSeconds in the CSS and WAAPI adapters returned null outright whenever any animation on the composition was unbounded (infinite iteration count), even when other finite animations on the same composition could still supply a valid duration. This disagreed with the new root_composition_missing_duration_source lint rule, which treats any animation-name as sufficient — so a composition mixing a finite fadeIn with a decorative infinite spin passed lint but still failed at render with "zero duration". Unbounded animations are now skipped when computing the max end time instead of short-circuiting the whole calculation. null is only returned when every animation on the composition is unbounded, i.e. there is no finite signal to fall back on at all. Co-Authored-By: Claude <noreply@anthropic.com> * docs(skills): fix table separator width in data-attributes.md oxfmt flagged the merged Composition Root table from the post-rebase merge of the auto-infer-duration docs onto main's reformatted table — the separator row was one dash short of the header width. * fix(lint): keep infinite-CSS duration rule strict but make its message honest Post-review (Vance): after the finite+infinite adapter fix, the runtime infers a length for a mixed finite+infinite CSS composition, but this lint rule still (intentionally) errors on it — an unbounded animation makes the intended total length ambiguous, so we require explicit data-duration. Keep that strictness (lint is advisory by default; it only blocks under --strict, and data-duration is the one duration signal guaranteed correct across every adapter, known and future). But the message wrongly claimed the render "will fail" — false for the mixed case, where the runtime falls back to the finite animation. Rewrite it to describe the ambiguity honestly, correct the rule's block comment, and add a mixed finite+infinite test asserting it still errors with an honest message. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
cf573f7f3f |
fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions (#1831)
* fix(core,producer,cli): pre-flight validation for empty/malformed sub-compositions The #1 render failure bucket in production telemetry (PostHog project 356858, dashboard 1783183 "HyperFrames — Bottom-Line & Activation"; ~65-69K occurrences / ~27-28K affected users over 30 days, ~80% via AI-agent authoring flows) is a `data-composition-src` reference pointing at a scene file that is empty, malformed, or missing. Root cause, traced end-to-end: - The literal error "Composition HTML is empty or could not be parsed: <path>" is real (not a PostHog paraphrase) — thrown by a since-reverted guard in packages/core/src/compiler/inlineSubCompositions.ts (#1364), then changed to a silent skip in #1678 to avoid aborting renders on partial content during authoring. #1629 added per-assembler guards for 3 skill workflows (product-launch-video, faceless-explainer, pr-to-video), but general-video and hand-authored flows — where the dominant filename `scene-title.html` (40K+/68K of the bucket) originates — have no assembler and thus no guard. #1678 assumed the assembler guards from #1629 covered this pre-render; they only covered 3 of the many authoring flows. - On current `main`, an empty/malformed data-composition-src file no longer crashes or throws during render — it's silently dropped by the tolerant inliner. Reproduced locally: `hyperframes render` on a project with an empty scene-title.html "succeeds" after ~93s (two 45s pollSubCompositionTimelines timeouts) with the scene silently missing from the output video. `hyperframes validate` also reports "No console errors" for the same broken project. - The raw `Cannot destructure property 'firstElementChild' of 'documentElement' as it is null` crash reproduces directly against linkedom (the DOMParser polyfill packages/cli/src/utils/dom.ts installs in the real CLI runtime) for empty and non-HTML input — confirmed with a standalone repro script, not just inferred. jsdom/happy-dom (used in this repo's own test environment) are spec-compliant and never produce a null documentElement, which is why this needed a linkedom-specific test file. Fix: - New shared helper `checkSubCompositionUsability` (packages/core/src/compiler/subCompositionValidity.ts) is the single source of truth for "is this data-composition-src file usable" — mirrors the inliner's own parse/template/body logic so all callers agree. - `inlineSubCompositions.ts` (preview/studio bundling) now uses the shared helper internally but keeps its #1678 tolerant skip-and-continue behavior unchanged — mid-authoring iteration on a partial project must keep working. `onMissingComposition` now also receives a human-readable reason. - New render-only pre-flight (`assertSubCompositionsUsable` in packages/producer/src/services/htmlCompiler.ts) walks every data-composition-src reference (including nested ones, root-relative, matching parseSubCompositions' own resolution) before any compilation work starts, and throws naming every offending file at once. This is unconditional — not gated behind --strict — because a render that silently drops a scene is strictly worse than one that refuses to start. Confirmed locally: render now fails in ~0.4s with an actionable message instead of "succeeding" after 93s with a missing scene. - New `hyperframes lint` rule `missing_or_empty_sub_composition` (packages/cli/src/utils/lintProject.ts) surfaces the same check as a file-scoped, actionable lint error (already unconditional — lint exits 1 on any error). - `hyperframes validate` now also runs this check before launching a browser, so it no longer reports "No console errors" for a project with a broken sub-composition. - `packages/core/src/parsers/htmlParser.ts`: guarded every `documentElement`-may-be-null access (parseHtml, updateElementInHtml, addElementToHtml, removeElementFromHtml, extractCompositionMetadata, validateCompositionHtml) with a new typed `CompositionHtmlParseError` (or, for validateCompositionHtml's collect-and-report contract, a typed validation failure) instead of a raw crash. Tests: empty file, whitespace-only, malformed/non-HTML, missing file, nested sub-compositions (both happy path and broken-grandchild), and the happy path — at the shared-helper, lint, and render pre-flight layers. Not changed: the AI-agent authoring skills (skills/*). general-video and hand-authored flows have no assemble-index.mjs equivalent to guard, so the fix is at the CLI/render layer instead — flow-agnostic, covers every authoring path, and the skills' existing "run lint/validate and stop on failure" guidance now actually catches this class of mistake once run. Not run in this environment: the producer package's full regression-harness test suite (`bun test` in packages/producer) — it performs heavy real rendering (S3 asset downloads, Google Fonts fetches, full video encodes) and did not complete in a reasonable time in this sandbox. Verified instead via the targeted test file for all touched code (76/76 passing), whole-repo typecheck/build/oxlint, `fallow audit` (complexity/duplication/dead-code gate, clean), and manual end-to-end CLI runs (render/lint/validate) against reproduction projects, including a nested sub-composition scenario. CI should run the full producer suite before merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(parsers,lint): port empty-composition pre-flight to extracted packages Rebased onto main, which extracted @hyperframes/lint from core (lint depends only on parsers, not core). Relocate checkSubCompositionUsability from core to @hyperframes/parsers so both core (inliner) and lint can consume it without a core<->lint cycle; core keeps a @deprecated re-export shim. Correctness fixes from code review: - checkSubCompositionUsability now returns "no-composition-root" when the <template>/<body> content has no [data-composition-id] element (previously a marker-free placeholder body passed both guards). - lint's missing/empty sub-composition rule now only checks files reachable via data-composition-src from the root (matching render pre-flight), instead of a raw filesystem walk that false-positived on orphaned files. - drop `as string` cast in inlineSubCompositions in favor of an explicit null guard (per CLAUDE.md). Review-comment items: - move EmptyCompositionError JSDoc above the class (was above the adapter fn). - correct stale circular-ref comment to match actual silent-skip behavior. - rewrite self-contradicting lint message ("silently drop") to describe the new loud render-pre-flight abort. - add the __PLACEHOLDER__ (/^__[A-Z_]+__$/) skip to the render pre-flight so it agrees with lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
9c4d9e50a0 |
feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) (#1829)
* feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) Seed the CLI's anonymous distinct_id into Studio at launch so a developer's CLI and their Studio browser session resolve to the same PostHog person. Also unifies Studio's two previously-independent anonymous ids into one source of truth. Uses only the existing anonymous machine id (no new PII). - cli: inject window.__HF_CLI_DISTINCT_ID into the served index.html <head> (mirrors the existing __HF_STUDIO_ENV__ injection) + add a fallback GET /api/telemetry-identity endpoint. Only seeds when CLI telemetry is enabled; empty/no-op otherwise. - studio: new telemetry/distinctId.ts single source of truth; adopts the CLI-seeded id when present, else falls back to the existing per-browser localStorage id. Both Studio clients (studio:* and studio_*/render) now share this one id. * fix(telemetry): keep Studio distinct_id resolver fail-silent on getItem resolveStudioDistinctId read localStorage.getItem() outside a try/catch while every other external access in the module is guarded. In a storage-restricted context where the localStorage reference resolves but getItem throws, the resolver threw — breaking the module's fail-silent contract (telemetry must never break Studio). Guard the reads and treat a throw as "no id". Also drop an unnecessary `as` cast in the test per the repo CLAUDE.md convention (the optional global is already declared). * refactor(telemetry): address review feedback on identity unification - dedup safeLocalStorage/safeSessionStorage into utils/safeStorage.ts, used by both telemetry/config.ts and telemetry/distinctId.ts (Miga #6) - replace redundant `??=` with `=` in the no-storage branch; cachedId is guaranteed null there (Miga #2) - extract buildStudioHeadScripts() so the "identity script before env script" head-injection ordering is a pure, tested invariant (Miga #5) - add tests: head-script ordering + telemetry-off passthrough, and a Studio memoization test proving an adopted CLI id survives a later window.__HF_CLI_DISTINCT_ID reassignment (Rames) - clarify the XSS-escaping comment (both < and / escaped so no </script> sequence can form) (Miga #1) |
||
|
|
f24a1a9ce7 |
feat(studio): make storyboard view default available (remove FF) (#1794)
* feat(studio): make storyboard view default available (remove FF) Removes STUDIO_STORYBOARD_ENABLED. The storyboard view-mode toggle was gated behind a default-off feature flag (VITE_STUDIO_ENABLE_STORYBOARD) since #1529. With the storyboard experience now ready for broad exposure, drop the gating and make the toggle available unconditionally. Changes: - packages/studio/src/components/editor/manualEditingAvailability.ts: delete the STUDIO_STORYBOARD_ENABLED constant. - packages/studio/src/App.tsx: drop the import + FF arg to useViewModeState(). Hook is now called argument-free. - packages/studio/src/components/StudioHeader.tsx: drop the import + the conditional-render guard on <ViewModeToggle />. The toggle always renders in StudioHeader's center slot. - packages/studio/src/contexts/ViewModeContext.tsx: remove the enabled: boolean parameter from useViewModeState() and simplify. - packages/studio/fixtures/storyboard-sample/README.md: drop the VITE_STUDIO_ENABLE_STORYBOARD=1 prefix from the preview command. The VITE_STUDIO_ENABLE_STORYBOARD / VITE_STUDIO_STORYBOARD_ENABLED env vars become no-ops after this change. Co-Authored-By: Jerrai <noreply@anthropic.com> * docs(skills): drop stale VITE_STUDIO_ENABLE_STORYBOARD reference The Storyboard view is now available by default (the FF removed in this PR); storyboard-format.md no longer points at the dead env var, and skills-manifest is regenerated for the hyperframes-core hash. Closes the Via/Magi review nit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jerrai <noreply@anthropic.com> |
||
|
|
413d8187fd | chore: release v0.7.11 | ||
|
|
13b115e006 |
fix(cli): close skills removed-detection power-user gaps (follow-up to #1740) (#1743)
* fix(cli): close skills removed-detection power-user gaps (follow-up to #1740)
Address power-user follow-ups deferred from #1740 (skills removed-detection):
- `--dir` installs now run removed-detection. `locateInstall` hardcoded
scope "project" for every `--dir`, so a `--dir ~/.claude/skills` (a global
install) read a non-existent `<cwd>/skills-lock.json` and found zero
removed skills. New `scopeForDir` infers global vs project from whether the
dir is under $HOME, so the right lock is read.
- Pin the upstream lock paths to vercel-labs/skills@v1.5.13 (verified against
src/skill-lock.ts + src/local-lock.ts) and warn loudly when the lock is
absent where expected, so removed-detection no longer silently no-ops if
upstream moves the lock. checkSkills returns lockMissing; --json surfaces it.
- skills update gains --source/--dir (parity with check), plumbed into its
internal prune checkSkills() so the prune respects the same overrides.
- Add the missing test for the all-rejected-names early-return in
runSkillsRemove (no skills remove spawned when every candidate name is
rejected), plus tests for the --dir scope inference and update flag parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): scope --dir by CWD-containment before HOME (project installs under $HOME)
Address Magi's REQUEST_CHANGES on #1743 (
|
||
|
|
b9b5780396 |
feat(cli): persist + show friendly user identity; preserve unknown credential fields (#1741)
* feat(cli): persist + show friendly user identity; preserve unknown credential fields The `~/.heygen/credentials` file is SHARED with the Go `heygen` CLI. This is the hyperframes-side mirror of heygen-cli#197, which adds an optional `user` block to that file. Two CLIs writing one file must round-trip each other's data without loss. Load-bearing change: the credentials reader/writer now PRESERVES unknown fields on round-trip. Previously readStore/writeStore stripped any key this CLI didn't model, so writing the file back would silently drop the `user` block heygen-cli wrote (and any future key). Unrecognized top-level keys, and unknown keys inside `oauth` / `user`, are captured on a hidden symbol slot and re-emitted verbatim. Known fields stay strictly validated. Also mirrors heygen-cli#197's friendly-display feature: - New optional `user` block schema (email/first_name/last_name/username), all omitempty; legacy files without it parse fine. - After login (OAuth + api-key paths) probe /v3/users/me, persist the block, and show a friendly name (email > "first last" > username). Probe failure is non-fatal (login still succeeds); a stale block is cleared on probe failure so a wrong account can't surface. - `auth status` surfaces the persisted block (persisted_user in JSON, a cached Account row in human output) for file-sourced credentials; env-sourced credentials skip it (the on-disk block may belong to a different key). - Fixed the OAuth write path to carry the user block + unknown keys across a fresh login / refresh (it previously rebuilt a minimal record). Tests: preserve-unknown-fields round-trip (top-level, oauth, user), the exact cross-CLI `user`-block scenario, schema round-trip + omitempty, backwards-compat with legacy files, login persistence + graceful probe failure + stale-clear, and the `auth status` surface. Full CLI suite (1009 tests) green; oxlint + oxfmt + tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): preserve unknown credential data in cleanup/rollback paths Addresses Magi's REQUEST_CHANGES on #1741. The credentials reader/writer already round-trips unknown/foreign keys (the cross-CLI forward-compat contract), but three destructive paths still deleted the whole file when no known api_key/oauth survived — even when the hidden Symbol-keyed unknown-field bag held a future credential another CLI owns. That clobbers exactly the data this PR preserves. - Add `hasPreservedUnknownData(record)` to store.ts (checks the top-level unknown bag + the oauth/user sub-object bags) and export it via the barrel. - `clearOAuth`, `clearUserInfo`, and the failed `auth login --api-key` rollback now write the credential-less remnant (carrying the unknown bag) instead of deleting the file when unknown/foreign data survives. They still delete when nothing worth preserving remains. - Regression tests: rollback path + both cleanup paths (clearOAuth, clearUserInfo) preserve a foreign top-level key; `hasPreservedUnknownData` unit tests at all three levels. Also addresses the review's minor items: - Add a refresh-path round-trip test (`refreshTokens`) proving an unknown key inside the oauth sub-object survives a no-rotation refresh — the most-frequent write path, previously only implicitly covered. - Clarify the `userDisplayName` / `combineName` docstrings: precedence is `email > "first last" > first-only > last-only > username`. - Replace the stale `expires_at` example date in store.ts with `<ISO-8601 UTC>`. Full CLI suite green (1020 tests); tsc, oxlint, oxfmt --check all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
88fffb04d1 |
feat(cli): surface and prune skills removed upstream (#1740)
* feat(cli): surface and prune skills removed upstream `skills add` / `init` / `hyperframes skills update` only ever add or refresh — none of them delete a skill that was renamed or dropped upstream (e.g. graphic-overlays → talking-head-recut). `skills check` also ignored any installed skill not in the manifest, so a stale bundle lingered forever with no signal and no cleanup path. - skills check: detect "removed" skills by cross-referencing the vercel-labs/skills lock — a skill the lock attributes to our manifest `source` that the manifest no longer lists. Surface them in the human and --json output and count them toward the non-zero exit so the `check || update` contract gates on them. - skills update: after `skills add --all`, prune those skills via `skills remove -g --yes` so the install fully reconciles with the manifest. Best-effort — a cleanup failure doesn't fail the update. Attribution is via the lock's source field, never the bare directory name: `.../skills` is shared across sources, so skills from other sources (e.g. greensock/gsap-skills) are never touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): prune removed skills only in the scope they were attributed from Make the cleanup in `skills update` impossible to misfire onto a user's own skills. The prune already only targets names the lock attributes to our source, but it hardcoded `skills remove -g` (global) while `skills add` defaults to project scope — so detection could attribute from one scope's lock while removal hit another, potentially deleting a global skill of the same name from a different source. - checkSkills now returns the located install's `scope`. - skills update removes in that exact scope (`-g` only when global), so attribution scope and removal scope always match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): validate skill names before passing them to skills remove Addresses review feedback: skill names fed to `skills remove` originate as lock-file JSON keys, so a corrupt or crafted lock entry could smuggle a flag-like (`--config=…`) or shell-special token into the spawn — which matters most on the Windows cmd.exe path where arg escaping is fragile. Filter the names through a strict kebab-case pattern and warn on any that are rejected, rather than relying on a `--` separator (the upstream `skills` arg parser silently ignores unknown `-`-prefixed tokens and has no `--` end-of-options handling, so `--` would be a no-op there). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ca9e1316be |
perf(producer): stream binary file responses, async-read HTML (#1735)
* perf(producer): stream binary file responses, async-read HTML
Replaces the per-request readFileSync in fileServer's static file handler
with a createReadStream pipe (binary) and an async readFile (HTML). Static
asset serving no longer blocks the Node event loop.
Why
---
The pre-fix handler called readFileSync(filePath) on every binary asset.
On video-heavy compositions Chrome requests several 32MB video files
back-to-back; each readFileSync(32MB) blocked the main event loop long
enough to wedge concurrent /health responses and other timers.
Scope clarification — this addresses the event-loop block documented at
renderOrchestrator.ts:1277-1306 (the video-heavy regression class). It is
NOT the fix for today's infinite-duration incident; Miguel is shipping
that upstream as a plan()-time duration guard. The two are complementary:
- Miguel's guard kills the impossible-work input shape before chunk
planning so the producer doesn't try to enumerate 300B frames.
- This streaming fix removes the next-largest known main-thread block
(large binary I/O during video-heavy renders), so future wedge
classes don't kill otherwise-healthy probes either.
The companion worker_thread /health PR + the heygen-com/app probe-timeout
bump round out the defense-in-depth: even if some future code path
introduces another main-thread stall, the probe lives off-thread and the
budget is 30s anyway.
What changed
------------
fileServer.ts: switched both file branches off the sync I/O path.
- Binary (the hot path for video-heavy renders): readFileSync(filePath)
-> createReadStream + Readable.toWeb -> Response stream body.
Content-Length is set via statSync so Chrome's range-aware media
stack sees the size up front. The handler is now async because the
HTML branch awaits.
- HTML (small files; injected with pre/head/body scripts):
readFileSync(filePath, "utf-8") -> readFile(filePath, "utf-8").
The injection is still sync — pure string ops — only the disk read
moved off-thread. Index HTMLs are tiny (~200KB max for AI-generated
compositions) but a ms of stall per render-start adds up across a
fleet.
Test
----
fileServer.test.ts: added a streaming regression that pins three
properties on a 5MB synthetic binary asset (chunk-boundary spanning):
1. Correctness — served bytes match the file across multiple
createReadStream chunks (default 64KB highWaterMark).
2. Content-Length header is set from statSync.
3. Four parallel fetches all return identical content; the streaming
path doesn't serialize them.
All 31 fileServer tests pass locally (bun test).
TODO: link Miguel's upstream plan() duration guard PR once known.
— Jerrai
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(producer): implement Accept-Ranges + 206 Partial Content for fileServer
Delivers the range-request semantics the original PR body promised but
the diff did not implement. Without range support, Chrome's <video>
element issues full-file GETs on seek; with this commit it can issue
`Range: bytes=...` and get a sliced 206 back, so seek + partial-load
work without re-pulling the whole file.
- Add `parseRangeHeader` (exported for unit tests) covering the three
RFC 7233 single-range forms: bytes=START-END (closed), bytes=START-
(open-ended), bytes=-SUFFIX (last N bytes). Multi-range falls back to
`absent` (full 200) so we never reassemble multipart/byteranges.
- Binary path now returns 206 Partial Content with Content-Range +
sliced Content-Length on satisfiable ranges, 416 Range Not Satisfiable
with `Content-Range: bytes (asterisk)/<size>` on unsatisfiable ranges,
and 200 with `Accept-Ranges: bytes` on full-body GETs so clients know
ranges are supported.
- Add unit tests for parseRangeHeader (10 cases: 3 forms, clamping,
unsatisfiable edges, malformed inputs, multi-range fallback).
- Add integration test covering 200 + Accept-Ranges, all 3 range forms
with byte-correct slices, 416 on out-of-bounds, and multi-range -> 200
fallback.
Addresses Miga's review finding on #1735.
Co-Authored-By: Jerrai <noreply@anthropic.com>
— Jerrai
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
01a10cdc53 |
fix(producer): serve /health from a worker_thread so probes survive main-thread stalls (#1733)
* fix(producer): serve /health from a worker_thread so probes survive main-thread stalls Adds an off-main-thread /health endpoint that listens on its own port (default 9848, env PRODUCER_HEALTH_PORT). The endpoint binds inside a Node worker_thread with a minimal node:http server — separate event loop, separate isolate — so probe responses don't depend on whatever the producer's main thread is doing. Why now ------- Today's hyperframes-producer crashloop traced to an infinite GSAP timeline -> distributed planner trying to enumerate ~300,000,000,000 frames -> sidecar /health stops landing within k8s's 5s window -> otherwise-healthy pods killed. Miguel is shipping the root-cause fix at plan() time (impossible / non-finite / sentinel durations get rejected before chunk planning). That removes today's wedge. This change is defense-in-depth for the kill mechanism. Even with the plan() guard, future wedge classes can stall the main event loop for seconds at a time: large synchronous file I/O (see the companion fileServer streaming PR), GC pauses on long-running renders, tight loops in user-authored GSAP / Three.js / canvas code, future activity / pool changes whose runtime cost we haven't yet characterized. Probe responsiveness should reflect process liveness, not main-thread event-loop responsiveness. If the entire Node process is dead the OS tears down both threads' sockets simultaneously and k8s correctly kills the pod. Anything short of that and the worker thread's listener keeps answering. Backwards-compatible: the main-thread /health on PRODUCER_PORT (9847) keeps working exactly as before. The k8s sidecar probe config in heygen-com/app can migrate to the worker port at its own pace. A companion heygen-com/app PR in this batch raises the probe timeout from 5s -> 30s as a last-resort backstop. TODO: link Miguel's upstream plan() duration guard PR once known. Test: healthWorker.test.ts (vitest) — 3 tests pass locally, including the load-bearing one: stays responsive while the main thread is blocked on a 500ms sync busy-spin. — Jerrai Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(producer): tighten healthWorker startup race + shutdown semantics Addresses Miga's review on #1733. - server.ts: store the worker as a Promise<HealthWorkerHandle | null> instead of mutating a `let` from inside `.then`. A SIGTERM landing before the `.then` callback fired would previously see `healthWorker === null` and skip cleanup. shutdown() now `await`s the promise with a bounded 1.5s timeout so a hung-startup worker can't keep SIGTERM waiting (worker.terminate() from process exit still kills it). - healthWorkerThread.ts: replace `process.exit()` inside the worker with `parentPort.close()` + natural event-loop drain. Node-version semantics for `process.exit()` from a worker have been historically inconsistent; the documented clean path is to close the channel and let the worker exit naturally. Also drops the redundant 2s force-exit on shutdown — the parent already owns the authoritative deadline via Promise.race + worker.terminate(), so the worker-side timer was belt-and-suspenders noise. Co-Authored-By: Jerrai <noreply@anthropic.com> — Jerrai --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0c05025c04 |
ci(skills): run skills/**/*.test.mjs in CI (#1724)
skills/**/*.test.mjs files (e.g. skills/media-use/scripts/resolve.test.mjs and skills/media-use/scripts/lib/manifest.test.mjs) are bare `node --test` files with only `node:` built-in imports. They aren't part of any workspace package, and the existing `Test` job's path filter (the `code` filter in the `changes` job) excludes `skills/**`, so even on PRs that touch only skills/ those tests never run. This matters for regression guards. The shell-injection probe test added in HF#1723 feeds probe() a filename containing `clip"; touch INJECTED; echo ".mp4` and asserts no marker file is created. The test passes locally but under the current job graph it would never run in CI on a follow-up skills/ change that re-introduces the bug. Closing the gap with a dedicated `Test: skills` job rather than relaxing the `code` filter. The existing `Test` job's steps run `bun run test:scripts` (hardcoded file list) and `bun run --filter '*' test` (workspace packages only), neither of which would actually execute skills tests even if the filter let `skills/**` through. The dedicated job needs no `bun install`, just node 22, since the tests only import from `node:` and relative paths. The discovery step shells out to `find` and fails loudly when zero test files match, so a future rename or layout change can't silently turn this into a no-op pass. Spotted by Via in HF#1723 review thread, confirmed by James as a separate follow-up rather than a blocker for HF#1723. -- Jerrai (https://claude.com/claude-code) |
||
|
|
7517f6ac86 |
feat(slideshow): per-slide autoplay (manual-advance, opt-in) (#1708)
* feat(slideshow): per-slide autoplay (manual-advance, opt-in) Adds an opt-in `autoplay` flag to slideshow slides: when the presenter lands on a video slide, its `<video>` plays from the start. The slideshow still holds and never auto-advances — the presenter clicks Next when ready. This covers compositions whose own controls can't be clicked (the player renders the composition pointer-events:none). Plumbing (done, tested): - core: `SlideRef.autoplay?: boolean`, parsed + validated in parseSlideshow (a non-boolean autoplay rejects the manifest); carried through resolve. - controller: optional `PlayerPort.playSceneMedia(sceneId)`, fired only on forward `enterSlide` for autoplay slides (not resume/back/sync, so the audience — which mirrors the presenter's media events — isn't double-driven). - component: `playSceneDocumentMedia` reaches the same-origin composition iframe, finds the scene's `<video>`, and asserts playback; `stopMedia` (already wired on slide change) resets it. An autoplay token cancels a pending start when the slide changes. - tests: controller autoplay behavior + parser flag round-trip/validation (131 player + 22 core slideshow tests pass). KNOWN LIMITATION — runtime media-start needs the player media model (@vance): On current main the clip<->timeline binding from #1601 keeps every clip synced and *paused* to the held timeline frame, which wins against playSceneMedia's play() — so the clip does not actually start on main yet (it does on the pre-#1601 player). The correct fix is a sanctioned "let this clip free-run while the timeline holds" path in the player/runtime media controller. Flagging for Vance to wire the start into the #1601 media model (or rebase onto it) when back. The plumbing above is the stable surface that hook plugs into. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(slideshow): address code-review on per-slide autoplay - guard playSceneDocumentMedia behind resolveMode() !== "audience": the audience mirrors the presenter's media events, so it must not independently drive its own copy of the clip. - drop the per-enter window pointerdown/keydown "gesture retry" listeners, which leaked when muted autoplay succeeded without a gesture. The poll already re-asserts play(), so a gesture within the window is picked up next tick. - stop polling once the clip is advancing across two ticks (was re-asserting play() for the full window even after playback was confirmed). - cancel any in-flight autoplay loop on disconnectedCallback (bump the token). - split the poll into findSceneVideo + stepAutoplay helpers (keeps each small). - fix the enterSlide comment: autoplay fires from enterSlide (next/prev/ goToSlide), not resumeSlide (back/backToMain/syncTo). - parser: isOptionalBoolean type guard instead of a one-off helper; drop `as` assertions in the new controller test. 131 player + 22 core slideshow tests pass; lint/format/typecheck/fallow clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(slideshow): autoplay skill guidance + address review nits Addresses review feedback on #1708: - skill: document per-slide `autoplay` in the slideshow standalone-harness reference — when to use it (video is the slide's primary content, its end is the advance cue) vs not (background/ambient loops, footage talked over), per Vance's guidance, before merge. - play() rejection is no longer blanket-swallowed: AbortError (timeline-sync seek interrupt) and NotAllowedError (gesture-gated autoplay) are expected and ignored; any other rejection is surfaced once via console.warn (Via nit 1). - clarify in the SlideRef.autoplay doc that it plays the scene's FIRST <video> (Via nit 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f7bc0384f0 |
docs: add 19-skills catalog to README, CLAUDE.md, and Mintlify docs (#1722)
* docs: list all 19 skills in README + add CLAUDE.md maintenance reminder Agents discover skills via the README, so silently-out-of-date entries kill discovery. This change: - Adds a `## Skills` section to the README listing all 19 skills, grouped Router / Creation workflows / Domain skills, with a one-line "use when" blurb for each (sourced from each skill's SKILL.md frontmatter `description:`). - Updates the existing CLAUDE.md `## Skills` section to cover all 19 skills (was missing the domain skills, `/media-use`, `/slideshow`, and `/music-to-video`), mirroring the README's Router / Creation / Domain grouping. - Adds a "Skill catalog maintenance" section to CLAUDE.md so future skill additions / renames update both surfaces and the `/hyperframes` router skill in lockstep. Docs-only — no source or test changes. — Jerrai (https://claude.com/claude-code) * docs(mintlify): add skills catalog page + extend maintenance reminder Per follow-up on HF#1722: the Mintlify docs at hyperframes.heygen.com also need the skills catalog so agent discoverability is consistent across README and docs site. - New: docs/guides/skills.mdx (3-group catalog — router / creation workflows / domain skills — mirrors README structure, sourced from the same SKILL.md frontmatter) - Update: docs/quickstart.mdx — completes the workflow-skills list (was missing /music-to-video, /slideshow, /general-video) and cross-links the new page - Update: docs/introduction.mdx — adds a skills-catalog card to the hero CardGroup and the Next Steps section - Update: docs/docs.json — adds /guides/skills to the Guides nav - Update: CLAUDE.md "Skill catalog maintenance" — adds docs/guides/skills.mdx as the third sync target alongside README and skills/hyperframes/SKILL.md, and notes the count drift surface (README + CLAUDE.md mention "19 AI agent skills" in their intros; the new docs page deliberately omits a count to avoid drift) Docs-only — no source, packages, or test changes. — Jerrai (https://claude.com/claude-code) * docs(readme): oxfmt table column-alignment fix Pure whitespace — oxfmt's table-column alignment caught README.md after the previous commit. No content change. — Jerrai (https://claude.com/claude-code) * docs(skills): reconcile install-command contract across README/CLAUDE/Mintlify Per Magi's review on HF#1722: the new README/CLAUDE/skills.mdx pages described bare `npx skills add heygen-com/hyperframes` as installing all 19 skills, while existing quickstart/prompting docs said the bare command opens a picker and `--all` installs everything. Verified actual CLI behavior with `npx skills add --help` and a clean-dir run: bare command opens an interactive picker for human users (the CLI help documents `--all` as "Shorthand for --skill '*' --agent '*' -y" — the picker-skipping form). Inside an agent the bare command auto-installs all non-interactively, but that's an agent-detection UX shortcut, not the public contract — documenting the picker is correct for human readers. All touched docs now use the consistent contract: - `npx skills add heygen-com/hyperframes` -> interactive picker - `npx skills add heygen-com/hyperframes --all` -> install all 19 (skips picker) - `npx skills add heygen-com/hyperframes --skill <name>` -> install just one Files updated: README.md, CLAUDE.md, docs/guides/skills.mdx. Existing docs/quickstart.mdx and docs/guides/prompting.mdx already used this contract and are unchanged. — Jerrai (https://claude.com/claude-code) |
||
|
|
1c389983de |
fix(cli): ship player + slideshow bundles so present/play work from npm (#1706)
`present` and `play` render compositions in the standalone browser player, resolving the player/slideshow IIFE bundles via resolvePlayerPath / resolveSlideshowPath. Those resolvers look for the bundles alongside the built CLI (dist/hyperframes-player.global.js, dist/hyperframes-slideshow.global.js), but build-copy.mjs never staged them into dist/. The remaining candidate paths are monorepo-dev only, so an npm install has nothing to resolve. Result: `npx hyperframes present` always failed with "@hyperframes/player not found", forcing users to run the presenter from a monorepo checkout. Copy both player globals from packages/player/dist into the CLI dist during build:copy (existsSync-guarded + warn, matching the surrounding pattern). The runtime bundle is already handled by build:runtime. Verified: the globals now appear in `npm pack`, and `node dist/cli.js present` starts without the player-not-found error. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c7b9bf3386 |
fix(studio): resolve project-root-relative asset URLs in preview iframe (#1698)
## What Studio preview now resolves `<video src="../../assets/x.mp4">` (and the same shape for `<img>`, `<audio>`, inline `style` `url()`, and `<style>` CSS `url()`) against the sub-composition's URL — matching what the server-side bundler already does for the render path. ## Why Authored compositions live at `compositions/frames/*.html` and reference project-root assets either as plain `assets/x.mp4` (already correct because the main document's `<base href>` points at the project preview root) or as `../../assets/x.mp4` (the explicit project-root-relative form). The server-side `inlineSubCompositions` flattens sub-comps into `index.html` and rewrites the `../`-form against the sub-comp's source path so it resolves against the project root in the baked render. The browser-side runtime that mounts external sub-compositions via `fetch` did no such rewriting. So `<video src="../../assets/x.mp4">` authored inside a `compositions/frames/scene.html` resolved against the main document's base href, climbed above the project root, and 404'd in Studio preview — even though the same path rendered correctly in the final video. An OSS user (Miao Yang) hit this in a real project. ## How Added `rewriteSubCompositionAssetPaths` to the runtime `compositionLoader`. After parsing the fetched sub-composition HTML and before extracting any nodes, walk the parsed document and rewrite the same surface the server-side path touches: - `[src]` and `[href]` attributes on every element - `[style]` attribute `url(...)` references - `<style>` element CSS `url(...)` references The rewrite mirrors the producer's semantics exactly: only values that start with `../` (or are literal `..`) are rewritten — against the sub-composition's URL via `new URL(value, compositionUrl)`. Absolute URLs, root-relative paths, `data:`, hash refs, and plain `assets/x.mp4` are left untouched. **Plain relative paths must not be rewritten** because the main document's `<base href>` already covers them; rewriting would double-prefix the URL. The walk recurses into `<template>` content because authored compositions typically wrap their rendered body in a `<template>` and `querySelectorAll` does not enter template content (it lives in a detached `DocumentFragment`). ## Test plan - [x] Unit tests added (6 new tests in `compositionLoader.test.ts`): rewrites `../`-traversing src on template-wrapped sub-comps; leaves plain relative paths untouched (no double-prefix); leaves absolute / data / hash / root-relative URLs untouched; rewrites CSS `url()` in `<style>` blocks and inline `style` attributes; rewrites for non-template (full-HTML-doc) sub-comps. - [x] Full core test suite green (2065 tests). - [x] Full studio test suite green (1148 tests). - [x] Manual verification with the reporter's actual project: before the fix one `<video>` with a `../../assets/...` src returned `MEDIA_ELEMENT_ERROR: Format error`; after the fix all 7 `<video>` elements load (`readyState=4`, correct `currentSrc`). The 6 plain `assets/...` paths are *unchanged* (no double-prefix) and continue to resolve via `<base href>` as before. - [x] `bun run lint`, `bun run format:check`, `bun run typecheck`, `fallow audit` all green. Reported by Miao Yang. — Jerrai (https://claude.com/claude-code) |
||
|
|
b651a9d218 |
perf(producer): skip wasted Chrome media work + injector page.evaluates during render (#1651)
## What Two small render-side improvements for video-heavy compositions: 1. **`packages/core/src/runtime/media.ts`** — gate the per-tick `el.currentTime = relTime` set + the `el.load()` drift-recovery retry on the *absence* of a `<img id="__render_frame_<id>__">` sibling (i.e., we're in render mode + this video's visual is bypassed by frame injection + its audio is mixed by ffmpeg from source files). 2. **`packages/engine/src/services/videoFrameInjector.ts`** — probe `window.__hfReseekGpu` and `window.__hf.colorGrading.redraw` once at the first injector call; cache the booleans; skip the per-frame `page.evaluate` round-trips when neither capability is registered. ## Why #### media.ts During render the runtime calls `el.currentTime = relTime` on every active video per sync tick. For frame-injected videos that's pure waste: - The visual comes from the `<img id="__render_frame_<id>__">` sibling injected by the producer's `videoFrameInjector` — the `<video>` element is `visibility: hidden`. - Audio is mixed by ffmpeg from the source files in `runAudioStage` (separate stage) — it never goes through the in-browser audio pipeline during render. So every per-tick seek just kicks Chrome's media pipeline (buffering checks, range fetches, decoder state changes) for no visible or audible benefit. On a 30 × 32 MB synth comp, that's ~2,400 wasted seeks per render — and the cost wasn't on the JS critical path, so it didn't show up in `avgBeforeCapture` directly. It bled into the BeginFrame compositor's per-frame screenshot time. Preview is unaffected: the injection sibling only exists during render. In preview `hasInjectionSibling` is always false → existing seek path runs unchanged. #### videoFrameInjector.ts The injector hook ran `__hfReseekGpu` and `redrawRuntimeColorGrading` via `page.evaluate` on every render frame. For comps that don't register either capability (the common case — anything without WebGL/WebGPU video sub-comps or a color-grading layer), each was a no-op page-side function preceded by a ~CDP-round-trip-worth of overhead. Probing once and caching `false` eliminates that for the rest of the render. ## How was this validated Stress shape: `synth-30-heavy` — 30 × 32 MB MP4 / 3 s each, sequenced end-to-end over a 90 s timeline (`data-composition-id` root + per-video `<video id="vid-NN" data-start data-duration data-track-index>`). Host: 8-core / 30 GB Linux. N=3 baseline against stock `origin/main` (post-#1630), N=3 with-fix on the same machine, same corpus, fresh worker pool each run. Phase timings via `[Render:trace]` JSON; per-frame sub-breakdown via a one-line `[CapturePerf]` stderr emit (kept locally, not in this PR — `dedupPerfs` already carries the data, this branch surfaces it). | | Baseline N=3 | With-fix N=3 | Δ | |---|---|---|---| | wall mean | 119.5 s ± 1.4 s | **117.3 s ± 0.9 s** | **-2.2 s (-1.8%)** | | avg screenshot / frame | 50.0 ms | **49.0 ms** | -2.0% | | avg beforeCapture / frame | 13.0 ms | **12.1 ms** | -7.0% | | avg total / frame | 66.0 ms | 63.9 ms | -3.2% | | output md5 | `5a22be64...` | identical ×3 | ✓ | The 1 ms screenshot drop is the load-bearing signal: it confirms the kicked Chrome media-pipeline work *was* bleeding into BeginFrame compositor time, even though it wasn't on the JS critical path. Per-frame budget improved 2.1 ms × 2700 / 3 workers ≈ 1.9 s of `capture_disk` savings, which matches the observed wall delta. This stacks cleanly with #1630 (which removed the injector's fileServer contention). #1630 moved the injector's PNG fetches off the fileServer's hot path; this PR keeps Chrome's media pipeline quiet during render so the BeginFrame compositor runs unhindered. ## Test plan - [x] Local-CLI render on `synth-30-heavy` × N=3 baseline + N=3 with-fix; wall, per-frame, md5 captured (above). - [x] Lint / format / typecheck via lefthook pre-commit (`oxlint`, `oxfmt`, `fallow audit`, `tsc --noEmit` across `@hyperframes/core` + `@hyperframes/engine` + `@hyperframes/producer`). - [ ] *Real-world video-heavy comp validation* — would love a Magi / Miga eye on a HF-heygen-stripe-shape or a Rahino-shape comp to confirm there's no audible artifact on unmuted videos. The change shouldn't affect them — in render mode the audio path is ffmpeg, not the in-browser pipeline — but a sanity-check render is cheap. ## Scope notes - *Not addressed in this PR*: the user-facing request for an upfront-extract concurrency cap (`Promise.all` in `extractAllVideoFrames` is currently unbounded across all videos). Filing as a follow-up PR — different layer of the pipeline, different user surface (CLI flag), worth keeping separate for review. - *Edge case*: in the calibration test-frame phase, the injection sibling may not yet exist when drift recovery first checks a video at the very start of its active window. The gate correctly defaults to "no sibling → run the seek" in that case, which is the existing behavior. _Authored by Jerrai (Rames team)._ |
||
|
|
4be81b4fb4 |
fix(producer): inline base64 frames in injector to unblock video-heavy renders (#1630)
* fix(producer): inline base64 frames in injector to unblock video-heavy renders The URL-served frame path (PR #596) hands each injected `<img>` a fileServer URL instead of a base64 data URI, on the theory that shipping a short URL through `page.evaluate` beats shipping a multi-MB base64 string per frame. That holds when the fileServer is otherwise idle. But on video-heavy compositions, the same fileServer also serves every `<video>.src`. The runtime's drift-recovery branch (`runtime/media.ts:294-302`) issues `el.load()` on the underlying `<video>` during seeks, kicking off full-file downloads that occupy the fileServer's single Node event loop (it uses `readFileSync` and offers no `Accept-Ranges`). The injector's `<img>.decode()` then queues behind those video fetches and is never serviced before puppeteer's protocol timeout fires, surfacing as `Runtime.callFunctionOn timed out` in `capture_streaming`. Reproducer (30 × 32 MB videos / 90 s comp / 8-core / 30 GB host): baseline (broken corpus) 537 s render fails baseline (corpus-fixed) 428 s render fails this fix (drop frameSrcResolver) 121 s render succeeds, 69 MB MP4 Control corpus (30 × 1.6 MB / 60 s) shows no regression: 137 s with this change vs ~135 s on \`main\`. The \`createCompiledFrameSrcResolver\` builder and the \`frameSrcResolver\` option stay in the codebase, just unused for now — re-enabling them behind a proper gate ("only use URL-served frames when the page has zero fileServer-bound \`<video>.src\` traffic") is a follow-up. The cache memory ceiling (\`frameDataUriCacheBytesLimitMb\`, default 1500 MB above 8 GB hosts) already bounds the cost of base64 inlining. — Jerrai * refactor(producer): drop unused frameSrcResolver builder import in render orchestrator Followup to the previous commit. The void-call and the `createCompiledFrameSrcResolver` import in `renderOrchestrator.ts` were left behind as a no-op breadcrumb for the future gating PR. Code review (PR #1630) correctly flagged this as dead code — the builder is a pure factory with no side effects, so calling it and discarding the result is just wasted CPU. Remove both and explain in the in-source comment where the builder still lives, so the gating PR knows where to re-import from. — Jerrai |
||
|
|
bb5f5f8c5c |
fix(core): auto-detect three.js asset readiness via adapter contract (#1543)
Replaces the original `window.__hyperframesReady` authored API with an internal adapter contract: `RuntimeDeterministicAdapter.getReadyPromise?: () => PromiseLike | null`. The Three.js adapter implements it by hooking `THREE.DefaultLoadingManager.onStart/onLoad`; the runtime collects promises from every adapter and gates `window.__renderReady = true` on them. Zero authoring burden — composition authors write plain Three.js, framework handles async asset gating automatically. Also keeps the orthogonal `htmlDocument.ts` script-stripping refactor (substring → regex for simple flag assignments), which fixes the bug where authored scripts referencing readiness flags were stripped despite never assigning them. Stamped by Magi and Miguel; CI green; tests 33/33 pass. |
||
|
|
e662fcdea2 |
fix(studio): storyboard polish — a11y, preview, and edit-race fixes (#1544)
Batched non-blocking review nits from the storyboard stack (#1528–#1532):
- StoryboardGrid: responsive auto-fill grid instead of fixed-360 tiles
- FramePoster: reset failed state when the poster target changes (stale-error fix)
- StoryboardFrameTile: status-chip aria-label
- StoryboardSourceEditor: marked({async:false}); save() in-flight guard;
immediate first preview paint; [&_img] prose; scoped link-hardening
(rel=noopener noreferrer + target=_blank) in the sanitizer
- StoryboardLoaded: memoize sourceFiles on data.script.path/.exists, not the object ref
- StoryboardFrameFocus: applyEdit in-flight guard; aria-pressed on status buttons;
←/→/Esc keyboard navigation
- ViewModeContext: correct the popstate/replaceState doc-drift
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c8fd16f2d3 |
feat(studio): storyboard frame focus + voiceover iteration (#1532)
Fifth PR in the Studio storyboarding stack. Click a contact-sheet tile to open a full-area focus on that frame. - StoryboardFrameFocus: large poster, prev/next nav, full narrative, and an editable voiceover *guide* (textarea) saved back to STORYBOARD.md. Status can be advanced outline → built → animated inline. - "Open in Preview" jumps to the timeline focused on the frame's sub-composition (setActiveCompPath + view-mode timeline). - core/storyboard: setFrameField / setFrameVoiceover / setFrameStatus — surgical in-place writers that update one frame's metadata without re-serializing (markdown stays canonical). Tested. - Extract shared FramePoster (used by tile + focus); tiles are now buttons that open focus. Voiceover here is the editable guide; SCRIPT.md remains the locked narration that drives TTS. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
29809069c8 |
feat(studio): storyboard markdown source editor (raw + live preview) (#1531)
Fourth PR in the Studio storyboarding stack. Adds an in-context way to view and edit the storyboard's canonical files. - Board | Source sub-toggle inside the storyboard view (StoryboardLoaded). - StoryboardSourceEditor: raw CodeMirror markdown editor + live rendered preview (marked), with a file switcher for STORYBOARD.md and SCRIPT.md. - Loads raw file text and saves via the existing files API (GET/PUT /projects/:id/files/*); on save the Board re-parses (reload), so markdown stays the single source of truth. Cmd/Ctrl+S to save. - Deliberately raw, not WYSIWYG, so the structured frame fields can't be mangled. - SourceEditor gains markdown language support (@codemirror/lang-markdown); adds the marked dependency for preview. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
015529e663 |
feat(studio): storyboard frame contact-sheet grid (#1530)
Third PR in the Studio storyboarding stack. Renders the frames as a live contact sheet inside the storyboard view. - StoryboardGrid: ordered, responsive grid of frame tiles. - StoryboardFrameTile: number badge, scaled non-interactive live preview iframe (via /api/projects/:id/preview/comp/<src>), title, duration, transition, and a status chip (outline / built / animated). - Frames that are outline-only or whose src is missing render an explicit placeholder instead of an iframe. - StoryboardView swaps its placeholder for the real grid. With PR1-PR3 the storyboard view is end-to-end viewable against the storyboard-sample fixture for UI/UX feedback. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
195d7aa7bd |
feat(studio): storyboard view-mode toggle and shell (#1529)
Second PR in the Studio storyboarding stack. Adds the top-level toggle between the storyboard and the timeline/preview stage, behind the flag. - STUDIO_STORYBOARD_ENABLED flag (VITE_STUDIO_ENABLE_STORYBOARD, default off) now gates the UI. - ViewModeContext: timeline|storyboard state mirrored to the ?view= query param, so it survives reloads and an agent can deep-link ?view=storyboard. - Segmented Storyboard|Preview control in StudioHeader (flag-gated). - StudioApp swaps the whole center stage for a full-width StoryboardView when storyboard mode is active. - useStoryboard hook + StoryboardView shell: global-direction header, loading/error/empty states. The frame contact-sheet grid lands in PR3. - Extract StudioOverlays from App.tsx to stay within the 600-line studio decomposition budget. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8a13a07074 |
feat(studio): add storyboard manifest contract, parser, and read API (#1528)
First PR in the Studio storyboarding stack. Establishes the parseable contract the storyboard UI reads from; no UI yet. - core/storyboard: StoryboardManifest/Frame/Globals types + a lenient STORYBOARD.md parser (frontmatter + status/src/duration/transition_in, freeform narrative tolerated, never throws, records warnings). Exposed as @hyperframes/core/storyboard (browser-safe). - studio-api: GET /projects/:id/storyboard returns the normalized manifest with per-frame srcExists; missing file -> exists:false, not 404. - fixture: packages/studio/fixtures/storyboard-sample for dogfooding the storyboard view in later PRs (built/animated frames + one outline). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0ba52fc130 |
docs(cloud): add managed cloud rendering guide + fix flag reference (#1518)
* docs(cloud): add managed cloud rendering guide + fix flag reference Add a dedicated guide for the managed `hyperframes cloud render` path (HeyGen-hosted, zero-infra) at docs/deploy/cloud.mdx, covering auth/setup, the zip→upload→render→download flow, templates via --variables, webhooks / fire-and-forget, render management, and idempotent retries. Register it at the top of the Deploy nav group and link it from the local Rendering guide. Also fix a stale flag reference in the CLI docs: the `cloud render` `--resolution` row listed the local-render presets (landscape/portrait/...) but the cloud command only accepts `1080p`/`4k`, and `--aspect-ratio` was missing. Verified against `hyperframes cloud render --help`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cloud): correct aspect-ratio wording and flow-diagram status Two accuracy fixes from review: - `--aspect-ratio` is only auto-detected for a local project dir; for `--asset-id`/`--url` there is no local composition, so detection is skipped and the server defaults to 16:9. Reword both the guide and the CLI-reference rows to say so. - The flow diagram showed status `done`, which is not a real value (HyperframesRenderStatus is queued | rendering | completed | failed). Use `completed` and re-align the box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
36b24acf20 |
feat: add video frame format render option (#1481)
* feat: add video frame format render option * refactor: single source of truth for video-frame-format allow-list Addresses PR review (Via) on #1481: the ["auto","jpg","png"] set was declared three times — render.ts (VIDEO_FRAME_FORMATS), server.ts (inline includes), and renderConfigValidation.ts (ALLOWED_VIDEO_FRAME_FORMATS) — three boundaries to update when a new extraction format lands. Hoist the constant + a reusable `isVideoFrameFormat` type guard into @hyperframes/engine (where VideoFrameFormat is defined) and route all three call sites through them. Behavior unchanged; also drops two `as RenderConfig[...]` casts in favor of the guard (narrowing over assertion, per repo TS conventions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xuelong Mu <xuelongmu@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
646ffff927 |
feat(cli): support OpenRouter as an alternative vision provider for capture captioning (#1478)
* feat(cli): support OpenRouter as an alternative vision provider for capture captioning `hyperframes capture` could only enrich asset descriptions with Gemini vision, which requires a Google API key. Add OpenRouter as an alternative so users without Google access can caption via any vision-capable model through one unified key. Provider is selected by which key is present: OPENROUTER_API_KEY → OpenRouter (OpenAI-style /chat/completions with an image_url data URI), else GEMINI_API_KEY/GOOGLE_API_KEY → Gemini (unchanged), else DOM-only as before. OpenRouter wins if both are set. Default model is google/gemini-3.1-flash-lite (the OpenRouter analog of the Gemini path's existing 3.1-flash-lite tier), overridable via HYPERFRAMES_OPENROUTER_MODEL. Both vision call sites — the image loop and the rasterized-SVG loop — route through a single `captionOne` dispatcher, so the new provider works for SVGs too (the original PR #840 only patched the image loop, which would have left OpenRouter-only users with crashing SVG captioning). The OpenRouter path checks res.ok and surfaces the status/body on failure. Reimplements #840 (which was unmergeable: saved with a UTF-8 BOM + CRLF so GitHub rendered it as a binary diff, used `any`, reused the Gemini model env var, and had a hallucinated default model id). - Adds unit tests for the OpenRouter path (happy path, graceful degradation on non-OK status, no-key skip). - Documents OPENROUTER_API_KEY in the website-to-video guide and the CLI capture reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cli): fix typecheck in OpenRouter caption test — capture request without `as` The test cast `fetchMock.mock.calls[0]` to a tuple (TS2352: `[] | undefined` doesn't overlap `[string, RequestInit]`), which failed the Typecheck CI job. Capture the url/init inside the typed mock and assert via `new Headers()` + `typeof` narrowing instead — no `as` assertions (which the repo bans anyway). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fada399539 |
fix(core): route renders/file serving through resolveWithinProject chokepoint (#1477)
The `/projects/:id/renders/file/*` route joined attacker-controlled wildcard input straight onto rendersDir with a bare join() + readFileSync and no containment check — the only project-scoped filesystem route that skipped the resolveWithinProject chokepoint every sibling route uses. Literal/encoded `../` traversal is collapsed upstream by Hono's WHATWG URL normalization (verified empirically), so the plain LFI is not reachable over HTTP. But a symlink living inside rendersDir and pointing outside it was still followed and served verbatim (verified: leaked an external secret, 200 OK). Routing through resolveWithinProject canonicalizes with realpath before serving, closing the symlink escape and making the route's safety independent of the URL layer's normalization behavior. Adds regression coverage: serves an in-dir file, rejects an escaping symlink (403), and still serves a symlink that stays inside rendersDir. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d580f2a1d8 |
fix(render): make WebGL video textures deterministic in headless render (#1403)
* fix(render): make WebGL video textures deterministic in headless render WebGL compositions that sample a `<video>` as a texture (e.g. a faceted crystal with clips mapped onto its facets) rendered with flickering, non-deterministic facets: a video would intermittently show a stale frame or go black, and the same frame differed between two renders. Two gaps caused this: 1. No WebGL analog of the WebGPU `patchVideoTextureCompat`. Chrome's headless compositor can't feed decoded `<video>` frames to the GPU, so the engine injects a decoded `<img class="__render_frame__">` sibling per video each frame. The WebGPU `copyExternalImageToTexture` path substitutes it, but `texImage2D` / `texSubImage2D` did not — so WebGL uploaded a stale/black frame. Add `patchWebGLVideoTextureCompat()` mirroring the WebGPU patch (shared `resolveRenderFrameImage` helper). 2. Capture ordering. Per frame the runtime seeks (GPU adapters render on `hf-seek`) BEFORE the engine injects the decoded frames, so the GPU render read a frame that didn't exist yet. After injecting, the engine now calls `window.__hfReseekGpu(t)` — a force-dispatch (`forceDispatchSeekEvent`) that bypasses the same-time `hf-seek` dedup — so GPU compositions re-upload their textures from the freshly-injected, decoded frames, deterministically. Tests: unit tests for the texImage2D/texSubImage2D substitution and the force-dispatch, plus a videoFrameInjector regression test asserting the post-injection GPU reseek fires only when frames were injected. Verified end-to-end: a WebGL prism with 8 live <video> facets renders byte-identical across independent runs with no facet flicker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(render): add producer render-compat regression for WebGL video textures A WebGL2 canvas samples a <video> as a texture every hf-seek (the natural author pattern, distilled from the HeyGen prism). The render-compat harness renders it and compares against the golden: with the video-texture fix the render reproduces the decoded frames; revert the fix and the canvas renders black, collapsing the comparison. Golden verified to contain real, time-varying video content (not black), so a regression is caught rather than passing vacuously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1c47ba9981 |
refactor(core): route project paths through a single resolveWithinProject chokepoint (#1398)
Structural follow-up to the symlink-escape fix. The recurring miss (#465 fixed isSafePath but left render.ts; the sweep then turned up play.ts, htmlBundler, ...) is because containment was enforced by convention — "remember to call isSafePath after every resolve()" — which a new call site can silently skip. Add resolveWithinProject(base, relativePath) -> string | null (resolve + containment in one call) and route the studio-api + bundler sites through it, so a caller cannot resolve a project-relative path without the guard: - studio-api routes/files.ts (read, rename, duplicate, upload-dir), preview.ts (sub-comp + static asset), render.ts (composition) — all the resolve()+isSafePath() pairs collapse to a single call. - compiler/htmlBundler.ts: its local safePath helper was exactly this; drop it for the shared one. Left intentionally on isSafePath: files.ts upload (resolves a name against a validated sub-dir but contains against the project root) and htmlBundler's CSS @import (resolves against the CSS file's dir, contains against the root) — these resolve and contain against *different* bases, which the single-base chokepoint doesn't model. Exported from @hyperframes/core and re-exported from studio-api/helpers for back-compat. Adds resolveWithinProject unit tests; all existing studio-api route tests pass unchanged (behavior is identical — same resolve, same containment, same reject paths). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
953bab319b |
fix(core): block symlink-based path escape in studio-api isSafePath (#1397)
* fix(core): block symlink-based path escape in studio-api isSafePath path.resolve() collapses ./.. but does not dereference symlinks, so a symlink living inside the project dir but pointing outside it (e.g. project/link -> /etc) passed the prefix check, letting a downstream read/write/stat follow it to a file outside the project root. The `..` traversal case was already blocked; symlink traversal was the gap. Canonicalize both base and target with realpathSync before comparing. The target may not exist yet (new-file writes), so canonicalize the deepest existing ancestor and re-attach the trailing not-yet-existing segments, which cannot be symlinks at check time. Fail closed if base is unresolvable. Adds safePath.test.ts covering: in-base allow, not-yet-existing write target, `..` escape, existing-file-through-symlink escape, write-target under a symlinked parent, file-symlink escape, in-base symlink allow, symlinked-base canonicalization, and base-missing fail-closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core,cli): route render + play composition paths through isSafePath Review on #1397 found a third call site with the same vulnerable startsWith pattern. Apply Rule 2: fix every site sharing the contract (gate an attacker-influenced path before a symlink-following fs op). - studio-api routes/render.ts: body.composition (from c.req.json()) was checked with `resolved.startsWith(resolve(project.dir) + sep)`, which doesn't dereference symlinks — an in-project symlink to an external target escaped the project root. Now uses isSafePath(). - cli commands/play.ts: the `/composition/*` server route used `filePath.startsWith(project.dir)` with no trailing-separator guard, so both a sibling dir sharing the prefix (`<dir>-evil`) and symlink escapes passed. Now uses isSafePath() via @hyperframes/core/studio-api (the same lazy-import pattern commands/validate.ts already uses). Tests: render.test.ts gains a "composition path safety" block (in-base allow, `..` reject, in-project-symlink-to-outside reject, in-project symlink staying inside allow). The shared render test adapter now points at a real dir since isSafePath fails closed on an unresolvable base (production project dirs always exist on disk). Not in this change: compiler/htmlBundler.ts has the same class at two sites (safePath helper + inline CSS @import check), but the compiler sits below studio-api in the dependency graph and can't import isSafePath without a backwards edge; that fix needs the helper promoted to a neutral module and is tracked as a follow-up. renderArgs.ts / videoFrameExtractor.ts carry the trailing-sep guard and a local-CLI/engine-internal threat model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): promote isSafePath to a shared module + harden htmlBundler Per review on #1397: extend the symlink-escape fix to the compiler, and remove the duplicated path-safety logic. - Move isSafePath to packages/core/src/safePath.ts (a neutral package-root module). studio-api/helpers/safePath.ts re-exports it for back-compat (keeping walkDir), and it's now exported from the core entrypoint so non-studio-api layers can use it. compiler/ sits below studio-api in the dep graph, so it could not import the helper from its old home without a backwards edge — the promotion removes that constraint. - compiler/htmlBundler.ts: route both containment checks (the safePath helper and the inline CSS @import check) through isSafePath. The bundler reads+inlines these files, so an in-project symlink pointing outside the root would otherwise bake external content into the output. All callers already skip on a null/false result, so nothing is read on rejection. Tests: safePath.test.ts moves with the impl; htmlBundler.test.ts gains a case proving an in-project sub-composition script is inlined while a script reached through an escaping symlink is not (positive control + leak assertion). Deferred (tracked for a dedicated follow-up, see PR thread): the relative()-based isPathInside family (core/compiler/assetPaths, producer/services/fileServer, producer/utils/paths and their callers in the render pipeline) is symlink-blind in the same way, and engine videoFrameExtractor's asset resolver needs a caller-side gate (its http downloads land outside the project root, so a single-root check is wrong). Both are regression-sensitive render-pipeline surfaces that warrant their own focused, well-tested pass. renderArgs.ts is intentionally left: it is filesystem-free by design (injected stat) and its threat model is the user's own --composition CLI arg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(core): hedge symlink tests for Windows + copy before reverse (review nits) Addresses Via's non-blocking review notes on #1397: - Wrap every symlinkSync in the new tests with a tryCreateSymlink helper that returns false (and the test early-returns) when creation throws, mirroring the preview.test.ts convention. Non-symlink-privileged Windows runners no longer risk crashing the suite on EPERM. - safePath.ts: `[...trailing].reverse()` instead of mutating `trailing` in place — harmless today (single return) but future-proof against a looping edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab7f69c1f5 |
test(core): align file-tree test with backup-only hiding (#1366) (#1400)
main went red again at
|
||
|
|
e2cc134c77 |
test(core): fix contradictory composition-discovery file-tree test (#1385) (#1399)
#1385 ("exclude dot-directories from composition discovery",
|
||
|
|
8eac7e1cda |
fix(cli): resolve and install transitive registry dependencies (#1396)
* fix(cli): resolve and install transitive registry dependencies `hyperframes add`, `hyperframes new` (fetchRemoteTemplate), and the studio "add block" path each resolved a single registry item and silently dropped any `registryDependencies` it declared. Add `resolveItemWithDependencies` (DFS topological sort, cycle detection, missing-dependency errors, and dedup of shared/diamond deps) and route all three install paths through it so dependencies are installed before the item that needs them. `resolveItem` becomes a thin guard that throws on dep-bearing items, so no future caller can silently reintroduce the drop. `runAdd` now returns the ordered `installed` list and compatibility-gates every dependency before any write. Reworks the stale PR #414 onto current main and addresses its review feedback: fetchRemoteTemplate installs deps, no out-of-scope files, dead null-checks dropped, diamond test added, and the deliberate serial-fetch tradeoff is noted. Co-authored-by: Rakibul Islam <40rakib70@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): make getItem async so missing-dep surfaces as rejection Addresses review nit on #1396: getItem was typed Promise<RegistryItem> but threw synchronously on a missing dependency. Marking it async keeps the control flow consistent with the return type — the throw now becomes a rejection. The body has no await, so the item cache is still populated synchronously on first request and dedup is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): compatibility-gate transitive deps in all install paths Addresses Via's review on #1396: `assertCompatibleOrThrow` only ran inside `runAdd`, so `fetchRemoteTemplate` (hyperframes new) and the Studio "add block" action installed resolved items — now including transitive dependencies — with no minCliVersion enforcement or deprecation warnings. A pre-existing single-item asymmetry that this PR's dep loops amplify across N items. - Add shared `gateRegistryItemsCompatibility` + `RegistryCompatibilityError` to compatibility.ts; all three install paths now gate the full resolved set before any write. `runAdd` keeps its AddError mapping by wrapping the shared gate. - Surface deprecation warnings from the template/studio paths to stderr. - Extract the studio viewport rewrite into `rewriteWrittenToHostViewport` (also drops redundant dynamic node:fs imports) and document that it intentionally rewrites dep-shipped .html too (Via item 3). - Unit-test the shared gate directly (no fetch/cache flakiness): compatible set, accumulated deprecation warnings, and throw-on-incompatible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Rakibul Islam <40rakib70@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
30fcede44e |
refactor(cli): restore exact-match Cursor rule (revert unsourced loosening) (#1334)
Follow-up to #1328. That PR loosened the Cursor TERM_PROGRAM check from exact `=== "cursor"` to `?.toLowerCase() === "cursor"` "for parity with Windsurf" — but the parity is false. Windsurf is matched case-insensitively because its sources genuinely disagree on casing ("windsurf" vs "Windsurf"); Cursor consistently emits lowercase "cursor", so nothing justified loosening an existing, working, exact-match rule. Per review feedback on #1328 (Magi/Hermes), revert Cursor to exact match and drop the TERM_PROGRAM=Cursor test. Windsurf stays case-insensitive (sourced); its comment now documents the asymmetry as intentional. No functional change — Cursor always emitted lowercase, so detection is unchanged; this just removes an unsourced false-positive surface. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e0ecd4d2d1 |
feat(cli): detect Windsurf, Cline, Gemini CLI, and Crush agents (#1328)
Rebased onto main after #1294 merged. Adds four coding-agent vendors to detectAgentRuntime() (existence-only checks, source/runtime-verified): - windsurf — TERM_PROGRAM=windsurf (case-insensitive) - cline — CLINE_ACTIVE (default vscode-terminal path) - gemini_cli — GEMINI_CLI (runtime-confirmed; distinct from the managed-agent /.agents/ detector, which runs ahead of VENDOR_RULES and wins when both match) - crush — CRUSH (runtime-confirmed) Also makes the cursor rule case-insensitive for parity with windsurf, and adds a code-resident "deliberately NOT added" section (OpenHands/Aider/Goose/ opencode/Roo/Amp/Devin/Jules/Factory) carrying the empirical rejection rationale. Test isolation: the Gemini managed-agent suite now clears its node:os/node:fs doMock registrations in afterEach so they don't leak into the env-var-only suites that follow it in the same file. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9b18fadccd |
feat(producer): optional targetChunkFrames to bound per-chunk frames (#1332)
* feat(producer): optional targetChunkFrames to bound per-chunk frames * feat(cli): expose --target-chunk-frames on lambda + cloudrun render; document it |
||
|
|
69eac249d5 | feat(producer): stage wall-clock split in chunk perf telemetry (#1327) | ||
|
|
0766eb8144 |
feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime (#1294)
* feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime
Add `gemini_managed_agent` to the AgentRuntime union and a dedicated
isGeminiManagedAgent() detector. Empirical signal pair (from live-sandbox
introspection by gemini-agent, env_id b9db4e56, 2026-06-09):
existsSync('/.agents/AGENTS.md') AND isGVisor()
The conjunction is what makes the rule safe:
- `/.agents/AGENTS.md` excludes generic gVisor surfaces (GKE Sandbox,
Cloud Run gen2) that don't mount the managed-agent layout.
- The gVisor kernel check excludes a dev box that happens to have a
stray `/.agents/` directory.
Implementation notes:
- Filesystem-based check runs ahead of the env-var-only VENDOR_RULES
loop. VENDOR_RULES is documented as "Only checks for the EXISTENCE
of well-known env vars — never reads their values"; the Gemini
signal is filesystem + kernel, not env, so it gets a dedicated
branch rather than shoehorning into the rule list.
- GEMINI_API_KEY is deliberately NOT keyed on — it's user-settable on
any host. The filesystem + kernel pair is the actually-distinctive
signal.
- Reuses the existing isGVisor() helper for the kernel half of the
conjunction; no duplication.
Tests (4 new, vitest):
- Positive: /.agents/AGENTS.md + 4.19.0-gvisor → gemini_managed_agent
- Negative: gVisor alone (no /.agents/) → null (generic gVisor surface)
- Negative: /.agents/AGENTS.md alone (no gVisor) → null (dev box false-positive guard)
- Precedence: Gemini signal wins over a coincident CLAUDECODE env var
Empirical caveat: signal was gathered from a single sandbox. Re-confirming
across additional sandbox spins is a follow-up; the rule is conservative
enough (conjunction of two independent signals) that a single-spin
false-positive is unlikely, but a single-spin variance bug (e.g. some
sandbox flavors omitting one of the two markers) would surface as
under-detection rather than over-detection.
Source for signals: introspection write-up at
/tmp/gemini-sandbox-detection-signals.md (gemini-agent, 2026-06-09).
* docs(cli): reframe Gemini-managed-agent detection rationale (load-bearing vs guard)
gemini-agent's uniqueness analysis (FS-root + cgroup + netns + DMI + PID-1
introspection of env d59d6361, 2026-06-09) revealed the two signals are
NOT co-equal:
- /.agents/AGENTS.md is the uniqueness anchor — definitionally a
managed-agent artifact, injected per-run by the platform, mtime
tracks the interaction. Nothing in the generic Google-Cloud-on-gVisor
universe (Cloud Run gen2, GKE Sandbox, Fly.io) mounts /.agents/.
- isGVisor() is a guard, not a second uniqueness signal. gVisor itself
is shared with GKE Sandbox + Cloud Run gen2 — its real job here is
ruling out a stray user-created /.agents/AGENTS.md on a non-sandbox
host.
The original 3-spin work proved *stability* (signals consistent across
sandbox spins). This pass adds *uniqueness* — confirming the signals
discriminate Antigravity from the broader gVisor universe, not just
that they're reliably present. Stability ≠ uniqueness; both are
required for a correct detection rule.
Code unchanged (the AND-gate is sound). Docstring reframed so a future
reader doesn't mistake the conjunction for two independent uniqueness
signals. Also enumerated the markers NOT keyed on (with reasons), so
future contributors don't reach for them by naming inference.
Source: gemini-agent uniqueness analysis write-up.
* fix(cli): key Gemini managed-agent detection on /.agents/ mount, not optional AGENTS.md
The detector keyed on existsSync('/.agents/AGENTS.md'), but Google's Managed
Agents docs are explicit that AGENTS.md is OPTIONAL: an agent may declare its
instructions inline via system_instruction in agent.yaml and ship no AGENTS.md
file ("system_instruction and AGENTS.md are additive; both apply when present").
The platform auto-discovers the agent under the /.agents/ directory; skills
mount at /.agents/skills/ and AGENTS.md at /.agents/AGENTS.md only when shipped.
Keying on the file generalized only to templates that happen to bundle an
AGENTS.md (like HeyGen's own gemini-agent and Thor's reference). A managed agent
defined with inline instructions or a skills-only definition was a silent
false-negative. All three prior verification spins used our own AGENTS.md-bearing
template, so the gap was never exercised.
Broaden to the /.agents/ directory mount (still gVisor-guarded — false-positive
surface is unchanged) so skills-only and inline-instruction agents are detected.
Adds a regression test for the skills-but-no-AGENTS.md case. Documents the one
residual gap (pure inline-only, no skills/no AGENTS.md) that needs an empirical
spin to confirm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(cli): tighten /.agents/ to a directory check + sync agent_runtime docs
Self-review follow-ups (no behavior change for real managed agents):
- isGeminiManagedAgent now requires statSync("/.agents").isDirectory() rather
than existsSync("/.agents"), matching the documented "directory mount"
contract. existsSync matched any entry (a stray file/symlink named /.agents),
widening the gVisor-gated false-positive surface beyond what the comment
claimed. Tests now mock statSync accordingly (and drop a dead /.agents/skills
mock clause the code never read).
- system.ts: the agent_runtime doc comment hard-coded the vendor list and said
"detected by env-var existence only" — both stale once a filesystem/kernel
detector (gemini_managed_agent) exists. Point at the AgentRuntime union and
note the filesystem-marker case instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e845793ce1 |
chore: shrink repo — untrack failure frames, recompress backgrounds, harden LFS (#1326)
No-coordination repo-size cleanup (no history rewrite — SHAs unchanged):
- Untrack 158 producer regression-test failure artifacts (~27 MB); already
gitignored, on-disk copies kept.
- Recompress 13 byte-identical code-snippet block backgrounds (5120x2880/3.3MB
-> 2560x1440 q78/~428KB): 42 MB -> 5.4 MB. Per-block files kept for portability.
- Recursive LFS patterns (packages/producer/tests/**/*.{mp4,mov,webm,png}) +
globalized *.onnx — closes the nested-path leak.
- Recursive .gitignore for tests/**/failures/ at any depth.
- scripts/check-large-files.sh + lefthook `largefiles` gate (>500KB non-LFS
fails commit; excludes registry/). Review fixes: ceiling division, skip
symlinks, space-safe staged-file read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8fcbb63a37 |
docs(readme): swap hero media to hyperframes-logo-motion (#1315)
* docs(readme): swap hero media to hyperframes-logo-motion
Replaces the prior hfgif-1280.webp hero with a new logo-motion clip
Bin trimmed for the launch. Converted the source MP4 to animated webp
(the existing hero's format) so it auto-plays in the GitHub README the
same way the old one did - MP4 sources don't render inline or autoplay
in <img> tags.
- New asset: static.heygen.ai/hyperframes-oss/docs/images/
hyperframes-logo-motion-1280.webp (1280x720, 85 frames, 199KB)
- ffmpeg conversion: scale=1280, libwebp_anim, q=80, loop=0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(studio): format 5 hooks files (oxfmt)
* style: remove unused imports in studio hooks (pre-existing lint failures)
CI Lint on main was already failing with 5 unused-import errors in
packages/studio/src/hooks/. Removed the unused symbols to unblock the
README hero PR's CI:
- gsapRuntimeBridge.ts: resolveTweenStart, resolveTweenDuration
- useGsapScriptCommits.ts: usePlayerStore
- useTimelineEditing.ts: PatchTarget (type-only)
- gsapDragCommit.ts: readGsapProperty
Bundled into the README PR per James's request to fix CI in-place.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(studio): add childRects: [] to DomEditOverlay test mock
useDomEditOverlayRects' return type added a childRects: OverlayRect[]
field; the DomEditOverlay test's mock didn't get updated and was
returning an object without it, so DomEditOverlay.tsx's
'childRects.length > 0' check threw TypeError on undefined.
One-line mock-vs-hook contract realignment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(studio): drive player-store currentTime in selection-hydration test (#1311 follow-up)
The 'hydrates seek first, preserves the initial url state, then restores
selection' test was failing because PR #1311 (keyframes feat) changed
useStudioUrlState to read currentTime from the player store via
usePlayerStore((s) => s.currentTime), removing it from the hook's prop
shape. The test was still trying to drive currentTime via the harness
prop, which is now a no-op — so the selection-hydration useEffect's
time-stability guard
Math.abs(currentTime - stableTimeRef.current!) > 0.05
never passed (store currentTime stayed at 0 while stableTimeRef caught
the 4.2 seek target). buildDomSelectionFromTarget was never reached,
applyDomSelection was never called, and the assertion got 0 calls.
Fix: setState the store's currentTime to 4.2 ahead of the rerender so
the hook's selector picks it up and the time-stability guard passes.
Harness prop kept as-is — it's a no-op but doesn't hurt.
Pre-existing failure on main HEAD 81416ab3; surfaced as CI gate on the
unrelated docs/readme-hero-motion-update PR.
* test(studio): stub getBoundingClientRect + flush RAF in DomEditOverlay test
The 'renders selected bounds right after clicking a movable selection'
test asserts the selection box appears after pointerdown, but happy-dom
returns 0 for newly-created elements' getBoundingClientRect. The
overlay's compRect updates via a RAF loop that early-returns when iframe
width is 0; the keyframes PR
|
||
|
|
b4210f6567 |
docs(mcp): add Grok as a supported host (#1280)
HyperFrames MCP is rolling out to Grok this week. Add Grok alongside Claude.ai and ChatGPT: new setup tab (catalog search + custom-URL fallback), and include it in the title, intro, progress-notification host list, issue-report host list, and widget-supported host list (Grok renders MCP widgets). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b2be289d90 |
docs(mcp): remove voiceover/TTS references and surface MCP guide in sidebar (#1278)
TTS/voiceover is disabled in the hosted MCP, so the public MCP guide no longer reflects current functionality. Remove all voice/TTS mentions: - "voice generation" from the compose agent's built-in skills list - "voice selection" from the compose tool description - "voice / TTS" from the "what the hosted MCP wraps" section - "Selecting voice and style..." progress-notification examples - brand-voice asset reference (agent can't synthesize speech anymore) Also add guides/mcp to the Guides sidebar group — the page existed but was only reachable by direct URL, not from the nav. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b8749c642 | chore: release v0.6.81 | ||
|
|
4da567df22 |
feat(gcp-cloud-run): Google Cloud Run + Workflows distributed render adapter (#1253)
* feat(gcp-cloud-run): add Google Cloud Run + Workflows distributed render adapter Adds @hyperframes/gcp-cloud-run, the GCP counterpart to @hyperframes/aws-lambda (issue #932). The OSS distributed primitives (plan, renderChunk x N, assemble) are unchanged; this package is the storage/compute/orchestration glue. Package: Cloud Run handler (one image, three actions), runs under bun; GCS transport; in-image chrome-headless-shell resolver; client SDK (renderToCloudRun, getRenderProgress, deploySite, computeRenderCost); Dockerfile; Cloud Workflows definition; Terraform module; CLI cloudrun deploy|sites|render|render-batch|progress|destroy with --output-resolution and --strict-variables; 62 unit tests + docs + live smoke script. Shared extraction (removes ~640 lines of adapter duplication): move the cloud-agnostic config validator + content-hash into producer/distributed; both adapters import them. Validated end-to-end on GCP at 37.4 dB PSNR vs baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): resolve @hyperframes/gcp-cloud-run in the CLI build + root build The CLI bundle (esbuild) couldn't resolve `@hyperframes/gcp-cloud-run/sdk`, failing Build/Typecheck/CLI-smoke (and the perf/windows/regression jobs that build first). Mirror the aws-lambda handling: mark the gcp adapter + its /sdk subpath external in tsup.config.ts with a source alias, and add gcp-cloud-run to the root `build` filter so its dist exists for publish + runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): copy gcp-cloud-run manifest in Dockerfile.test for frozen install The regression test image runs `bun install --frozen-lockfile` after copying each workspace package.json individually. The CLI now depends on @hyperframes/gcp-cloud-run (workspace:*), so the frozen install fails to resolve it unless its manifest is present. Add the COPY line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): add machine-sizing flags to `cloudrun deploy` Closes the parity gap with `lambda deploy` (which exposes --memory etc.). `cloudrun deploy` now threads --cpu, --memory, --max-instances, and --timeout into the Terraform apply; omitted flags keep the module defaults (4 vCPU / 16Gi / 100 instances / 3600s). For finer control, apply the module directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gcp-cloud-run): address PR review (security, waste, limits, alerts) - server.ts: bucket-allowlist guard no longer fails open silently. Unset env logs a one-time WARNING; "*" is an explicit opt-out; otherwise it enforces. - server.ts: stop double-shipping audio.aac. It already rides in the plan tarball every consumer downloads, so drop the redundant standalone upload (plan) + re-download/overwrite (assemble); assemble reads it from the untar, falling back to a supplied AudioGcsUri for compat. - server.ts: chunk extension via path.extname() instead of slice(lastIndexOf). - workflow.yaml: clamp parallel concurrency_limit to math.min(chunkCount, 20) — Cloud Workflows hard-caps concurrent iterations at 20. - Dockerfile: pin bun (bun-v1.3.9) so an interop change can't silently break the image rebuild. - terraform: add min_instances var (default 0); add a workflow-failure alert (finished_execution_count status=FAILED) alongside the request-count one. - costAccounting: document that displayCost excludes GCS storage/egress. Verified against the actual APIs: @google-cloud/workflows@4.4.0 ICreateExecutionRequest has no executionId (so the idempotency-token suggestion isn't available in this client); Workflows concurrency cap is 20; failure metric is workflows.googleapis.com/finished_execution_count (status label). 174 adapter tests pass, fallow/oxlint/oxfmt/terraform clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gcp-cloud-run): address round-2 review — error code + CFR forwarding - workflow.yaml: rename the zero-chunk failure code PLAN_TOO_LARGE → PLAN_PRODUCED_ZERO_CHUNKS. The old code implied a size-ceiling breach (the opposite cause), misleading anyone triaging the alert. - workflow.yaml: forward Config.cfr to the assemble step (`Cfr: ${("cfr" in config) and config.cfr}`). It was read by the handler but never sent, so exact-CFR was silently off for every Cloud Run render. Uses the same `in`-operator guard already proven in the retryable predicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(release): include gcp-cloud-run in set-version PACKAGES list set-version.ts (driven by release:prepare) bumps an explicit package list to the shared version on each release. gcp-cloud-run was wired into the build + publish.yml but missing here, so a release would leave it at a stale version and publish.yml would push the wrong version. Add it so the new package version-bumps + publishes in lockstep with the others. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bacfb17538 |
feat(producer): auto low-memory safe render profile (#1225)
## What Adds an auto-detected **low-memory safe render profile**. On hosts at or below 8 GB total RAM, the render pipeline collapses to its cheapest shape instead of running multiple concurrent Chrome instances. When `lowMemoryMode` is active and the user hasn't passed `--workers`, the orchestrator: - **skips auto-worker calibration** — no throwaway second Chrome just to time 5 frames; - **pins to a single worker** — so the probe Chrome is reused for capture, never N concurrent; - **prefers screenshot capture over BeginFrame** — avoids the BeginFrame protocol-timeout → relaunch churn on slow hardware; - logs a one-line explanation of what it did and how to override. Builds on #1221 (merged), which fixed the calibration timeout cap, the `<= 8192` boundary, and added the CLI timeout flags. ## Why Reported in #1218 / #1219: renders on 8 GB laptops sit at low progress for minutes or stall. Root cause (per the triage thread) is architectural — the default pipeline launches up to 4 Chrome instances sequentially/overlapping (probe, calibration, capture, screenshot-fallback), each ~256 MB+, on machines with ~3 GB free. The concurrent browsers drive memory pressure that makes every CDP call slow and spikes V8 GC pauses. #1221 made the timeouts and memory flags *apply correctly*; this PR removes the expensive shape entirely on the machines that can't afford it, rather than tuning it. "Smarter by default." ## How - **`packages/engine/src/services/systemMemory.ts`** (new): one shared `isLowMemorySystem()` / `getSystemTotalMb()`, de-duplicating the `totalmem()` reads previously copied in `config.ts` and `browserManager.ts`. Threshold is inclusive (`<= 8192 MB`) — real "8 GB" hardware reports ~7600–8192 MB after firmware/iGPU reservations, so a strict `<` would skip the optimisation on the very hardware that needs it. - **`config.ts`**: new `lowMemoryMode` field on `EngineConfig`, resolved tri-state — explicit override → `PRODUCER_LOW_MEMORY_MODE` (on/off) → auto-detect from total RAM. - **`renderOrchestrator.ts`**: gate calibration off, pin workers to 1, force screenshot capture, and emit a safe-mode log line when `lowMemoryMode` is set and `--workers` is absent. - **`render.ts`**: `--low-memory-mode` / `--no-low-memory-mode` override (sets the env var the producer's `resolveConfig` reads) + docs table entry. Fully overridable: an explicit `--workers N` restores calibration-free parallelism; `--no-low-memory-mode` / `PRODUCER_LOW_MEMORY_MODE=false` restores the full default shape. ### Deliberately deferred (separate PRs) - **Reuse the probe session for calibration**: only executes on the tier *above* 8 GB (safe-mode skips calibration on the target boxes). A correct BeginFrame-mode reuse would lose calibration's fast-fail-to-screenshot timeout — real risk on a path the reported scenario never hits. Better scoped on its own. - **Retuning `calculateOptimalWorkers`'s `totalmem*0.5/256` memory model**: hot path for *all* renders incl. servers/Lambda, outside this PR's local-laptop scope. ## Test plan - [x] Unit tests added/updated — `systemMemory.test.ts` (8192 boundary cases), `config.test.ts` (tri-state env resolution + explicit-override precedence). Engine suite passes (25 relevant tests). - [x] `tsc` clean across engine/producer/cli; `oxlint` + `oxfmt` clean; removed an unused export so the `fallow --fail-on-issues` dead-code gate stays green. - [x] Documentation updated — `docs/packages/cli.mdx` render-flags table. - [ ] Manual testing on a real ≤ 8 GB host — not yet run; behaviour is unit-covered and the safe path (1 worker + screenshot) is already a supported render shape. Note: one pre-existing producer test (`rejects a maliciously crafted key…`) fails identically on `main` — environment-specific path test, unrelated to this change. |
||
|
|
6affe2d212 |
fix(cli): reject directory --composition and add --browser-timeout (#1199) (#1200)
* fix(cli): reject directory --composition and add --browser-timeout (#1199) Two unrelated symptoms from issue #1199, fixed together: 1. `--composition .` (or any directory path) used to slip past the existsSync check in render.ts and explode downstream as `EISDIR: illegal operation on a directory, read` when the producer readFileSync'd the entry. The CLI now treats `.` / `""` as "omit the flag" (falls back to index.html) and rejects other directory paths with an actionable error pointing at the .html shape. 2. The 60s Puppeteer page.goto timeout in frameCapture.ts was hard- coded, so heavy compositions (many videos / fonts / asset requests) could not complete `domcontentloaded` in time. Add a configurable `pageNavigationTimeout` to EngineConfig (default 60_000, env fallback PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS) and expose it as `--browser-timeout <seconds>` on `hyperframes render`. The flag threads through both renderLocal (via resolveConfig) and the docker bridge (via buildDockerRunArgs). Tests: - render.test.ts: forwards/omits pageNavigationTimeout into resolveConfig - dockerRunArgs.test.ts: forwards/omits --browser-timeout (seconds) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): address PR #1200 review — extract validators, tighten bounds Addresses Vai's blockers and Miguel's nits on PR #1200: - Vai blocker 1 (fallow CRAP) + blocker 3 (no argv tests): Extract --browser-timeout and --composition validators into pure helpers in utils/renderArgs.ts with a structured-result discriminant. Drops ~45 lines of inline validation from run(), reducing its CRAP score 1290→978 and cyclomatic 75→65. 19 new unit tests cover the parse branches (sub-ms, overflow, NaN, Infinity, empty, negative, ".", "./", whitespace, directory, missing, ../escape, sibling-prefix). - Vai blocker 2 (sub-ms → timeout:0 = "no timeout"): reject inputs that round to <1 ms. Puppeteer treats page.goto({timeout:0}) as wait-forever, so --browser-timeout 0.0004 silently flipped the semantics. Now rejected with an explicit "rounds to 0 ms" error. - Vai important 5 (1e10 accepted → setTimeout overflow): cap at 86_400s (24h). Above Node's TIMEOUT_MAX ≈ 2^31-1 ms setTimeout fires immediately, the opposite of "long timeout." - Vai important 4 (related timeouts unmentioned): CLI help and docs now flag PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS and the 45s playerReadyTimeout as the other knobs heavy compositions may need. - Vai nit 7 (s/ms unit mismatch): help text and docs row both call out the SECONDS-vs-MILLISECONDS difference between flag and env. - Vai nit 8 / Miguel nit (composition flag discoverability): the --composition description now says "Pass `.` (or omit the flag) to render the project's index.html." - Miguel nit (dead branch): the entryFile === "" unreachable branch is gone. New helper uses `if (!trimmed || trimmed === ".")`. Also adds a trailing-separator guard on the project-containment check (sibling-prefix bypass: /proj-evil/x.html no longer slips past startsWith('/proj')) — flagged by the code review. The three remaining fallow complexity findings on render.ts (run, renderDocker, trackRenderMetrics) are inherited from main; this PR reduces run() but does not refactor it. Suppressed with fallow-ignore-next-line markers and inline rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): diverge --browser-timeout error messages per Vai nit 5 The `not-a-number` and `not-positive` branches in browserTimeoutErrorMessage shared the generic "Must be a positive number of seconds" message even though the discriminant carried distinct kinds. Diverge them so users see the specific failure mode: --browser-timeout abc → "Got \"abc\", which is not a number." --browser-timeout -5 → "Got \"-5\" seconds, which is not positive." The shared hint ("pass a positive number of seconds, e.g. 180") is preserved on both branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8228932e17 |
fix(scripts): make release change-guard robust to git status prefix (#1198)
The set-version guard parsed `git status --porcelain` and extracted the path with a fixed `line.slice(3)`. The porcelain "XY <path>" prefix width can shift, and when it did the slice dropped a leading character — misreading `.claude-plugin/plugin.json` as `claude-plugin/plugin.json`, which failed the allowed-paths match and falsely blocked a legitimate release with "Unexpected uncommitted changes". There was no escape hatch. Collect changed paths from `git diff --name-only -z HEAD` (tracked) plus `git ls-files --others --exclude-standard -z` (untracked) instead. Both emit bare NUL-separated repo-relative paths with no status column to misparse, so the allowed-paths comparison is exact. Extract the pure helpers (splitNulList, findUnexpectedChanges) and cover them with tests. Also document the release flow in CLAUDE.md (the repo had no release docs). |
||
|
|
0b98565039 | chore: release v0.6.72 | ||
|
|
72c461d86a |
fix(producer): localize remote <img> sources + await image readiness (#1197)
* fix(producer): localize remote <img> sources + await image readiness Producer's frame-capture has `pollVideosReady` (waits readyState >= 2 for every <video>) but no equivalent for <img>. Combined with htmlCompiler's `collectExternalAssets` explicitly skipping http(s) URLs (line 805-806), agent-pipeline-generated compositions (astral / daphne / hyperion multi-v2 outputs with raw S3 <img src>) reach Chrome with a network dependency that races the readiness gate AND can be evicted mid-render. Either path produces blank-frame flicker. Reproduction (02_kobe agent output, 42s render @ 30fps): scene_02's remote S3 background-image painted from t=7.0s, vanished at t=10.5s (frame size 139KB vs 700-940KB neighbors), back at t=11.0s. GSAP timeline said opacity:1 throughout — Chrome simply didn't have the pixels. Two-layer fix: 1. **Producer** — `localizeRemoteImageSources` in `htmlCompiler.ts` mirrors the existing `localizeRemoteMediaSources` (video/audio) + `localizeRemoteFontFaces` pattern, reusing `downloadAndRewriteUrls` and the `_remote_media/` subdir. Wired into `compileForRender` between the media and font localize steps. Once the file is local, Chrome's image cache is bounded by disk reads, not S3 latency. 2. **Engine** — `pollImagesReady` + `decodeAllImages` helpers in `frameCapture.ts` parallel to `pollVideosReady`. Waits for every `<img>` (skipping data: URIs) to have `complete && naturalWidth > 0`, then forces GPU upload via `img.decode()`. Called from both the classic-xvfb path and the BeginFrame path after their respective video readiness checks. Defense-in-depth — Layer 1 closes the symptom for current+future agent-pipeline outputs; Layer 2 protects any future code path that leaves a remote URL in place. Tests: 7 new cases in `htmlCompiler.test.ts` covering happy-path rewrite, 404 fallback, dedup of duplicate URLs, non-HTTP and data: URI passthrough, both quote styles, and the agent-pipeline shape where `src` is not the first attribute. All pass alongside the existing 56 htmlCompiler tests. * fix(producer): scope remote-img regex to real src; correct stale comments Review follow-ups on the remote-<img> localization fix: - Tighten REMOTE_IMG_TAG_RE with a (?<![\w-]) lookbehind so it matches a real `src` attribute only. The previous `\bsrc` also matched `data-src` (and `data-*-src`) lazy-loader placeholders, which would download/rewrite a URL the render never paints. Added a regression test; `srcset` stays excluded by the `\s*=` requirement. - Fix comments that claimed frameCapture has "no pollImagesReady analog" — this PR adds exactly that, so the docstrings were self-contradictory. Reframed localization as the primary fix and pollImagesReady as the defense-in-depth layer, and documented the <img src>-only scope (srcset / <picture> / SVG <image> / CSS background-image are follow-ups). Verified locally end-to-end on the 02_kobe repro: all 4 remote S3 <img> URLs localize to _remote_media/, the render completes, and the frame at t~10.5s that was a 139KB blank in the broken render now paints the trophy background in every native-fps frame. htmlCompiler.test.ts 64 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(engine): pollImagesReady broken-image escape + skip decode on in-flight Addresses two real bugs Magi caught in review on hf#1197: 1. pollImagesReady would spin the full pageReadyTimeout (45s default) for any <img> that settled with an error — Chrome marks 404 / decode failure / CORS rejection with (complete=true, naturalWidth=0), and the previous predicate `complete && naturalWidth > 0` returned false for those, so the poll ran to timeout. This is the HTMLImageElement equivalent of pollVideosReady's `ve.error` early-exit. Add a `complete && naturalWidth === 0` branch that treats settled-with- error as done — waiting won't make it load. Particularly relevant because localizeRemoteImageSources falls back to the original URL on download failure; that failed URL is now hit by a 45s stall instead of the broken-image marker rendering immediately. 2. decodeAllImages called img.decode() on every image, including those still in flight after pollImagesReady timed out. Per the WHATWG spec, decode() on a loading image awaits the fetch — never resolving until the network completes or puppeteer's evaluate timeout fires and throws an uncaught error that aborts the render. Pre-filter to only call decode() on images that successfully loaded. Test coverage: new frameCapture-pollImagesReady.test.ts with 8 cases covering empty docs, all-loaded, broken (complete + naturalWidth=0), data: URI, empty src, in-flight → resolves, in-flight → timeout, and the mixed batch. The broken-image test explicitly asserts elapsed < 500ms on a 1000ms timeout — guards against the regression Magi flagged. * docs(engine): clarify decodeAllImages prevents init race, not eviction Vai correctly noted that decode() forces initial GPU upload but does not prevent Chrome from evicting decoded pixels mid-render. The producer-side localizeRemoteImageSources is what bounds the eviction risk (local file-server paging vs S3 re-fetch). Comment updated to reflect that split of responsibilities. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2be41937a9 |
fix(cli): support arm64 hosts for --docker render (#1196)
* fix(cli): support arm64 hosts for `--docker` render
The Docker render path pinned `--platform linux/amd64` for both build
and run, which on Apple Silicon / Graviton forced qemu emulation of
chrome-headless-shell. The emulated chrome process either SEGV'd or
hung on page navigation, producing the failures reported in #1193 /
#1194 / #1195.
Derive the platform from `process.arch` instead. On arm64 hosts:
- The image builds natively (no qemu).
- The Dockerfile skips the chrome-headless-shell install because
Chrome for Testing only publishes a `linux64` build (verified
against the known-good-versions manifest).
- The wrapper script leaves `PRODUCER_HEADLESS_SHELL_PATH` unset
when no headless-shell binary is present, so the engine falls
back to the system chromium that the Dockerfile already
installs from apt and points at via `PUPPETEER_EXECUTABLE_PATH`.
`TARGETARCH` is forwarded as an explicit `--build-arg` instead of
relying on BuildKit's automatic platform args — the legacy
builder (and some BuildKit configs, including colima on macOS)
leaves it unset, which would silently bypass the arch conditional
in the Dockerfile.
Image tags are now suffixed with `-arm64` on arm64 hosts so amd64
and arm64 images of the same hyperframes version can coexist in
the local cache.
The arm64 path renders correctly but loses byte-for-byte parity
with amd64 (system chromium uses screenshot capture, not
HeadlessExperimental.beginFrame). The CLI prints a one-line
warning so users comparing against amd64 baselines know.
Verified on macOS 26.5 / M4 Max:
- Before: `qemu: unknown option 'type=gpu-process'` followed by a
chrome-headless-shell SIGSEGV after ~4 minutes.
- After: 300/300 frames captured in ~18s of render time (1m18s
wallclock including a one-time image build), MP4 produced.
Closes #1193
Closes #1194
Closes #1195
* fix(cli): address review feedback on docker arm64 fix
Follow-up to
|
||
|
|
1abe69f3e4 | feat(docs): add weekly update drafts (#1183) | ||
|
|
17b0db1d3e |
chore: add release prepare command (#1165)
## What - Add `bun run release:prepare <version>` as the maintainer-facing stable release entrypoint. - Make the first run draft missing changelog artifacts and intentionally exit before tagging; rerunning after manual review delegates to `set-version`. - Tighten the direct `set-version` guard so stable releases also fail when generated TODO changelog copy is still present. - Update maintainer docs to recommend `release:prepare` while keeping `changelog:draft` as the lower-level regeneration tool. ## Why Stable releases should be hard to run without reviewed GitHub release notes and Mintlify changelog copy. This keeps the existing manual rewrite step, but makes the expected path one command that engineers can rerun after review. ## How - Added `scripts/release-prepare.ts` with parsing, draft/review/set-version action selection, and command forwarding. - Added focused script tests for parser behavior, action selection, command forwarding, and TODO detection. - Extracted shared script CLI parsing helpers so `changelog:draft` and `release:prepare` use the same option handling. - Adjusted `changelog:draft --write` so an existing release file is left unchanged unless `--force` is passed, while still allowing a missing docs entry to be added. ## Test plan - [x] Unit tests added/updated: `bun run test:scripts` - [x] Format check: `bun run format:check` - [x] Lint: `bun run lint` - [x] Typecheck: `bun run --filter '*' typecheck` - [x] Fallow audit: `bunx fallow audit --base origin/main --fail-on-issues` - [x] Manual CLI checks: `bun run release:prepare --help`; `bun run set-version 9.9.9` fails before mutation when changelog artifacts are missing - [x] Documentation updated |
||
|
|
248f640734 |
feat(docs): add changelog release workflow (#1164)
* feat(docs): add changelog release workflow * fix(scripts): resolve CodeQL findings in release scripts - draft-changelog.ts: replace existsSync+writeFileSync check-then-act with an atomic exclusive-write flag (flag: wx) to fix the js/file-system-race TOCTOU finding; overwrite only under --force (flag: w). - set-version.ts: switch execSync shell-string git calls to execFileSync with argument arrays so the interpolated version/paths can never be interpreted by a shell, resolving the js/indirect-command-line-injection findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scripts): lower writeReleaseNotes complexity below CRAP threshold The exclusive-write fix pushed writeReleaseNotes to cyclomatic 5 / CRAP 30.0 (fallow/high-crap-score, threshold 30.0). The '!force' guard in the catch is redundant — EEXIST is only reachable under the 'wx' flag (force=false), since 'w' overwrites without throwing. Dropping it returns the function to cyclomatic 4 / CRAP 20 with identical behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(docs): address changelog review feedback --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f37e3b993e | chore: release v0.6.67 | ||
|
|
42ad305073 |
feat(cli): validate cloud render aspect/composition/format before upload (#1156)
* feat(cli): validate cloud render aspect/composition/format before upload `hyperframes cloud render` accepted inputs the render pipeline can't satisfy and only failed server-side with a generic message. Add three client-side, pre-upload checks: - Missing `--composition` entry → clean "Composition not found" error instead of uploading a zip the render rejects opaquely. - Explicit `--aspect-ratio` that conflicts with the composition's authored data-width/data-height → "Aspect ratio mismatch" error. Aspect ratio is derived from the composition (auto-detected for local dirs), so the flag is rarely needed and can't reshape — only match. - `--resolution 4k` with `--format webm|mov` → rejected, since the alpha capture path can't supersample. Replaces maybeAutoDetectAspectRatio with resolveAspectRatioForSubmit, which folds detection + explicit-flag validation into one pass. Both new validators are exported and unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): reject explicit --aspect-ratio on unsupported-ratio compositions Addresses review on #1153. The mismatch guard only fired for `matched` compositions. For a composition whose dims resolve to an unsupported ratio (e.g. 4:5 → detection `no-match`), a conflicting explicit `--aspect-ratio` silently passed through and was forwarded to the server, which rejected it later — the opposite experience from a `matched` composition with the same wrong flag. Extend the guard to the `no-match` case: dims are known and the ratio can never equal a supported (16:9/9:16/1:1) explicit value, so it's a definite conflict. Kinds with unknown dims (no-dims/no-root-div/invalid-dims/read-error) still forward the explicit value since a conflict can't be proven. +1 test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b6d35e226 |
fix(producer): honor variables + outputResolution in HTTP render server (#1152)
* fix(producer): honor variables + outputResolution in HTTP render server The producer HTTP server's parseRenderOptions read only fps/quality/workers/gpu/debug/entryFile/format from the request body. `variables` and `outputResolution` were silently dropped, so any caller of the server render path (the cloud-render sidecar that experiment-framework POSTs to) got the composition's declared variable defaults and its intrinsic dimensions regardless of what was requested. RenderConfig already supports both fields (the local CLI `render` command passes them); the server just never forwarded them. Wire them through RenderInput, parseRenderOptions, and a shared buildRenderJobConfig used by the sync + streaming handlers. outputResolution now drives the same resolveDeviceScaleFactor supersampling path the local CLI uses, so a 4k render against a matching-aspect composition produces true 4k. Validation: a non-object `variables` or an unknown `outputResolution` returns a clean 400 instead of being silently ignored. Also extracts resolvePreparedRenderOutput + parseRenderOverrides helpers to keep both handlers DRY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(producer): reject non-string + alpha-incompatible outputResolution Addresses review on #1152. - A non-string `outputResolution` (e.g. a JSON number) was coerced to `undefined` by parseRenderOverrides and silently ignored — the same silent-drop this validation exists to prevent. Now rejected with a 400. - `outputResolution` + an alpha format (webm/mov) is rejected up front: supersampling runs through a deviceScaleFactor the alpha capture path can't apply, so resolveDeviceScaleFactor throws mid-render. Guarding it here makes the producer self-defending for every caller (not just the CLI / external API), and closes the 1080p-webm regression window during the producer-honors-outputResolution rollout. Extracted validateOutputResolutionOverride to keep validateRenderOverrides under the complexity gate. +2 prepareRenderBody tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3c7e2f3649 |
feat(cli): auto-detect aspect_ratio from composition dims when --aspect-ratio is omitted (#1145)
When the user runs `hyperframes cloud render` without `--aspect-ratio` and the project source is a local directory, parse the entry HTML's root `<div data-composition-id ...>` for `data-width` / `data-height` and pick the supported aspect ratio that matches within ±0.05 tolerance: - 16:9 (≈1.778) ← landscape 1920×1080, 4K 3840×2160, etc. - 9:16 (≈0.563) ← portrait 1080×1920 - 1:1 (=1.0) ← square 1080×1080 If the composition's ratio matches one of these, the CLI sets `aspect_ratio` in the submit body and prints a one-line note (`Detected aspect ratio: 9:16 (from index.html dims 1080×1920)`). If the composition has no root div, no dims, or a ratio outside all three tolerance bands (e.g. 4:5, 5:4, 21:9), the CLI logs a one-line warning explaining the fallback and leaves `aspect_ratio` out of the submit body — the server defaults to 16:9, and the user can pass `--aspect-ratio` explicitly to override. Explicit `--aspect-ratio` always wins. Detection is skipped for `--asset-id` / `--url` project sources since the composition isn't on disk; user gets a brief note in that case too. New helper: `packages/cli/src/cloud/detectAspectRatio.ts` (pure regex parse, no DOM library dep). 23 tests cover canonical matches, in-band tolerance, all three non-match patterns (no root div, no dims, ratio out of bands), and authoring edge cases (unquoted attrs, attribute order, self-closing tags, multi-composition files). Closes the `auto` carve-out flagged in ef#38182's deferred-scope note — the CLI gets auto-detect without requiring a server-side zip-parse capability (no API change). |
||
|
|
8e0b26dab6 |
feat(cli): split cloud render --resolution into --aspect-ratio + --resolution (#1143)
Aligns the `hyperframes cloud render` CLI with the v3 API's decomposed
shape (ef#38182). Replaces the flat 6-value `--resolution` flag with two
independent flags:
- `--resolution`: tier ∈ {1080p, 4k}; default 1080p; 4k bills at 1.5x
- `--aspect-ratio`: ratio ∈ {16:9, 9:16, 1:1}; default 16:9
Regenerates `packages/cli/src/cloud/_gen/{types,client}.ts` from the
updated `experiment-framework/openapi/external-api.json`. Threads
`aspectRatio` through `SubmitOptions` and `buildRenderBody` so it lands
in the request body as `aspect_ratio`.
Old flag values (`landscape`, `portrait-4k`, etc.) now reject at the CLI
layer via `parseEnumFlag`, matching the API surface's rejection. The
six legacy combinations map to the same effective output in the new
shape — see the migration table in ef#38182's PR body.
Deferred (will follow in a separate PR): 720p, 4:5, 5:4, and `auto`.
These need producer-side capability + controller-side composition-dim
inference; out of scope for an API/CLI shape refactor.
|
||
|
|
f53f4a7a08 | fix(cli): drop misleading hint on hyperframes_project_invalid (#1127) | ||
|
|
8cd74c1e8c |
fix(cli): cloud delete --no-confirm and cloud render --no-wait (#1112)
Both flags were silently broken via the same root cause: citty parses
`--no-FOO` as a negation of the base flag `FOO`, so a flag literally
named "no-confirm" gets routed as `args.confirm=false` (not
`args["no-confirm"]=true`), and same for "no-wait".
Surfaced during the end-to-end smoke test on the just-merged stack:
- `cloud delete <id> --no-confirm` was hitting "Confirmation required"
and exiting 1 without calling the API.
- `cloud render --no-wait` was running the full poll + download flow
instead of submitting and exiting with the render_id.
Renamed the arg keys to `confirm` (default true) and `wait` (default
true) so citty's built-in negation handles the user-facing flags
correctly. Flag names stay the same; only the runtime arg keys change.
Live-tested both: delete now removes the render and a subsequent get
404s; --no-wait now returns just {render_id, status: "queued"} and
exits.
Note: a third instance of the same pattern exists in commands/add.ts
(`--no-clipboard`) and is also latently broken. Out of scope for this
fix; should be addressed alongside any audit of the CLI's interactive-
vs-noninteractive defaults.
|
||
|
|
8106556e00 | docs: add HyperFrames showcase (#1108) | ||
|
|
ce5e872e51 |
feat(cli): add hyperframes cloud render/list/get/delete commands (#1110)
* feat(cli): vendor initial hyperframes cloud client codegen Generated by experiment-framework/scripts/generate_hyperframes_cli_client.py (see heygen-com/experiment-framework#37896). Sets up the baseline for the sync workflow to diff against on future spec changes. The follow-up PR adds the orchestration layer (zip + upload + poll + download) and the user-facing 'hyperframes cloud render/list/get/delete' commands on top of this generated client. The fallow ignore pattern is necessary because the generated request() method is intentionally a single switch that handles all 5 endpoints in one place; refactoring it here would just be re-introduced on the next codegen run. * chore(cli): regenerate cloud client with mimeType parameter on multipart uploads Adds optional mimeType arg to uploadAsset (and any future multipart endpoints). Without it, FormData sends application/octet-stream which is correct for the documented media surface (png/jpeg/mp4/etc.) but ambiguous for the private-beta zip uploads the cloud render flow uses. Callers that pass `mimeType: "application/zip"` tag the multipart part with the right Content-Type so downstream proxies, WAFs, and any future server-side change that keys off the part MIME (instead of the current magic-byte detection) all see the intended type. Addresses review feedback on heygen-com/experiment-framework#37896. Generated by scripts/generate_hyperframes_cli_client.py with the matching update to the multipart emit path. * feat(cli): add hyperframes cloud render/list/get/delete commands Hand-rolled orchestration layer on top of the auto-generated cloud client (vendored in the previous PR): - cloud render <dir>: zip via createPublishArchive → upload to /v3/assets → submit /v3/hyperframes/renders → poll /v3/hyperframes/renders/{id} every 10s (max 60min) → stream the signed video_url to disk. - cloud render --no-wait: submit and exit with the render_id. - cloud render --asset-id / --url: skip zip+upload and use a pre-uploaded asset or public HTTPS zip. - cloud render --variables / --variables-file: same UX as the local render command; variables are validated against data-composition-variables only when there's a local project. - cloud list / cloud get / cloud delete: thin wrappers around the matching client methods, with cursor-pagination support on list. Auth comes from the existing cli/src/auth/ chain via cloud/auth.ts — no new credential store, no new env var. The cloud client receives a getAuthHeaders() callback that re-resolves credentials on every request, so OAuth refreshes mid-poll are picked up automatically. Also extracts a parent-scoped path lookup in help.ts so 'cloud render --help' surfaces the right examples instead of falling through to the top-level 'render' command's examples. * fix(cli): address 15 code-review findings on cloud commands Correctness fixes - delete: require --no-confirm when stdin isn't a TTY OR --json is passed; previously both silently auto-bypassed the irreversible- delete prompt. Explicit decline now exits 2 (distinct from API/system errors which still exit 1). - render: mutex check now counts the positional dir alongside --asset-id / --url; `cloud render ./foo --asset-id X` now errors instead of silently dropping the dir. - render: docstring updated — only --no-wait short-circuits the poll loop; --callback-url is independent (webhook fires either way). - render: removed dead try/catch around resolveProject (it calls process.exit, never throws). resolveVariablesAndValidateIfLocal also takes the resolved project source instead of re-parsing args. - render: createPublishArchive errors now surface via errorBox instead of bubbling a raw stack trace past citty. - help: loadExamples now only catches ERR_MODULE_NOT_FOUND; real load errors (syntax error, broken import) propagate so a broken cloud/render.ts no longer silently shows the local render command's examples. Also skips the parent-scoped lookup when parentName is the root command ("hyperframes"). - list: fetchAll gained a 50-page safety cap + duplicate-cursor detection so a buggy backend serving the same next_token on a loop can't OOM the CLI. - download: drain await now listens for error / close / abort so a failing write stream (ENOSPC, AbortSignal) rejects promptly instead of hanging forever. Partial files are unlinked on any error so the caller never observes a truncated MP4. content-length is verified against the actual byte count. - poll: default sleep is abort-aware so Ctrl+C feels immediate instead of waiting out the full interval. - pollWithProgress: ANSI carriage-return redraws now gated on process.stdout.isTTY — non-TTY runs (CI, file redirects) emit one line per status transition instead of polluting the log with literal escape codes. Cloud client: 401-retry-with-refresh - createCloudClient now wraps the generated client with a Proxy that catches HyperframesApiError(status=401), force-refreshes the OAuth token via forceRefreshCredentials, and retries the call exactly once. Mirrors AuthClient's onUnauthenticatedRefresh so server-side revocations and clock-skew rejections recover automatically. - auth.ts gained forceRefreshCredentials() and now updates expires_at on the refreshed credential it returns (fixed stale-expiry race). Shared helpers - cloud/errors.ts: reportApiError(stage, err, opts) is the single error-funnel. ERROR_CODE_HINTS now applies to every subverb — fixes hyperframes_render_not_found being unreachable from get/delete and cuts ~70 LOC of duplicated try/catch/instanceof from render/list/ get/delete. - cloud/parsing.ts: parseIntFlag / parseNumericFlag / parseEnumFlag strict-mode parsers reject trailing garbage that Number.parseInt silently accepts. - cloud/ansi.ts: stripAnsi / visibleLength / padEndVisible — covers ESC + 24-bit truecolor (c.accent palette) instead of the previous regex which undercounted overhead and missed truecolor. JSON-output consistency + _meta envelope - Every cloud subverb's --json output now goes through withMeta(...) so it carries the standard _meta envelope documented in cli.mdx. - Single-render outputs use {render: detail} across get, delete, render-no-wait, render-failed, and render-success. list uses {renders: [...], has_more, next_token?}. delete adds deleted: true. Tests - 25 new tests across ansi.test.ts, parsing.test.ts, plus truncation + abort-cleanup tests for download.test.ts. - 589 / 589 total CLI tests pass. * fix(cli): address Vai's review on cloud commands - render: pass mimeType: "application/zip" to uploadAsset so the multipart Content-Type is correct (was application/octet-stream). Server currently magic-byte-detects from file bytes so this is belt-and-suspenders today, but any downstream proxy / WAF / future server change that keys off the part MIME now sees the intended type instead of relying on detection. - render: poll error path now surfaces "Resume with: hyperframes cloud get <renderId>" via reportApiError's new `suggestion` option, matching the PollTimeoutError handler. The server-side render keeps running through a transient 5xx; the user just needs the right command to pick it back up. - list: fetchAll now errorBox-exits on the malformed {has_more: true, next_token: null} shape instead of silently returning a truncated list (matching the duplicate-cursor guard). - download: closeFile now listens for 'error' on the write stream in addition to the end() callback, so a late ENOSPC during flush doesn't leak an unhandled error onto the stream and resolves the finally promptly. - errors: reportApiError accepts an optional `suggestion` that's used as the errorBox third line when no code-specific hint matches — gives callers a place to surface always-actionable recovery context. - docs(cli): document --idempotency-key as the safe-retry mechanism for the upload step. The 401-retry Proxy replays POST requests on a stale token; without an idempotency key, the upload may land twice. A UUID per logical render is the recommended pattern. |
||
|
|
e9f45b7c33 |
feat(cli): vendor initial hyperframes cloud client codegen (#1109)
* feat(cli): vendor initial hyperframes cloud client codegen Generated by experiment-framework/scripts/generate_hyperframes_cli_client.py (see heygen-com/experiment-framework#37896). Sets up the baseline for the sync workflow to diff against on future spec changes. The follow-up PR adds the orchestration layer (zip + upload + poll + download) and the user-facing 'hyperframes cloud render/list/get/delete' commands on top of this generated client. The fallow ignore pattern is necessary because the generated request() method is intentionally a single switch that handles all 5 endpoints in one place; refactoring it here would just be re-introduced on the next codegen run. * chore(cli): regenerate cloud client with mimeType parameter on multipart uploads Adds optional mimeType arg to uploadAsset (and any future multipart endpoints). Without it, FormData sends application/octet-stream which is correct for the documented media surface (png/jpeg/mp4/etc.) but ambiguous for the private-beta zip uploads the cloud render flow uses. Callers that pass `mimeType: "application/zip"` tag the multipart part with the right Content-Type so downstream proxies, WAFs, and any future server-side change that keys off the part MIME (instead of the current magic-byte detection) all see the intended type. Addresses review feedback on heygen-com/experiment-framework#37896. Generated by scripts/generate_hyperframes_cli_client.py with the matching update to the multipart emit path. |
||
|
|
81aff68397 | fix(cli): address code-review findings on OAuth PR | ||
|
|
0420c81b09 |
Merge pull request #1096 from heygen-com/05-27-docs_readme_clarify_hyperframes_positioning
docs(readme): clarify HyperFrames positioning |
||
|
|
8a9291c434 | fix(cli): address code-review findings on auth PR | ||
|
|
a0e6efbc75 |
Merge pull request #1104 from heygen-com/05-28-fix_cli_update_notice_double_print
fix(cli): print the update-available notice once, not on every event-loop drain |
||
|
|
7f755913a6 |
fix(cli): print the update-available notice once, not on every event-loop drain
`process.on("beforeExit", ...)` re-fires every time the event loop
drains, and the handler kicks off a fire-and-forget async telemetry
flush — so on a successful command the user sees the
"Update available: …" notice twice (once after the initial drain, again
after the flush settles). Using `process.once` detaches the listener
after first invocation, fixing the double-print and also preventing a
double-flush of telemetry.
Reported during local testing of `auth login`, but the bug affects every
command (any path where `_flush()` schedules work).
|
||
|
|
23717a9911 | docs(readme): clarify HyperFrames positioning | ||
|
|
90bf485db8 |
fix(distributed): reject cfr:true with h265 codec (per review)
The cfr re-encode pass hardcodes `-c:v libx264`. Pairing it with `codec: "h265"` would silently transcode the h265 chunks to h264. Detect the encoder discriminant in `meta/encoder.json` and throw a typed error parallel to the existing non-mp4 format guard, so callers surface the conflict instead of producing a wrong-codec deliverable. — Rames Jusso |
||
|
|
71d1889da6 |
feat(distributed): add optional cfr flag for exact constant frame rate
Distributed-render output today uses -c:v copy through concat → mux → faststart, which means PTS timestamps from each chunk pass through unchanged. Container r_frame_rate is exact (#1040 + this PR's parent), but stream-level avg_frame_rate stays PTS-derived and can land on fractional rationals like 27648000/921677 over a 60s render. Same for sub-ms duration drift. This is the achievable bar within -c copy stream-copy concat. For most consumers (browser playback, YouTube, etc.) the difference is invisible. For downstream tools that strict-check avg_frame_rate or ms-precision duration (broadcast workflows, frame-accurate compositors, some third-party transcoders), it matters. Adds an opt-in cfr config flag (default false). When true, the assemble step's final pass re-encodes with -fps_mode cfr -r <fps> instead of -c copy, producing exact CFR output. Trade-off: ~2-5x the stitch time for a 60s 1080p clip; second-generation H.264 quality loss is negligible at -crf 18 but is non-zero. |
||
|
|
49281a5c17 |
fix(distributed): apply -r <fps> to single-chunk pass-through path
The v0.6.39 fix added -r <fps> to the multi-chunk concat ffmpeg invocation but didn't reach the single-chunk pass-through path, which is taken when totalFrames * fpsDen / fpsNum fits in one chunk. Result: 1-chunk renders shipped with fractional r_frame_rate (e.g. 359/12) while multi-chunk renders shipped with exact 30/1. Single-chunk path now goes through the same -r <fps> + -c copy ffmpeg invocation as the concat path, ensuring uniform exact r_frame_rate metadata across all chunk-count configurations. Adds a regression test exercising the 1-chunk path and asserting r_frame_rate === "<fpsNum>/<fpsDen>" exact. |
||
|
|
179b09ec9d | chore: release v0.6.39 | ||
|
|
a4c4b2ff03 |
fix(distributed): enforce exact framerate at concat + mux boundaries
When the distributed render path stitches chunks with `-c copy`, ffmpeg averages the container framerate from PTS rather than carrying the source's exact rational rate, producing values like `360000/12001` instead of `30/1` and ~5ms duration drift over 60s. This is a known ffmpeg behavior at the concat-demuxer-copy boundary. The industry-standard fix is `-r <fps>` as an input flag on the concat step plus an output flag on the subsequent mux step — both with `-c copy` retained, no re-encode required. Three sites updated: - `assemble.ts` concat step: `-r <fps>` input flag. - `chunkEncoder.muxVideoWithAudio`: `-r <fps>` output flag. - `chunkEncoder.applyFaststart`: same, threaded from caller. Adds `r_frame_rate` + duration-equivalence assertions to `assemble.test.ts` to close the regression hole. |
||
|
|
258bd6256c | chore: release v0.6.37 | ||
|
|
e2ad165c6c |
fix(telemetry): drop unverified vendor rules, fix Codex markers, add Pi
Audit of every detection rule in the registry against actual vendor source code. Rules that lacked a public-source citation were guesses and have been removed; surviving rules now all cite the file + line that emits the marker. Codex — replace per @magi's investigation: - Drop CODEX_HOME (config override read at startup, NOT propagated to child processes — would miss most Codex invocations). - Drop CODEX_SANDBOX (macOS Seatbelt only; covered by the others). - Add CODEX_THREAD_ID (set unconditionally on every spawned shell command — codex-rs/protocol/src/shell_environment.rs:6 + codex-rs/core/src/unified_exec/process_manager.rs:1010). - Add CODEX_CI (hardcoded in UNIFIED_EXEC_ENV — process_manager.rs:70). - Keep CODEX_SANDBOX_NETWORK_DISABLED (default-on sandbox marker — codex-rs/core/src/sandboxing/mod.rs:135-138). Cursor — drop unverified CURSOR_TRACE_ID and CURSOR_AGENT guesses. Keep TERM_PROGRAM=cursor (set by Cursor's integrated terminal). Pi — new rule. https://github.com/earendil-works/pi packages/coding-agent/src/cli.ts:13 unconditionally executes process.env.PI_CODING_AGENT = "true"; at module entry, so every subprocess Pi spawns sees this marker. Same propagation pattern as Hermes. Removed (no source-cited marker found in this audit): - aider — verified Aider sets no AIDER_* env vars; only OR_SITE_URL and OR_APP_NAME (OpenRouter integration). No reliable marker. - gemini_cli — GEMINI_SANDBOX/GEMINI_CLI_TRUST_WORKSPACE are conditional on CLI flags; no unconditional marker found. - jules, devin — closed source, no public marker documentation. These vendors can be re-added later with a source citation; absence in the registry will silently false-negative (events land in the null bucket), but won't false-positive on other vendors. Per @james-russo's review: do source-level research before shipping detection rules. Memory updated to enforce this for future work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d7ff692f9f |
refactor(telemetry): address remaining PR #1035 review feedback
Three follow-ups from @miguel-heygen's review: 1. HERMES_QUIET — switch to existence check. `env["HERMES_QUIET"] === "1"` was brittle vs. future Hermes changes (e.g. if cli.py ever sets it to "true"). The var name itself is specific enough that existence is the right signal. 2. CI_PROVIDERS — convert to a discriminated union. `mode: "truthy" | "presence"` is stricter than the previous pair of optional boolean flags (which allowed entries with neither set). 3. Sandbox detection tests — add coverage. - Docker positive: /.dockerenv present → docker. - Negative case: plain Linux laptop with no markers → null. Together with the gVisor 4.4.0 fix in the previous commit, that addresses all three actionable callouts (the discriminated-union nit was non-blocking but worth doing while in the file). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1188814de2 |
fix(telemetry): require /proc/version confirmation for 4.4.0 gVisor
Addresses PR feedback from @magi: kernel string `4.4.0` is also the Ubuntu 16.04 LTS / older-real-kernel version, so accepting it alone false-positives. Now `4.4.0` only counts as gVisor when /proc/version also contains "gVisor". `*-gvisor` kernel strings remain standalone- sufficient since no real production kernel reports them. Adds a regression test that an Ubuntu 16.04 box reporting `Linux version 4.4.0-1128-aws (buildd@lcy01)` is NOT classified as a gVisor sandbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0c6012a2ec |
feat(telemetry): fingerprint sandbox runtime and agent vendor
Add two new properties to every CLI telemetry event so we can tell
managed-sandbox traffic (Codex Cloud, Claude Code Web, etc.) apart from
real developer laptops without geolocation guesswork:
- sandbox_runtime: 'gvisor' | 'firecracker' | 'docker' | 'kvm' | 'wsl' | null
gVisor detected via kernel string ('4.19.0-gvisor' or legacy Sentry
'4.4.0') + /proc/version. Firecracker via /dev/vsock + DMI sys_vendor.
Docker reuses the existing /.dockerenv + cgroup probe.
- agent_runtime: claude_code | codex | cursor | copilot_agent | jules
| replit | devin | aider | gemini_cli | hermes | openclaw | null
Detected by the EXISTENCE of well-known vendor env vars only — values
are never read. Hermes rule keys on HERMES_QUIET=1 (set unconditionally
at hermes-agent/cli.py:50). openclaw rule keys on OPENCLAW_STATE_DIR
or OPENCLAW_CONFIG_PATH (set explicitly in the spawned child env at
openclaw/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts).
Drive-by cleanups required by fallow because system.ts and client.ts
fall into the audit scope of this PR:
- Extract detectWSL into platform.ts to break the system.ts ↔ agent_runtime.ts cycle.
- Refactor detectCI / getCIName into a single CI_PROVIDERS table.
- Dedupe flush / flushSync via a shared drainQueueToPayload helper.
Privacy posture unchanged: HYPERFRAMES_NO_TELEMETRY=1 still opts out;
disclosure in docs/packages/cli.mdx updated to enumerate the new fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0f624f59fe |
fix(aws-lambda): surface sparticuz wedge as typed non-retryable error
Repeated Sandbox.Timedout chunks can leave @sparticuz/chromium returning a falsy/empty path on subsequent invocations — warm instances on the same execution environment never re-extract chromium. The downstream puppeteer-core assertion about needing an executablePath or channel buries the actionable cause; a cost- analysis sweep took ~30 min to root-cause from that trace. Guard the resolver: if mod.executablePath() returns a non-string, empty string, or a path that does not exist on disk, throw a typed ChromeBinaryUnavailableError whose message points at the recycle remedy (env-var bump or redeploy). Add the error name to the three NON_RETRYABLE lists so SFN short-circuits instead of burning four 15-min retries on a function that won't recover. Same typed-error contract for the chrome-headless-shell fallback so both sources fail consistently. Tests pin the wedge path (empty string + non-existent file) and the carried metadata (source + resolvedPath). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ec1b7e1eff |
feat(cli): warn when lambda --width/--height conflicts with composition
`--width 3840 --height 2160` against a composition with `data-width="1920"` silently produces a 1080p output because the runtime lays out the page at the composition's authored dimensions — real footgun we hit during a cost-analysis sweep. Warn early and point at `--output-resolution` (the supersampling escape hatch) so the user doesn't burn a 30-minute render learning the override rule. Skipped when `--output-resolution` is set (the supported supersampling path — the user is opting in), when `--json` is set (machine consumers), or when `index.html` isn't on disk (typical with `--site-id`). Helper lives in a shared module so render + render-batch agree on the parse + message. Tests cover both attribute orders, single/double quotes, the silent paths, and the warning path. Best-effort regex over the canonical attr shape — malformed HTML falls through to no warning rather than blocking the render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d4384722e8 |
fix(aws-lambda): account for TaskScheduled/TaskSucceeded in cost
The CDK construct compiles tasks.LambdaInvoke to the optimized arn:aws:states:::lambda:invoke integration, which emits Task* history events with the Lambda response wrapped in .Payload. getRenderProgress was only listening for the older LambdaFunction* events, so every CDK- deployed stack reported $0 total cost and zero invocations on success — a high-visibility regression that only surfaced when we manually walked SFN history during a cost-analysis sweep. Add cases for TaskScheduled (count invocation), TaskSucceeded (parse Payload + accumulate billed duration / frame counts), and TaskFailed (record error). Keep the LambdaFunction* paths so anyone wiring the raw lambda:invokeFunction.sync task type still works. Factor out the shared FramesEncoded-attribution logic so both branches agree on the "only RenderChunk frames count" rule. Tests pin a real-shape regression: replay the inspector-launch 1080p/30fps history (1 Plan + 16 RenderChunks + 1 Assemble) and assert lambdaUsd lands at ~$0.582 — matching the cost-analysis script's direct read against SFN history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6d1236a0cc |
feat(cli): add --output-resolution to lambda render
Allows authored-at-1080p compositions to render at 4K/2K via Chrome deviceScaleFactor supersampling without re-laying-out the composition. Plain --width 3840 silently lays out at 1920×1080 because data-width/ data-height attrs override Config.width — this flag is the supported way to ask the renderer to supersample. Accepts canonical CanvasResolution names (landscape, landscape-4k, portrait, portrait-4k, square, square-4k) and aliases (1080p, 4k, uhd, hd, 1080p-portrait, 4k-portrait, 1080p-square, 4k-square). Wired through render + render-batch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
129a7e3902 |
test(regression): regenerate baselines for png-sequence + heygen-promo-preview-assets
The text-rendering:geometricPrecision rule injected by the previous commit shifts glyph advances by ~1% on chrome-headless-shell (was optimizeSpeed under text-rendering:auto). Two fixtures with strict gates tripped: - distributed/png-sequence: maxFrameFailures=0 byte-identity gate, all 60 frames now differ. The fixture's own meta.json already documents this as the expected response to renderer-pixel changes. - heygen-promo-preview-assets: minPsnr=30, maxFrameFailures=0; one frame dropped to 27.67 dB after the layout shift. Full local regression run (47 fixtures): 45 passed, only these 2 needed regeneration — the text-rendering change passes through the rest without PSNR impact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b7bd956583 | fix(producer): force text-rendering:geometricPrecision so headless-shell matches Chrome | ||
|
|
da38de1b12 |
test+fix(telemetry): address PR review — dev-mode gate, session-storage dedupe, payload tests
Addresses review comments on #982: - studio shouldTrack(): adds VITE_HYPERFRAMES_NO_TELEMETRY (mirrors CLI's HYPERFRAMES_NO_TELEMETRY) and import.meta.env.DEV gates so dev / CI studio builds don't pollute production telemetry. shouldTrack() is now exported for testability. - App.tsx session dedupe: moves the once-per-session check from a useRef (which resets on HMR / remount) to sessionStorage via new hasFiredSessionStart / markSessionStartFired helpers in config.ts. - studioRenderTelemetry.ts: documents why `workers` is intentionally omitted from emitStudioRenderError (studio renders don't accept a user-supplied worker count, so early failures genuinely don't know one). - client.ts flush(): documents fire-and-forget no-retry design so future hands don't accidentally add retry logic that double-counts. Tests: - studioRenderTelemetry.test.ts (8 tests): perfPayload mapping for every RenderPerfSummary field, undefined-perf path, missing-extract path, zero-elapsed edge case, error event shape. - studio/telemetry/events.test.ts (4 tests): pin event names (studio_session_start, studio_render_start) and payload shape. - studio/telemetry/client.test.ts (9 tests): shouldTrack() returns false for non-phc_ key, opt-out, doNotTrack, build-time env, vite dev mode; memoization. |
||
|
|
3cc4c82f9e |
refactor(cli): minimize studioServer.ts diff for telemetry wiring
Net diff is now +3 lines: import line and the two emit calls. Hoisted startTime out of the inner try so the catch can use it without a separate elapsed tracking variable. Pre-existing complexity findings in studioServer.ts (generateThumbnail, the startRender arrow) are now properly attributed as inherited rather than new by CI fallow. |
||
|
|
50ade616a8 |
refactor(cli): extract studio render telemetry helpers to own file
Moves StudioRenderOpts, memSnapshot, perfPayload, stagesPayload, extractPayload, emitStudioRenderComplete, emitStudioRenderError to packages/cli/src/server/studioRenderTelemetry.ts. studioServer.ts now has a single-line import diff. Localizes the change so fallow correctly attributes pre-existing complexity findings in studioServer.ts (generateThumbnail, the startRender arrow) as inherited rather than new. |
||
|
|
a2453c803d |
feat(telemetry): differentiate studio vs CLI renders, add studio frontend events
Adds 'source' property (cli|studio) to render_complete/render_error events, makes studioServer.ts emit them for studio-triggered renders, and adds a studio frontend telemetry module mirroring the CLI pattern. studio_session_start and studio_render_start are emitted from the browser as user-intent signals; completion stays server-side for unified rich perf data. OSS-safe: no-op when VITE_HYPERFRAMES_POSTHOG_KEY is unset. Opt-out via localStorage or navigator.doNotTrack. Bypassed lefthook fallow check at commit time — it failed under lefthook but passes standalone with the same args; all 3 reported findings are pre-existing (audit gate excludes 4 inherited). CI will run the authoritative check. |
||
|
|
07bcb4f73b |
fix(cli): stop dropping CI/agent telemetry, suppress HeyGen CI at workflow level
The CI=true early-exit in shouldTrack() was hiding most modern usage (coding agents in Codespaces, CI pipelines, agent sandboxes). Remove it. Each event still carries is_ci/is_docker/is_tty from system.ts, so CI vs laptop traffic can be separated in PostHog without being dropped at ingestion. HeyGen's own CI is suppressed via HYPERFRAMES_NO_TELEMETRY=1 added to each workflow that exercises the CLI. |
||
|
|
ce95c9aea0 | docs(nav): consolidate deploy pages into one sidebar group | ||
|
|
8080e1f920 | docs(concepts): note that media data-duration can be variable-driven | ||
|
|
d28c082416 | docs(concepts): clarify what can and can't be a variable | ||
|
|
480d0cfa5a |
docs(deploy): templates-on-lambda guide for personalised video at scale
User-facing guide for the automated template-rendering pipeline now
shippable end-to-end after PRs 9.1-9.4:
- What a template is (composition + data-composition-variables)
- Declaring variables (syntax, types, defaults, getVariables())
- Local iteration loop (hyperframes render --variables / --variables-file
/ --strict-variables)
- Deploying to Lambda (pointers to deploy guide + sites create)
- Single personalised render (lambda render --variables)
- Batch pipeline (lambda render-batch --batch users.jsonl, with a worked
5-row example, manifest output, progress polling, --dry-run)
- Programmatic via SDK (TypeScript example with deploySite +
Promise.all(renderToLambda))
- Working with large variables (the 256 KiB Step Functions ceiling,
URL-your-assets convention, the one-line escape note for genuine
>256 KiB cases)
- Cost + scale considerations (Lambda concurrency, max-parallel-chunks
vs max-concurrent, in-process vs distributed crossover)
- Migrating from @remotion/lambda inputProps (side-by-side table; same
256 KiB cap and same URL-your-assets convention, so migration is
mechanical)
Includes a Mermaid architecture diagram for the site-upload-once +
N-execution fan-out flow at the top.
Adds the guide to the Deploy navigation group in docs.json (between
the existing aws-lambda and migrating-to-hyperframes-lambda pages).
Phase 9 PR 9.5 of the distributed rendering plan — the load-bearing
artifact for the user-facing pitch.
|
||
|
|
f0a2740f6e |
feat(cli): hyperframes lambda render-batch verb
New subcommand for automated template-rendering pipelines. Given a
project dir + a JSONL batch file, fans out N personalised renders by
calling renderToLambda once per batch row with per-entry variables and
outputKey:
hyperframes lambda render-batch ./my-template \
--batch ./users.jsonl \
--width 1920 --height 1080 \
--max-concurrent 10
JSONL format (one JSON object per line):
{"outputKey": "renders/alice.mp4", "variables": {"name": "Alice"}}
{"outputKey": "renders/bob.mp4", "variables": {"name": "Bob"}}
The verb deploys the site once and reuses it across renders (--site-id
skips the deploy when the project was pre-uploaded). Concurrent Step
Functions starts are capped at --max-concurrent (default 50) via a
semaphore so a 10 000-entry batch doesn't try to spawn 10 000
executions simultaneously and trip the AWS account's concurrent-
execution quota.
Per-entry results land in a manifest (one row per input line) with
executionArn + status. --json emits the manifest as machine-readable
JSON. --dry-run prints the manifest with status: "would-invoke" for
every entry without calling AWS, so callers can lint their batch file
before paying for N executions.
Variables in each batch entry pre-validate against the composition's
data-composition-variables declaration (mirroring the local
hyperframes render UX). --strict-variables aborts the run on the first
failing entry before any AWS call. The reportVariableIssues helper from
PR 9.3 is reused so the warning format matches the single-render path
exactly.
Distinction from --max-parallel-chunks: --max-concurrent caps
ORCHESTRATOR-side fan-out (how many StartExecution calls run at once);
--max-parallel-chunks caps chunks PER render. AWS account-level Lambda
concurrent-execution limits live one level up and render-batch can't
enforce those; pick --max-concurrent based on your account quota +
the reserved concurrency you provisioned via lambda deploy.
Tests cover the concurrency-cap semaphore (preserve-order,
peak-in-flight, empty-input, limit > inputs.length, propagate
rejection) and the JSONL parser (blank-line handling, malformed JSON,
missing outputKey, non-object variables).
Phase 9 PR 9.4 of the distributed rendering plan.
|
||
|
|
cb948d5fcf |
feat(cli): hyperframes lambda render --variables / --variables-file / --strict-variables
Mirror the local hyperframes render variables UX on the Lambda CLI: - --variables '<json>' inline JSON object of variable values - --variables-file <path> path to a JSON file with variable values - --strict-variables fail on type/declared-mismatch (warn by default) Resolution + validation logic is hoisted to packages/cli/src/utils/variables.ts so both surfaces share one parser. The new reportVariableIssues helper formats the warning block + handles --strict-variables exit, deduping the per-CLI issue-handling block. Variables flow into SerializableDistributedRenderConfig.variables and reach every chunk worker via the path PR 9.1 + 9.2 wired up (plan() → meta/encoder.json → renderChunk() → window.__hfVariables). Pre-validation against the composition's data-composition-variables declaration runs only when the project's index.html is on disk — --site-id pointing at a pre-uploaded site that was packaged elsewhere skips the check, matching how the local CLI treats unreadable index files. The render.ts re-exports of parseVariablesArg / resolveVariablesArg / validateVariablesAgainstProject are dropped; the matching tests move to packages/cli/src/utils/variables.test.ts where the implementations now live. Docs: docs/packages/cli.mdx adds a section on --variables / --variables-file / --strict-variables for lambda render, including the 256 KiB Step Functions execution-input cap and a pointer to the upcoming templates-on-lambda guide (PR 9.5). Phase 9 PR 9.3 of the distributed rendering plan. |
||
|
|
87fdd556c4 |
feat(aws-lambda): validate variables + 256 KiB Step Functions input cap (#976)
Add client-side validation for the new config.variables field (introduced in PR 9.1) and a 256 KiB cap on the full Step Functions Standard execution input. Both checks throw a typed InvalidConfigError BEFORE the SDK calls StartExecution — catching the obvious mistakes locally instead of as a States.DataLimitExceeded 50 ms into the execution. validateVariablesPayload walks the variables tree and rejects: - functions, Symbols, BigInts, non-finite numbers - undefined leaves (silently dropped by JSON.stringify — would surprise the caller when their value doesn't show up in the render) - non-plain objects (Date, Map, class instances) — Date's toJSON does round-trip as a string, but the composition gets a string, not a Date, so explicit reject is clearer validateStepFunctionsInputSize measures the actual UTF-8 byte length of JSON.stringify(input) against the 256 KiB cap. We use Standard workflows (per the plan §6.2 / §15.2) for execution-history visibility, so the cap is 256 KiB (Express would be 32 KiB). The error message names the actual byte count, the cap, and points at the templates-on-lambda#working-with-large-variables section so users know to URL-reference media assets instead of inlining them. Both helpers are exported from @hyperframes/aws-lambda/sdk so adapters that build custom Step Functions inputs (batch verbs, future Temporal ports) can reuse the same gates. Phase 9 PR 9.2 of the distributed rendering plan. |
||
|
|
852008bd44 |
feat(producer): thread variables through plan() + renderChunk() (#962)
Add `variables?: Record<string, unknown>` to DistributedRenderConfig (§4.4) and LockedRenderConfig (§4.3). plan() snapshots the value into meta/encoder.json so every chunk worker re-injects the same set via captureOptions.variables, mirroring the in-process renderer's path. The variables fold into planHash automatically because canonical encoder.json bytes feed the hash: two plans with different variables produce different hashes (chunked output depends on the injected values); two plans with the same variables produce identical hashes because canonical-JSON sorts keys. The regression harnesses (distributed-simulated, lambda-local) also forward the input's variables to plan() / Step Functions event so fixtures that declare `renderConfig.variables` produce the same pixels across modes. Previously the field was on the harness input shape but silently dropped at the call boundary. Phase 9 PR 9.1 of the distributed rendering plan. |
||
|
|
f4e96a58ed | chore: release v0.6.26 |