Files
hyperframes/skills/pr-to-video/scripts/fetch-pr.mjs
T
WaterrrForeverandClaude Opus 4.8 1967901b57 refactor(skills): move product-launch / pr-to-video / faceless-explainer onto the script-driven architecture (#1635)
* refactor(product-launch-video): restructure onto script-driven architecture

Move product-launch-video onto the shared script-driven authoring flow:
build-frame remixes a hyperframes-creative preset onto brand tokens, audio
routes through the shared hyperframes-media engine, per-preset caption skins,
and every frame is authored as a directed shot. Removes the old bespoke
scripts (captions/validate/prep/hoist/…) in favour of the shared lib.

assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard
(reject an empty or markup-less scene file at assembly, before emitting
data-composition-src, and re-dispatch) carried onto the restructured reader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(pr-to-video): restructure onto script-driven architecture

Move pr-to-video onto the shared script-driven authoring flow: ingest.mjs
folds the gh PR artifacts into the synthetic capture package the shared
backend (build-frame / captions / assemble-index) reads, add the mechanism
beat, route audio through hyperframes-media, and remix a hyperframes-creative
preset onto brand tokens via the shared lib.

- Fix skill name: pr-to-video-refactor -> pr-to-video (match directory).
- Drop a stale faceless-explainer-refactor reference in an ingest.mjs comment.
- assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(faceless-explainer): restructure onto script-driven architecture

Move faceless-explainer onto the shared script-driven authoring flow:
every visual is invented (typography / abstract graphics / diagram / data-viz)
and authored through the shared backend (build-frame remixes a
hyperframes-creative preset onto tokens, audio via hyperframes-media,
assemble-index builds the standalone index.html) using the shared lib.

- Fix skill name: faceless-explainer-refactor -> faceless-explainer (match directory).
- assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(skills): refresh test-skills-fresh.sh workflow roster

Update the install-and-verify harness to the current surface: 10 workflows
(adds website-to-video, embedded-captions, graphic-overlays, slideshow;
drops the removed footage-recut) and refreshed example prompts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(product-launch-video): oxfmt storyboard.mjs

Run oxfmt over lib/storyboard.mjs — formatting only, no logic change.
Fixes the Format / Preflight CI check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): import commitGsapPositionFromDrag from its actual module

The function was split out into gsapDragPositionCommit.ts in #1605, but the
test kept importing it from ./gsapDragCommit, which no longer exports it —
yielding 'is not a function' at runtime. Import from the correct module.

Inherited main breakage (same fix as #1631); fixes the Test CI check on this
branch independently of merge order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(hyperframes): refine router skill metadata tags

Update the entry router's metadata tags (video / animation / router focus);
oxfmt collapses the now-shorter metadata to a single line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): tighten caption comment-strip + document audio --only merge

Review follow-ups (#1635):

- captions.mjs (x3): the HTML-comment strip used a single global replace, which
  CodeQL flags as incomplete multi-character sanitization (a nested/partial pair
  can re-form a marker the single pass misses). Strip in a fixpoint loop instead.
  Input is preset-library content, not user-controlled, so this is lint-
  cleanliness, not XSS defense.
- audio.mjs (x3): document that fetch-sfx (--only sfx) MERGES into the neutral
  audio_engine_meta.json sidecar — the engine reads prev and recomputes only the
  sfx section, so voices/bgm from the generate pass are preserved (review Q).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): remove existsSync->write TOCTOU in workflow scripts

Clears the 9 js/file-system-race CodeQL alerts (captions/audio/transitions x3).
Each was an existsSync precheck followed by a later write of the same path:

- captions.mjs: caption-overrides shim -> atomic writeFileSync({ flag: 'wx' }).
- audio.mjs (sync-durations) + transitions.mjs (inject): drop the existsSync
  precheck and read directly, surfacing the same friendly error from a try/catch
  on readFileSync — no check->write gap.

Behavior is unchanged (same error messages); these are local single-process
deterministic scripts so the race was never a real risk, but this clears the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skills): paint root composition ground color in assemble-index

Per-frame roots carry data-start/data-duration and get clip-gated against the
global timeline at render, so only the first frame's window overlaps global 0 —
a frame's own full-bleed background can't serve as the video ground, and every
frame after the first renders on the bare body color (black). Paint the ground
on the always-present root composition using the project's frame.md canvas color
(the same role the caption skin maps to --cap-canvas); fall back to the body
letterbox color when frame.md is absent or has no resolvable ground.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(hyperframes): drop router-tag edit (moved to the foundation PR)

The entry SKILL.md is rewritten wholesale by the frame-presets/media foundation
PR (#1632); editing it here too guaranteed a merge conflict. Restore this file
to main and let the router-tag tweak live with the rewrite in #1632, so the two
PRs no longer both touch it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:49:42 +08:00

165 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Step 1 — PR fetch (deterministic; runs gh; large-PR-safe; NO scratch dir).
//
// Replaces a bare `gh pr view … > capture/pr.json` in the orchestrator. It folds
// the PR into the two artifacts ingest.mjs consumes:
// capture/pr.json the gh pr view core (title, body, author, refs, commits,
// reviews, comments, assignees, +/ stats, …) with its `files`
// list COMPLETED via a paginated `gh api .../pulls/N/files`
// call — `gh pr view --json files` truncates at ~100 files, so a
// big PR would otherwise lose the tail. Commits keep gh pr view's
// rich `authors[]` (co-authors) — only `files` needs the override.
// capture/diff.patch the full unified diff (`gh pr diff`).
//
// gh runs HERE so auth / not-found / private-repo errors surface with gh's own stderr
// and exit 1 (the orchestrator then stops). Intermediates are held in memory — this
// writes ONLY the two files above, so there is no `_ingest_tmp/` scratch to clean up
// (the previous "let the agent fetch in pieces" approach polluted videos/ and was
// non-deterministic). ingest.mjs stays a pure offline transform downstream.
//
// Usage:
// node fetch-pr.mjs --pr "<url | owner/repo#N | N>" [--out-dir ./capture]
//
// Exit 0 = capture/pr.json + capture/diff.patch written + summary on stdout.
// Exit 1 = gh not authenticated / PR not found / pr view failed.
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
const argv = process.argv.slice(2);
const flag = (name, def) => {
const i = argv.indexOf(`--${name}`);
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
};
function die(msg) {
console.error(`✗ fetch-pr.mjs: ${msg}`);
process.exit(1);
}
const prRef = flag("pr", null);
if (!prRef) die('--pr "<url | owner/repo#N | N>" is required');
const outDir = resolve(flag("out-dir", "./capture"));
// Run gh, capture stdout. Returns { ok, stdout, stderr } — never throws (callers
// decide whether a failure is fatal). 64 MB buffer covers large diffs / file lists.
function ghTry(args) {
try {
const stdout = execFileSync("gh", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
return { ok: true, stdout, stderr: "" };
} catch (e) {
return {
ok: false,
stdout: (e.stdout || "").toString(),
stderr: (e.stderr || e.message || "").toString().trim(),
};
}
}
// ── 0. auth — fail fast with gh's own hint ───────────────────────────────────
if (!ghTry(["auth", "status"]).ok) {
die("gh is not authenticated — run: gh auth login");
}
// ── 1. core PR object (gh pr view) ───────────────────────────────────────────
const FIELDS = [
"number",
"title",
"body",
"author",
"url",
"baseRefName",
"headRefName",
"commits",
"files",
"additions",
"deletions",
"changedFiles",
"labels",
"reviews",
"latestReviews",
"comments",
"assignees",
"reviewDecision",
"mergedBy",
].join(",");
const view = ghTry(["pr", "view", prRef, "--json", FIELDS]);
if (!view.ok) die(`gh pr view "${prRef}" failed (auth / not found / private?):\n${view.stderr}`);
let pr;
try {
pr = JSON.parse(view.stdout);
} catch (e) {
die(`gh pr view returned unparseable JSON (${e.message})`);
}
// ── 2. complete the files list via paginated gh api (the truncation fix) ──────
// gh pr view --json files caps at ~100 files; the REST endpoint paginates with no
// cap. --jq runs per page, so the output is NDJSON (one file object per line).
const number = pr.number;
const m = /github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(pr.url || "");
const owner = m?.[1];
const repo = m?.[2];
let filesNote = `${Array.isArray(pr.files) ? pr.files.length : 0} (from pr view)`;
if (owner && repo && number != null) {
const apiFiles = ghTry([
"api",
"--paginate",
`repos/${owner}/${repo}/pulls/${number}/files`,
"--jq",
".[] | {path: .filename, additions, deletions, status}",
]);
if (apiFiles.ok) {
const files = apiFiles.stdout
.split("\n")
.filter(Boolean)
.map((l) => {
try {
return JSON.parse(l);
} catch {
return null;
}
})
.filter(Boolean);
if (files.length) {
pr.files = files;
if (pr.changedFiles == null || files.length > pr.changedFiles) pr.changedFiles = files.length;
filesNote = `${files.length} (completed via gh api)`;
}
} else {
console.error(
` (warn: gh api files failed — keeping pr view's files: ${apiFiles.stderr.split("\n")[0]})`,
);
}
} else {
console.error(" (warn: could not parse owner/repo from PR url — keeping pr view's files)");
}
// ── 3. write capture/pr.json + capture/diff.patch ────────────────────────────
mkdirSync(outDir, { recursive: true });
const prJsonPath = join(outDir, "pr.json");
writeFileSync(prJsonPath, JSON.stringify(pr, null, 2) + "\n");
const diff = ghTry(["pr", "diff", prRef]);
const diffPath = join(outDir, "diff.patch");
if (diff.ok) {
writeFileSync(diffPath, diff.stdout);
} else {
// The brief still builds without the diff (ingest treats it as optional), so this
// is a warning, not fatal — but surface it.
console.error(
` (warn: gh pr diff failed — brief builds without it: ${diff.stderr.split("\n")[0]})`,
);
}
// ── 4. summary ───────────────────────────────────────────────────────────────
const repoLabel = owner && repo ? `${owner}/${repo}` : "(repo?)";
console.log(
[
`✓ fetch-pr: ${repoLabel} PR #${number ?? "?"} — "${(pr.title || "").slice(0, 72)}"`,
` files: ${filesNote}; diff: ${diff.ok ? `${diff.stdout.length} chars` : "MISSING"}`,
` wrote ${prJsonPath}${diff.ok ? ` + ${diffPath}` : ""}`,
].join("\n"),
);