* feat(scripts): typecheck the scripts directory
scripts/ was the one TypeScript surface nothing typechecked. Adding a project for it surfaced real errors rather than style: a preview generator passing string | undefined where a string was required, a readdir result indexed without a bound, and two non-null assertions standing in for a filter that could have narrowed the type instead.
The two preview generators had also drifted into sharing a capture setup, down to the comment explaining why the capture is opaque. That lifts into scripts/preview-capture.ts, so the reason is written once and both callers own the handles they have to close.
@hyperframes/core and @hyperframes/producer become dev dependencies because the scripts import them; without that the project resolves on a machine with a warm node_modules and fails in CI.
* fix(scripts): use source imports consistently
**The catalog generator destroys hand-written documentation on every run.** It
does `rmSync(dir, { recursive: true })` on both catalog folders before
regenerating, so any section a human added to a generated page is deleted the
next time anyone runs it. There is now a carry-forward pass that reads those
sections off the existing pages first and re-emits any heading the template does
not own. Note this prevents future loss only — the earlier regeneration in this
stack already removed what was there, and it is recoverable from git if wanted.
It was also clobbering `docs.json`: rebuilding the Catalog tab dropped its icon
and unlinked `catalog/index.mdx` from the sidebar entirely.
And 27 of 36 component pages told the reader to "see the comment header in the
file" when no such header exists. The generator reads the file now and only says
it when true.
On presentation: 1,365 table rows across 168 pages became 0. `## Details` was a
table whose rows were "Type: Block" and the duration; `## Files` was three
columns where most items have exactly one file. Both are one sentence now. The
preview leads the page instead of sitting under title, description, tags, a
warning and a credit — four of which Mintlify already renders from frontmatter.
**Two guides that showed nothing now show the thing.** Background removal plays a
real cutout with the matte magnified at the hair edge and at a shoulder a plant
overlapped, because those are where matting fails. HDR is the honest one: you
cannot show HDR brightness on an SDR page, so it shows the `ffprobe` verdict and
a measured round-trip — 4000 nits authored, 4012 back out — and says on screen
for all 26 seconds that it is not simulating anything.
Both films were checked before publishing: no black or frozen stretches, and read
at the real 590px docs column width rather than full size. Nav after regeneration:
307 pages, 0 dangling, 0 redirect collisions.
set-version printed 'git push origin main; git push origin v<version>' as the
way to trigger publish. That is the PRERELEASE flow. publish.yml's push trigger
is 'v*-*', so a stable tag push fires nothing, and stable publishes only from a
merged release/v* PR.
Following the old text put a release commit on main with an unpublishable tag:
publish never ran, and the stray tag then fails the next release's
verify_remote_tag check. Now prints the branch + PR commands and says not to
push the local tag, matching docs/contributing/release-channels.mdx.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: extract @hyperframes/studio-server package from core
Moves all studio-api routes, helpers, and Hono server wiring from
packages/core/src/studio-api/ into a new standalone packages/studio-server
package (@hyperframes/studio-server).
Core keeps thin re-export stubs at @hyperframes/core/studio-api and the
subpath helpers (screenshot-clip, draft-markers, etc.) for backward
compatibility. Consumer imports (cli studioServer, vite adapter/config,
producer htmlCompiler, studio manualEditsTypes) are updated to import from
@hyperframes/studio-server directly.
Also exports rewriteInlineStyleAssetUrls from @hyperframes/core root (was
in compiler/rewriteSubCompPaths.ts but not re-exported), required by
@hyperframes/studio-server/helpers/subComposition.
Removes postcss-selector-parser from @hyperframes/core dependencies (moved
to @hyperframes/studio-server which owns the routes that used it).
Depends on @hyperframes/parsers (PR #1755).
* fix(ci): add parsers+studio-server to Dockerfile and build before preview tests
* fix(ci): build @hyperframes/studio-server before Test and studio load smoke
Studio's vite.config.ts imports @hyperframes/studio-server, which resolves
via its "node" export condition to built dist. The Test and studio-load-smoke
jobs only built parsers + core, so esbuild's config load failed to resolve the
package entry. Build studio-server too.
* fix(studio): repoint sdkCutoverParity test import to studio-server
sourceMutation moved from core's studio-api to @hyperframes/studio-server;
the test still imported the deleted core path. This was masked while studio's
vite.config failed to load (couldn't resolve studio-server); now that the
config loads, the test runs and the stale import surfaced.
* refactor: extract @hyperframes/lint package from core
Moves all lint rules, hyperframeLinter, lintProject, and related types
from packages/core/src/lint/ into a new standalone packages/lint package.
Core keeps a thin re-export stub at @hyperframes/core/lint for backward
compatibility. Consumer imports (cli lint command, producer hyperframeLint)
are updated to import from @hyperframes/lint directly.
Depends on @hyperframes/parsers (PR #1755).
* fix: restore postcss-selector-parser in core (sourceMutation.ts still uses it)
* fix(ci): add parsers+lint to Dockerfile and build before preview tests
* chore: update bun.lock after restoring postcss-selector-parser dep
* test(cli): update lintProject test for string-dir signature from @hyperframes/lint
* refactor(core): single-source the lint engine in @hyperframes/lint
Delete core's byte-identical copy of the lint rule engine and re-point
staticGuard at @hyperframes/lint, so the render-time render-gate and the
studio preview share one rule engine instead of two copies that could
silently diverge. Back-compat preserved via the @hyperframes/core/lint stub.
Addresses review feedback on the dual-copy footgun.
## Summary
Extracts the GSAP parser/writer suite, HTML parser, hf-ids, spring-ease, and the shared composition data types out of `@hyperframes/core/src/parsers/` into a new, independently-publishable **`@hyperframes/parsers`** package.
This is the foundation of the [#1749](https://github.com/heygen-com/hyperframes/issues/1749) effort: make HyperFrames' parsing/linting/validation usable as plain libraries in a Node app, without shelling out to the CLI. Parsers is the standalone base every other extracted package builds on.
**Part 1 of 3** — splits #1754 into independently-reviewable pieces. Parts 2 (lint) and 3 (studio-server) stack on this branch.
## What moves
| | |
|---|---|
| Source moved out of core | **~9,900 LOC** (`src/parsers/` → `packages/parsers/src/`) |
| Total lines removed from core (incl. tests + goldens) | ~19,600 |
| Files relocated | 39 |
| Tests carried over | **660 passing** (5 skipped, 3 todo) |
The big movers: `gsapParser` / `gsapParserAcorn` (the recast + acorn dual parsers), `gsapWriterAcorn`, `gsapSerialize`, `gsapUnroll`, `htmlParser`, `hfIds`, `springEase`, `stableIds`, plus the `__goldens__` corpus.
## Bundle footprint of the new package
| Artifact | Size |
|---|---|
| `dist/` (unpacked) | 1.7 MB |
| npm tarball (packed) | 409 KB |
| `dist/index.js` | 90 KB (**~21 KB gzipped**) |
| Heaviest entries | `gsapWriterAcorn.js` 93 KB · `gsapParser.js` 91 KB |
Most of the weight is the GSAP AST machinery (recast/babel/acorn). It's tree-shakeable via subpath entries (`@hyperframes/parsers/hf-ids`, `/gsap-constants`, etc.) so a consumer that only needs `hf-ids` (2 KB) doesn't pull the parsers.
## How `@hyperframes/core` changes
The interesting part: **core sheds its entire AST toolchain.**
| core `dependencies` | before | after |
|---|---|---|
| count | 9 | 6 |
| removed | — | `@babel/parser`, `acorn`, `acorn-walk`, `magic-string`, `recast` |
| added | — | `@hyperframes/parsers`, `linkedom` |
Before this PR, importing `@hyperframes/core` at all dragged in babel + recast + acorn just to construct types. Now those live behind `@hyperframes/parsers`, and a consumer that only wants core's runtime/compiler types never resolves the parser stack. Core keeps thin `@deprecated` re-export stubs at the old subpaths (`@hyperframes/core/gsap-parser`, `/gsap-constants`, …) so nothing downstream breaks.
## Design notes
- **`"bun"` export condition before `"node"`** in every package export. Bun resolves the TypeScript source directly (no pre-built `dist/`), while Node/tsx/Docker contexts fall through to `"node"` → `dist/`. This keeps the dev loop zero-build while published artifacts stay Node-consumable.
- `@hyperframes/parsers` is **standalone** — zero `@hyperframes/*` dependencies — so it can be the base of the stack.
## Test plan
- [x] `bun run --filter @hyperframes/parsers test` — 660 tests pass
- [x] `bun run --filter @hyperframes/sdk test` — 382 tests pass
- [x] `bun run build` — full monorepo build succeeds
- [x] Fallow audit passes on CI
The guard blocked on any semver-higher v* tag, including orphan tags on dead
branches (e.g. a stray `chore: release v1.0.3` never merged or published).
Such tags can't appear in the release history and shouldn't block a legitimate
release. Now only tags that are BOTH higher AND reachable from HEAD block;
extracted `findBlockingTags` with unit coverage for orphan/reachable/lower cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set-version's stable-release next-steps printed `git push origin main --tags`,
which pushes every local tag and fails the whole push on any pre-existing tag
(it broke the v0.6.107 release). #1517 fixed CONTRIBUTING.md + annotated the tag
but missed this console message. Now prints `git push origin main` +
`git push origin v<version>`, matching the pre-release branch.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- set-version: create the release tag with `git tag -a -m` instead of a
lightweight `git tag`, which fails ("no tag message?") when a contributor has
tag.forceSignAnnotated / required-annotation set globally — it silently broke
the v0.6.107 tag step.
- CONTRIBUTING: replace `git push origin main --tags` (pushes every local tag →
whole push rejected on any pre-existing collision) with pushing the specific
tag, and document the monotonicity guard (stale higher tag blocks tagging).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(release): guard against non-monotonic tags in set-version
Add a pre-tag check that rejects stable releases when an existing tag
has a higher semver. This prevents tag-sorting installers (npx skills,
simple-git tags.latest) from resolving stale versions.
Closes#1282
* fix(review): add --skip-monotonicity-check escape hatch, fix formatting
Address review feedback:
- Add --skip-monotonicity-check flag for legitimate backport scenarios
- Soften error message to show both options (delete tag or skip check)
- Fix oxfmt formatting
- Add test for new flag parsing
* 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>
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).
## 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>
scripts/set-version.ts only bumped the npm package.json files, so the
Claude Code / Codex / Cursor plugin manifests stayed at 0.1.0 across every
release. Claude Code gates /plugin update on plugin.json's version, so users
never received accumulated changes (closes#1048).
Add a PLUGINS list and a parallel loop that rewrites each manifest's version,
and include the manifests in the clean-tree guard and the staged file set.
The loop replaces only the version string (not a JSON round-trip) to preserve
each file's exact formatting, since oxfmt keeps their short arrays inline and
JSON.stringify would expand them and fail the pre-commit format check.
Two follow-ups to keep the new package in lockstep with the rest of the
@hyperframes/* release cadence from day one:
- Bump packages/aws-lambda/package.json version 0.6.18 → 0.6.20 so it
matches what main released while this PR was in review. Without this,
the package would land below the rest of the lockstep and the next
release-bump would jump aws-lambda from 0.6.18 → 0.6.21 in one step.
- Add packages/aws-lambda to PACKAGES in scripts/set-version.ts so the
next `chore: release vX.Y.Z` commit bumps aws-lambda alongside the
other publishable packages. Without this, set-version silently skips
aws-lambda — package.json stays frozen, pnpm publish would re-publish
the same version on every release, and the npm-view precheck in
publish.yml would skip-with-success and never actually push a new
version of the package.
The --tag flag was optional, which led to v0.1.11 and v0.1.12 being
bumped without tags — skipping npm publish entirely. Invert the default:
always commit + tag, with --no-tag as the escape hatch.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): update publish workflow to use bun install
pnpm-lock.yaml was removed in the bun migration but publish.yml
still referenced it. Use bun for install/build, keep pnpm for
publish (publishConfig overrides + --provenance).
* docs: update stale pnpm references to bun across docs and scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>