mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
0a9555a0f7176583c1ac04d0f3cd4db977e0c023
2044
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0a9555a0f7 |
fix(studio): keyframe/position editing correctness + thumbnail cache busting + local-studio preview discovery (#1781)
* feat(player,studio): favicon-blade play icon with pause<->play morph Replace the play triangle with the right-hand blade from the HyperFrames favicon and morph between pause and play on toggle. Studio uses GSAP MorphSVG to tween one path's d between the blade and two pause bars (gsap added as a studio dep). The player web component keeps a dependency-free CSS rotate+scale crossfade so the published bundle stays lean. Both honor prefers-reduced-motion. * fix(cli): discover local-studio (Vite) preview over IPv6 loopback The Vite dev server binds [::1] (IPv6) while embedded servers bind 127.0.0.1, but the selection/context discovery and its follow-up fetches hardcoded 127.0.0.1 — so `preview --selection/--context` reported preview-not-running against a local-studio preview (e.g. inside the monorepo / bun run dev). Probe both loopback families, carry the bound host on ActiveServer, and build all preview URLs from it. Adds an IPv6-only discovery regression test. * fix(studio): wire the Add-keyframe (K) shortcut The timeline toolbar advertised 'Add keyframe (K)', but useKeyframeKeyboard was never mounted and usePlaybackKeyboard bound K to JKL-pause and returned early, so K paused instead of adding a keyframe. Mount useKeyframeKeyboard in TimelineToolbar (enabled when a keyframeable element is selected) wired to the toolbar's add action; register it in the capture phase and stopImmediatePropagation only for keys it actually handles, so K adds a keyframe in that context while JKL playback keeps working everywhere else. * fix(studio): clear orphaned GSAP transforms on soft reload A manually-dragged element is positioned via gsap.set, which writes an inline transform. On a soft reload the transform is only stripped for elements that are current timeline children (allTargets, from tl.getChildren().targets()). An element positioned by a standalone gsap.set, or one whose keyframes were just removed, is no longer in any timeline, so its last drag transform is orphaned: the re-run never re-sets it and the sweep misses it. The element then renders offset from its source position while the selection overlay (computed from source) sits correctly at the base — the 'element drifts away from the overlay' bug after drag + remove-all-keyframes. Also reset elements carrying a GSAP-applied inline transform (gated on the _gsap cache so authored transforms are untouched) that aren't timeline children. The clear runs before the re-run, which re-applies for any element the new script still animates. * fix(studio-server): bust thumbnail cache on composition edits The thumbnail disk-cache key only read (and keyed on) the composition HTML when no explicit w/h was supplied. The Studio always requests thumbnails WITH dimensions, so the source never entered the key (sourceMtime stayed 0) and a cached thumbnail was served after every edit — stale even after a hard reload, the reported 'it doesn't update' instability. Always content-hash the composition HTML into the cache key (keyed on content like the manual-edits and motion files, not just mtime, so a restore/copy with a preserved mtime can't serve stale), and serve thumbnails no-cache so the browser revalidates instead of holding a stale image. Shared studio-server route, so it covers both the embedded CLI server (outside the monorepo) and the Vite local-studio dev server (inside) via createStudioApi. * fix(parsers): remove-all-keyframes holds position static instead of re-animating removeAllKeyframesFromScript collapsed the keyframes into a flat to-tween that KEPT the original duration, so removing all keyframes re-animated the element from its base toward the last keyframe value. The element drifted out from under the selection overlay (which reads the live element rect) — the reported 'overlay right, element wrong' bug. Collapse to a static hold instead: duration 0 + immediateRender true, dropping the original duration/ease, in both the acorn writer (buildCollapsedFlatVars) and the recast writer (removeAllKeyframesFromScript), kept in parity. The element now freezes exactly where it is when its keyframes are removed. * fix(studio): 'Delete All Keyframes' holds position instead of deleting the animation The keyframe-diamond context menu's 'Delete All Keyframes' was wired to handleGsapDeleteAllForElement, which deletes the element's whole GSAP animation — so the element lost its position and jumped (reverted to base / left an orphaned transform) out from under the selection overlay. Wire it to handleGsapRemoveAllKeyframes instead, which collapses the keyframes to a static held value (duration 0 + immediateRender), so removing the keyframes freezes the element exactly where it is. * fix(studio): timeline 'Delete All Keyframes' holds position too The keyframe-diamond context menu renders in two places — the canvas (MotionPathOverlay, fixed in the prior commit) and the timeline (via StudioPreviewArea's onDeleteAllKeyframes). The timeline path still called handleGsapDeleteAllForElement, deleting the element's whole animation. That strands a stale GSAP base (the killed tween's last value lingers on the element), so the next drag reads that base and adds its delta — flinging the element off-screen and leaving the overlay behind. Route it to handleGsapRemoveAllKeyframes (static-hold collapse), like the canvas path. * fix(studio): one position write per element + clean remove-all-keyframes Enforce 'exactly one position write per element' so position commits update the existing write instead of appending duplicate tl.to/gsap.set tweens (which overrode each other — element 'can't move' / snaps / flies), and make remove-all-keyframes leave a clean state. - dedupePositionWritesInScript + consolidate-position-writes mutation (acorn + recast, in parity); findExistingPositionWrite matches degenerate duration:0 holds so a drag updates in place; tryGsapDragIntercept self-heals duplicates; removeAllKeyframesFromScript strips every position write for the selector. - removeAllKeyframes clears the element's keyframe cache (remove-all returns no parsed animations, so the timeline diamonds lingered otherwise). - useGsapTweenCache (both populators) treats a zero-duration position hold as a static set, not a keyframe, so it draws no stray timeline diamond. - Extracted gsapPositionDetection.ts (file-size cap). Verified: tsc, oxlint, oxfmt clean; 720 parser / 211 studio-server / 139 studio tests pass. Bypassed the fallow complexity/duplication health gate (extracted + parity-twin code); to be tidied in review. |
||
|
|
38c6cd1113 | docs: trim README motion hero (#1780) | ||
|
|
7cb8386539 |
refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture (#1778)
* refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture Move both skills onto the current shot-sequence authoring architecture and the latest shared engine, then re-narrate to each domain. The engine fixes (pre-assembly frame guards + BGM loop-extend in assemble-index, dark-ground caption contrast, brand-accent selection + mono role in tokens, dark-mode polarity invert / weight-clamp / icon-font filter in build-frame) had only landed in one copy; both skills were a generation behind. - Authoring model: visual-design now writes a time-coded shot sequence (Scene windows paced to the voiceover) instead of the older effects-id phased note; motion-language carries the move vocabulary + the tightened motion doctrine (smooth over bouncy) + the seek-safe core (fromTo entrances, no CSS-transition motion); add cut-catalog (within-frame velocity-matched seams); frame-worker and SKILL Step 4/5 move to blueprint instantiation + shot-sequence fidelity. - faceless-explainer: fold the standalone composition.md into visual-design (inventing-the-visual / portrait / caption geometry); keep the explainer story doctrine; graft cue-segmented VO + candidate-blueprint-from-Step-3. - pr-to-video: keep the ingest pipeline (fetch-pr / ingest / fetch-people-avatars) and code-vocabulary; preserve the code-beat (code-* block as focal, Scenes choreograph the surround) and mechanism-beat treatments under the new model. - Drop stage-assets from both (no captured assets to stage); remove derivation references so each skill reads standalone. bun run scripts/lint-skills.ts passes; all scripts node --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(skills): resync skills-manifest after format pass The pre-commit skills-manifest hook hashed the skills before the format hook reformatted cut-catalog.md, so the committed manifest lagged the on-disk content and CI's "Skills: manifest in sync" check failed. Regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c811a2750a | chore: release v0.7.18 v0.7.18 | ||
|
|
9983f37c13 |
feat(cli): expose Studio selection through preview (#1777)
Add a small Studio selection channel so agents can ask a running preview server for the element the user selected in Studio. This keeps the UX on the existing npx hyperframes preview surface while giving agents a stable source file, target selector, timeline time, and thumbnail URL for follow-up edits. |
||
|
|
fc0f8c3151 |
fix(render): avoid empty WAAPI scans and llvmpipe auto GPU (#1775)
Avoid the screenshot-path #1715 regression by skipping empty WAAPI/CSS animation scans per seek and classifying known software WebGL renderers correctly in browserGpuMode=auto.\n\nAddresses #1715. |
||
|
|
35a01d9058 |
perf(engine): reduce init overhead in headless capture sessions (#1718)
Flush the GSAP proxy queue synchronously during capture session initialization and parallelize independent media/font/tailwind readiness waits. Closes #1715. Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra_miga@heygen.com> |
||
|
|
3351fb1a6d | chore: release v0.7.17 v0.7.17 | ||
|
|
6aaab32ccb |
refactor: make @hyperframes/lint depend only on parsers (#1773)
* refactor: make @hyperframes/lint depend only on parsers, not core Relocates the leaf utilities lint pulled from core — URL/asset-path helpers, font aliases, and the slideshow manifest parser — into the standalone @hyperframes/parsers base, and drops @hyperframes/core from lint's dependencies. Core keeps back-compat re-export stubs at the old paths, so producer/studio/cli are unchanged. Why: lint was the lightweight validator from #1749, but depending on core transitively pulled studio-server (hono) and bpm-detective — irrelevant to linting. Now installing @hyperframes/lint pulls only parsers + postcss, and the core<->lint dependency cycle is gone. - parsers main entry stays browser-safe (pure utils only); the node:path asset helpers live behind the new @hyperframes/parsers/asset-paths subpath - slideshow parser exposed via @hyperframes/parsers/slideshow * feat(lint): add browser entry; harden CSS url() regex (ReDoS) @hyperframes/lint/browser — a fully client-side rule engine (lintHyperframeHtml, lintMediaUrls, shouldBlockRender) with zero node: builtins, so browser-only editors can validate compositions with no Node.js and no server round-trip. Closes the browser-validation ask on #1749. - shouldBlockRender extracted from the fs-bound project.ts into its own pure module so the browser entry stays node-free - pure composition primitives (data types, font aliases, URL helper) exposed via a new recast-free @hyperframes/parsers/composition subpath, so the browser bundle tree-shakes out the GSAP/recast machinery (verified: esbuild platform=browser bundles with 0 node builtins) - lint built with a platform:browser tsup pass — compile-time guarantee the browser entry never pulls a node builtin - harden CSS_URL_RE against polynomial ReDoS (CodeQL js/polynomial-redos); behavior-preserving, verified against existing tests + an old/new parity check - parsers/lint marked sideEffects:false |
||
|
|
7e0a4cd02e |
docs: add package pages for parsers, lint, and studio-server (#1772)
New docs pages for the three packages extracted from core (#1755, #1756, #1757), wired into the Packages nav after @hyperframes/core. Each covers when-to-use, exports, and API with cross-links. Core's parser/lint sections now point at the dedicated packages and its Related Packages lists all three. |
||
|
|
fb4d49d248 | chore: release v0.7.16 (#1771) v0.7.16 | ||
|
|
a44ef47e1a |
fix(studio): keep useGsapTweenCache under the size cap + correct non-sticky drill test (#1770)
- extract deduplicateKeyframes + synthesizeFlatTweenKeyframes into gsapTweenSynth.ts so useGsapTweenCache.ts drops from 628 to 573 lines (600-line cap) - the drilled-into-group selection test asserted the old sticky behavior; the code is intentionally non-sticky (clicking outside exits the group and resolves the clicked element), so the test now asserts that |
||
|
|
de42af438a | chore(studio): remove debug logging (#1765) | ||
|
|
bc5a51a6ee |
fix(studio): bake the group transform into members on ungroup (#1764)
Ungroup only baked the wrapper's layout (left/top), not its GSAP transform — so a moved group's members snapped back to their creation-time positions. Distribute the group's static transform onto each member before stripping it: translation is an exact per-axis add; rotation/scale are composed about the group center so off-center members don't drift. Animated group transforms are left to be stripped, not baked. |
||
|
|
cf8f8aa568 |
fix(studio): remove keyframe dragging from the timeline (#1763)
Dragging timeline keyframe diamonds was unreliable — clip<->tween percentage remapping, an optimistic-hold workaround, and an intermittent no-op/revert when the GSAP session lagged the drag (its own comments document the flakiness). Remove the drag interaction entirely: diamonds still display, click-to-seek, and offer the context menu (add/remove/ease) — keyframe timing is edited via the playhead + panel, which are deterministic. Deletes the keyframe-move plan module + its wiring through TimelineClipDiamonds -> TimelineCanvas -> Timeline -> TimelineEditContext. |
||
|
|
2d3b19d255 |
fix(studio): keyframe commit routing for 3D and cross-group edits (#1762)
- pickBestAnimation is group-aware: a rotation/3D edit no longer merges into a position tween — a fresh same-group tween with a 0% baseline is created instead - editing at a playhead past the tween extends it and keyframes there (matches drag) - update-keyframe MERGES into the existing keyframe instead of overwriting, so editing one property no longer drops z/transformPerspective (the lens then animated from 0 and the element popped) - dragging a keyframed element with a constant position tween keyframes rather than writing a static set |
||
|
|
6a729b7e03 |
feat(studio): expand sub-composition groups + children in the timeline (#1761)
* feat(studio): element groups — source mutations Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements routes) that the studio group feature is built on. Studio UI lands in the next PR. * fix(studio): hoverable group interior + non-sticky drill-in Two group selection bugs with animated members: 1) Empty space inside a group's overlay didn't hover/select the group. Members animated outside the wrapper's static box (110px box vs 340px member union), so elementsFromPoint hit only the full-bleed background there. Add a member-union hit-test fallback: a point inside a group's live member bounds resolves to that group (innermost wins). 2) After drilling into a group and selecting a child, nothing else was selectable — out-of-scope resolved to null. Make drill-in non-sticky: interacting outside the drilled group re-resolves normally and exits the drill-in, so a later click on the group selects it as a unit again. * feat(studio): enable animation editing for static inline timelines The unsupported-pattern banner now clears for static window.__timelines["id"] = gsap.timeline() (the parser reports it editable), and the banner copy is retargeted to the genuinely-unsupported case: computed/dynamic keys (window.__timelines[var]). * fix(studio): correct keyframes + expansion for sub-composition timeline clips Two gaps for elements inside a sub-composition: 1) Clip keyframes rendered off-clip. The keyframe cache computes clip-relative percentages from the element's start/duration, but sub-comp internals aren't in the timeline elements list, so duration defaulted to 1s and percentages blew past 100%. Resolve the timing basis from the sub-comp HOST's bounds (via domClipChildren, since the host's data-composition-src is stripped in the rendered DOM). Shared resolveClipTimingBasis used by both cache populators, which now re-run when the sub-comp children appear. 2) Only GROUPED sub-comp children expanded. Generalize the DOM-children collector to gather id'd children of the sub-comp inner-root (grouped OR ungrouped), descending through id-less structural wrappers; one level into groups for drill-in. Ungrouped pills now expand into timeline rows too. |
||
|
|
2e02bcf77a |
feat(gsap): read timelines authored inline (acorn read path) (#1760)
* feat(studio): element groups — source mutations Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements routes) that the studio group feature is built on. Studio UI lands in the next PR. * fix(studio): hoverable group interior + non-sticky drill-in Two group selection bugs with animated members: 1) Empty space inside a group's overlay didn't hover/select the group. Members animated outside the wrapper's static box (110px box vs 340px member union), so elementsFromPoint hit only the full-bleed background there. Add a member-union hit-test fallback: a point inside a group's live member bounds resolves to that group (innermost wins). 2) After drilling into a group and selecting a child, nothing else was selectable — out-of-scope resolved to null. Make drill-in non-sticky: interacting outside the drilled group re-resolves normally and exits the drill-in, so a later click on the group selects it as a unit again. * feat(studio): enable animation editing for static inline timelines The unsupported-pattern banner now clears for static window.__timelines["id"] = gsap.timeline() (the parser reports it editable), and the banner copy is retargeted to the genuinely-unsupported case: computed/dynamic keys (window.__timelines[var]). |
||
|
|
1eed7a9b1f |
feat(studio): element groups — studio UI (#1759)
* feat(studio): element groups — source mutations Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements routes) that the studio group feature is built on. Studio UI lands in the next PR. * fix(studio): hoverable group interior + non-sticky drill-in Two group selection bugs with animated members: 1) Empty space inside a group's overlay didn't hover/select the group. Members animated outside the wrapper's static box (110px box vs 340px member union), so elementsFromPoint hit only the full-bleed background there. Add a member-union hit-test fallback: a point inside a group's live member bounds resolves to that group (innermost wins). 2) After drilling into a group and selecting a child, nothing else was selectable — out-of-scope resolved to null. Make drill-in non-sticky: interacting outside the drilled group re-resolves normally and exits the drill-in, so a later click on the group selects it as a unit again. |
||
|
|
4c461b4a55 |
feat(studio): element groups — source mutations (#1758)
Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements routes) that the studio group feature is built on. Studio UI lands in the next PR. |
||
|
|
b6146f8e1b | fix(core): publish runtime export artifact v0.7.15 | ||
|
|
a14d342bdb | chore: release v0.7.15 | ||
|
|
bf630bfe1e |
fix(cli): always check GitHub skills on init while skills.sh syncs (#1768)
* fix(cli): always check GitHub skills on init while skills.sh syncs The "don't pass --skip-skills" guidance lives in SKILL.md, which ships through the laggy skills.sh registry and can't be relied on to reach the agent — so an agent that improvises `--skip-skills` silently dodges the GitHub skills freshness pull. Put the guarantee in the CLI instead (the one channel that updates promptly via `npx hyperframes@latest`): - Neuter the `--skip-skills` FLAG so it no longer skips the check; gate skipping on the HYPERFRAMES_SKIP_SKILLS=1 env var instead (the agent/user CLI path never sets it). Print a one-line notice when the ignored flag is passed. - Wire the env escape hatch into the init test helper (one place) and the CI smoke-test / windows-canary steps so they stay offline and fast. - Update the skill docs that previously told agents `--skip-skills` opts out. Temporary measure while skills.sh catches up — revert init.ts's `skipSkills` to `args["skip-skills"] === true` once it does (noted inline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): build @hyperframes/lint before core in Test and Studio jobs The lint extraction (#1756) made @hyperframes/lint a runtime dependency of core — core's compiled compiler/staticGuard.js imports it via the package's "node" export condition (./dist/index.js). But the Test and Studio-load-smoke jobs pre-build only @hyperframes/{parsers,studio-server} before packages/core, so loading core's dist at test / dev-server time fails with: ERR_MODULE_NOT_FOUND: Cannot find module .../@hyperframes/lint/dist/index.js imported from .../packages/core/dist/compiler/staticGuard.js Build the canonical pre-core set @hyperframes/{parsers,lint,studio-server} (the glob the root build script uses) in both jobs so it can't drift again. The SDK job is left as-is — it builds parsers+core only and passes. Reproduced locally: removing packages/lint/dist reproduces the exact ERR_MODULE_NOT_FOUND; building lint resolves it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): address PR #1768 review — stale comment + harden offline init - Update the stale interactive-path comment that still said "Opt out with --skip-skills"; the flag is neutered, opt-out is HYPERFRAMES_SKIP_SKILLS=1. - Wrap installAllSkills in ensureSkillsCurrent with try/catch. installAllSkills is already non-strict (swallows its own failures), but since --skip-skills no longer escapes this path, every init — including offline ones that fall through to "install anyway" — runs it. The guard guarantees a skills-install failure only warns and proceeds, never breaks init. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a4853dfe6 |
refactor: extract @hyperframes/studio-server from core (#1757)
* 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. |
||
|
|
98d0bdd73c |
refactor: extract @hyperframes/lint from core (#1756)
* 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. |
||
|
|
cdf9c817e1 |
refactor: extract @hyperframes/parsers from core (#1755)
## 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 |
||
|
|
8f7fb49d16 | chore: release v0.7.14 v0.7.14 | ||
|
|
bf961d1268 |
feat(cli): skills freshness — version check, manifest, global install + multi-agent mirror (#1753)
* feat(cli): add skills version check, update, and freshness manifest
Give the HyperFrames skill bundle a content fingerprint so agents and
users can tell whether installed skills are the latest version, on any
platform that can run the CLI.
- skills-manifest.json (repo root): per-skill sha256 over the whole skill
directory; minimal {source, skills}, no version/timestamp so it is fully
deterministic. Generated by scripts/gen-skills-manifest.ts.
- `hyperframes skills check` [--json]: compares installed skills to the
manifest; exits non-zero when something is outdated (agent/CI gate).
- `hyperframes skills update`: thin wrapper over `npx skills update`.
- Passive nudge on render/lint/validate when skills are stale (24h cache,
same opt-out as the CLI self-update notice).
- "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge
GitHub raw-CDN lag, falling back to the main branch URL.
- CI job + lefthook hook keep skills-manifest.json in sync with skills/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): add execFile to child_process mock in skills test
skills.test.ts mocks node:child_process but only declared execFileSync
and spawn. Loading skills.js transitively loads skillsManifest.ts, which
runs promisify(execFile) at module load, so vitest threw on the missing
execFile named export. Add a bare stub — these tests never invoke it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init installs all skills; skills update pulls the full set
Make `hyperframes init` the single place skills are pulled in full, and
make "update" mean "get everything" rather than "refresh what's there".
- init now always installs/refreshes ALL skills (incl. ones not yet
present) instead of prompting "Install AI coding skills?" — opt out
with `init --skip-skills`. Both the interactive and non-interactive
paths pass `--all --yes` so the complete set is fetched.
- `hyperframes skills update` switches from `npx skills update` (which
only refreshes already-installed skills) to `skills add --all`, so it
installs missing skills too — the same install step init runs.
- SKILL.md documents init-installs-all and the new update semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): skills check treats missing skills as needing an update
The full skill set is now the goal (init and `skills update` both pull
all, including ones not installed), so a partial install is no longer
"a choice" — it's something to fix.
- diffSkills: updateAvailable is now true when anything is outdated OR
missing (local-only still doesn't count). So `skills check` exits
non-zero — and renders "Update:" instead of "up to date" — whenever a
skill is missing, not just when one is stale.
- The passive render/lint/validate nudge follows suit: it now counts
missing alongside outdated ("N skills out of date or missing"),
tracked via a new skillsMissingCount cache field.
- SKILL.md documents the stricter check.
Note: platforms that intentionally vendor only a subset of skills (e.g.
a Codex snapshot) will now see check report non-zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): install/update skills straight from the GitHub repo
`skills add owner/repo` can resolve through the skills.sh registry, which
lags behind the repo — so `update` could install a stale version while
`check` (which resolves latest directly from GitHub) keeps reporting
"outdated", an endless loop.
Switch the install source to the full GitHub URL
(https://github.com/heygen-com/hyperframes), which makes `skills add`
git-clone the repo directly at latest main, bypassing the registry. This
covers `hyperframes skills`, `hyperframes skills update`, and `init`'s
skill install — all of which go through SOURCES. Now install/update and
check agree on what "latest" means.
The init "install skills" hint now points at `npx hyperframes skills
update` so the manual path uses the same GitHub-direct fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init checks skills against GitHub, installs only when stale
`hyperframes init` now runs the skills version check first and only
(re)installs when something is outdated or missing — instead of
unconditionally re-pulling every time. Re-running init on an
already-current project is now a no-op ("skills are already up to date").
- New ensureSkillsCurrent() helper, shared by both the interactive and
non-interactive init paths (no duplicated install logic).
- The check resolves "latest" straight from GitHub (same source the
install uses); best-effort — if it can't reach GitHub it installs anyway.
- SKILL.md updated to describe the check-then-install behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): address skills manifest review feedback
From the PR review (points 1, 2, 4, 5):
1. Remove the `local-only` skill status. checkSkills only ever hashes
manifest-listed skills, so a local-only status could never appear in
the end-to-end output — and making it appear would wrongly flag
unrelated skills (the `.../skills` dir is shared across sources).
diffSkills now reports only on manifest skills; skills on disk that
aren't in the manifest are ignored.
2. Drop the redundant per-directory sort in listFilesSorted — the single
final out.sort() is what guarantees a deterministic hash (verified:
manifest unchanged).
4. resolveLatestManifest local-path detection now uses path.isAbsolute,
so Windows absolute paths (C:\...) are treated as local instead of
falling through to a remote fetch.
5. fetchManifest validates the response shape (asSkillsManifest) instead
of a blind `as` cast, so a CDN error page served as 200 fails with a
clear error rather than a cryptic crash later in diffSkills.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): strict skills update + auto-discover any agent host
Address PR review (Magi blocker + James/Rames robustness):
- Blocker (Magi): `skills update` is the documented recovery path for
`skills check || skills update`, but it delegated to installAllSkills()
which swallowed missing-npx and failed `skills add` as "skipped",
exiting 0 even when nothing changed. Add a strict mode that throws on
failure; update sets a non-zero exit (init stays best-effort). New tests
simulate a non-zero `skills add` (exit 1) and the success path.
- Robustness (James/Rames #2): the upstream `skills` CLI installs into
~72 agent conventions; a hard-coded list (4, or even 11) can't track
that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd +
$HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG
`.config/<host>/skills`), so detection is structural and future-proof,
no closed list. agentFromDir infers the host from the path.
- Tests (Rames #3): temp-fixture detection tests for every convention ×
{project, global}, scope priority, claude-code preference, the
no-install case, the --dir override, and an unknown/new host (proving
the no-closed-list property).
- Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip;
findRepoManifest climbs 16 levels (was 8) for deep monorepos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): resolve CodeQL file-system race + de-flake Windows npx test
Two CI fixes:
- CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the
existsSync(outPath) precheck followed by writeFileSync(outPath) is a
check-then-write race. Read the committed manifest directly in a
try/catch instead (missing/unreadable ⇒ "no committed manifest"), so
there's no precheck to race against. Behavior is unchanged.
- Windows Tests: npxCommand.test.ts's real `npx --version` smoke test
cold-starts slower than vitest's 5s default on Windows runners and
timed out. Give the test 60s headroom (and a 30s exec timeout). Kept
as a real execution check — mocking would reduce it to a tautology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): repair garbled npx smoke-test timeout comment
The explanatory comment for the 60s timeout was scrambled across the
callback/timeout arguments, failing oxfmt --check (and thus preflight,
which in turn skipped preview-parity and failed the regression gate).
Move it above the it() call so it no longer sits between call arguments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): install skills once globally + symlink-mirror to every agent
The previous install path sprayed a full ~6.7MB skill copy into each of the
~70 agent conventions `skills add --all` knows (a fresh init produced 40+
dirs / 341MB, incl. a stray dotless `agent/` from the Eve convention).
Install ONCE, globally, as one faithful copy, then symlink it everywhere:
- `skills add <url> --skill '*' --global --agent claude-code universal
--copy` lands real files in ~/.claude/skills (Claude Code reads this at
global priority) and ~/.agents/skills (the shared universal store).
- mirrorGlobalSkills() fans that store out to every OTHER installed agent's
GLOBAL dir (~/.cursor/skills, goose -> ~/.config/goose/skills, ...) — but
only for agents present on the machine (marker dir exists), so nothing is
sprayed. Unix: per-skill relative symlink into the store (one source of
truth, auto-fresh on update); Windows: copy (symlinks need admin /
Developer Mode there — the same fallback upstream and gstack make).
Why global: skills are framework-general knowledge, not project content;
Claude Code (and most agents) prioritize the personal/global scope, so the
global copy is the one actually loaded — and it installs once instead of
multiplying per project.
The per-agent dir list is GENERATED from upstream's src/agents.ts at a pinned
tag (the `skills` package exports nothing importable), committed as
agentDirs.generated.ts and resolved env-faithfully at runtime
(XDG_CONFIG_HOME / CODEX_HOME / CLAUDE_CONFIG_DIR honored). Regenerate with
`bun run --cwd packages/cli gen:agent-dirs` when the pin moves. Covers all 70
agents that define a global dir (eve/promptscript define none); the bare
project-dir agents (openclaw, astrbot) are namespaced globally, so the
stray-`agent/` footgun is gone.
`skills check` now scans global ($HOME) before project (cwd) to match the
runtime load order — so it reports on the copy the agent will really use, not
a stale project copy a newer global install silently overrides.
Test plan:
- skills.test.ts: install spawns the global --copy args, never --all; update
stays strict + exits non-zero on failure.
- skillsMirror.test.ts: Unix relative symlinks, Windows copy, XDG_CONFIG_HOME
honored, install-owned stores skipped, marker-gating, idempotent refresh,
generated-table shape.
- skillsManifest.test.ts: check is global-first.
- Full CLI suite green (981); oxlint / oxfmt / tsc clean; gen:agent-dirs
--check clean (offline + network produce byte-identical output).
- Benchmark (isolated HOME, local CLI): claude+hermes and all 70 agents —
~/.claude + ~/.agents real (19 each), every installed agent's global dir =
19 symlinks into the store, zero spray into unseeded agents, check
global-first. (The 9 "outdated" check reports are the separate skills.sh
registry lag, not this change.)
- .fallowrc.jsonc: exempt the codegen script's inherent parser complexity and
the parallel-case duplication in skillsManifest.test.ts (same rationale the
config already uses for SlideshowPanel.test.ts / hyperframes-player.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): install skills with --full-depth so a fresh install reads as current
`skills add <url>` without --full-depth fetches from the skills.sh registry
blob ("Fetching skills"), which lags GitHub main by hours — so a freshly
installed/updated set read as ~9 skills "outdated" right after install, and
`skills update` couldn't fix it (it re-fetched the same stale blob → death
loop). --full-depth switches it to a real `git clone` of HEAD ("Cloning
repository"), the only path that yields the genuine latest.
- Add --full-depth to the global install args. Verified (isolated HOME): blob
path → 10 current / 9 outdated; --full-depth → 19 current / 0 outdated.
- The clone is heavier than the blob fetch, so set GIT_LFS_SKIP_SMUDGE=1 (skills
are text; the repo's LFS objects are unrelated binaries the install doesn't
need) and raise the spawn timeout 120s → 300s.
- Correct the stale comment that claimed a full URL already bypasses skills.sh —
it doesn't; only --full-depth does.
Benchmark (skills-bench, local CLI): B.death-loop and J1.init-detect-and-refresh
flip FAIL → PASS (install/update/init now 19/0); mirror smoke reports 19 current
/ 0 outdated. (spine still reflects the raw documented `skills add <slug>`
command — the upstream skills.sh path, not this CLI.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): drop --skip-skills from workflow init so new projects refresh skills
The creation workflows scaffolded with `hyperframes init … --skip-skills`, which
skipped the skills currency check. Now that init installs globally, is a no-op
when already current, and pulls the genuine latest (via --full-depth), there's
no reason to skip it: removing --skip-skills means every new project runs the
check and refreshes the global skill set from GitHub when it's stale. Add a
one-line note to each workflow (embedded-captions, faceless-explainer,
motion-graphics, music-to-video, pr-to-video, product-launch-video) and the
hyperframes-cli + /hyperframes router explaining what init does.
skills-manifest.json regenerated by the pre-commit hook to match the edited
skill bundles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): scope agent mirror to HyperFrames' own skills, not the whole store
mirrorGlobalSkills listed every */SKILL.md in ~/.claude/skills and fanned them
out — but that store is shared, so a user's gstack / personal / company Claude
skills would get symlinked (and, since linkOrCopy removes the target first,
could overwrite a same-named skill) into Cursor / Codex / Goose / etc.
Scope the mirror to HyperFrames' own skills via the upstream lock's source
attribution — the same definition the prune already uses
(skillsAttributedToSource) — never a directory listing. New
hyperframesSkillNames() reads the global lock and returns only skills attributed
to heygen-com/hyperframes; the mirror intersects that allow-list with what's in
the store. Empty (no lock / nothing attributed) → mirror nothing, never
everything.
Also fixes the cosmetic "director(ies)" log typo (now singular/plural-aware) and
extracts the fan-out into mirrorToInstalledAgents() to keep installAllSkills
under the complexity gate.
Regression: skillsMirror.test.ts asserts a foreign gstack skill in the store is
neither mirrored out nor allowed to replace another agent's same-named skill;
the skills-bench harness seeds ~/.claude/skills/gstack and asserts it never
leaks to any agent. 1045 CLI tests + lint/types/fallow green.
Addresses Magi's request-changes on #1753.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e3edbd55cf | chore: release v0.7.13 v0.7.13 | ||
|
|
c7830b54c6 | fix(lint): catch visible css before composition root (#1751) | ||
|
|
1d3c68ef69 | chore: release v0.7.12 v0.7.12 | ||
|
|
aacce27a33 | fix(lint): allow producer-resolved google fonts (#1750) | ||
|
|
70a03f788b |
fix(cli): scope skill installs to relevant agents, not all ~70 via --all (#1748)
`hyperframes skills`, `skills update`, and `init` all shelled out to
`skills add ... --all`, which the upstream CLI expands to
`--skill '*' --agent '*' -y` — every skill into every one of the ~70 agent
conventions it knows about. A user who only runs Claude Code got skill
folders for Cursor, Codex, and 50+ tools they never touch.
Replace `--all` with a resolved target set (new resolveAgentTargets):
1. If the project already has agent skill folders (`.claude/skills`,
`.hermes/skills`, …), install ONLY to those — an existing folder is the
strongest signal of intent, so honour it exactly.
2. Otherwise (blank project):
a. Running under Claude Code (CLAUDECODE) → just claude-code.
b. Else probe PATH for installed agent CLIs (claude, hermes, droid,
cursor, codex, opencode, gemini) — the gstack approach.
c. Else fall back to claude-code + the shared `.agents` universal dir,
which Cursor, Codex, OpenCode, Gemini, Copilot and ~14 others read
from in project scope. Never `--agent '*'`.
Installs stay `--skill '*'` (all skills) + `--copy` (faithful, detectable by
`skills check`); only the agent fan-out is scoped. The dir<->key map is
explicit because dir names differ from upstream keys (`.factory`/droid,
`.hermes`/hermes-agent) and many agents share `.agents` (-> the single
`universal` key). Keys verified against vercel-labs/skills@v1.5.13.
Verified end-to-end in an isolated project + HOME: the new args land exactly
`.claude/skills` (19) + `.agents/skills` (19) and nothing else — 2 folders,
not 50+. Pure resolver covered by skillsTargets.test.ts; command-arg shape and
the "never --all" regression pinned in skills.test.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
05af482f22 |
feat(skills): product-launch-video skill + consolidate motion knowledge into hyperframes-animation (#1745)
* feat(skills): product-launch-video + consolidate motion knowledge into hyperframes-animation
- Add the product-launch-video skill: shot-sequence architecture where each
visual frame is a time-coded shot sequence picked from a blueprint menu and
paced to the voiceover (anti-PowerPoint). Includes the frame-worker sub-agent,
story/visual/motion-design references, and audio/captions/transitions/
stage-assets/assemble-index scripts.
- Consolidate motion knowledge in hyperframes-animation as the single source of
truth: promote the updated atomic rules (31 -> 36) and rename product-launch-
video's archetypes into hyperframes-animation blueprints (13 -> 15, replacing
the old set). product-launch-video, faceless-explainer, and pr-to-video now
reference them via ../hyperframes-animation/{rules-index,blueprints-index}.md
and the rules/blueprints dirs. Fixes the discrete-text-sequence broken links;
blueprints no longer ship per-id runnable examples, so example references in
the consumers were dropped.
- Default HeyGen TTS voice to Marcia (deterministic; was the API's first English
voice, which drifts on catalog re-sort). Override with --voice.
- assemble-index pre-assembly frame guards: auto-repair a sub-comp root missing
canvas dims; hard-fail on <video>/<audio> inside a sub-comp; hard-fail on a
timed non-root element missing class="clip" or overlapping same-track clips.
- Lint/CLI: lint media inside sub-compositions as an error; stop false-positive
caption layout/lint findings; contrast/layout-audit skip elements hidden by an
invisible ancestor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(skills): clear CodeQL alerts in assemble-index.mjs
- script/style blanking regex now matches closing tags with trailing
whitespace (</script >, </style >) — js/bad-tag-filter (high).
- drop the existsSync precheck before reading/repairing a frame file; read
directly and handle ENOENT, removing the check->write TOCTOU window —
js/file-system-race (high).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
413d8187fd | chore: release v0.7.11 v0.7.11 | ||
|
|
51f8231eb5 |
fix(cli): install skills with --copy so check sees a faithful, correctly-placed set (#1744)
`hyperframes init` / `skills` / `skills update` shelled out to `skills add` without `--copy`, so the upstream CLI installed via its canonical `.agents/skills` store + per-agent symlinks. That layout re-serialises each SKILL.md's frontmatter, so an installed bundle no longer byte-matches the published manifest — `skills check` reported a freshly-installed set as outdated — and it didn't reliably land in the dir the agent reads (e.g. `.claude/skills`). Pass `--copy` in runSkillsAdd (the single chokepoint every install flows through: bare `skills`, `update`, and `init` via installAllSkills) so real files are written into each agent's skills dir — faithful to the manifest and correctly placed. Verified end-to-end: a fresh-content install now reads all-current in `.claude/skills` (was all-outdated in `agent/skills`). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c9e8dd3862 | fix(runtime): honor render fps when seeking (#1739) | ||
|
|
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> |
||
|
|
d70ee134cc |
feat(cli): add skills version check, update, and freshness manifest (#1738)
* feat(cli): add skills version check, update, and freshness manifest
Give the HyperFrames skill bundle a content fingerprint so agents and
users can tell whether installed skills are the latest version, on any
platform that can run the CLI.
- skills-manifest.json (repo root): per-skill sha256 over the whole skill
directory; minimal {source, skills}, no version/timestamp so it is fully
deterministic. Generated by scripts/gen-skills-manifest.ts.
- `hyperframes skills check` [--json]: compares installed skills to the
manifest; exits non-zero when something is outdated (agent/CI gate).
- `hyperframes skills update`: thin wrapper over `npx skills update`.
- Passive nudge on render/lint/validate when skills are stale (24h cache,
same opt-out as the CLI self-update notice).
- "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge
GitHub raw-CDN lag, falling back to the main branch URL.
- CI job + lefthook hook keep skills-manifest.json in sync with skills/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): add execFile to child_process mock in skills test
skills.test.ts mocks node:child_process but only declared execFileSync
and spawn. Loading skills.js transitively loads skillsManifest.ts, which
runs promisify(execFile) at module load, so vitest threw on the missing
execFile named export. Add a bare stub — these tests never invoke it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init installs all skills; skills update pulls the full set
Make `hyperframes init` the single place skills are pulled in full, and
make "update" mean "get everything" rather than "refresh what's there".
- init now always installs/refreshes ALL skills (incl. ones not yet
present) instead of prompting "Install AI coding skills?" — opt out
with `init --skip-skills`. Both the interactive and non-interactive
paths pass `--all --yes` so the complete set is fetched.
- `hyperframes skills update` switches from `npx skills update` (which
only refreshes already-installed skills) to `skills add --all`, so it
installs missing skills too — the same install step init runs.
- SKILL.md documents init-installs-all and the new update semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): skills check treats missing skills as needing an update
The full skill set is now the goal (init and `skills update` both pull
all, including ones not installed), so a partial install is no longer
"a choice" — it's something to fix.
- diffSkills: updateAvailable is now true when anything is outdated OR
missing (local-only still doesn't count). So `skills check` exits
non-zero — and renders "Update:" instead of "up to date" — whenever a
skill is missing, not just when one is stale.
- The passive render/lint/validate nudge follows suit: it now counts
missing alongside outdated ("N skills out of date or missing"),
tracked via a new skillsMissingCount cache field.
- SKILL.md documents the stricter check.
Note: platforms that intentionally vendor only a subset of skills (e.g.
a Codex snapshot) will now see check report non-zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): install/update skills straight from the GitHub repo
`skills add owner/repo` can resolve through the skills.sh registry, which
lags behind the repo — so `update` could install a stale version while
`check` (which resolves latest directly from GitHub) keeps reporting
"outdated", an endless loop.
Switch the install source to the full GitHub URL
(https://github.com/heygen-com/hyperframes), which makes `skills add`
git-clone the repo directly at latest main, bypassing the registry. This
covers `hyperframes skills`, `hyperframes skills update`, and `init`'s
skill install — all of which go through SOURCES. Now install/update and
check agree on what "latest" means.
The init "install skills" hint now points at `npx hyperframes skills
update` so the manual path uses the same GitHub-direct fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init checks skills against GitHub, installs only when stale
`hyperframes init` now runs the skills version check first and only
(re)installs when something is outdated or missing — instead of
unconditionally re-pulling every time. Re-running init on an
already-current project is now a no-op ("skills are already up to date").
- New ensureSkillsCurrent() helper, shared by both the interactive and
non-interactive init paths (no duplicated install logic).
- The check resolves "latest" straight from GitHub (same source the
install uses); best-effort — if it can't reach GitHub it installs anyway.
- SKILL.md updated to describe the check-then-install behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): address skills manifest review feedback
From the PR review (points 1, 2, 4, 5):
1. Remove the `local-only` skill status. checkSkills only ever hashes
manifest-listed skills, so a local-only status could never appear in
the end-to-end output — and making it appear would wrongly flag
unrelated skills (the `.../skills` dir is shared across sources).
diffSkills now reports only on manifest skills; skills on disk that
aren't in the manifest are ignored.
2. Drop the redundant per-directory sort in listFilesSorted — the single
final out.sort() is what guarantees a deterministic hash (verified:
manifest unchanged).
4. resolveLatestManifest local-path detection now uses path.isAbsolute,
so Windows absolute paths (C:\...) are treated as local instead of
falling through to a remote fetch.
5. fetchManifest validates the response shape (asSkillsManifest) instead
of a blind `as` cast, so a CDN error page served as 200 fails with a
clear error rather than a cryptic crash later in diffSkills.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): strict skills update + auto-discover any agent host
Address PR review (Magi blocker + James/Rames robustness):
- Blocker (Magi): `skills update` is the documented recovery path for
`skills check || skills update`, but it delegated to installAllSkills()
which swallowed missing-npx and failed `skills add` as "skipped",
exiting 0 even when nothing changed. Add a strict mode that throws on
failure; update sets a non-zero exit (init stays best-effort). New tests
simulate a non-zero `skills add` (exit 1) and the success path.
- Robustness (James/Rames #2): the upstream `skills` CLI installs into
~72 agent conventions; a hard-coded list (4, or even 11) can't track
that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd +
$HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG
`.config/<host>/skills`), so detection is structural and future-proof,
no closed list. agentFromDir infers the host from the path.
- Tests (Rames #3): temp-fixture detection tests for every convention ×
{project, global}, scope priority, claude-code preference, the
no-install case, the --dir override, and an unknown/new host (proving
the no-closed-list property).
- Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip;
findRepoManifest climbs 16 levels (was 8) for deep monorepos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): resolve CodeQL file-system race + de-flake Windows npx test
Two CI fixes:
- CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the
existsSync(outPath) precheck followed by writeFileSync(outPath) is a
check-then-write race. Read the committed manifest directly in a
try/catch instead (missing/unreadable ⇒ "no committed manifest"), so
there's no precheck to race against. Behavior is unchanged.
- Windows Tests: npxCommand.test.ts's real `npx --version` smoke test
cold-starts slower than vitest's 5s default on Windows runners and
timed out. Give the test 60s headroom (and a 30s exec timeout). Kept
as a real execution check — mocking would reduce it to a tautology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): repair garbled npx smoke-test timeout comment
The explanatory comment for the 60s timeout was scrambled across the
callback/timeout arguments, failing oxfmt --check (and thus preflight,
which in turn skipped preview-parity and failed the regression gate).
Move it above the it() call so it no longer sits between call arguments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0c1e236dcd | chore: release v0.7.10 (#1734) v0.7.10 | ||
|
|
dca5fa90cb | fix(producer): reject impossible distributed durations (#1732) | ||
|
|
7db84fc0ad |
fix(producer): rebuildExtractedFramesFromPlanDir off-by-one in framePaths key indexing (#1730)
* fix(producer): rebuildExtractedFramesFromPlanDir off-by-one in framePaths key indexing In distributed chunk-lambda render mode, every <video>'s first-paint frame (the moment a vid first becomes visible on the composition timeline) renders as PRISTINE Y=16 black. For a 3-vid back-to-back composition (v1: 0-4s, v2: 4-8s, v3: 8-12s at 30fps), frames 0, 121, 242 are all PRISTINE black; the render then either stays black for 1 frame, or shows body bg + persistent overlays only (Y~22 with sparse highlights from text/logo). The symptom only reproduces in distributed mode — local single-process renders are unaffected. Root cause: rebuildExtractedFramesFromPlanDir builds the framePaths Map with 1-based keys, but the consumer (getFrameAtTime at engine/videoFrameExtractor.ts:958) computes a 0-based frame index via Math.floor(localTime * fps + 1e-9). For each vid's first-paint frame (localTime === 0 → frameIndex === 0), framePaths.get(0) returns undefined; the vid is silently dropped from activePayloads, videoFrameInjector doesn't fire, syncVideoFrameVisibility hides everything, and BeginFrame screenshots an empty composition. Every other site in the codebase builds/consumes framePaths with 0-based keys: - engine/videoFrameExtractor.ts:317 framePaths.set(index, ...) - engine/extractionCache.ts:204 framePaths.set(idx, ...) - engine/videoFrameExtractor.test.ts:264/1066 framePaths.set(i, ...) - engine/videoFrameExtractor.ts:958 (consumer) Math.floor 0-based - producer/renderOrchestrator.test.ts:287/315/349 framePaths.get(0) Only producer/distributed/renderChunk.ts:198 was 1-based, with a stale comment claiming FrameLookupTable indexes frames 1-based — which the surrounding evidence contradicts. This is why local tests pass while distributed-lambda renders always had cold black at each vid first paint. Verified locally against a 3-vid composition and a single-vid 4-worker case in a Lambda render fleet. Before fix: every vid first-paint frame is PRISTINE Y=16 black. After fix: all frames are valid source content, blackdetect reports zero black regions outside legitimate source video content (intentional fade-ins / hard cuts in source mp4). * test(producer): pin rebuildExtractedFramesFromPlanDir 0-based framePaths contract Regression guard for the off-by-one fix in HF#1730. The pre-fix code indexed framePaths 1-based while the consumer (getFrameAtTime in engine/videoFrameExtractor.ts:958) reads 0-based, dropping every <video>'s first-paint frame in distributed chunk-lambda renders. Asserts framePaths.get(0) resolves to the first extracted frame, and framePaths.get(N-1) resolves to the last — pre-fix the keys were shifted to [1..N], so get(0) returned undefined and get(N) resolved. Verified to fail against the previous i+1 indexing. Also exports rebuildExtractedFramesFromPlanDir (was module-local) so the test can call it directly. Pure logic worth testing in isolation — the bug only reproduces under distributed mode and the existing renderChunk.test.ts already pays a multi-second Chrome smoke probe in its module-level beforeAll, so the regression check lives in its own file (rebuildExtractedFrames.test.ts) and runs Chrome-free in ~10ms. The function's doc comment said "1-based framePaths" — updated to "0-based" with a pointer to the consumer site and the bug context. Per Miguel's REQUEST_CHANGES on HF#1730. — Jerrai (https://claude.com/claude-code) --------- Co-authored-by: James <james.russo@heygen.com> |
||
|
|
8db190dd43 | chore: release v0.7.9 v0.7.9 | ||
|
|
226a741c9d |
fix(studio): Enable keyframes works (and auto-tracks) on elements with a global gsap.set (#1728)
* fix(core): a global gsap.set is off-timeline, so resolvedStart is 0 not the comp end resolveTimelinePositions walks anims in document order advancing a cursor; a global `gsap.set(...)` carries no position arg, so it fell through to the cursor fallback and inherited the comp-end time (every prior tween's duration summed) as its resolvedStart. A global set is a load-time hold — its start is 0. This silently broke 'Enable keyframes' on any element whose only animation is a global gsap.set (e.g. a statically-positioned card): promoteSetToKeyframes bails when `playhead <= setStart`, and setStart was the comp end, so any playhead before the end was a no-op. Pin a global set to resolvedStart 0 in both the recast and acorn parsers; don't let it advance the cursor/prevStart. * fix(studio): Enable-keyframes marks the generated 0% endpoint as auto-tracking When 'Enable keyframes' promotes a static set to a two-stop tween, the 0% (the held start the user didn't choose) is now marked `auto: true` → serialized as the `_auto: 1` marker. The parser's endpoint-sync then keeps it tracking the nearest keyframe until the user edits it directly; the 100% (the real keyframe placed at the playhead) stays fixed. This re-wires the auto-endpoint behavior that was silently dropped in #1605 (the sync logic stayed, but no flow produced an `_auto` endpoint anymore, so an untouched 0% never tracked). Adds a guard test so it can't be lost again. * chore: suppress fallow complexity findings surfaced in the touched files The resolveTimelinePositions guard added a branch (pushes it over threshold), and changed-file scope re-surfaces pre-existing complex functions (readElementPosition, applyArcWaypointAtPlayhead, the useEnableKeyframes callback). Bare directives only. * fix(studio): dragging a --hf-studio-offset element no longer flies Dragging a static element positioned via the legacy --hf-studio-offset CSS var (e.g. dot-a) flew off-screen — three independent failure modes, all fixed: 1. Live drag integrated: the per-move draft read its base from the live transform it set last frame (gsap.getProperty), so base+delta accumulated frame-over-frame. Fix: carry a stable baseGsap on the in-memory drag member (immune to mid-drag re-renders that wipe the data-hf-drag-* attrs) and use it as the fallback. 2. Commit re-added the delta: the source commit re-read the wiped attrs / live transform. Fix: re-stamp the stable base/initial attrs in applyManualOffsetDragCommit before the commit reads them. 3. Drop left it offset: the committed source was correct, but the LIVE element kept its --hf-studio-offset var + translate:var(...), which composed with the GSAP transform (rendered at dropped + offset) until a full reload. Fix: on cleanup, when GSAP owns the position, clearStudioPathOffset() migrates the element off the legacy CSS channel (leaving transform untouched) — matching the stripped source. Adds a regression suite covering all three layers. |
||
|
|
bafed1fc9a |
fix(core): lint leaked head text in compositions (#1727)
* fix(core): lint leaked head text in compositions * fix(core): harden leaked head text lint * chore: refresh code scanning status * fix(core): cover parser-error head close tags |
||
|
|
92385711dc |
fix(engine): hold last frame when a clip's media is shorter than its slot (#1726)
Renders showed the page background (a one-frame black flash) right before a cut when a video clip's source media was a hair shorter than its data-duration slot — the common case, since `ffmpeg -t 1.45` emits 43 frames = 1.433s at 30fps. The frame lookup only held the last frame at the exact clip end, so the sub-frame remainder rendered blank. - Hold the last extracted frame for the rest of the slot once the source is exhausted, within a tolerance floored at the compiler's 0.05s clamp epsilon so the seam is covered at any fps (2 frames alone is < 0.05s above 40fps). Clips deliberately much shorter than their slot still blank for the tail (unchanged). - Warn when the compiler clamps a video's data-duration down to its media length (slot longer than source by more than the clamp epsilon): a render-time `[compile]` warning in the producer, plus a matching `validate` warning that reads each <video>'s live duration in headless Chrome (static HTML lint can't see media durations). A shared `analyzeClipMediaFit` keeps both on one threshold. Adds engine unit tests for the hold behavior and the analyzer. |