commit 85453da49fa26de4c12f2f9a4e21232ea8dc2ce2 Author: wehub-resource-sync Date: Mon Jul 13 12:58:35 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..16b0e67 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,17 @@ +{ + "name": "hyperframes", + "owner": { + "name": "HeyGen", + "email": "hyperframes@heygen.com", + "url": "https://hyperframes.heygen.com" + }, + "plugins": [ + { + "name": "hyperframes", + "source": "./", + "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "homepage": "https://hyperframes.heygen.com", + "license": "Apache-2.0" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..0ccd995 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "hyperframes", + "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "version": "0.7.55", + "author": { + "name": "HeyGen", + "email": "hyperframes@heygen.com", + "url": "https://hyperframes.heygen.com" + }, + "homepage": "https://hyperframes.heygen.com", + "repository": "https://github.com/heygen-com/hyperframes", + "license": "Apache-2.0" +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..684c524 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,48 @@ +{ + "name": "hyperframes", + "version": "0.7.55", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "author": { + "name": "HeyGen", + "email": "hyperframes@heygen.com", + "url": "https://hyperframes.heygen.com" + }, + "homepage": "https://hyperframes.heygen.com", + "repository": "https://github.com/heygen-com/hyperframes", + "license": "Apache-2.0", + "keywords": [ + "hyperframes", + "video", + "html", + "tailwind", + "gsap", + "lottie", + "three", + "waapi", + "animejs", + "animation", + "composition", + "rendering", + "captions", + "tts", + "audio-reactive" + ], + "skills": "./skills/", + "interface": { + "displayName": "HyperFrames by HeyGen", + "shortDescription": "Write HTML, render video", + "longDescription": "Build videos from HTML with HyperFrames. Author compositions with HTML, CSS, Tailwind v4 browser-runtime styles, GSAP, Anime.js, Lottie, Three.js, and WAAPI adapter patterns, use the CLI for the dev loop (init/preview/render), preprocess assets (tts/transcribe/remove-background) for compositions, install reusable registry blocks and components, and turn any website into a video with the 7-step capture-to-video pipeline.", + "developerName": "HeyGen", + "category": "Creativity", + "capabilities": ["Read", "Write"], + "websiteURL": "https://hyperframes.heygen.com", + "defaultPrompt": [ + "Turn this website into a 20-second product promo", + "Create an animated title card with kinetic type", + "Add synced captions to this voiceover" + ], + "brandColor": "#0a0a0a", + "composerIcon": "./assets/icon.png", + "logo": "./assets/logo.png" + } +} diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..31a623a --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\\\s+commit\\\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const cwd = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try { execSync(script, { cwd, stdio: 'pipe' }); }\n catch (e) {\n failures.push(label + ':\\\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n continue: false,\n stopReason: '\\u274c Pre-commit checks failed:\\\\n\\\\n' + failures.join('\\\\n\\\\n') + '\\\\n\\\\nFix the issues above before committing.',\n }));\n }\n});\"", + "timeout": 180, + "statusMessage": "Running build + lint + typecheck before commit…" + } + ] + } + ] + } +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..2e4be5f --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://cursor.com/schemas/cursor-plugin/plugin.json", + "name": "hyperframes", + "displayName": "HyperFrames by HeyGen", + "version": "0.7.55", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "author": { + "name": "HeyGen", + "email": "hyperframes@heygen.com" + }, + "publisher": "HeyGen", + "homepage": "https://hyperframes.heygen.com", + "repository": "https://github.com/heygen-com/hyperframes", + "license": "Apache-2.0", + "logo": "assets/logo.png", + "category": "developer-tools", + "keywords": [ + "hyperframes", + "video", + "html", + "tailwind", + "gsap", + "lottie", + "three", + "waapi", + "animejs", + "animation", + "composition", + "rendering", + "captions", + "tts", + "audio-reactive" + ], + "tags": ["video", "animation", "design", "creative", "workflow"], + "skills": "./skills/" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b85803 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# No environment variables required for basic usage. +# Run `bun run dev` to start the studio, `npx hyperframes render` to render video. + +# Optional integrations: +# GEMINI_API_KEY= # AI image captioning during website capture (~$0.001/image, https://aistudio.google.com/apikey) diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc new file mode 100644 index 0000000..7fbf76b --- /dev/null +++ b/.fallowrc.jsonc @@ -0,0 +1,624 @@ +{ + "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/config-schema.json", + "entry": [ + "packages/producer/src/**/*.test.ts", + "packages/aws-lambda/src/**/*.test.ts", + "packages/gcp-cloud-run/src/**/*.test.ts", + "packages/producer/src/regression-harness.ts", + "packages/producer/src/regression-harness-distributed.test.ts", + "packages/producer/src/regression-harness-lambda-local.ts", + "packages/producer/src/transparency-test.ts", + "packages/producer/src/parity-harness.ts", + "packages/producer/src/parity-fixtures.ts", + "packages/producer/src/perf-gate.ts", + "packages/producer/src/runtime-conformance.ts", + "packages/producer/src/benchmark.ts", + "packages/producer/scripts/generate-font-data.ts", + "packages/producer/we-render.mjs", + "packages/producer/scripts/validate-fast-video.ts", + "packages/cli/scripts/generate-font-data.ts", + "packages/engine/scripts/test-fitTextFontSize-browser.ts", + "packages/aws-lambda/scripts/*.ts", + // Built as standalone IIFE for the browser-side sandbox runtime; + // referenced by file path (not import) in build-hyperframes-runtime-artifact.ts. + "packages/core/src/runtime/entry.ts", + // Bundled as a standalone IIFE by the position-edits render artifact + // generator; referenced by file path rather than imported by TypeScript. + "packages/core/stubs/position-edits-render-entry.ts", + // In-page audit scripts read as raw strings and injected via + // page.addScriptTag (see layout.ts / validate.ts) — referenced by file + // path, never imported, so they have no import-graph referrer. + "packages/cli/src/commands/layout-audit.browser.js", + "packages/cli/src/commands/contrast-audit.browser.js", + "packages/cli/src/commands/motion-sample.browser.js", + // Worker entry points loaded dynamically by their *Pool.ts companions. + "packages/producer/src/services/pngDecodeBlitWorker.ts", + "packages/producer/src/services/shaderTransitionWorker.ts", + // Off-main-thread /health endpoint, spawned by path from healthWorker.ts. + "packages/producer/src/services/healthWorkerThread.ts", + // Test fixture worker, spawned by path via the pools' workerEntryPath + // option from the crash-recovery tests; has no import-graph referrer. + "packages/producer/src/services/__fixtures__/crashOnMessageWorker.mjs", + "scripts/*.{ts,mjs,js}", + "scripts/*/run.mjs", + // Keyframe UI components — wired dynamically via EaseCurveSection/MotionPanel. + "packages/studio/src/components/editor/KeyframeDiamond.tsx", + "packages/studio/src/components/editor/SpringEaseEditor.tsx", + // NLE notice — rendered conditionally via NLELayout when timeline is first shown. + "packages/studio/src/components/nle/TimelineEditorNotice.tsx", + // Zoom hook extracted for downstream razor-blade PRs (#1330, #1331). + "packages/studio/src/player/components/useTimelineZoom.ts", + // Cached O(1) GSAP target lookup, replacing O(n²) inline checks. + // Consumers migrate in a follow-up once useDomGeometryCommits adopts it. + "packages/studio/src/hooks/gsapTargetCache.ts", + // Preview helper consumed dynamically from the studio iframe bridge. + "packages/studio/src/hooks/gsapRuntimePreview.ts", + // TEMP(studio-dnd): shipped unwired ahead of the NLE integration; + // the app-shell swap PR (studio-dnd/pr22) wires the consumers and removes this block. + "packages/studio/src/components/editor/CanvasContextMenu.tsx", + ], + "ignorePatterns": [ + "docs/**", + // Chrome browser binaries downloaded by puppeteer — not project source. + "chrome/**", + // Standalone spike scripts and benchmark runners used during R&D. + "packages/engine/spikes/**", + "packages/producer/de-*.mjs", + "packages/producer/tests/**", + "packages/player/tests/**", + "packages/engine/tests/**", + "skills/**/test-corpus/**", + "skills/**/scripts/**", + // Agent-invoked motion-graphics tools co-located with their docs (run via + // `node ` per grounding/PROTOCOL.md / categories/maps/module.md + // prose), not import-graph reachable. + "skills/motion-graphics/grounding/**", + "skills/motion-graphics/categories/**", + // Agent-invoked reference materials (template + motion-primitive HTML, catalogs), + // forked by path by the frame-worker per SKILL.md prose, not import-graph reachable. + "skills/music-to-video/references/**", + // Bundled @font-face data (read at runtime via fs.readFileSync, invisible + // to the import graph) + its manual rebuild tool. + "skills/**/fonts/**", + // Golden snapshot files: data consumed by toMatchFileSnapshot, not importable modules. + "packages/**/__goldens__/**", + "registry/**", + "examples/**", + "packages/sdk/examples/**", + ".github/workflows/fixtures/**", + // Auto-generated TS client for the HeyGen cloud API. Regenerated by + // experiment-framework/scripts/generate_hyperframes_cli_client.py via + // the sync-hyperframes-codegen.yml workflow; complexity/dead-code + // findings on this file are not actionable from this repo. + "packages/cli/src/cloud/_gen/**", + ], + "ignoreExports": [ + // TEMP(studio-dnd): consumers land later in the stack; removed by studio-dnd/pr22. + { + "file": "packages/studio/src/components/editor/canvasContextMenuZOrder.ts", + "exports": ["readEffectiveZIndex"], + }, + // drawElementService is the bottom of the fast-capture Graphite stack + // (#1917): its consumers (frameCapture in #1919) land two PRs upstack, so + // a per-PR audit diffing against the merge base sees these exports as + // unused. Consumed for real once the stack merges; safe to drop this + // entry after #1919 lands. + { + "file": "packages/engine/src/services/drawElementService.ts", + "exports": [ + "instrumentAcceleratedCanvases", + "injectDrawElementCanvas", + "captureDrawElementFrame", + "initDrawElementWorkerEncode", + "cleanupDrawElementWorkerEncode", + "produceDrawElementFrame", + ], + }, + // CLI command files: every command exports a const `examples` per the + // convention documented in CLAUDE.md. This is a namespace barrel, not a + // collision. + { "file": "packages/cli/src/commands/*.ts", "exports": ["examples"] }, + // Options type for the render-queue hook's public startRender callback — + // consumed structurally by callers (StudioRightPanel), not by import. + { + "file": "packages/studio/src/components/renders/useRenderQueue.ts", + "exports": ["StartRenderOptions"], + }, + // Independent ML model managers each declare their own DEFAULT_MODEL / + // MODELS_DIR / ensureModel for their model namespace. + { + "file": "packages/cli/src/{background-removal,tts,whisper}/manager.ts", + "exports": ["DEFAULT_MODEL", "MODELS_DIR", "ensureModel"], + }, + // `isPathInside` is documented as exported-for-tests only in fileServer.ts; + // it has different semantics (symlink resolution) from utils/paths.ts. + { + "file": "packages/producer/src/services/fileServer.ts", + "exports": ["isPathInside"], + }, + // Studio telemetry: consumed by useRenderQueue.ts / StudioFeedbackBar.tsx + // (deep relative imports) but fallow's static analyzer doesn't trace + // them. Same path-resolution quirk — trackStudioSessionStart from the + // same file resolves fine. + { + "file": "packages/studio/src/telemetry/events.ts", + "exports": ["trackStudioRenderStart", "trackStudioFeedback"], + }, + // domEditingLayers: these exports are consumed via the browser iframe + // runtime context (not traceable by static import analysis from the + // studio entry point) or re-exported through the domEditing barrel but + // have no downstream consumers yet. + { + "file": "packages/studio/src/components/editor/domEditingLayers.ts", + "exports": [ + "isEditableTextLeaf", + "collectDomEditTextFields", + "buildElementLabel", + "refreshDomEditSelection", + ], + }, + // domEditing barrel: re-exports consumed throughout the studio but + // fallow's static analyzer can't trace re-exports through barrel files. + { + "file": "packages/studio/src/components/editor/domEditing.ts", + "exports": ["*"], + }, + // Exported for render.test.ts (exported-for-tests pattern). + { + "file": "packages/cli/src/commands/render.ts", + "exports": ["resolveBrowserGpuForCli", "renderLocal", "checkRenderResolutionPreflight"], + }, + // initThreeDProjectionInPage: passed to page.evaluate() for browser-side execution — + // Puppeteer serializes it at runtime, not an import-graph consumer. + { + "file": "packages/engine/src/services/threeDProjection.ts", + "exports": ["initThreeDProjectionInPage"], + }, + // captureCost.ts: constants and helpers consumed by the runCaptureCalibration + // orchestration function and tests, but the entry-point graph doesn't + // reach them because the orchestrator's caller resolves them dynamically. + { + "file": "packages/producer/src/services/render/captureCost.ts", + "exports": [ + "CAPTURE_CALIBRATION_TARGET_MS", + "MAX_MEASURED_CAPTURE_COST_MULTIPLIER", + "CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS", + "measureCaptureCostFromSession", + "logCaptureCalibrationResult", + "createFailedCaptureCalibrationEstimate", + ], + }, + // gsapParserExports.ts is the public-API barrel that re-exports constants, + // types, and utilities from gsapConstants, gsapSerialize, and springEase. + // The re-exports are intentional public API consumed by callers outside the + // changed-file set (e.g. studio, aws-lambda) and therefore appear unused + // to fallow's static analysis of the PR diff. + { + "file": "packages/parsers/src/gsapParserExports.ts", + "exports": [ + "PROPERTY_GROUPS", + "classifyPropertyGroup", + "classifyTweenPropertyGroup", + "SPRING_PRESETS", + "generateSpringEaseData", + "GsapMethod", + "GsapKeyframesData", + "GsapKeyframeFormat", + "PropertyGroupName", + "SpringPreset", + ], + }, + // Shared test helpers consumed by gsapParser.test.ts (same file, + // fallow doesn't trace intra-file test consumption). + { + "file": "packages/parsers/src/gsapParser.test-helpers.ts", + "exports": [ + "expectKeyframe", + "expectKeyframesFormat", + "convertAndReparse", + "parseSplitAndAssert", + ], + }, + // hfIds: EXCLUDED_TAGS is consumed by tests (htmlParser.test.ts) and + // hfIdPersist.ts but fallow's static analyzer may not trace all consumers. + { + "file": "packages/parsers/src/hfIds.ts", + "exports": ["EXCLUDED_TAGS", "mintHfId"], + }, + // Shared timeline components extracted for downstream PRs in the + // razor-blade stack (#1330, #1331). Consumers live on those branches. + { + "file": "packages/studio/src/player/components/timelineCallbacks.ts", + "exports": ["*"], + }, + // Shared timeline DOM barrel for downstream consumers; fallow does not + // trace all re-export-only modules. + { + "file": "packages/studio/src/player/lib/timelineDOM.ts", + "exports": ["*"], + }, + // gsapTargetCache: cached O(1) GSAP target lookup, consumed by + // useDomEditCommits and intended to replace the local copy in + // useDomGeometryCommits once callers migrate. + { + "file": "packages/studio/src/hooks/gsapTargetCache.ts", + "exports": ["isElementGsapTargeted"], + }, + // Re-exports from useDomEditCommits: barrel-style re-exports + // consumed by downstream studio code. + { + "file": "packages/studio/src/hooks/useDomEditCommits.ts", + "exports": ["GSAP_CSS_FALLBACK_BLOCKED_MESSAGE", "PersistDomEditOperations"], + }, + { + "file": "packages/studio/src/utils/timelineElementSplit.ts", + "exports": ["buildPatchTarget", "readFileContent"], + }, + // freezeUrl and freezeLocalFile are public API re-exported from the figma + // barrel (index.ts) for Task 4 manifest flow and the /figma skill integration; + // not yet imported by current code outside the module. + { + "file": "packages/core/src/figma/freeze.ts", + "exports": ["freezeUrl", "freezeLocalFile"], + }, + // mediaDir, typeDirPath, isFigmaManifestRecord: re-exported from the figma + // barrel (index.ts) via manifest.ts per Task 8 wiring. Consumed only by the + // /figma skill integration, not by code in the current codebase. + { + "file": "packages/core/src/figma/manifest.ts", + "exports": ["mediaDir", "typeDirPath", "isFigmaManifestRecord"], + }, + ], + "ignoreDependencies": [ + // Runtime/dynamic deps not visible to static analysis: tsup `external`, + // dynamic require() resolution, peer/static-file consumption in tests, + // and bun-hoisted workspace devDeps (e.g. happy-dom in root package.json + // resolves for every workspace, so workspaces don't redeclare it). + // Required by @puppeteer/browsers and puppeteer-core at runtime; listed + // as a direct dep to guarantee installation even when transitive + // resolution fails (corrupted cache, dedup edge cases). + "debug", + "puppeteer", + "puppeteer-core", + "esbuild", + "giget", + "gsap", + "happy-dom", + "ffmpeg-static", + "ffprobe-static", + "@hyperframes/core", + "@hyperframes/studio", + "@hyperframes/producer", + "@fontsource/archivo-black", + "@fontsource/eb-garamond", + "@fontsource/ibm-plex-mono", + "@fontsource/inter", + "@fontsource/jetbrains-mono", + "@fontsource/league-gothic", + "@fontsource/montserrat", + "@fontsource/nunito", + "@fontsource/oswald", + "@fontsource/outfit", + "@fontsource/space-mono", + "@fontsource/lato", + "@fontsource/noto-sans-jp", + "@fontsource/open-sans", + "@fontsource/playfair-display", + "@fontsource/poppins", + "@fontsource/roboto", + "@fontsource/source-code-pro", + ], + "duplicates": { + // Raise from the default 5 to 6 lines so trivially short Hono route-handler + // preambles (resolveProject + 404 + body-parse) are below the threshold. + // The three 5-line groups in files.ts / render.ts are structural boilerplate + // that naturally converges and is unlikely to diverge; extraction would + // require intrusive middleware changes beyond this PR's scope. + "minLines": 6, + "ignore": [ + // sourcePatcher.ts: pre-existing internal clones between the inline-style + // and attribute tag-patchers; only the PatchOperation type gained two + // optional fields here, but the line shift makes fallow re-flag them. + "packages/studio/src/utils/sourcePatcher.ts", + // gsapParser.ts: recast/babel GSAP writer — intentional duplication between + // recast and acorn parallel implementations (pre-existing, moved from core). + "packages/parsers/src/gsapParser.ts", + // hfIds.ts: 7-line clone with sdk/engine/mutate.ts — pre-existing duplication + // from when hfIds lived in packages/core/src/parsers/. Moving the file to the + // new package makes fallow see it as a fresh finding; the underlying clone + // predates this refactor. + "packages/parsers/src/hfIds.ts", + // Parser test files: parallel arrange/act/assert test cases — pre-existing + // duplication moved from packages/core/src/parsers/. + "packages/parsers/src/gsapParser.test.ts", + "packages/parsers/src/gsapParser.test-helpers.ts", + "packages/parsers/src/gsapWriter.parity.test.ts", + "packages/parsers/src/gsapWriterParity.corpus.test.ts", + "packages/parsers/src/gsapWriterParity.acorn.test.ts", + "packages/parsers/src/htmlParser.roundtrip.test.ts", + "packages/parsers/src/htmlParser.test.ts", + // @hyperframes/studio-server test files: parallel arrange/act/assert test cases + // (pre-existing structure from when studio-api lived in packages/core/src/studio-api/). + "packages/studio-server/src/routes/files.test.ts", + "packages/studio-server/src/routes/render.test.ts", + "packages/studio-server/src/routes/lint.test.ts", + "packages/studio-server/src/routes/preview.test.ts", + "packages/studio-server/src/routes/projects.test.ts", + "packages/studio-server/src/helpers/backupJournal.test.ts", + "packages/studio-server/src/helpers/finiteMutation.test.ts", + "packages/studio-server/src/helpers/hfIdPersist.test.ts", + "packages/studio-server/src/helpers/manualEditsRenderScript.test.ts", + "packages/studio-server/src/helpers/mediaValidation.test.ts", + "packages/studio-server/src/helpers/previewAdapter.test.ts", + "packages/studio-server/src/helpers/safePath.test.ts", + "packages/studio-server/src/helpers/sourceMutation.test.ts", + "packages/studio-server/src/helpers/studioMotionRenderScript.test.ts", + "packages/studio-server/src/helpers/subComposition.test.ts", + // @hyperframes/lint rule test files: parallel arrange/act/assert test cases + // (pre-existing structure from when lint lived in packages/core/src/lint/). + "packages/lint/src/rules/adapters.test.ts", + "packages/lint/src/rules/captions.test.ts", + "packages/lint/src/rules/composition.test.ts", + "packages/lint/src/rules/core.test.ts", + "packages/lint/src/rules/fonts.test.ts", + "packages/lint/src/rules/gsap.test.ts", + "packages/lint/src/rules/media.test.ts", + "packages/lint/src/rules/slideshow.test.ts", + "packages/lint/src/rules/textures.test.ts", + "packages/lint/src/hyperframeLinter.test.ts", + // slideshowPanelHelpers.ts: setSlideNotes/addFragment/addHotspot share an + // intentional parallel shape (signature + mapSlidesIn → exists-check → + // map/append); the per-slide mutation differs, so a shared abstraction + // would obscure more than it dedupes. + "packages/studio/src/components/panels/slideshowPanelHelpers.ts", + // SlideshowPanel.test.ts: parallel arrange/act/assert test cases — collapsing + // them would hurt readability of what each case verifies. + "packages/studio/src/components/panels/SlideshowPanel.test.ts", + // hyperframes-player.test.ts: parallel arrange/act/assert test cases verifying + // distinct behaviors (same-origin vs realm media, audio-locked permutations, + // seek bridge variants). Each case is self-contained for readability; + // extracting the iframe / mock-audio setup helpers would over-couple + // unrelated scenarios under a shared fixture. + "packages/player/src/hyperframes-player.test.ts", + // present.ts mirrors play.ts's server startup + console-output block. The + // shared low-level pieces (resolve*/injectRuntime/listenOnFreePort) are in + // utils/compositionServer.ts; the remaining clone is per-command logging text + // (different labels/help lines) — extracting it would over-abstract. + "packages/cli/src/commands/present.ts", + // skillsManifest.test.ts: parallel arrange/act/assert cases for locateInstall + // (project vs global scope, per-agent host conventions, claude-code priority). + // Each case seeds a dir then asserts the resolved location/agent; collapsing + // the shared seed/assert shape would obscure what each scope/host verifies. + "packages/cli/src/utils/skillsManifest.test.ts", + // skills.test.ts: parallel prune cases (removed-in-global vs project, non-slug + // rejection, --source/--dir plumbing) share a mock-checkSkills → runSkillsUpdate + // → assert-remove-spawn shape; each verifies a distinct prune behavior, so + // extracting the shared scaffold would obscure what each case asserts. + "packages/cli/src/commands/skills.test.ts", + // figma test files: freeze.test.ts and manifest.test.ts share a common + // arrange/act/assert setup preamble (file creation + test dir handling) that + // is minimal boilerplate; collapsing it into a shared fixture would reduce + // readability of each test's independent setup. + "packages/core/src/figma/freeze.test.ts", + "packages/core/src/figma/manifest.test.ts", + // layout-audit.browser.test.ts: parallel arrange/act/assert cases per audit + // rule (overflow / overlap / occlusion) that each install their own mocked + // geometry + computed-style before asserting. The clone groups are this + // pre-existing per-rule scaffold; adding a clip-path case shifts lines and + // re-flags it. Collapsing the per-rule installers would obscure each case. + "packages/cli/src/commands/layout-audit.browser.test.ts", + // gsapParserAcorn.motionEval.test.ts: parallel arrange/act/assert cases for + // the staggered-collection honesty pass (.from reveal vs .to landing on the + // rest pose). Each asserts a distinct keyframe shape; collapsing the shared + // parse/keyframes-extraction scaffold would obscure what each case verifies. + "packages/core/src/parsers/gsapParserAcorn.motionEval.test.ts", + // Studio UX-review sweep (148 findings fixed across ~90 studio files): + // heavily-edited files re-flag line-shifted inherited complexity, and the + // new small handlers (a11y keydown/menu/dialog/error-state paths, mostly + // 5-6 cyclomatic) trip the CRAP threshold absent coverage data. Reviewed + // individually in the studio-ux PR stack rather than refactored here. + "packages/studio/src/player/components/TimelineClipDiamonds.test.tsx", + "packages/studio/src/hooks/useFileManager.ts", + "packages/studio/src/captions/hooks/useCaptionSync.ts", + "packages/studio/vite.adapter.ts", + "packages/studio/src/components/sidebar/AudioRow.tsx", + "packages/studio/src/components/sidebar/AssetsTab.tsx", + "packages/studio/src/components/editor/propertyPanelFill.tsx", + "packages/studio/src/captions/store.ts", + "packages/studio/src/captions/components/CaptionOverlay.tsx", + "packages/cli/src/server/studioServer.ts", + // Sub-composition html/body scoping fix: inlineSubCompositions.ts has a + // pre-existing head-vs-content script-extraction clone, and the scoping + // test file has pre-existing parallel arrange/act/assert cases. Adding the + // scopeRootSelectors handling shifts lines and re-flags both. + "packages/core/src/compiler/inlineSubCompositions.ts", + "packages/core/src/compiler/compositionScoping.test.ts", + ], + }, + "health": { + // useGsapTweenCache.ts: pre-existing large React-effect hooks (the populate + // and runtime-scan effects, the per-element animations memo) whose + // complexity pre-dates the computed-timeline work. Exempted at file level + // rather than refactored as scope creep. + "ignore": [ + // TEMP(studio-dnd): coexistence-window complexity flare (inherited/CRAP-no-coverage); + // removed by studio-dnd/pr22 when the final config lands. + "packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts", + "packages/studio/src/components/editor/CanvasContextMenu.tsx", + "packages/studio/src/components/editor/canvasContextMenuZOrder.test.ts", + "packages/studio/src/player/components/timelineCollision.test.ts", + "packages/studio/src/player/components/timelineStackingSync.test.ts", + // sourcePatcher.ts: resolveSourceFile / splitInlineStyleDeclarations / + // patch*InTag pre-date this PR; only the PatchOperation type gained two + // optional fields, but the line-shift fingerprint re-flags the inherited + // complexity. + "packages/studio/src/utils/sourcePatcher.ts", + // timeline.ts: collectRuntimeTimelinePayload (CRITICAL) pre-dates this PR; + // only an import line changed here (slideshow/sceneId → slideshow/index), + // but the line-shift fingerprint makes fallow re-flag inherited complexity. + "packages/core/src/runtime/timeline.ts", + // sceneId.ts: trivial 5-cyclomatic guard, moved verbatim from + // packages/core/src/slideshow/ into parsers; flagged only because the move + // makes fallow treat it as a fresh finding. + "packages/parsers/src/slideshow/sceneId.ts", + // assetPaths.ts / rewriteSubCompPaths.ts: pure URL/asset-path helpers moved + // verbatim from packages/core/src/compiler/ into parsers. The move makes + // fallow score them as fresh (high CRAP = inherited complexity with no + // coverage mapping yet); the logic is unchanged. + "packages/parsers/src/assetPaths.ts", + "packages/parsers/src/rewriteSubCompPaths.ts", + // gsapParser.ts: the recast/babel GSAP writer is a 2500-line legacy parser; + // moved from packages/core/src/parsers/ — same complexity rationale. + "packages/parsers/src/gsapParser.ts", + // htmlParser.ts has pre-existing complexity (moved from packages/core). + "packages/parsers/src/htmlParser.ts", + // studio-server files: pre-existing complexity (moved from packages/core/src/studio-api/). + // files.ts: executeGsapMutationRecast/Acorn are CRITICAL; excluded as files.ts + // was already in health.ignore at the old path (packages/core/src/studio-api/routes/files.ts). + "packages/studio-server/src/routes/files.ts", + "packages/studio-server/src/routes/render.ts", + "packages/studio-server/src/routes/thumbnail.ts", + "packages/studio-server/src/helpers/manualEditsRenderScript.ts", + "packages/studio-server/src/helpers/studioMotionRenderScript.ts", + "packages/studio-server/src/helpers/subComposition.ts", + // lint rule implementations and project linter: pre-existing complexity + // (moved from packages/core/src/lint/). File-level exemption avoids the + // line-shift fingerprint problem for inherited findings. + "packages/lint/src/rules/media.ts", + "packages/lint/src/rules/textures.ts", + "packages/lint/src/rules/gsap.ts", + "packages/lint/src/project.ts", + // SlideshowPanel.tsx: top-level editor panel that wires several independent + // sections (slides/inspector/branches/hotspot). Its cyclomatic count comes + // from that fan-out; splitting it would scatter shared state without + // reducing real complexity. File-level exemption (not an inline comment) + // avoids the line-shift fingerprint problem noted above. + "packages/studio/src/components/panels/SlideshowPanel.tsx", + // play.ts / present.ts: CLI command entrypoints whose cyclomatic count is + // browser/arg validation + server wiring (same shape as preview.ts). The + // serving logic is factored into utils/compositionServer.ts; the remaining + // body is linear validation that reads clearly inline. + "packages/cli/src/commands/play.ts", + "packages/cli/src/commands/present.ts", + // sync-agent-dirs.ts: a build-time codegen that regex-parses upstream + // agents.ts. parseAgents/resolveGlobalExpr are branchy by nature (literal + // vs base-var args, validation throws) but small and well-tested via the + // generated table's shape test; this is dev tooling, not shipped runtime. + "packages/cli/scripts/sync-agent-dirs.ts", + // Files modified only for import-path updates (one-line changes to switch + // from @hyperframes/core/* subpaths to the new packages). Their complexity + // is pre-existing; the line-shift fingerprint problem makes fallow treat + // the violations as new even though no logic changed. + "packages/cli/src/server/studioServer.ts", + "packages/core/src/core.types.ts", + "packages/core/src/generators/hyperframes.ts", + "packages/producer/src/services/htmlCompiler.ts", + "packages/studio/src/hooks/gsapRuntimeBridge.ts", + "packages/studio/src/hooks/gsapShared.ts", + "packages/studio/src/hooks/gsapDragPositionCommit.ts", + "packages/studio/src/hooks/gsapKeyframeCacheHelpers.ts", + "packages/studio/vite.config.ts", + "packages/cli/src/commands/lint.ts", + "packages/cli/src/commands/preview.ts", + "packages/cli/src/commands/publish.ts", + "packages/cli/src/server/studioServer.ts", + // set-version.ts: compareSemver helper has pre-existing complexity from + // semver string parsing logic; line-shift fingerprint problem from new + // packages added to PACKAGES array makes fallow treat it as new. + "scripts/set-version.ts", + // gsapRuntimeReaders.ts: pre-existing complexity in readAllAnimatedProperties; + // line-shift fingerprint from import-path updates triggers the violation. + "packages/studio/src/hooks/gsapRuntimeReaders.ts", + // In-page audit scripts: single large browser-side IIFE wrappers + // (auditLayout/__contrastAudit) with pre-existing CRITICAL helpers + // (textOverflowFixHint, selectorFor, the WCAG walk). Adding the small + // clip-path probe helpers shifts every function below it, so the line-shift + // fingerprint re-flags inherited complexity even though the new helpers are + // all at the lowest tier (<=5 cyclomatic). + "packages/cli/src/commands/layout-audit.browser.js", + "packages/cli/src/commands/contrast-audit.browser.js", + // lottie.ts: the flagged `seek` handler (lottie-web/dotLottie dual-API + // dispatch, unchanged by the duration-auto-inference work in this PR) + // pre-dates this PR's scope. New functions added earlier in the file + // (getInferredDurationSeconds + helpers) shifted its line numbers, + // breaking fallow's inherited-detection fingerprint. File-level + // exemption avoids the line-shift problem for this inherited finding. + "packages/core/src/runtime/adapters/lottie.ts", + // info.ts: the flagged `run` command handler pre-dates the keyframes-cli + // PR; that PR only added the orientation()/durationFromHtml() helpers and + // swapped a few `maxEnd` reads for the new `duration` variable, without + // adding branches. The line-shift fingerprint re-flags the inherited + // complexity even though the logic is unchanged. + "packages/cli/src/commands/info.ts", + // gsapParserAcorn.ts: the flagged `sameMemberAccess` (structural equality + // of two member-access nodes) pre-dates this PR's motion-eval work + // (added in #1760, unchanged since). The new const-folding / set-seeding / + // label-position code added earlier in the file shifted its line number, + // so the line-shift fingerprint re-flags this inherited finding. + "packages/parsers/src/gsapParserAcorn.ts", + // isFigmaManifestRecord: type guard with 19 cyclomatic due to chained + // field validation (id/type/path/source types + value shape guards). + // This is the correct shape for a discriminating type guard; refactoring + // into smaller guards would obscure the unified validation contract. + "packages/core/src/figma/manifest.ts", + // Studio UX-review sweep (148 findings fixed across ~90 studio files): + // heavily-edited files re-flag line-shifted inherited complexity, and the + // new small handlers (a11y keydown/menu/dialog/error-state paths, mostly + // 5-6 cyclomatic) trip the CRAP threshold absent coverage data. Reviewed + // individually in the studio-ux PR stack rather than refactored here. + "packages/studio/src/App.tsx", + "packages/studio/src/captions/components/CaptionAnimationPanel.tsx", + "packages/studio/src/captions/components/CaptionOverlay.tsx", + "packages/studio/src/captions/components/CaptionOverlayUtils.ts", + "packages/studio/src/captions/components/CaptionPropertyPanel.tsx", + "packages/studio/src/captions/components/shared.tsx", + "packages/studio/src/captions/hooks/useCaptionSync.ts", + "packages/studio/src/components/AskAgentModal.tsx", + "packages/studio/src/components/editor/BlockParamsPanel.tsx", + "packages/studio/src/components/editor/DomEditOverlay.tsx", + "packages/studio/src/components/editor/FileTree.tsx", + "packages/studio/src/components/editor/FileTreeNodes.tsx", + "packages/studio/src/components/editor/LayersPanel.tsx", + "packages/studio/src/components/editor/MotionPathNode.tsx", + "packages/studio/src/components/editor/PropertyPanel.tsx", + "packages/studio/src/components/editor/propertyPanelFont.tsx", + "packages/studio/src/components/editor/propertyPanelMediaSection.tsx", + "packages/studio/src/components/editor/propertyPanelStyleSections.tsx", + "packages/studio/src/components/editor/Transform3DCube.tsx", + "packages/studio/src/components/LintModal.tsx", + "packages/studio/src/components/MediaPreview.tsx", + "packages/studio/src/components/nle/NLELayout.tsx", + "packages/studio/src/components/nle/NLEPreview.tsx", + "packages/studio/src/components/sidebar/AudioRow.tsx", + "packages/studio/src/components/sidebar/BlocksTab.tsx", + "packages/studio/src/components/sidebar/CompositionsTab.tsx", + "packages/studio/src/components/sidebar/LeftSidebar.tsx", + "packages/studio/src/components/storyboard/StoryboardLoaded.tsx", + "packages/studio/src/components/StudioPreviewArea.tsx", + "packages/studio/src/components/StudioRightPanel.tsx", + "packages/studio/src/components/StudioToast.tsx", + "packages/studio/src/components/ui/Tooltip.tsx", + "packages/studio/src/components/ui/useDialogBehavior.ts", + "packages/studio/src/hooks/useAppHotkeys.ts", + "packages/studio/src/hooks/useCaptionDetection.ts", + "packages/studio/src/hooks/useFileManager.ts", + "packages/studio/src/hooks/useFrameCapture.ts", + "packages/studio/src/hooks/usePanelLayout.ts", + "packages/studio/src/player/components/menuKeyboardNav.ts", + "packages/studio/src/player/components/Timeline.tsx", + "packages/studio/src/player/components/TimelineCanvas.tsx", + "packages/studio/src/player/components/TimelineClip.tsx", + "packages/studio/src/player/components/TimelineClipDiamonds.tsx", + // Sub-composition html/body scoping fix: these files carry pre-existing + // CRITICAL/HIGH functions (inlineSubCompositions, bundleToSingleHtml, + // mountCompositionContent/loadExternalCompositions, scopeSelector/ + // replaceAuthoredRootIdSelectors) that pre-date this PR. The change only + // threads a scopeRootSelectors option through the existing scoping calls; + // the line-shift fingerprint re-flags the inherited complexity. + "packages/core/src/compiler/compositionScoping.ts", + "packages/core/src/compiler/inlineSubCompositions.ts", + "packages/core/src/compiler/htmlBundler.ts", + "packages/core/src/runtime/compositionLoader.ts", + ], + }, +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f5f9640 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,32 @@ +# Normalize text files to LF on checkout and in the repo, regardless of the +# contributor's OS. Without this, files saved on Windows can land in PRs with +# CRLF endings or a UTF-8 BOM, which makes every line differ at the byte level +# and trips GitHub's "Binary file not shown" diff heuristic. +* text=auto eol=lf + +# Regression-test media under packages/producer/tests — RECURSIVE (`**`) so +# media in ANY nested fixture dir routes to LFS. The earlier non-recursive +# patterns (`tests/*/output/...`, `tests/*/src/...`) only matched one level +# deep, so binaries in nested dirs like `tests/hdr-regression/hdr-pq/assets/*.mp4` +# and `.../_renders/*.mp4` committed straight into the git pack and now live in +# history forever. `**` closes that leak: any video/baseline anywhere under +# tests/ is LFS, regardless of how deeply it is nested. +packages/producer/tests/**/*.mp4 filter=lfs diff=lfs merge=lfs -text +packages/producer/tests/**/*.mov filter=lfs diff=lfs merge=lfs -text +packages/producer/tests/**/*.webm filter=lfs diff=lfs merge=lfs -text +packages/producer/tests/**/*.png filter=lfs diff=lfs merge=lfs -text +packages/producer/tests/**/output/compiled.html filter=lfs diff=lfs merge=lfs -text + +# ONNX models must ALWAYS use LFS regardless of location: a 31 MB ppmattingv2 +# model was once committed raw into skills/ then deleted — but a raw commit +# lives in history forever. No path in the repo legitimately ships a raw .onnx. +# (Audio masters / .wav are intentionally NOT globalized here — registry blocks +# ship raw audio so they stay portable when installed via `hyperframes add`. +# The wav-in-LFS question is a coordinated policy decision left for later.) +*.onnx filter=lfs diff=lfs merge=lfs -text + +# GitHub Linguist overrides — HTML files are compositions (user content / templates), +# not the framework source. Hide them from the repo language stats so TypeScript, +# which is the actual implementation, surfaces as the dominant language. +registry/**/*.html linguist-vendored +*.html linguist-detectable=false diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..02a8127 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,81 @@ +name: Bug Report +description: Report a bug to help us improve +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Before filing, please: + - Search [existing issues](https://github.com/heygen-com/hyperframes/issues) to avoid duplicates + - Make sure the issue reproduces without custom extensions or plugins + - Test on the latest version (`npx hyperframes upgrade`) + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear description of what the bug is. + validations: + required: true + + - type: input + id: reproduction-link + attributes: + label: Link to reproduction + description: | + A link to a **public** GitHub repository with a minimal reproduction of the issue. + + Start from a clean project with `npx hyperframes init repro --non-interactive --example blank`, make only the changes needed to demonstrate the bug, push to a public repo, and paste the link here. + + **Issues without a minimal reproduction may be closed.** A reproduction helps us diagnose and fix your issue faster — often in hours instead of days. + placeholder: "https://github.com/username/my-hyperframes-repro" + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Step-by-step instructions using the linked reproduction. + placeholder: | + 1. Clone the repo above + 2. Run `npx hyperframes render` + 3. See error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What you expected to happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What actually happened. Include error messages or screenshots if applicable. + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Run `npx hyperframes doctor` and paste the output here. + render: shell + placeholder: | + ✓ Version 0.x.x (latest) + ✓ Node.js v22.x.x (darwin arm64) + ✓ FFmpeg ffmpeg version 7.x ... + ✓ Chrome bundled: /path/to/chrome + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Any other context, logs, or screenshots. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..df75254 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,36 @@ +name: Feature Request +description: Suggest an idea for Hyperframes +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting an improvement! Please search [existing issues](https://github.com/heygen-com/hyperframes/issues) first to avoid duplicates. + + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? What's the use case? + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How should it work? Include code examples or mockups if helpful. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any alternative approaches you've thought about. + + - type: textarea + id: additional + attributes: + label: Additional context + description: Any other context, mockups, or examples. diff --git a/.github/actions/install-ffmpeg-windows/action.yml b/.github/actions/install-ffmpeg-windows/action.yml new file mode 100644 index 0000000..9b55cc7 --- /dev/null +++ b/.github/actions/install-ffmpeg-windows/action.yml @@ -0,0 +1,60 @@ +name: Install FFmpeg (Windows) +description: >- + Download a pinned FFmpeg GPL build for Windows from BtbN/FFmpeg-Builds and put + ffmpeg.exe / ffprobe.exe on PATH. We bypass `choco install ffmpeg` because + the Chocolatey community feed regularly returns 503 / 504 / NuGet resolver + errors with no retry, which makes it unsuitable as a CI dependency. From the + consumer's perspective this is equivalent — `where ffmpeg` (the PR #336 + validation) and `findFFmpeg()` both pass. + +inputs: + release-url: + description: >- + URL of a BtbN/FFmpeg-Builds zip to install. Pinned to a specific + autobuild tag so a new upstream nightly can't silently change encoder + behavior under us. The asset filename embeds the git hash, so both + the tag and the filename must be bumped together when upgrading. + required: false + default: https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-04-30-13-44/ffmpeg-N-124278-gcc3ca17127-win64-gpl.zip + max-attempts: + description: Max download attempts before failing. + required: false + default: "8" + +runs: + using: composite + steps: + - name: Install FFmpeg from BtbN/FFmpeg-Builds + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $url = '${{ inputs.release-url }}' + $maxAttempts = [int]'${{ inputs.max-attempts }}' + + $zip = Join-Path $env:RUNNER_TEMP 'ffmpeg.zip' + $dir = Join-Path $env:RUNNER_TEMP 'ffmpeg' + New-Item -ItemType Directory -Force -Path $dir | Out-Null + + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + Write-Host "--- Downloading ffmpeg from BtbN/FFmpeg-Builds (attempt $attempt/$maxAttempts) ---" + try { + Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing + break + } catch { + Write-Warning "Download failed: $($_.Exception.Message)" + if ($attempt -eq $maxAttempts) { throw } + Start-Sleep -Seconds (30 * $attempt) + } + } + + Write-Host "--- Extracting ffmpeg ---" + Expand-Archive -Path $zip -DestinationPath $dir -Force + + $bin = Get-ChildItem -Path $dir -Recurse -Filter 'ffmpeg.exe' | Select-Object -First 1 + if (-not $bin) { throw "ffmpeg.exe not found after extracting $url" } + + Add-Content -Path $env:GITHUB_PATH -Value $bin.Directory.FullName + + Write-Host "--- ffmpeg sanity check ---" + & $bin.FullName -version | Select-Object -First 1 diff --git a/.github/actions/preflight/action.yml b/.github/actions/preflight/action.yml new file mode 100644 index 0000000..dd78d68 --- /dev/null +++ b/.github/actions/preflight/action.yml @@ -0,0 +1,31 @@ +name: Preflight +description: | + Cheap lint + format gate that runs before expensive CI jobs (regression + shards, perf shards, parity renders, Windows renders, catalog previews). + Single source of truth for the gate's bun/node/cache/lint/format steps — + tweak here and every preflight job picks it up. + +runs: + using: composite + steps: + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - shell: bash + run: bun install --frozen-lockfile + + - shell: bash + run: bun run lint + + - shell: bash + run: bun run format:check diff --git a/.github/actions/prepare-ffmpeg-bin/action.yml b/.github/actions/prepare-ffmpeg-bin/action.yml new file mode 100644 index 0000000..de65407 --- /dev/null +++ b/.github/actions/prepare-ffmpeg-bin/action.yml @@ -0,0 +1,28 @@ +name: Prepare FFMPEG_BIN for bun install +description: >- + Copies the system ffmpeg binary to a writable temp location and sets + FFMPEG_BIN in the environment. ffmpeg-static's postinstall script checks + whether a file already exists at FFMPEG_BIN; if it does and process.exit(0) + is respected by the runtime, the CDN download is skipped entirely. If the + runtime intercepts process.exit(0) and the download proceeds anyway, using + a writable target prevents the EACCES failure that occurs when the + destination is a system path like /usr/bin/ffmpeg. + +runs: + using: composite + steps: + - name: Copy ffmpeg to writable path + shell: bash + run: | + FFMPEG=$(which ffmpeg 2>/dev/null || echo "") + if [ -n "$FFMPEG" ]; then + cp "$FFMPEG" "$RUNNER_TEMP/hf-ffmpeg" + else + # ffmpeg not pre-installed; write a stub so ffmpeg-static's postinstall + # finds a file at FFMPEG_BIN and exits early (statSync check) without + # attempting the CDN download. Jobs that need a real ffmpeg binary + # install it via apt before calling this action. + printf '#!/bin/sh\nexec ffmpeg "$@"\n' > "$RUNNER_TEMP/hf-ffmpeg" + fi + chmod +x "$RUNNER_TEMP/hf-ffmpeg" + echo "FFMPEG_BIN=$RUNNER_TEMP/hf-ffmpeg" >> "$GITHUB_ENV" diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..a1fccea --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,64 @@ +name: "Hyperframes CodeQL config" + +# Use GitHub's default security-extended suite — it's a strict superset of the +# default suite (more queries, slightly higher false-positive rate). Pair it with +# the query-filters below so the extra queries don't drown the dashboard. +queries: + - uses: security-extended + +# Per-rule path filters. The intent is to silence rules that have known false +# positives on specific file shapes (generated test artifacts, CDN-script test +# fixtures, functional-cleanup regex) WITHOUT excluding those paths from all +# analysis — a malicious contributor adding e.g. a command-injection sink into +# a "test fixture" would still get caught. +# +# To audit what changed: look at PR diffs touching this file. Reviewers should +# treat it like CODEOWNERS — adding a new path exclusion is a policy change. +query-filters: + # Generated test artifacts (golden baselines written by the producer test + # harness). Every compiled.html re-rasterizes the regex-stripped composition; + # the same alerts fire on every fixture and on every re-render. + - exclude: + id: js/incomplete-sanitization + paths: + - "packages/producer/tests/**/output/compiled.html" + - "packages/producer/tests/**/failures/*.html" + + # Test fixtures and skill test corpora intentionally load CDN scripts without + # SRI — pinning hashes there would fight the test's purpose (we want the test + # to use whatever the registry hands back, the same way a composition would). + - exclude: + id: js/functionality-from-untrusted-source + paths: + - "packages/producer/tests/**" + - "skills/**/test-corpus/**" + - "skills/**/assets/test-corpus/**" + + # The hand-rolled HTML cleanup regex in our build-time tooling looks like a + # sanitizer to CodeQL but isn't one — it strips framework bootstraps from + # captured pages before they're fed back into our own renderer (Puppeteer, + # not a user-facing DOM). Same for the text normalizer in the whisper path + # (caption text → SRT/VTT, no DOM emission). Scope these exclusions to the + # exact files that contain functional regex, not to whole directories, so + # any new code in cli/, core/, or producer/ that LOOKS like a sanitizer + # still trips the rules. + - exclude: + id: js/bad-tag-filter + paths: + - "packages/cli/src/capture/index.ts" + - "packages/cli/src/whisper/normalize.ts" + - "packages/core/src/lint/utils.ts" + - "packages/producer/src/services/htmlCompiler.ts" + - exclude: + id: js/incomplete-multi-character-sanitization + paths: + - "packages/cli/src/capture/index.ts" + - "packages/cli/src/whisper/normalize.ts" + + # Golden baselines inline vendor libraries (Three.js, GSAP) whose bundled + # Math.random() calls trip insecure-randomness. These are test artifacts + # from compiled compositions, not production code paths. + - exclude: + id: js/insecure-randomness + paths: + - "packages/producer/tests/**/output/compiled.html" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0ea369e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## What + +Brief description of the change. + +## Why + +Why is this change needed? + +## How + +How was this implemented? Any notable design decisions? + +## Test plan + +How was this tested? + +- [ ] Unit tests added/updated +- [ ] Manual testing performed +- [ ] Documentation updated (if applicable) diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..1147a43 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,43 @@ +changelog: + exclude: + labels: + - skip-changelog + - dependencies + authors: + - dependabot + - dependabot[bot] + categories: + - title: Breaking Changes + labels: + - breaking-change + - breaking + - title: Features + labels: + - enhancement + - feature + - title: Fixes + labels: + - bug + - fix + - title: Docs & Examples + labels: + - documentation + - docs + - examples + - title: Catalog + labels: + - catalog + - registry + - title: Performance + labels: + - performance + - perf + - title: Internal + labels: + - internal + - refactor + - tests + - ci + - title: Other Changes + labels: + - "*" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..5627776 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", ":semanticCommits", ":dependencyDashboard"], + "schedule": ["before 6am on monday"], + "timezone": "America/Los_Angeles", + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "rangeStrategy": "bump", + "lockFileMaintenance": { + "enabled": true, + "schedule": ["before 6am on monday"] + }, + "vulnerabilityAlerts": { + "enabled": true, + "labels": ["security"] + }, + "packageRules": [ + { + "matchUpdateTypes": ["minor", "patch"], + "matchDepTypes": ["devDependencies"], + "groupName": "dev dependencies (non-major)" + }, + { + "matchUpdateTypes": ["patch"], + "matchDepTypes": ["dependencies"], + "groupName": "prod patch updates" + }, + { + "matchUpdateTypes": ["major"], + "dependencyDashboardApproval": true + }, + { + "matchManagers": ["github-actions"], + "groupName": "github-actions", + "pinDigests": true + }, + { + "enabled": false, + "matchPackageNames": ["/^@hyperframes//"] + } + ] +} diff --git a/.github/workflows/catalog-previews.yml b/.github/workflows/catalog-previews.yml new file mode 100644 index 0000000..5b9af86 --- /dev/null +++ b/.github/workflows/catalog-previews.yml @@ -0,0 +1,96 @@ +name: Catalog Previews + +permissions: + contents: read + +# Suppress hyperframes CLI telemetry from HeyGen's own CI runs. +# External users' CI continues to emit telemetry unless they set this themselves. +env: + HYPERFRAMES_NO_TELEMETRY: "1" + +on: + pull_request: + branches: [main] + paths: + - "registry/blocks/**" + - "registry/components/**" + - "scripts/generate-catalog-previews.ts" + - ".github/workflows/catalog-previews.yml" + +concurrency: + group: catalog-previews-${{ github.ref }} + cancel-in-progress: true + +jobs: + preflight: + name: Preflight (lint + format) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: ./.github/actions/preflight + + render-previews: + name: Render catalog previews + needs: preflight + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - run: bun install --frozen-lockfile + + - run: bun run build + + # Chrome headless shell for rendering + - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1 + with: + chrome-version: stable + + - name: Render changed block/component previews + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + # Find which blocks/components changed in this PR + CHANGED_ITEMS=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- registry/blocks/ registry/components/ \ + | grep -E '^registry/(blocks|components)/' \ + | sed 's|^registry/[^/]*/\([^/]*\)/.*|\1|' \ + | sort -u) + + if [ -z "$CHANGED_ITEMS" ]; then + echo "No block/component changes detected." + exit 0 + fi + + echo "Changed items: $CHANGED_ITEMS" + FAILED=0 + + for item in $CHANGED_ITEMS; do + echo "Rendering preview for: $item" + if ! timeout 120 npx tsx scripts/generate-catalog-previews.ts --only "$item" --skip-video; then + echo "::warning::Failed to render preview for $item" + FAILED=$((FAILED + 1)) + fi + done + + if [ "$FAILED" -gt 0 ]; then + echo "::warning::$FAILED item(s) failed to render" + exit 1 + fi + + - name: Upload preview artifacts + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: catalog-previews + path: docs/images/catalog/ + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3865705 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,682 @@ +name: CI + +permissions: + contents: read + pull-requests: read + +# Suppress hyperframes CLI telemetry from HeyGen's own CI runs. +# External users' CI continues to emit telemetry unless they set this themselves. +env: + HYPERFRAMES_NO_TELEMETRY: "1" + +on: + pull_request: + # `edited` is required so the workflow re-fires when a PR's base ref is + # set back to `main` after a Graphite stack restack momentarily flips + # the base off of `main`. Without it, `pull_request` triggers are not + # re-evaluated on `base_ref_changed`, leaving required checks skipped + # for that head SHA forever. + types: [opened, synchronize, reopened, edited] + branches: [main] + push: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + timeout-minutes: 2 + outputs: + code: ${{ steps.filter.outputs.code }} + cli: ${{ steps.filter.outputs.cli }} + skills: ${{ steps.filter.outputs.skills }} + steps: + # Force git-based change detection instead of the pull_request REST API. + # The API path can fail the whole workflow on transient listFiles + # timeouts before any real CI work starts. + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 + id: filter + with: + token: "" + filters: | + code: + - "packages/**" + - "scripts/**" + - "package.json" + - "bun.lock" + - "tsconfig*.json" + - "Dockerfile*" + - ".github/workflows/**" + cli: + - "packages/cli/**" + - "package.json" + - "bun.lock" + - ".github/workflows/ci.yml" + skills: + - "skills/**" + - "skills-manifest.json" + - "package.json" + - ".github/workflows/ci.yml" + + build: + name: Build + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - run: corepack enable + - run: corepack prepare pnpm@10.17.1 --activate + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run build + - run: bun run verify:packed-manifests + + lint: + name: Lint + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run lint + + # `fallow audit` runs dead-code + complexity + duplication analysis scoped to + # the changed files. The default `--gate new-only` means existing legacy + # findings don't fail the build — only NEW issues introduced by the PR do. + # This stops bleeding while letting incremental cleanup land separately. + # + # On findings, the job posts (or updates) a sticky comment on the PR so + # reviewers see the full list inline instead of digging through CI logs. + fallow: + name: Fallow audit + needs: changes + if: needs.changes.outputs.code == 'true' && github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + # Scope write access to this single job — the rest of `ci.yml` keeps the + # workflow-level `pull-requests: read` default so build / lint / test + # tokens can't post or modify PR comments. Job-level permissions override + # the workflow block. + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + # Full history so `--base origin/main` can diff against the merge + # base on stacked PRs, not just the shallow tip. + fetch-depth: 0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - name: Run fallow audit + id: audit + # `bun install` above made `bunx fallow` resolve from node_modules, so + # we don't re-download fallow each run. The script disables `errexit` + # so the audit's non-zero exit (on findings) doesn't abort before we + # write the exit code to the step output. The size check guards + # against fallow crashing before producing markdown (e.g. transient + # parse failure) — without it we'd post a blank sticky comment. + run: | + set +e + bunx fallow audit --base origin/main --fail-on-issues \ + --format pr-comment-github \ + > /tmp/fallow-comment.md + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + if [ ! -s /tmp/fallow-comment.md ]; then + echo "fallow produced no output — see the job logs above." > /tmp/fallow-comment.md + fi + - name: Post sticky comment (findings) + if: steps.audit.outputs.exit_code != '0' + # Fork PRs run with a read-only GITHUB_TOKEN regardless of the + # workflow's `permissions:` block, so the comment post will fail on + # forks. Don't fail the whole job — the audit gate below still fires. + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + with: + # `header` matches fallow's built-in `` + # sentinel so subsequent runs update the same comment. + header: fallow-results + path: /tmp/fallow-comment.md + - name: Remove stale sticky comment (clean run) + if: steps.audit.outputs.exit_code == '0' + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + with: + header: fallow-results + delete: true + - name: Fail if audit found issues + if: steps.audit.outputs.exit_code != '0' + run: | + echo "::error::Fallow audit found new issues — see the PR comment above for details." + exit 1 + + format: + name: Format + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run format:check + + typecheck: + name: Typecheck + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run build + - run: bun run --filter '*' typecheck + + test: + name: Test + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run test:scripts + - run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build + - run: bun run --cwd packages/core build + - run: bun run --cwd packages/core build:hyperframes-runtime + - run: bun run --filter '!@hyperframes/producer' test + + producer-source-tests: + name: "Producer: ${{ matrix.lane }} tests" + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + lane: [unit, integration] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - name: Install FFmpeg for integration tests + if: matrix.lane == 'integration' + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends ffmpeg + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build + - run: bun run --cwd packages/core build + - run: bun run --filter @hyperframes/engine build + - run: bun run producer:test:${{ matrix.lane }} + + # Tests under skills/**/*.test.mjs are bare `node --test` files with only + # `node:` built-in imports. They aren't part of any workspace package, and + # the main `Test` job's `code` path filter excludes `skills/**`, so without + # this dedicated job they'd never run in CI. Examples: + # * skills/media-use/scripts/resolve.test.mjs + # * skills/media-use/scripts/lib/manifest.test.mjs + # Several of these are regression guards (e.g. shell-injection cases), so + # the whole point is that they fire on PRs that touch skills/. + test-skills: + name: "Test: skills" + needs: changes + if: needs.changes.outputs.skills == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - name: Discover and run skills tests + # We expand the test list via bash so the job fails loudly when the + # matcher comes back empty, rather than silently no-op'ing (which + # would defeat the whole point of this job). + run: | + set -euo pipefail + mapfile -t SKILLS_TESTS < <(find skills -type f -name "*.test.mjs" | sort) + if [ "${#SKILLS_TESTS[@]}" -eq 0 ]; then + echo "::error::No skills/**/*.test.mjs files found. Did the layout change?" + exit 1 + fi + printf 'Running %d skills test file(s):\n' "${#SKILLS_TESTS[@]}" + printf ' * %s\n' "${SKILLS_TESTS[@]}" + node --test "${SKILLS_TESTS[@]}" + + # Guards that skills-manifest.json (the published freshness fingerprint read + # by `hyperframes skills check`) was regenerated when a skill changed. Runs + # `gen:skills-manifest --check`, which compares per-skill content hashes; the + # manifest carries no version/timestamp, so it only fails on real content + # drift. bun runs the TS script directly, no install needed. + skills-manifest: + name: "Skills: manifest in sync" + needs: changes + if: needs.changes.outputs.skills == 'true' + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - name: Verify skills-manifest.json matches skills/ + run: bun packages/cli/scripts/gen-skills-manifest.ts --check + + cli-npx-shim: + name: "CLI: npx shim (${{ matrix.os }})" + needs: changes + if: needs.changes.outputs.cli == 'true' + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - name: Install dependencies + if: runner.os != 'Windows' + run: bun install --frozen-lockfile --ignore-scripts + - name: Install dependencies + if: runner.os == 'Windows' + run: bun install --frozen-lockfile --ignore-scripts --linker=hoisted + - run: bun run --cwd packages/cli test src/utils/npxCommand.test.ts src/commands/skills.test.ts + + sdk-tests: + name: "SDK: unit + contract + smoke" + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - run: bun install --frozen-lockfile + # Build workspace deps so the sdk's @hyperframes/parsers + core subpath + # imports resolve via the "node" export condition (dist) under vitest. + - run: bun run --filter '@hyperframes/parsers' build + - run: bun run --cwd packages/core build + - run: bun run --filter @hyperframes/sdk test + + test-runtime-contract: + name: "Test: runtime contract" + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + # Runtime coverage now imports core modules that consume workspace + # subpaths. Build their dist exports before Vitest resolves them. + - run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build + - run: bun run --filter @hyperframes/core test:hyperframe-runtime-ci + + studio-load-smoke: + name: "Studio: load smoke" + needs: [changes] + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + # Build workspace deps so the studio vite.config.ts (loaded by Node) can + # resolve @hyperframes/core and @hyperframes/studio-server via the "node" + # export condition (dist). + - run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build + - run: bun run --cwd packages/core build + - run: bun run --cwd packages/core build:hyperframes-runtime + - name: Start studio and check for runtime errors + run: | + # Start the studio Vite dev server (fast — no bundle step) + bun run --filter '@hyperframes/studio' dev -- --port 5199 & + SERVER_PID=$! + + # Wait for the server to be ready (up to 20s) + for i in $(seq 1 40); do + if curl -sf http://localhost:5199/ >/dev/null 2>&1; then break; fi + sleep 0.5 + done + + if ! curl -sf http://localhost:5199/ >/dev/null 2>&1; then + echo "FAIL: studio dev server did not start" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + + # Load the studio in headless Chrome with API mocking to trigger + # the full splash→main transition (catches hooks-after-early-return bugs) + node scripts/studio-runtime-smoke.mjs http://localhost:5199/#project=smoke-test + + kill $SERVER_PID 2>/dev/null || true + + smoke-global-install: + name: "Smoke: global install" + needs: [changes, build] + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - uses: ./.github/actions/prepare-ffmpeg-bin + - run: bun install --frozen-lockfile + - run: bun run build + + # Pack the CLI as a tarball (simulates what `npm publish` produces) + - name: Pack CLI tarball + run: cd packages/cli && npm pack + + # Install globally using --prefix to avoid sudo + - name: Install globally via npm + run: npm install -g --prefix /tmp/hf-smoke ./packages/cli/hyperframes-cli-*.tgz + + # Scaffold a blank project + - name: Init blank project + run: | + export PATH="/tmp/hf-smoke/bin:$PATH" + mkdir /tmp/hf-project && cd /tmp/hf-project + hyperframes init test-project --example blank + + # Start preview, probe the runtime endpoint, assert no esbuild errors + - name: Smoke-test preview server + run: | + export PATH="/tmp/hf-smoke/bin:$PATH" + cd /tmp/hf-project/test-project + + # Start the preview server in the background; capture stderr + CI=true hyperframes preview --port 3099 2>/tmp/hf-stderr.log & + SERVER_PID=$! + + # Wait for the server to be ready (up to 15 s) + for i in $(seq 1 30); do + if curl -sf http://localhost:3099/ >/dev/null 2>&1; then + break + fi + sleep 0.5 + done + + # Probe the runtime JS endpoint + BODY=$(curl -sf http://localhost:3099/api/runtime.js | head -c 200 || true) + if [ -z "$BODY" ]; then + echo "FAIL: /api/runtime.js returned empty response" + kill $SERVER_PID 2>/dev/null || true + cat /tmp/hf-stderr.log + exit 1 + fi + + kill $SERVER_PID 2>/dev/null || true + wait $SERVER_PID 2>/dev/null || true + + # Assert stderr does not contain esbuild / runtime load errors + if grep -qE '✘ \[ERROR\]|Failed to load runtime' /tmp/hf-stderr.log; then + echo "FAIL: preview emitted runtime errors:" + cat /tmp/hf-stderr.log + exit 1 + fi + + echo "PASS: global install smoke test succeeded" + + cli-smoke-required: + name: "CLI smoke (required)" + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + lfs: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - name: Install FFmpeg + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + - uses: ./.github/actions/prepare-ffmpeg-bin + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Build monorepo + run: bun run build + + - name: Create smoke input video + run: | + set -euo pipefail + ffmpeg -hide_banner -loglevel error \ + -f lavfi -i testsrc2=size=640x360:rate=30 \ + -f lavfi -i sine=frequency=880:sample_rate=48000 \ + -t 3 \ + -c:v libx264 \ + -pix_fmt yuv420p \ + -c:a aac \ + -shortest \ + -y /tmp/hf-cli-input.mp4 + test -s /tmp/hf-cli-input.mp4 + + - name: Smoke-test CLI from monorepo source + # init's --skip-skills flag is neutered (see init.ts); opt out of the + # GitHub skills check via this env so the smoke test stays offline/fast. + env: + HYPERFRAMES_SKIP_SKILLS: "1" + run: | + set -euo pipefail + rm -rf /tmp/hf-cli-inside + + bun run --filter @hyperframes/cli dev -- init /tmp/hf-cli-inside \ + --example warm-grain \ + --video /tmp/hf-cli-input.mp4 \ + --skip-transcribe \ + --non-interactive \ + --skip-skills + + bun run --filter @hyperframes/cli dev -- lint /tmp/hf-cli-inside + bun run --filter @hyperframes/cli dev -- validate /tmp/hf-cli-inside --timeout 3000 + bun run --filter @hyperframes/cli dev -- render /tmp/hf-cli-inside \ + --quality standard \ + --workers auto \ + --strict \ + --output /tmp/hf-cli-inside/renders/inside.mp4 2>&1 | tee /tmp/hf-cli-render.log + + test -s /tmp/hf-cli-inside/renders/inside.mp4 + + - name: Assert page.goto completes under 5s budget + run: | + set -euo pipefail + # Extract the longest page.goto time from render logs. + # Format: [initSession:MODE] page.goto complete (NNNms) + MAX_MS=$(grep -oP 'page\.goto complete \(\K[0-9]+' /tmp/hf-cli-render.log | sort -n | tail -1) + if [ -z "$MAX_MS" ]; then + echo "::error::No page.goto timing found in render logs — log format may have changed. Update the grep pattern." + exit 1 + fi + echo "Slowest page.goto: ${MAX_MS}ms (budget: 5000ms)" + if [ "$MAX_MS" -gt 5000 ]; then + echo "::error::page.goto took ${MAX_MS}ms — exceeds 5s budget. Possible Proxy/stub regression." + exit 1 + fi + + - name: Pack CLI tarball + run: | + set -euo pipefail + mkdir -p /tmp/hf-cli-pack + cd packages/cli + PACKED_TARBALL="$(npm pack --pack-destination /tmp/hf-cli-pack | tail -n 1)" + test -n "$PACKED_TARBALL" + test -f "/tmp/hf-cli-pack/$PACKED_TARBALL" + echo "HF_CLI_TARBALL=/tmp/hf-cli-pack/$PACKED_TARBALL" >> "$GITHUB_ENV" + + - name: Install packed CLI outside monorepo + run: | + set -euo pipefail + npm install -g --prefix /tmp/hf-cli-global "$HF_CLI_TARBALL" + + - name: Smoke-test packed CLI outside monorepo + # init's --skip-skills flag is neutered (see init.ts); opt out of the + # GitHub skills check via this env so the smoke test stays offline/fast. + env: + HYPERFRAMES_SKIP_SKILLS: "1" + run: | + set -euo pipefail + export PATH="/tmp/hf-cli-global/bin:$PATH" + rm -rf /tmp/hf-cli-outside + + hyperframes init /tmp/hf-cli-outside \ + --example warm-grain \ + --video /tmp/hf-cli-input.mp4 \ + --skip-transcribe \ + --non-interactive \ + --skip-skills + + hyperframes lint /tmp/hf-cli-outside + hyperframes validate /tmp/hf-cli-outside --timeout 3000 + hyperframes render /tmp/hf-cli-outside \ + --quality standard \ + --workers auto \ + --strict \ + --output /tmp/hf-cli-outside/renders/outside.mp4 + + test -s /tmp/hf-cli-outside/renders/outside.mp4 + + filesize: + name: File size check + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - name: Check file sizes (max 600 lines) + # Scoped to files THIS PR changed under packages/studio. Walking the + # whole tree blamed every unrelated PR for pre-existing offenders. + # Falls back to a full scan on push events (no base ref available) + # so the rule still guards main. + run: | + set -e + if [ -n "${{ github.base_ref }}" ]; then + mapfile -t files < <( + git diff --name-only --diff-filter=ACMR \ + "origin/${{ github.base_ref }}...HEAD" -- \ + 'packages/studio/**/*.ts' 'packages/studio/**/*.tsx' \ + | grep -vE '\.(test|spec)\.(ts|tsx)$|\.generated\.' || true + ) + else + mapfile -t files < <( + find packages/studio -path '*/node_modules' -prune -o \ + \( -name '*.ts' -o -name '*.tsx' \) -print \ + | grep -vE '\.(test|spec)\.(ts|tsx)$|\.generated\.' + ) + fi + EXIT=0 + for f in "${files[@]}"; do + [ -z "$f" ] && continue + [ -f "$f" ] || continue # skip files deleted in this PR + lines=$(wc -l < "$f") + if [ "$lines" -gt 600 ]; then + echo "::error file=$f::$f has $lines lines (max 600)" + EXIT=1 + fi + done + exit $EXIT + + semantic-pr-title: + name: Semantic PR title + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3ab551d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +# CodeQL advanced setup. Replaces GitHub's default code-scanning setup; the +# repo must have default setup disabled in Security → Code scanning → "Set up" +# before this workflow can run. +# +# Languages were taken from the existing default-setup config (JS/TS, Python, +# Actions). Triggers mirror what default setup ran: push to main, every PR +# against main, and a weekly schedule. +# +# The rules and path filters live in .github/codeql/codeql-config.yml so policy +# changes show up as a normal PR diff. +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Mondays at 14:39 UTC — matches the cadence default setup was running on. + - cron: "39 14 * * 1" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..eafc2e7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,52 @@ +name: Docs + +permissions: + contents: read + +# Suppress hyperframes CLI telemetry from HeyGen's own CI runs. +# External users' CI continues to emit telemetry unless they set this themselves. +env: + HYPERFRAMES_NO_TELEMETRY: "1" + +on: + pull_request: + branches: [main] + paths: + - "docs/**" + - "DOCS_GUIDELINES.md" + - "packages/core/schemas/**" + - "scripts/sync-schemas.ts" + push: + branches: [main] + paths: + - "docs/**" + - "DOCS_GUIDELINES.md" + - "packages/core/schemas/**" + - "scripts/sync-schemas.ts" + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Validate docs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Check schema mirror (core → docs) + run: npx tsx scripts/sync-schemas.ts --check + + - name: Validate build + working-directory: docs + run: npx mint validate + + - name: Check broken links + working-directory: docs + run: npx mint broken-links diff --git a/.github/workflows/fast-video-validation.yml b/.github/workflows/fast-video-validation.yml new file mode 100644 index 0000000..7e66d14 --- /dev/null +++ b/.github/workflows/fast-video-validation.yml @@ -0,0 +1,55 @@ +# Validates the experimental fast-capture (drawElementImage) VIDEO path on a +# native amd64 Linux runner — where chrome-headless-shell's per-frame BeginFrame +# drives a real paint each frame, so drawElementImage's snapshot is fresh and +# video captures correctly. This is the one part of the feature that could not be +# validated locally (macOS has no BeginFrame; Docker-on-rosetta hung). +# See docs/fast-capture-limitations.md (Limitation 2). +# +# Manual trigger: Actions → "Fast-capture video validation" → Run workflow. +name: Fast-capture video validation + +permissions: + contents: read + +on: + # Manual only. NOTE: currently fails by design — fast capture cannot capture + # video on any platform yet (see docs/fast-capture-limitations.md, Limitation 2). + # This is the regression gate for if/when fast video is implemented. + workflow_dispatch: + inputs: + composition: + description: Test composition to render (must contain