diff --git a/skills-manifest.json b/skills-manifest.json index fcd78fcbb6..cf3d5ba607 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,8 +18,8 @@ "files": 1 }, "hyperframes-animation": { - "hash": "57458f4308708e21", - "files": 115 + "hash": "f789d7f95d9ad2fe", + "files": 116 }, "hyperframes-cli": { "hash": "9b36a367a0e3a332", @@ -30,8 +30,8 @@ "files": 13 }, "hyperframes-creative": { - "hash": "bb248c1bc5cc28b5", - "files": 67 + "hash": "8573b34712e14ab1", + "files": 68 }, "hyperframes-media": { "hash": "096b2ab0a43b05dd", diff --git a/skills/hyperframes-animation/SKILL.md b/skills/hyperframes-animation/SKILL.md index 416c7c10a4..8e7c03250d 100644 --- a/skills/hyperframes-animation/SKILL.md +++ b/skills/hyperframes-animation/SKILL.md @@ -75,6 +75,8 @@ node skills/hyperframes-animation/scripts/animation-map.mjs \ Reads every GSAP timeline registered on `window.__timelines`, enumerates tweens, samples bboxes, computes flags, outputs `animation-map.json`. Use it to audit choreography (dead zones, stagger consistency, lifecycle warnings) after authoring. +`animation-map.mjs` resolves helper packages from the current project first, then can bootstrap the bundled HyperFrames package version. Set `HYPERFRAMES_SKILL_PKG_VERSION=` only when running the skill outside the bundled CLI/skill install and you need to pin that bootstrap version explicitly. + ## See Also - `hyperframes-core` — composition structure, data attributes, sub-compositions, deterministic render contract diff --git a/skills/hyperframes-animation/scripts/animation-map.mjs b/skills/hyperframes-animation/scripts/animation-map.mjs index 95771ab795..f95bb09331 100644 --- a/skills/hyperframes-animation/scripts/animation-map.mjs +++ b/skills/hyperframes-animation/scripts/animation-map.mjs @@ -8,6 +8,11 @@ // Usage: // node skills/hyperframes-animation/scripts/animation-map.mjs \ // [--frames N] [--out ] [--min-duration S] [--width W] [--height H] [--fps N] +// +// Env: +// HYPERFRAMES_SKILL_PKG_VERSION — pin the @hyperframes/producer version used +// when bootstrapping (global skill installs cannot infer it; falls back to +// @latest with a warning otherwise). import { mkdir, writeFile } from "node:fs/promises"; import { resolve, join } from "node:path"; diff --git a/skills/hyperframes-animation/scripts/package-loader.mjs b/skills/hyperframes-animation/scripts/package-loader.mjs index d3e388225a..063bf4fe0c 100644 --- a/skills/hyperframes-animation/scripts/package-loader.mjs +++ b/skills/hyperframes-animation/scripts/package-loader.mjs @@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); +const VERSION_OVERRIDE_ENV = "HYPERFRAMES_SKILL_PKG_VERSION"; const BOOTSTRAP_ENV = "HYPERFRAMES_SKILL_DEPS_BOOTSTRAPPED"; const BOOTSTRAP_CONFIRM_ENV = "HYPERFRAMES_SKILL_BOOTSTRAP_DEPS"; const NODE_MODULES_ENV = "HYPERFRAMES_SKILL_NODE_MODULES"; @@ -46,16 +47,24 @@ export async function importPackagesOrBootstrap(packageNames, options = {}) { } export function hyperframesPackageSpec(packageName) { + const override = process.env[VERSION_OVERRIDE_ENV]?.trim(); + if (override) return `${packageName}@${override}`; + const version = readBundledHyperframesVersion(); - if (!version) { - throw new Error( - [ - `Could not determine the bundled HyperFrames version for ${packageName}.`, - "Install the package yourself or pass a pinned options.npmPackages entry.", - ].join("\n"), - ); - } - return `${packageName}@${version}`; + if (version) return `${packageName}@${version}`; + + // Global skill installs (e.g. ~/.claude/skills) have no hyperframes package.json + // in their ancestor chain, so the bundled version is unknowable. Fall back to + // @latest instead of throwing: already-installed packages still import, and a + // bootstrap install can still proceed (@latest satisfies the pinned-spec guard). + process.stderr.write( + [ + `hyperframes: could not determine the bundled version for ${packageName}; using @latest.`, + `Set ${VERSION_OVERRIDE_ENV}= to pin it.`, + "", + ].join("\n"), + ); + return `${packageName}@latest`; } function resolvePackageEntry(packageName) { diff --git a/skills/hyperframes-animation/scripts/package-loader.test.mjs b/skills/hyperframes-animation/scripts/package-loader.test.mjs new file mode 100644 index 0000000000..7b36a3ceb3 --- /dev/null +++ b/skills/hyperframes-animation/scripts/package-loader.test.mjs @@ -0,0 +1,62 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ENV = "HYPERFRAMES_SKILL_PKG_VERSION"; + +// (a) env override wins — no ancestor lookup, exact version echoed back. +test("hyperframesPackageSpec: env override wins", async () => { + const prev = process.env[ENV]; + process.env[ENV] = "9.9.9"; + try { + const { hyperframesPackageSpec } = await import("./package-loader.mjs"); + assert.equal(hyperframesPackageSpec("@hyperframes/producer"), "@hyperframes/producer@9.9.9"); + } finally { + if (prev === undefined) delete process.env[ENV]; + else process.env[ENV] = prev; + } +}); + +// (b) resolvable version (in-repo) pins the bundled hyperframes/@hyperframes/cli version. +test("hyperframesPackageSpec: resolvable in-repo version pins it", async () => { + const prev = process.env[ENV]; + delete process.env[ENV]; + try { + const { hyperframesPackageSpec } = await import("./package-loader.mjs"); + const spec = hyperframesPackageSpec("@hyperframes/producer"); + assert.match(spec, /^@hyperframes\/producer@\d+\.\d+\.\d+/); + } finally { + if (prev !== undefined) process.env[ENV] = prev; + } +}); + +// (c) unresolvable + no override -> @latest fallback, no throw (global-install case). +// Copy the loader into an isolated temp dir whose ancestor chain has no hyperframes +// package.json, and run node from there so cwd cannot resolve one either. +test("hyperframesPackageSpec: unresolvable falls back to @latest without throwing", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-pkgloader-")); + try { + copyFileSync(join(HERE, "package-loader.mjs"), join(dir, "package-loader.mjs")); + const probe = join(dir, "probe.mjs"); + writeFileSync( + probe, + [ + 'import { hyperframesPackageSpec } from "./package-loader.mjs";', + 'process.stdout.write(hyperframesPackageSpec("@hyperframes/producer"));', + "", + ].join("\n"), + ); + const res = spawnSync(process.execPath, [probe], { cwd: dir, encoding: "utf8" }); + assert.equal(res.status, 0, res.stderr); + assert.equal(res.stdout.trim(), "@hyperframes/producer@latest"); + assert.match(res.stderr, /using @latest/); + assert.match(res.stderr, new RegExp(ENV)); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/skills/hyperframes-creative/SKILL.md b/skills/hyperframes-creative/SKILL.md index e249645944..e76ff5e78d 100644 --- a/skills/hyperframes-creative/SKILL.md +++ b/skills/hyperframes-creative/SKILL.md @@ -52,6 +52,8 @@ For motion patterns, scene blueprints, transitions, and CSS marker effects, use - `scripts/extract-audio-data.py` — pre-extract audio bands for audio-reactive compositions. - `scripts/package-loader.mjs` — support script for bundled creative tooling. +`contrast-report.mjs` resolves helper packages from the current project first, then can bootstrap the bundled HyperFrames package version. Set `HYPERFRAMES_SKILL_PKG_VERSION=` only when running the skill outside the bundled CLI/skill install and you need to pin that bootstrap version explicitly. + Run from the repo root with explicit paths, for example: ```bash diff --git a/skills/hyperframes-creative/scripts/contrast-report.mjs b/skills/hyperframes-creative/scripts/contrast-report.mjs index cd18eaeb7e..ee9b7835b8 100644 --- a/skills/hyperframes-creative/scripts/contrast-report.mjs +++ b/skills/hyperframes-creative/scripts/contrast-report.mjs @@ -12,6 +12,11 @@ // node skills/hyperframes-creative/scripts/contrast-report.mjs \ // [--samples N] [--out ] [--width W] [--height H] [--fps N] // +// Env: +// HYPERFRAMES_SKILL_PKG_VERSION — pin the @hyperframes/producer version used +// when bootstrapping (global skill installs cannot infer it; falls back to +// @latest with a warning otherwise). +// // The composition directory must contain an index.html. Raw authoring HTML // works — the producer's file server auto-injects the runtime at serve time. // Exits 1 if any text element fails WCAG AA. diff --git a/skills/hyperframes-creative/scripts/package-loader.mjs b/skills/hyperframes-creative/scripts/package-loader.mjs index d3e388225a..063bf4fe0c 100644 --- a/skills/hyperframes-creative/scripts/package-loader.mjs +++ b/skills/hyperframes-creative/scripts/package-loader.mjs @@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); +const VERSION_OVERRIDE_ENV = "HYPERFRAMES_SKILL_PKG_VERSION"; const BOOTSTRAP_ENV = "HYPERFRAMES_SKILL_DEPS_BOOTSTRAPPED"; const BOOTSTRAP_CONFIRM_ENV = "HYPERFRAMES_SKILL_BOOTSTRAP_DEPS"; const NODE_MODULES_ENV = "HYPERFRAMES_SKILL_NODE_MODULES"; @@ -46,16 +47,24 @@ export async function importPackagesOrBootstrap(packageNames, options = {}) { } export function hyperframesPackageSpec(packageName) { + const override = process.env[VERSION_OVERRIDE_ENV]?.trim(); + if (override) return `${packageName}@${override}`; + const version = readBundledHyperframesVersion(); - if (!version) { - throw new Error( - [ - `Could not determine the bundled HyperFrames version for ${packageName}.`, - "Install the package yourself or pass a pinned options.npmPackages entry.", - ].join("\n"), - ); - } - return `${packageName}@${version}`; + if (version) return `${packageName}@${version}`; + + // Global skill installs (e.g. ~/.claude/skills) have no hyperframes package.json + // in their ancestor chain, so the bundled version is unknowable. Fall back to + // @latest instead of throwing: already-installed packages still import, and a + // bootstrap install can still proceed (@latest satisfies the pinned-spec guard). + process.stderr.write( + [ + `hyperframes: could not determine the bundled version for ${packageName}; using @latest.`, + `Set ${VERSION_OVERRIDE_ENV}= to pin it.`, + "", + ].join("\n"), + ); + return `${packageName}@latest`; } function resolvePackageEntry(packageName) { diff --git a/skills/hyperframes-creative/scripts/package-loader.test.mjs b/skills/hyperframes-creative/scripts/package-loader.test.mjs new file mode 100644 index 0000000000..7b36a3ceb3 --- /dev/null +++ b/skills/hyperframes-creative/scripts/package-loader.test.mjs @@ -0,0 +1,62 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const ENV = "HYPERFRAMES_SKILL_PKG_VERSION"; + +// (a) env override wins — no ancestor lookup, exact version echoed back. +test("hyperframesPackageSpec: env override wins", async () => { + const prev = process.env[ENV]; + process.env[ENV] = "9.9.9"; + try { + const { hyperframesPackageSpec } = await import("./package-loader.mjs"); + assert.equal(hyperframesPackageSpec("@hyperframes/producer"), "@hyperframes/producer@9.9.9"); + } finally { + if (prev === undefined) delete process.env[ENV]; + else process.env[ENV] = prev; + } +}); + +// (b) resolvable version (in-repo) pins the bundled hyperframes/@hyperframes/cli version. +test("hyperframesPackageSpec: resolvable in-repo version pins it", async () => { + const prev = process.env[ENV]; + delete process.env[ENV]; + try { + const { hyperframesPackageSpec } = await import("./package-loader.mjs"); + const spec = hyperframesPackageSpec("@hyperframes/producer"); + assert.match(spec, /^@hyperframes\/producer@\d+\.\d+\.\d+/); + } finally { + if (prev !== undefined) process.env[ENV] = prev; + } +}); + +// (c) unresolvable + no override -> @latest fallback, no throw (global-install case). +// Copy the loader into an isolated temp dir whose ancestor chain has no hyperframes +// package.json, and run node from there so cwd cannot resolve one either. +test("hyperframesPackageSpec: unresolvable falls back to @latest without throwing", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-pkgloader-")); + try { + copyFileSync(join(HERE, "package-loader.mjs"), join(dir, "package-loader.mjs")); + const probe = join(dir, "probe.mjs"); + writeFileSync( + probe, + [ + 'import { hyperframesPackageSpec } from "./package-loader.mjs";', + 'process.stdout.write(hyperframesPackageSpec("@hyperframes/producer"));', + "", + ].join("\n"), + ); + const res = spawnSync(process.execPath, [probe], { cwd: dir, encoding: "utf8" }); + assert.equal(res.status, 0, res.stderr); + assert.equal(res.stdout.trim(), "@hyperframes/producer@latest"); + assert.match(res.stderr, /using @latest/); + assert.match(res.stderr, new RegExp(ENV)); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +});