* 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>
* feat(gsap): add innerText support to GSAP inspector for counter animations (#1244)
Adds 'innerText' as a supported GSAP property so number roll-up animations
(count-up from 0 to some value) are visible and editable in the GSAP inspector
panel.
- Add 'innerText' to SUPPORTED_PROPS in gsapConstants.ts
- Add label 'Counter Value', tooltip, and step constraint (1) in
gsapAnimationConstants.ts
The snap modifier that controls integer rounding is already preserved
verbatim via the EXTRAS_KEYS round-trip, so rounding behavior survives
edits without any additional UI changes.
Closes#1179
* feat(registry): add text-effects catalog section and morph-text component
Introduces a new "Text Effects" catalog section (below Effects) for text-focused visual components.
- Add `text-effects` BlockCategory to core registry types with violet color
- Add `text-effect` tag resolver in resolveBlockCategory (checked before generic `effect` tag)
- Tag caption-blend-difference, texture-mask-text, and morph-text with `text-effect`
- Update studio catalog order and color map to include text-effects
- Add morph-text component: gooey SVG threshold morph cycling through editable statements
using GSAP seekable proxy pattern for deterministic/seekable rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(registry): add morph-text preview video
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(registry): fix morph-text.html formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(catalog): add Text Effects section and morph-text page
Moves caption-blend-difference and texture-mask-text out of Effects into a new
"Text Effects" section below it. Adds morph-text component page with install
instructions and preview video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(registry): add demo.html for morph-text catalog preview rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(registry): address PR review feedback on morph-text and text-effects
- Restore `effect` tag on caption-blend-difference and texture-mask-text
alongside `text-effect` so existing tag-equality searches/analytics still match
- Fix morphPause script fallback from "0.25" to "1.5" to match data attribute default
- Add Math.max(0, ...) guard to blur values (intent clarity)
- Add prefers-reduced-motion: skip morph and show first word statically
- Remove CATEGORY_ORDER record from useBlockCatalog; derive order from
BLOCK_CATEGORIES array (single source of truth, no drift)
- Add comment to demo.html documenting its purpose (catalog preview script only)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## 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.
React components and DOM utilities for the snap system:
- SnapGuideOverlay: pre-allocated div pool (6 guides + 4 spacing)
for ref-driven guide line rendering during drag
- SnapToolbar: magnet/grid toggle with S/G keyboard shortcuts,
right-click grid popover for spacing config
- GridOverlay: CSS repeating-linear-gradient grid, GPU composited
- snapTargetCollection: walks iframe DOM tree to collect visible
elements as snap targets, cross-iframe safe (nodeType check)
* docs: add Reap as HyperFrames adopter
Reap (reap.video) is integrating HyperFrames as a renderer for lightweight
video edits and renders in its AI-driven video processing pipeline for
social content creation.
* docs: refine reap adopter entry and add to docs site
- ADOPTERS.md: rename "Reap" -> "reap" to match brand casing, and
update the use-case sentence to describe HyperFrames' role inside
reap (matches the HeyGen/tldraw convention on this page).
- docs/community/adopters.mdx: add reap card to the Production
CardGroup so the hosted adopters page at
hyperframes.heygen.com/community/adopters mirrors ADOPTERS.md.
Addresses review feedback from @miguel-heygen on #876.
* 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>
## 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
* 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>
* feat(registry): add Apple Terminal theme code snippet blocks
Adds 12 Apple Terminal built-in profile visualizer blocks to the
Code Snippets catalog section, following the same pattern as the
VS Code theme blocks (commit 5245e190).
Themes: Basic, Clear Dark, Clear Light, Grass, Homebrew, Man Page,
Novel, Ocean, Pro, Red Sands, Silver Aerogel, Solid Colors.
Each block is a self-contained 1920×1080 composition showing a
macOS Terminal.app window in the matching profile colors, with
per-character GSAP typing animation and window.__timelines contract.
Install individually:
npx hyperframes add code-snippet-apple-terminal-basic
Install all Apple Terminal themes:
npx hyperframes add apple-terminal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(registry): remove placeholder preview URLs for unrendered Apple Terminal themes
The 8 themes without rendered videos (clear-dark, homebrew, novel,
ocean, pro, red-sands, silver-aerogel, solid-colors) had preview.video
URLs pointing to non-existent S3 objects, returning 403. Removed the
preview field from their registry-item.json and the video embed from
their MDX pages until renders are available.
The 4 themes with uploaded videos (basic, clear-light, grass, man-page)
retain their preview URLs and are live on CDN.
* fix(registry): add apple-terminal tag so npx hyperframes add apple-terminal works
* feat(registry): render + upload all 12 Apple Terminal preview videos to CDN
Rendered the 8 missing themes (clear-dark, homebrew, novel, ocean, pro,
red-sands, silver-aerogel, solid-colors) using npx hyperframes@latest
render and uploaded to the hyperframes CDN. All 12 themes now have
live preview.video URLs and video embeds in their MDX pages.
All 12 CDN URLs return 200.
* style: format Apple Terminal HTML and JSON files with oxfmt
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(registry): add VS Code theme visualizer example
Full VS Code workbench recreation with per-character typing animation
across 12 built-in themes. Includes activity bar, sidebar, tabs,
editor with line-by-line cursor tracking, terminal panel, and status
bar — all driven by official VS Code theme JSON files.
Themes: Dark Modern, Dark 2026, Dark+, Light Modern, Light 2026,
Light+, Visual Studio Dark, Visual Studio Light, High Contrast,
High Contrast Light, Solarized Light, Monokai.
Includes build scripts to regenerate compositions from theme JSON.
* feat(registry): add 12 code snippet blocks for hyperframes add code
Individual blocks for each VS Code built-in theme, all tagged "code"
so `npx hyperframes add code` installs the full set.
Each block is a self-contained VS Code workbench with per-character
typing animation, activity bar, sidebar, tabs, terminal, and status
bar driven by official theme JSON data.
* docs: add mdx pages for code snippet blocks and example
- 12 block doc pages under catalog/blocks/code-snippet-*
- "Code Snippets" nav group in docs.json
- vscode-theme-visualizer entry in examples.mdx
* docs: revert examples.mdx — code snippets belong in catalog only
* docs: drop redundant 'Code Snippet' prefix from sidebar titles
* docs: add video previews to code snippet catalog pages
* style: format HTML, CSS, and MJS files for CI
* fix: address review feedback — build pipeline, LICENSE, dead code, nav order
1. Build script now regenerates both example compositions AND published
blocks in registry/blocks/code-snippet-*/, keeping them in sync.
2. Add MIT LICENSE for vendored VS Code theme JSONs (microsoft/vscode).
3. Remove dead `chars` variable from runtime, build script, all blocks,
and all example compositions.
4. Alphabetize Code Snippets nav group in docs.json to match catalog
convention.
* style: format all build-generated files (render-entries, CSS, index)
* docs: document feedback collection — cadence, data, opt-out
Adds guides/feedback.mdx covering: when CLI and Studio prompts
appear (render cadence, session cadence), what data is collected
(PostHog survey fields, doctor_summary shape), what is not
collected, the hyperframes feedback command for manual/agent
submission, agent runtime detection and structured hint,
config file fields, and all opt-out paths (HYPERFRAMES_NO_TELEMETRY,
DO_NOT_TRACK, CI guard, --quiet).
Also adds hyperframes feedback command entry to packages/cli.mdx
(Utilities tab, alongside telemetry) and registers guides/feedback
in the docs.json nav.
— Magi
* docs(feedback): fix cadence, agent env vars, docker gate, telemetry scope, why-we-ask
- Cadence: 1st/16th/31st (not 15th/30th/45th) per actual code
- Agent vars: CLAUDECODE/CLAUDE_CODE_ENTRYPOINT, CODEX_THREAD_ID/CODEX_CI,
TERM_PROGRAM=cursor, Copilot value checks; add Hermes/openclaw/Pi
- Remove docker gate claim (non-TTY only, not docker-specific)
- Telemetry disable only suppresses CLI prompt, not Studio bar
- Add why-we-ask opening section
- Remove Studio 'skip' action (CLI-only); fix 'counter resets' phrasing
- Fix 'values never read' — Cursor and Copilot do value comparisons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(feedback): remove invented Studio opt-out flags; document localStorage workaround
VITE_HYPERFRAMES_FEEDBACK_INTERVAL=0 falls through to default (n > 0 guard).
VITE_HYPERFRAMES_FEEDBACK feature flag doesn't exist. Bar is mounted
unconditionally. Document the localStorage key workaround instead and
note that a proper flag is a follow-up to hf#1101.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(feedback): fix localStorage workaround — only lastPromptedAt needs to be large
Setting both keys to the same value just delays 10 sessions before the bar
reappears. Setting only lastPromptedAt to 9999999 keeps count - lastAt
negative indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(studio): add VITE_HYPERFRAMES_NO_FEEDBACK build-time disable flag
Sets isFeedbackDisabled() guard in shouldShowFeedback() — when
VITE_HYPERFRAMES_NO_FEEDBACK=1, bar never shows regardless of session count.
Updates docs to document the flag and remove the localStorage workaround.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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.
## What
Introduces the `hyperframes auth` command group + a shared credential
store library that hyperframes-CLI and heygen-cli will both read from.
- `hyperframes auth login --api-key` saves a HeyGen API key to
`~/.heygen/credentials.json` (stdin pipe or hidden-input prompt).
- `hyperframes auth status` resolves the active credential (env vars
→ file) and verifies it against `GET /v3/users/me`, printing
identity + billing.
- `hyperframes auth logout` removes the credential (`--keep-api-key`
drops only the OAuth block).
Internals (`packages/cli/src/auth/`):
- `paths.ts` — `~/.heygen` layout, `HEYGEN_CONFIG_DIR` override.
- `store.ts` — read/write `credentials.json` (file 0600, dir 0700)
with legacy single-line plaintext fallback so existing heygen-cli
users don't lose their session.
- `resolver.ts` — chain: `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` →
file (unexpired OAuth wins over api_key).
- `client.ts` — hand-written typed wrapper for `GET /v3/users/me`
(intentionally not OpenAPI codegen — single endpoint).
- `errors.ts` — typed `AuthError` with discriminating `code`.
## Why
This is the foundation for `hyperframes cloud render`. Splitting it
out keeps the cloud-render PR small and lets users sign in today.
The plan originally called for a library-only PR followed by a
commands PR. The `fallow` dead-code gate flagged the library-only
shape as unused exports, so I bundled them — the library and its
first consumers ship together. PR 3 (OAuth PKCE) and PR 4
(heygen-cli read-side JSON support) follow.
## How
- Credential file format: JSON with optional `api_key` + `oauth`
blocks. Both CLIs read it; the resolver picks the freshest valid
credential.
- Auth header selection happens in the HTTP client: OAuth →
`Authorization: Bearer ...`, API key → `x-api-key: ...`.
- `HEYGEN_API_URL` lets dev testing target `api.dev.heygen.com`
without rebuilding.
- The new `auth` command lazy-loads its subverbs (same pattern as
`lambda`).
## Test plan
- [x] Unit tests added (`vitest`) for paths, store, resolver,
client, and errors — 45 tests, all green.
- [x] `bunx tsc --noEmit -p packages/cli/tsconfig.json` clean.
- [x] `bunx oxlint` + `bunx oxfmt --check` clean.
- [x] `bunx fallow audit --base origin/main --fail-on-issues` —
zero new findings.
- [ ] Smoke test against dev API:
`HEYGEN_API_URL=https://api.dev.heygen.com hyperframes auth login --api-key`
then `hyperframes auth status`.
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>
Two companion components inspired by the eBay Playbook hero transition:
- parallax-zoom: center card scales up to fill the frame while siblings
parallax outward. Single CSS variable (--pz-progress 0→1), fully
seekable and deterministic.
- parallax-unzoom: the reverse — focus card starts at full-frame scale
and shrinks back into its grid position while siblings parallax inward.
Uses --pu-progress with the pu prefix to avoid variable collisions when
both components live in the same composition.
Designed to chain: zoom INTO a card in scene 1, unzoom OUT of it in
scene 2 to reveal a fresh grid underneath.
Includes demo compositions, registry manifests, and catalog pages.
Preview assets rendered and uploaded to CDN.
Co-authored-by: Kanyini <onebenson@gmail.com>
The rational `Fps = { num, den }` refactor in 5dcc89c broke callers
passing `fps: 30` (the form documented in every code example and used
by external consumers). FFmpeg received `undefined/undefined` as the
framerate, causing a cryptic exit-code error.
Add `FpsInput = number | Fps` and `toFps()` normalizer in
@hyperframes/core. `createRenderJob` now accepts both forms —
plain integers are promoted to `{ num, den: 1 }` at the boundary;
`RenderConfig.fps` stays strict `Fps` internally so no downstream
code changes.
Also fixes the producer and engine docs, which showed phantom
`input`/`output` fields on `createRenderJob` and a wrong
`executeRenderJob(job)` signature (missing `projectDir`/`outputPath`
args).
Closes#1031
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.
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.
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.
## Summary
Closes#744 — adds dedicated setup guides for two AI coding tools that were missing from the documentation.
- **`docs/guides/antigravity.mdx`** — Google Antigravity IDE guide covering skills installation (workspace + global), semantic skill matching, Manager view parallelism for multi-scene videos, MCP alternative, and CLAUDE.md compatibility
- **`docs/guides/copilot-cli.mdx`** — GitHub Copilot CLI guide covering skills installation, slash command invocation, `/skills` management commands, agent mode for multi-step tasks, and MCP alternative
- **`docs/quickstart.mdx`** — updated agent list to mention both new tools with links to their guides
- **`docs/guides/prompting.mdx`** — updated description to include both tools
- **`docs/docs.json`** — added both guides to the Guides navigation group
## Test plan
- [x] Verify Mintlify docs build passes (`npx mintlify dev` in `docs/`)
- [x] Check navigation: both guides appear under Documentation → Guides
- [x] Verify links from quickstart.mdx to the new guide pages resolve correctly
- [x] Review guide content for accuracy against official Antigravity and Copilot CLI documentation
- Fix project skill directory to .agents/skills/ (plural) in both guides,
matching vercel-labs/skills agent registry
- Replace invented --mcp-server flag with correct --additional-mcp-config
flag and JSON/file syntax for Copilot CLI
- Remove non-existent /skills reload, /skills list, /skills info commands;
document only /skills (picker) and /skills add
- Remove non-existent github-copilot binary alias
- Antigravity MCP section now defers to Antigravity's own MCP docs for
the exact settings UI path
- Rename CLAUDE.md section to "Agent instruction files", document both
AGENTS.md (cross-agent) and CLAUDE.md
- Use --agent long form instead of -a in examples
- Add link to Antigravity Manager view docs
- Add token budget caveat for large skill sets in Copilot CLI
Add dedicated guide pages for two AI coding tools that were missing
from the documentation (closes#744):
- docs/guides/antigravity.mdx — skills install, semantic matching,
Manager view parallelism, MCP alternative, CLAUDE.md compat
- docs/guides/copilot-cli.mdx — skills install, slash commands,
/skills management, agent mode, MCP alternative
Also updates quickstart.mdx and prompting.mdx to mention both tools
alongside the existing agent list, and adds both pages to the docs
navigation in docs.json.
* docs(lambda): document webm support in distributed mode
PR 8.4 of the WebM distributed-rendering plan (v1.5 backlog #1; see
DISTRIBUTED-RENDERING-PLAN.md §7.2). User-facing docs catch up with the
shipped capability.
Updates docs/deploy/migrating-to-hyperframes-lambda.mdx:
- "Output format" row in the migration table now lists `webm` alongside
mp4 / mov / png-sequence with a note that webm uses libvpx-vp9 +
closed-GOP concat-copy. HDR mp4 remains the only refused format.
- "No webm distributed" caveat replaced with "webm uses closed-GOP VP9"
explainer covering the encoder args (`-g <chunkSize>`,
`-keyint_min <chunkSize>`, `-auto-alt-ref 0`, `-cpu-used 2`), why
alt-ref disable is load-bearing, and that the output preserves alpha
via yuva420p with Opus audio.
- Migration checklist no longer asks adopters to filter out webm
compositions; only HDR-dependent renders need to stay on the previous
framework.
aws-lambda.mdx doesn't currently call out webm as unsupported (only HDR
in the v1 surface list), so it gets no copy edits beyond the migration
guide.
The internal planning doc (DISTRIBUTED-RENDERING-PLAN.md §7.2, §8,
§12 — kept outside the repo) gets matching updates: format support
matrix flipped ✓, v1.5 backlog #1 marked shipped, HDR promoted to the
new top item, and the rev-12 → rev-13 status line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: address simplify-review findings on webm stack
Folds in cleanups identified by a multi-agent code-review pass over the
4-PR webm-distributed stack:
- plan.ts: `resolveEncoderTriple()` webm case now calls
`getEncoderPreset(quality, "webm")` for its preset string instead of
hardcoding "good". The hardcode was wrong for `quality: "draft"`
(`getEncoderPreset` returns "realtime" for that tier) — would have
silently overridden the draft → realtime mapping for distributed webm
renders.
- chunkEncoder.ts: trim the new VP9 closed-GOP comment block from ~18
lines of WHY narration down to the 6 lines that actually explain why
(alt-ref + cpu-used drift). Match the alpha branch's idempotent-push
comment to the same standard.
- chunkEncoder.test.ts: drop the duplicate WHY comment that restated
the implementation comment in plain words.
- webm-concat-copy.test.ts: rewrite the file-header docstring to
describe the contract being tested instead of the PR-8.1-gating
history; strip "PR 8.2 / Path A / Path B" references from error
messages (they belong in PR bodies, not in test output). Consolidate
the yuva420p alpha smoke into a single `it()` block (was a full
4-test describe with duplicated setup) — the yuv420p block already
covers the probe/decode/frame-count contract; the alpha smoke only
needs to prove the alpha args don't break concat-copy.
- plan.test.ts: drop the "PR 8.1 proved the contract" comment.
- webm-vp9 fixture: drop the aspirational "Other webm-with-audio
fixtures cover the mux path separately when added" sentence (no
other fixtures exist). Regenerated the baseline via
`docker:test:update webm-vp9` to reflect the updated comment.
- migrating-to-hyperframes-lambda.mdx: add a paragraph about
distributed webm's perf cost — ~10-25% larger files at constant CRF
due to forced keyframes, and slower per-chunk encode due to
`-cpu-used 2` being more conservative than the libvpx default.
All unit tests + the webm-vp9 distributed-simulated regression still
pass after these changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): accept --format=webm in `hyperframes lambda render`
The CLI's `lambda render` subcommand's FORMATS allowlist and the
`RenderArgs.format` type still narrowed to `mp4 | mov | png-sequence`,
so even though the producer + aws-lambda packages now support webm
end-to-end, the CLI surface rejected it with `--format must be mp4|mov|
png-sequence`. Add webm to both spots and update the --help description.
Surfaced during real-AWS deploy prep — the local lambda-local /
distributed-simulated tests didn't go through the CLI so the gap went
unnoticed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(producer): font cache writes to /tmp on Lambda (read-only \$HOME)
The deterministic Google Fonts cache was rooted at
`\$HOME/.cache/hyperframes/fonts`, which fails on AWS Lambda — the
runtime's `\$HOME` resolves to a `/home/sbx_*` directory tree that's
read-only. `mkdirSync(..., { recursive: true })` can't create that
path and the plan stage trips with `ENOENT: no such file or directory,
mkdir '/home/sbx_user1051/.cache/hyperframes/fonts/space-mono'` on
every Lambda render that pulls a Google Font (i.e. every distributed
fixture using `@import url("https://fonts.googleapis.com/...")`).
Detect Lambda via `\$AWS_LAMBDA_FUNCTION_NAME` and route the cache to
`tmpdir()/hyperframes/fonts` in that case. Lambda's `/tmp` survives
across invocations on a warm container, so cache hit rate is the same
as non-Lambda runs. Also honor an explicit
`\$HYPERFRAMES_FONT_CACHE_DIR` override for adopters who want a
different location regardless of the runtime.
Surfaced while verifying webm distributed end-to-end on real AWS — the
same bug affects mp4 fixtures using Google Fonts; webm just happened to
be the one I tried first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: extract DistributedFormat type + trim font-cache resolver
Second simplify-review pass on the webm stack flagged two cleanups:
1. **`DistributedFormat` type duplicated 10 times.** Every file in the
distributed pipeline carried its own copy of
`"mp4" | "mov" | "png-sequence" | "webm"` — adding a new format
meant a 10-place edit with no compile-time guarantee they stayed in
sync. Extract a single source of truth in
`packages/producer/src/services/distributed/shared.ts`, re-export
from `@hyperframes/producer/distributed` and
`@hyperframes/aws-lambda/sdk`, and have all callers pull from
there. The aws-lambda `ALLOWED_FORMATS` runtime tuple and the CLI's
`FORMATS` tuple now both use `satisfies readonly DistributedFormat[]`
so the compiler enforces the runtime allowlist stays in sync with
the type.
2. **`deterministicFonts.ts` font-cache resolver was over-commented.**
Trim the 7-line block to 4 lines (drop the aspirational
"and other read-only-FS execution environments" — only Lambda is
detected — and the warm-container `/tmp` persistence narration —
anyone reading already knows Lambda /tmp semantics). Collapse the
two-step `if (explicit && explicit.length > 0)` into a single
nullish-coalesce expression now that the empty-string defensive
check is gone (`process.env.X` is `string | undefined`, no third
shape to guard against).
Out-of-scope skips (called out by the agents, deferred):
- In-process `RenderConfig.format` and the in-process CLI's
`render.ts` format union still carry their own inline copies. The
union happens to coincide today but they're separate concerns —
leaving them alone limits this PR's blast radius.
- `fontCacheDir(slug)` / `resolveFontCacheRoot()` naming asymmetry
flagged as taste; skipping.
- Pre-existing redundant `existsSync` before `mkdirSync({ recursive:
true })` in `fontCacheDir` — out of scope.
All tests + typecheck still pass. Lambda render still works
end-to-end (no functional changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(lambda): drop plan-doc reference from migration checklist
PR review feedback: source/docs should not mention the
distributed-rendering planning doc. Tighten the migration checklist
sentence to describe the webm path directly rather than referencing
the doc's version label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(producer): split resolveEncoderTriple into mp4 + non-mp4 helpers
CI Fallow audit on PR #953 flagged `resolveEncoderTriple` at CRAP 31.6 —
the function interleaved (a) mp4 codec validation + dispatch, (b) the
non-mp4 codec-rejection throw, and (c) per-format dispatch. Splitting
into `resolveMp4EncoderTriple` + `resolveNonMp4EncoderTriple` drops the
top-level function's cyclomatic complexity below the threshold while
preserving every error message and code path. Behavior unchanged.
Also extracts an `EncoderTriple` type alias so the three functions
share the return shape declaratively rather than repeating it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the 7-step production pipeline (Capture → Design → Script → Storyboard
→ VO + Timing → Build → Validate) into its own dedicated guide so it serves
any Hyperframes project, not just website-to-video. Expand each step with
file contents, project layout, gates, and iteration patterns. Reference the
new page from website-to-video, quickstart, prompting, and launch-videos.