Skills › HyperFrames › media-use › recipe.mjs
recipe.mjs
101 lines · skills/media-use/scripts/recipe.mjs at a595755
1#!/usr/bin/env node
2/**
3 * Recipes CLI — the heavyweight tier of HyperFrames user memory.
4 *
5 * node recipe.mjs freeze --hyperframes . --name <n> [--workflow <w>] [--blocks a,b,c]
6 * Freeze the current approved run as a named recipe: frame.md + the
7 * storyboard skeleton (structure kept, content blanked) + the brief
8 * skeleton (when BRIEF.md exists) + the confirmed brief values.
9 * The workflow is read from BRIEF.md; --workflow only covers projects
10 * briefed before BRIEF.md existed. Re-freezing the same name bumps the
11 * version and archives the old folder as <name>@v<N>. Promotes to
12 * ~/.media/recipes/ immediately.
13 *
14 * node recipe.mjs list --hyperframes . [--workflow <w>] [--json]
15 * Two-tier merged listing (project wins), newest approval first.
16 *
17 * node recipe.mjs use --hyperframes . --name <n> [--json]
18 * Adopt a recipe: import it from the user tier if needed, copy its
19 * frame.md over the project's, and print the brief values + skeleton path.
20 *
21 * When recipes are offered and consumed: references/memory.md.
22 */
23import { parseArgs } from "node:util";
24import { freezeRecipe, listRecipes, useRecipe } from "./lib/recipe-store.mjs";
26const { values: args, positionals } = parseArgs({
27 options: {
28 hyperframes: { type: "string", default: "." },
29 name: { type: "string" },
30 workflow: { type: "string" },
31 blocks: { type: "string" },
32 json: { type: "boolean", default: false },
33 },
34 allowPositionals: true,
35});
37const verb = positionals[0];
39function fail(message) {
40 console.error(message);
41 process.exit(1);
42}
44try {
45 if (verb === "freeze") {
46 if (!args.name) fail("freeze needs --name");
47 const frozen = freezeRecipe({
48 projectDir: args.hyperframes,
49 name: args.name,
50 workflow: args.workflow,
51 blocks: args.blocks
52 ? args.blocks
53 .split(",")
54 .map((b) => b.trim())
55 .filter(Boolean)
56 : undefined,
57 });
58 if (args.json) console.log(JSON.stringify({ ok: true, ...frozen }));
59 else {
60 console.log(
61 `froze recipe ${frozen.slug} (v${frozen.version}, ${frozen.workflow}) → ${frozen.dir}`,
62 );
63 if (frozen.workflowOverridden)
64 console.log(` (BRIEF.md says "${frozen.workflow}" — the --workflow flag was ignored)`);
65 if (!frozen.briefSkeleton)
66 console.log(" (no BRIEF.md in the project — brief skeleton skipped)");
67 }
68 } else if (verb === "list") {
69 const list = listRecipes({ projectDir: args.hyperframes, workflow: args.workflow });
70 if (args.json) console.log(JSON.stringify(list));
71 else if (list.length === 0) console.log("no recipes yet");
72 else {
73 for (const r of list) {
74 console.log(
75 `${r.name} v${r.version} ${r.workflow} (${r.source}; approved ${String(r.approved_at ?? "?").slice(0, 10)})`,
76 );
77 }
78 }
79 } else if (verb === "use") {
80 if (!args.name) fail("use needs --name");
81 const used = useRecipe({ projectDir: args.hyperframes, name: args.name });
82 if (args.json) console.log(JSON.stringify({ ok: true, ...used }));
83 else {
84 console.log(
85 `using recipe ${used.recipe.name} (v${used.recipe.version}, ${used.recipe.workflow})`,
86 );
87 console.log(` frame spec → ${used.frameSpecPath} (copied over)`);
88 console.log(` storyboard skeleton → ${used.skeletonPath}`);
89 if (used.briefSkeletonPath) console.log(` brief skeleton → ${used.briefSkeletonPath}`);
90 for (const key of ["destination", "aspect", "language", "voice", "style_preset"]) {
91 if (used.recipe[key]) console.log(` ${key}: ${used.recipe[key]}`);
92 }
93 }
94 } else {
95 fail(
96 "usage: recipe.mjs <freeze|list|use> --hyperframes . [--name <n>] [--workflow <w>] [--blocks a,b] [--json]",
97 );
98 }
99} catch (err) {
100 fail(err instanceof Error ? err.message : String(err));
101}