Skip to content
Merged
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ jobs:
- name: Verify standalone skill imports
run: bun test scripts/check-skill-import-boundaries.test.mjs
- name: Discover and run skills tests
env:
HYPERFRAMES_MEDIA_HOME_REQUIRED: "1"
# 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).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
"package:codex-plugin": "node scripts/package-codex-plugin.mjs",
Expand Down
14 changes: 6 additions & 8 deletions packages/cli/src/media-use/lib/cache.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
import { join, basename } from "node:path";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { readManifest, appendRecord, normalizePrompt } from "./manifest.mjs";
import { globalMediaDir, mediaHome } from "./media-home.mjs";

export { globalMediaDir };

const SCHEMA_PREFIX = "mu-v1-";
const KEY_HEX_CHARS = 16;
const COMPLETE_SENTINEL = ".hf-complete";

export function globalMediaDir() {
return join(homedir(), ".media");
}

export function contentHash(filePath) {
const bytes = readFileSync(filePath);
return createHash("sha256").update(bytes).digest("hex");
Expand All @@ -30,11 +28,11 @@ function markComplete(entryDir) {
}

// The manifest helpers append their own ".media" to the dir they get, so the
// global manifest must be addressed by HOME, not by globalMediaDir() — passing
// global manifest must be addressed by mediaHome(), not by globalMediaDir() — passing
// the latter nested it at ~/.media/.media/manifest.jsonl, invisible to the
// Studio /api/assets/global route (which reads the documented flat path).
export function readGlobalManifest() {
return readManifest(homedir());
return readManifest(mediaHome());
}

// Resolve a content-sha (full or unambiguous prefix) to a reusable global-cache
Expand Down Expand Up @@ -105,7 +103,7 @@ export function cachePut(filePath, record) {
reusable: true,
cached_path: dest,
};
appendRecord(homedir(), globalRecord);
appendRecord(mediaHome(), globalRecord);
return { sha, cached_path: dest };
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/media-use/lib/candidates.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { tmpdir } from "node:os";
import { listCandidates, formatCandidates, CANDIDATE_CAP } from "./candidates.mjs";
import { findGlobalBySha } from "./cache.mjs";

// candidates + findGlobalBySha are offline (no heygen), so we can override HOME
// candidates + findGlobalBySha are offline (no heygen), so we can point HYPERFRAMES_MEDIA_HOME
// to a temp dir and seed a fake global ~/.media manifest deterministically.
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-cand-"));
const project = join(root, "proj");
const home = join(root, "home");
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, project, home };
}
function seedManifest(dir, records) {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/media-use/lib/manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
promote,
} from "./cache.mjs";

process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-media-home-"));

let tmp;

function setup() {
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/src/media-use/lib/media-home.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { homedir } from "node:os";
import { join } from "node:path";

// The one owner of where the person's media library lives. This repo's test runs set
// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead
// of writing fixtures into the media library apps list as the person's own files.
export function mediaHome() {
const override = process.env.HYPERFRAMES_MEDIA_HOME;
if (override) return override;
if (process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED) {
throw new Error(
"media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir",
);
}
return homedir();
}

export function globalMediaDir() {
return join(mediaHome(), ".media");
}
61 changes: 61 additions & 0 deletions packages/cli/src/media-use/lib/media-home.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

import { cachePut } from "./cache.mjs";
import { globalMediaDir } from "./media-home.mjs";

const HOME = mkdtempSync(join(tmpdir(), "media-home-"));
process.env.HYPERFRAMES_MEDIA_HOME = HOME;

test("the global cache writes into the media home a test points it at", () => {
const work = mkdtempSync(join(tmpdir(), "media-home-src-"));
const file = join(work, "clip.wav");
writeFileSync(file, "media-home regression bytes");
cachePut(file, { id: "bgm_001", type: "bgm", provenance: { prompt: "media home probe" } });
const manifest = join(HOME, ".media", "manifest.jsonl");
assert.ok(existsSync(manifest), "the temp media home holds the global manifest");
assert.match(readFileSync(manifest, "utf8"), /media home probe/);
assert.equal(globalMediaDir(), join(HOME, ".media"));
});

// A child process that resolves the media dir, with only the given media-home variables set.
function runProbe(env) {
const lib = new URL("./media-home.mjs", import.meta.url).href;
const childEnv = { ...process.env };
delete childEnv.HYPERFRAMES_MEDIA_HOME;
delete childEnv.HYPERFRAMES_MEDIA_HOME_REQUIRED;
delete childEnv.NODE_TEST_CONTEXT;
const run = spawnSync(
process.execPath,
[
"--input-type=module",
"-e",
`const m = await import(${JSON.stringify(lib)}); console.log("dir=" + m.globalMediaDir());`,
],
{ encoding: "utf8", env: { ...childEnv, ...env } },
);
return { status: run.status, output: `${run.stdout}${run.stderr}` };
}

test("a repo test run that never points the media home anywhere fails instead of writing", () => {
const { status, output } = runProbe({ HYPERFRAMES_MEDIA_HOME_REQUIRED: "1" });
assert.notEqual(status, 0, output);
assert.match(output, /set HYPERFRAMES_MEDIA_HOME to a temp dir/);
});

// Node sets NODE_TEST_CONTEXT for every project's test runs, so another project whose own tests
// spawn this CLI must get the person's real library, not the guard.
test("another project's node --test run still resolves the real media home", () => {
const home = mkdtempSync(join(tmpdir(), "media-home-user-"));
const { status, output } = runProbe({
HOME: home,
USERPROFILE: home,
NODE_TEST_CONTEXT: "child-v8",
});
assert.equal(status, 0, output);
assert.ok(output.includes(`dir=${join(home, ".media")}`), output);
});
12 changes: 6 additions & 6 deletions packages/cli/src/media-use/lib/misses.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { globalMediaDir } from "./media-home.mjs";

const MISSES_FILE = "misses.jsonl";

function missesPath() {
return join(homedir(), ".media", MISSES_FILE);
return join(globalMediaDir(), MISSES_FILE);
}

export function recordMiss({ type, intent, provider_override, local_only }) {
try {
const dir = join(homedir(), ".media");
mkdirSync(dir, { recursive: true });
const path = missesPath();
mkdirSync(dirname(path), { recursive: true });
appendFileSync(
join(dir, MISSES_FILE),
path,
JSON.stringify({
ts: new Date().toISOString(),
type,
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/media-use/lib/misses.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-misses-"));
const home = join(root, "home");
mkdirSync(home, { recursive: true });
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, home };
}

Expand Down Expand Up @@ -54,6 +54,17 @@ test("recordMiss swallows filesystem failures", () => {
}
});

test("recordMiss stays best-effort when the media home cannot be resolved", () => {
const savedEnv = { ...process.env };
delete process.env.HYPERFRAMES_MEDIA_HOME;
process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED = "1";
try {
assert.doesNotThrow(() => recordMiss({ type: "sfx", intent: "no home", local_only: true }));
} finally {
restoreEnv(savedEnv);
}
});

test("readMisses skips corrupt lines", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/media-use/lib/prefs-store.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { globalMediaDir } from "./media-home.mjs";

/**
* Remembered defaults — the lightweight tier of HyperFrames user memory.
Expand Down Expand Up @@ -41,7 +41,7 @@ export function projectPrefsPath(projectDir) {
}

export function userPrefsPath() {
return join(homedir(), ".media", PREFS_FILE);
return join(globalMediaDir(), PREFS_FILE);
}

function emptyFile() {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/media-use/lib/prefs-store.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function sandbox() {
mkdirSync(home, { recursive: true });
mkdirSync(projectA, { recursive: true });
mkdirSync(projectB, { recursive: true });
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, home, projectA, projectB };
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/media-use/lib/recipe-store.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { basename, join, resolve } from "node:path";
import { globalMediaDir } from "./media-home.mjs";
import { appendRecord, mediaDir, nextId } from "./manifest.mjs";
import { regenerateIndex } from "./index-gen.mjs";
import { mergedPreferences } from "./prefs-store.mjs";
Expand Down Expand Up @@ -58,7 +58,7 @@ export function projectRecipesDir(projectDir) {
}

export function userRecipesDir() {
return join(homedir(), ".media", "recipes");
return join(globalMediaDir(), "recipes");
}

export function slugifyRecipeName(name) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/media-use/lib/recipe-store.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function sandbox() {
mkdirSync(project, { recursive: true });
writeFileSync(join(project, "frame.md"), "# Frame spec\nbackground: #101014\n");
writeFileSync(join(project, "STORYBOARD.md"), STORYBOARD);
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, home, project };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/media-use/lib/stats.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function sandbox() {
const projectDir = join(root, "project");
mkdirSync(home, { recursive: true });
mkdirSync(projectDir, { recursive: true });
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, home, projectDir };
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/media-use/lib/telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { globalMediaDir } from "./media-home.mjs";

const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
const POSTHOG_HOST = "https://us.i.posthog.com";
Expand Down Expand Up @@ -72,7 +73,7 @@ function writeSharedConfig(config) {
// one — otherwise cross-surface continuity would start over on upgrade.
function legacyMediaAnonId() {
try {
const file = join(homedir(), ".media", "anon-id");
const file = join(globalMediaDir(), "anon-id");
if (existsSync(file)) {
const id = readFileSync(file, "utf8").trim();
if (id) return id;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/media-use/lib/telemetry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function sandbox() {
const home = join(root, "home");
mkdirSync(home, { recursive: true });
process.env.HOME = home;
process.env.HYPERFRAMES_MEDIA_HOME = home;
return { root, home };
}

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/media-use/resolve.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ const CAN_TSX =
spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", "0"], {
stdio: "ignore",
}).status === 0;
process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-resolve-home-"));

let tmp;

function setup() {
tmp = mkdtempSync(join(tmpdir(), "mu-resolve-test-"));
// A fresh global cache per test, apart from the project so the two manifests never coincide.
process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-resolve-home-"));
}

function cleanup() {
Expand Down
1 change: 1 addition & 0 deletions scripts/check-media-use-copy-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const MEDIA_USE_COPY_NAMES = [
"index-gen.mjs",
"manifest.mjs",
"media-fetch.mjs",
"media-home.mjs",
"npx-sync.mjs",
"parakeet-words.mjs",
"prefs-store.mjs",
Expand Down
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"files": 1
},
"media-use": {
"hash": "b35de041b87e763d",
"files": 98
"hash": "7915441c22070c00",
"files": 99
},
"motion-graphics": {
"hash": "ab9d2e9c205ce1b1",
Expand Down
2 changes: 2 additions & 0 deletions skills/media-use/scripts/compatibility.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { execFileSync } from "node:child_process";
import test from "node:test";
import { pathToFileURL } from "node:url";

process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "media-use-compat-home-"));

function run(scriptsDir, script, args) {
return execFileSync(process.execPath, [join(scriptsDir, script), ...args], {
encoding: "utf8",
Expand Down
20 changes: 20 additions & 0 deletions skills/media-use/scripts/lib/media-home.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { homedir } from "node:os";
import { join } from "node:path";

// The one owner of where the person's media library lives. This repo's test runs set
// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead
// of writing fixtures into the media library apps list as the person's own files.
export function mediaHome() {
const override = process.env.HYPERFRAMES_MEDIA_HOME;
if (override) return override;
if (process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED) {
throw new Error(
"media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir",
);
}
return homedir();
}

export function globalMediaDir() {
return join(mediaHome(), ".media");
}
4 changes: 2 additions & 2 deletions skills/media-use/scripts/lib/prefs-store.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { globalMediaDir } from "./media-home.mjs";

/**
* Remembered defaults — the lightweight tier of HyperFrames user memory.
Expand Down Expand Up @@ -41,7 +41,7 @@ export function projectPrefsPath(projectDir) {
}

export function userPrefsPath() {
return join(homedir(), ".media", PREFS_FILE);
return join(globalMediaDir(), PREFS_FILE);
}

function emptyFile() {
Expand Down
Loading
Loading