83 lines
3 KiB
TypeScript
83 lines
3 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
||
import { spawn } from "node:child_process";
|
||
import { fileURLToPath } from "node:url";
|
||
import { parseArgs } from "node:util";
|
||
|
||
import { z } from "zod";
|
||
import { defaultPreset } from "./presets.ts";
|
||
|
||
const assetSchema = z.object({
|
||
name: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/),
|
||
publicPath: z.string().min(1),
|
||
outputPath: z.string().min(1),
|
||
prompt: z.string().min(1),
|
||
usage: z.array(z.string()).min(1),
|
||
});
|
||
const root = fileURLToPath(new URL("../../", import.meta.url));
|
||
const assets = z
|
||
.record(z.string(), assetSchema.extend({ placements: z.record(z.string(), assetSchema) }))
|
||
.parse(
|
||
JSON.parse(
|
||
await readFile(
|
||
new URL("../../src/apps/product/content/persona-assets.json", import.meta.url),
|
||
"utf8",
|
||
),
|
||
),
|
||
);
|
||
const { values } = parseArgs({
|
||
options: {
|
||
persona: { type: "string" },
|
||
placement: { type: "string" },
|
||
all: { type: "boolean" },
|
||
"dry-run": { type: "boolean" },
|
||
help: { type: "boolean" },
|
||
},
|
||
});
|
||
if (values.help || (!values.persona && !values.all)) {
|
||
console.log(
|
||
"vp run generate:world --persona polaire [--placement frame] [--dry-run]\nvp run generate:world --all\nEdit src/apps/product/content/persona-assets.json to change a world’s prompt/name.\nPersonas: " +
|
||
Object.keys(assets).join(", "),
|
||
);
|
||
} else {
|
||
if (values.persona && values.all) throw new Error("Choose --persona or --all, not both.");
|
||
const ids = values.all ? Object.keys(assets) : [values.persona!];
|
||
for (const id of ids) {
|
||
if (!Object.hasOwn(assets, id)) throw new Error(`Unknown persona: ${id}`);
|
||
const world = assets[id]!;
|
||
const asset = values.placement ? world.placements[values.placement] : world;
|
||
if (!asset) throw new Error(`Unknown placement: ${values.placement}`);
|
||
const expected = `/assets/generations/${asset.name}.${defaultPreset}.webp`;
|
||
if (asset.publicPath !== expected || asset.outputPath !== `public${expected}`)
|
||
throw new Error(
|
||
`Path mismatch for ${id}: update publicPath and outputPath to match --name and the default preset.`,
|
||
);
|
||
const args = [
|
||
fileURLToPath(new URL("./generate.ts", import.meta.url)),
|
||
"--prompt",
|
||
asset.prompt,
|
||
"--name",
|
||
asset.name,
|
||
];
|
||
console.log(`${id} → ${asset.outputPath}\nUsed by: ${asset.usage.join(", ")}`);
|
||
if (values["dry-run"]) {
|
||
console.log(JSON.stringify({ executable: process.execPath, args }, null, 2));
|
||
continue;
|
||
}
|
||
// Argument array, never a shell: weighted prompts remain literal data.
|
||
const child = spawn(process.execPath, args, { cwd: root, stdio: "inherit" });
|
||
const interrupt = () => child.kill("SIGINT");
|
||
process.once("SIGINT", interrupt);
|
||
process.once("SIGTERM", interrupt);
|
||
const code = await new Promise<number>((resolve, reject) => {
|
||
child.once("error", reject);
|
||
child.once("exit", (code) => resolve(code ?? 1));
|
||
}).finally(() => {
|
||
process.off("SIGINT", interrupt);
|
||
process.off("SIGTERM", interrupt);
|
||
});
|
||
if (code) {
|
||
process.exitCode = code;
|
||
break;
|
||
}
|
||
}
|
||
}
|