Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"files": 1
},
"hyperframes-animation": {
"hash": "57458f4308708e21",
"files": 115
"hash": "f789d7f95d9ad2fe",
"files": 116
},
"hyperframes-cli": {
"hash": "9b36a367a0e3a332",
Expand All @@ -30,8 +30,8 @@
"files": 13
},
"hyperframes-creative": {
"hash": "bb248c1bc5cc28b5",
"files": 67
"hash": "8573b34712e14ab1",
"files": 68
},
"hyperframes-media": {
"hash": "096b2ab0a43b05dd",
Expand Down
2 changes: 2 additions & 0 deletions skills/hyperframes-animation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ node skills/hyperframes-animation/scripts/animation-map.mjs <composition-dir> \

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=<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
Expand Down
5 changes: 5 additions & 0 deletions skills/hyperframes-animation/scripts/animation-map.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
// Usage:
// node skills/hyperframes-animation/scripts/animation-map.mjs <composition-dir> \
// [--frames N] [--out <dir>] [--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";
Expand Down
27 changes: 18 additions & 9 deletions skills/hyperframes-animation/scripts/package-loader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}=<version> to pin it.`,
"",
].join("\n"),
);
return `${packageName}@latest`;
}

function resolvePackageEntry(packageName) {
Expand Down
62 changes: 62 additions & 0 deletions skills/hyperframes-animation/scripts/package-loader.test.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
});
2 changes: 2 additions & 0 deletions skills/hyperframes-creative/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<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
Expand Down
5 changes: 5 additions & 0 deletions skills/hyperframes-creative/scripts/contrast-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
// node skills/hyperframes-creative/scripts/contrast-report.mjs <composition-dir> \
// [--samples N] [--out <dir>] [--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.
Expand Down
27 changes: 18 additions & 9 deletions skills/hyperframes-creative/scripts/package-loader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}=<version> to pin it.`,
"",
].join("\n"),
);
return `${packageName}@latest`;
}

function resolvePackageEntry(packageName) {
Expand Down
62 changes: 62 additions & 0 deletions skills/hyperframes-creative/scripts/package-loader.test.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
});
Loading