mirror of
https://github.com/hanzoai/frames.git
synced 2026-09-24 00:08:03 +00:00
Forked from HyperFrames (Apache-2.0), full history preserved. Hosts are mapped explicitly and BEFORE the identifier rename, because hyperframes.heygen.com is not decoration: 175 of its occurrences are $schema identifiers that a validator dereferences. A blind rename would have left them naming a host that never serves them. The schema files are in this repo under docs/schema, so the mapped host is one we can actually serve -- that is the condition that makes the rewrite safe, and serving them is a deploy task, not an edit. docs/schema/hyperframes.json is renamed to frames.json to match the identifier it is now published under. api.heygen.com and the CLI auth against heygen.com are left UNRENAMED on purpose. They are a dependency on HeyGen's SaaS, not a name of ours to take, and the right move is to remove that integration rather than rebrand it. LICENSE and CREDITS.md are untouched; NOTICE records the lineage. Co-authored-by: Hanzo Dev <[email protected]>
104 lines
3.9 KiB
JavaScript
104 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Recipes CLI — the heavyweight tier of Frames user memory.
|
|
*
|
|
* node recipe.mjs freeze --frames . --name <n> [--workflow <w>] [--blocks a,b,c]
|
|
* Freeze the current approved run as a named recipe: frame.md + the
|
|
* storyboard skeleton (structure kept, content blanked) + the brief
|
|
* skeleton (when BRIEF.md exists) + the confirmed brief values.
|
|
* The workflow is read from BRIEF.md; --workflow only covers projects
|
|
* briefed before BRIEF.md existed. Re-freezing the same name bumps the
|
|
* version and archives the old folder as <name>@v<N>. Promotes to
|
|
* ~/.media/recipes/ immediately.
|
|
*
|
|
* node recipe.mjs list --frames . [--workflow <w>] [--json]
|
|
* Two-tier merged listing (project wins), newest approval first.
|
|
*
|
|
* node recipe.mjs use --frames . --name <n> [--json]
|
|
* Adopt a recipe: import it from the user tier if needed, copy its
|
|
* frame.md over the project's, and print the brief values + skeleton path.
|
|
*
|
|
* When recipes are offered/consumed is the review loop's and the intent
|
|
* layer's business — see frames-core/references/review-loop.md § 4 and
|
|
* the intent layer's recipe check (frames SKILL.md § 4).
|
|
*/
|
|
import { parseArgs } from "node:util";
|
|
import { freezeRecipe, listRecipes, useRecipe } from "./lib/recipe-store.mjs";
|
|
|
|
const { values: args, positionals } = parseArgs({
|
|
options: {
|
|
frames: { type: "string", default: "." },
|
|
name: { type: "string" },
|
|
workflow: { type: "string" },
|
|
blocks: { type: "string" },
|
|
json: { type: "boolean", default: false },
|
|
},
|
|
allowPositionals: true,
|
|
});
|
|
|
|
const verb = positionals[0];
|
|
|
|
function fail(message) {
|
|
console.error(message);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
if (verb === "freeze") {
|
|
if (!args.name) fail("freeze needs --name");
|
|
const frozen = freezeRecipe({
|
|
projectDir: args.frames,
|
|
name: args.name,
|
|
workflow: args.workflow,
|
|
blocks: args.blocks
|
|
? args.blocks
|
|
.split(",")
|
|
.map((b) => b.trim())
|
|
.filter(Boolean)
|
|
: undefined,
|
|
});
|
|
if (args.json) console.log(JSON.stringify({ ok: true, ...frozen }));
|
|
else {
|
|
console.log(
|
|
`froze recipe ${frozen.slug} (v${frozen.version}, ${frozen.workflow}) → ${frozen.dir}`,
|
|
);
|
|
if (frozen.workflowOverridden)
|
|
console.log(` (BRIEF.md says "${frozen.workflow}" — the --workflow flag was ignored)`);
|
|
if (!frozen.briefSkeleton)
|
|
console.log(" (no BRIEF.md in the project — brief skeleton skipped)");
|
|
}
|
|
} else if (verb === "list") {
|
|
const list = listRecipes({ projectDir: args.frames, workflow: args.workflow });
|
|
if (args.json) console.log(JSON.stringify(list));
|
|
else if (list.length === 0) console.log("no recipes yet");
|
|
else {
|
|
for (const r of list) {
|
|
console.log(
|
|
`${r.name} v${r.version} ${r.workflow} (${r.source}; approved ${String(r.approved_at ?? "?").slice(0, 10)})`,
|
|
);
|
|
}
|
|
}
|
|
} else if (verb === "use") {
|
|
if (!args.name) fail("use needs --name");
|
|
const used = useRecipe({ projectDir: args.frames, name: args.name });
|
|
if (args.json) console.log(JSON.stringify({ ok: true, ...used }));
|
|
else {
|
|
console.log(
|
|
`using recipe ${used.recipe.name} (v${used.recipe.version}, ${used.recipe.workflow})`,
|
|
);
|
|
console.log(` frame spec → ${used.frameSpecPath} (copied over)`);
|
|
console.log(` storyboard skeleton → ${used.skeletonPath}`);
|
|
if (used.briefSkeletonPath) console.log(` brief skeleton → ${used.briefSkeletonPath}`);
|
|
for (const key of ["destination", "aspect", "language", "voice", "style_preset"]) {
|
|
if (used.recipe[key]) console.log(` ${key}: ${used.recipe[key]}`);
|
|
}
|
|
}
|
|
} else {
|
|
fail(
|
|
"usage: recipe.mjs <freeze|list|use> --frames . [--name <n>] [--workflow <w>] [--blocks a,b] [--json]",
|
|
);
|
|
}
|
|
} catch (err) {
|
|
fail(err instanceof Error ? err.message : String(err));
|
|
}
|