@anvpx/motion-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/analyze/command.d.ts +2 -0
- package/dist/analyze/command.js +190 -0
- package/dist/approval/approval.d.ts +32 -0
- package/dist/approval/approval.js +141 -0
- package/dist/approval/command.d.ts +2 -0
- package/dist/approval/command.js +255 -0
- package/dist/approval/gate.d.ts +7 -0
- package/dist/approval/gate.js +23 -0
- package/dist/approval/hash.d.ts +71 -0
- package/dist/approval/hash.js +451 -0
- package/dist/args.d.ts +30 -0
- package/dist/args.js +53 -0
- package/dist/assets/command.d.ts +2 -0
- package/dist/assets/command.js +165 -0
- package/dist/captions/command.d.ts +2 -0
- package/dist/captions/command.js +341 -0
- package/dist/captions/cues.d.ts +10 -0
- package/dist/captions/cues.js +61 -0
- package/dist/catalog/command.d.ts +2 -0
- package/dist/catalog/command.js +60 -0
- package/dist/catalog/format.d.ts +13 -0
- package/dist/catalog/format.js +54 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +35 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +59 -0
- package/dist/compare/command.d.ts +4 -0
- package/dist/compare/command.js +144 -0
- package/dist/contact/argv.d.ts +24 -0
- package/dist/contact/argv.js +42 -0
- package/dist/contact/command.d.ts +2 -0
- package/dist/contact/command.js +79 -0
- package/dist/doctor/checks.d.ts +25 -0
- package/dist/doctor/checks.js +571 -0
- package/dist/doctor/command.d.ts +4 -0
- package/dist/doctor/command.js +88 -0
- package/dist/doctor/format.d.ts +7 -0
- package/dist/doctor/format.js +41 -0
- package/dist/doctor/heal.d.ts +14 -0
- package/dist/doctor/heal.js +71 -0
- package/dist/doctor/pins.d.ts +8 -0
- package/dist/doctor/pins.js +28 -0
- package/dist/doctor/skills.d.ts +15 -0
- package/dist/doctor/skills.js +58 -0
- package/dist/doctor/types.d.ts +36 -0
- package/dist/doctor/types.js +1 -0
- package/dist/doctor/vendored.d.ts +15 -0
- package/dist/doctor/vendored.js +90 -0
- package/dist/doctor/version.d.ts +8 -0
- package/dist/doctor/version.js +11 -0
- package/dist/exit-codes.d.ts +16 -0
- package/dist/exit-codes.js +15 -0
- package/dist/fit/command.d.ts +5 -0
- package/dist/fit/command.js +154 -0
- package/dist/fit/solve.d.ts +21 -0
- package/dist/fit/solve.js +80 -0
- package/dist/fit/target.d.ts +18 -0
- package/dist/fit/target.js +47 -0
- package/dist/generate3d/command.d.ts +3 -0
- package/dist/generate3d/command.js +38 -0
- package/dist/generate3d/run.d.ts +39 -0
- package/dist/generate3d/run.js +77 -0
- package/dist/icon/command.d.ts +2 -0
- package/dist/icon/command.js +113 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/install-mk.d.ts +31 -0
- package/dist/install-mk.js +168 -0
- package/dist/integration/preview.integration.d.ts +1 -0
- package/dist/integration/preview.integration.js +53 -0
- package/dist/integration/smoke.integration.d.ts +1 -0
- package/dist/integration/smoke.integration.js +91 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +4 -0
- package/dist/node-io.d.ts +14 -0
- package/dist/node-io.js +152 -0
- package/dist/optimize/command.d.ts +12 -0
- package/dist/optimize/command.js +110 -0
- package/dist/packs/command.d.ts +2 -0
- package/dist/packs/command.js +109 -0
- package/dist/patch/apply.d.ts +36 -0
- package/dist/patch/apply.js +186 -0
- package/dist/patch/command.d.ts +2 -0
- package/dist/patch/command.js +92 -0
- package/dist/plugin/install.d.ts +40 -0
- package/dist/plugin/install.js +170 -0
- package/dist/preview/command.d.ts +10 -0
- package/dist/preview/command.js +758 -0
- package/dist/preview/manifest.d.ts +94 -0
- package/dist/preview/manifest.js +19 -0
- package/dist/preview/pack.d.ts +24 -0
- package/dist/preview/pack.js +53 -0
- package/dist/preview/plan.d.ts +58 -0
- package/dist/preview/plan.js +160 -0
- package/dist/preview/provenance.d.ts +23 -0
- package/dist/preview/provenance.js +38 -0
- package/dist/publish/command.d.ts +2 -0
- package/dist/publish/command.js +70 -0
- package/dist/publish/player.d.ts +25 -0
- package/dist/publish/player.js +108 -0
- package/dist/publish/static.d.ts +16 -0
- package/dist/publish/static.js +78 -0
- package/dist/qc.d.ts +2 -0
- package/dist/qc.js +192 -0
- package/dist/scene/author.d.ts +16 -0
- package/dist/scene/author.js +260 -0
- package/dist/scene/command.d.ts +2 -0
- package/dist/scene/command.js +110 -0
- package/dist/scene/ops.d.ts +23 -0
- package/dist/scene/ops.js +151 -0
- package/dist/state/command.d.ts +2 -0
- package/dist/state/command.js +128 -0
- package/dist/state/evidence.d.ts +71 -0
- package/dist/state/evidence.js +243 -0
- package/dist/storyboard/check.d.ts +21 -0
- package/dist/storyboard/check.js +39 -0
- package/dist/storyboard/containment.d.ts +24 -0
- package/dist/storyboard/containment.js +43 -0
- package/dist/storyboard/custom.d.ts +15 -0
- package/dist/storyboard/custom.js +133 -0
- package/dist/storyboard/edit.d.ts +19 -0
- package/dist/storyboard/edit.js +39 -0
- package/dist/storyboard/load.d.ts +11 -0
- package/dist/storyboard/load.js +52 -0
- package/dist/storyboard/report.d.ts +5 -0
- package/dist/storyboard/report.js +8 -0
- package/dist/template/coerce.d.ts +6 -0
- package/dist/template/coerce.js +44 -0
- package/dist/template/command.d.ts +5 -0
- package/dist/template/command.js +163 -0
- package/dist/template/env.d.ts +34 -0
- package/dist/template/env.js +38 -0
- package/dist/template/fork.d.ts +8 -0
- package/dist/template/fork.js +156 -0
- package/dist/template/format.d.ts +21 -0
- package/dist/template/format.js +161 -0
- package/dist/template/list.d.ts +15 -0
- package/dist/template/list.js +116 -0
- package/dist/template/new.d.ts +6 -0
- package/dist/template/new.js +302 -0
- package/dist/testing/env.d.ts +8 -0
- package/dist/testing/env.js +19 -0
- package/dist/testing/fakes.d.ts +33 -0
- package/dist/testing/fakes.js +95 -0
- package/dist/testing/fixtures.d.ts +8 -0
- package/dist/testing/fixtures.js +6 -0
- package/dist/testing/probe.d.ts +15 -0
- package/dist/testing/probe.js +20 -0
- package/dist/testing/project.d.ts +22 -0
- package/dist/testing/project.js +26 -0
- package/dist/tooling/session.d.ts +40 -0
- package/dist/tooling/session.js +106 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/verify/audio-loudness.d.ts +46 -0
- package/dist/verify/audio-loudness.js +186 -0
- package/dist/verify/command.d.ts +2 -0
- package/dist/verify/command.js +161 -0
- package/dist/verify/determinism.d.ts +30 -0
- package/dist/verify/determinism.js +59 -0
- package/dist/verify/expectation.d.ts +15 -0
- package/dist/verify/expectation.js +19 -0
- package/dist/verify/media-events.d.ts +61 -0
- package/dist/verify/media-events.js +174 -0
- package/dist/verify/platform.d.ts +33 -0
- package/dist/verify/platform.js +59 -0
- package/dist/verify/probe.d.ts +14 -0
- package/dist/verify/probe.js +23 -0
- package/dist/verify/report.d.ts +15 -0
- package/dist/verify/report.js +45 -0
- package/dist/verify/run.d.ts +27 -0
- package/dist/verify/run.js +53 -0
- package/dist/voice/command.d.ts +3 -0
- package/dist/voice/command.js +49 -0
- package/dist/voice/manifest.d.ts +33 -0
- package/dist/voice/manifest.js +69 -0
- package/dist/voice/run.d.ts +21 -0
- package/dist/voice/run.js +181 -0
- package/dist/workflow/build.d.ts +2 -0
- package/dist/workflow/build.js +25 -0
- package/dist/workflow/draft.d.ts +9 -0
- package/dist/workflow/draft.js +201 -0
- package/dist/workflow/finish.d.ts +23 -0
- package/dist/workflow/finish.js +28 -0
- package/dist/workflow/project.d.ts +73 -0
- package/dist/workflow/project.js +198 -0
- package/dist/workflow/provenance.d.ts +18 -0
- package/dist/workflow/provenance.js +36 -0
- package/dist/workflow/render.d.ts +31 -0
- package/dist/workflow/render.js +705 -0
- package/dist/workflow/still.d.ts +5 -0
- package/dist/workflow/still.js +62 -0
- package/dist/workflow/validate.d.ts +2 -0
- package/dist/workflow/validate.js +49 -0
- package/package.json +38 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Result, SlotDef } from "@anvpx/motion-schema";
|
|
2
|
+
/**
|
|
3
|
+
* `--set k=v` values to slot values by slot type: text/enum as is, `text[]` as `a|b|c` (backslash-pipe is a literal pipe),
|
|
4
|
+
* number as a finite number. Objects (and lists of objects) need `--slots file.json`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function coerceSets(slots: Readonly<Record<string, SlotDef>>, sets: readonly string[]): Result<Record<string, unknown>>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fail = (path, message, suggestion) => ({
|
|
2
|
+
ok: false,
|
|
3
|
+
errors: [suggestion === undefined ? { path, message } : { path, message, suggestion }],
|
|
4
|
+
});
|
|
5
|
+
/** One `--set` value by slot type. */
|
|
6
|
+
function coerceOne(name, def, text) {
|
|
7
|
+
const path = `/set/${name}`;
|
|
8
|
+
if (def.type === "object") {
|
|
9
|
+
return fail(path, `slot "${name}" is an object; --set cannot express it`, "put it in --slots file.json");
|
|
10
|
+
}
|
|
11
|
+
if (def.type === "number") {
|
|
12
|
+
const number = Number(text);
|
|
13
|
+
return text.trim() === "" || !Number.isFinite(number)
|
|
14
|
+
? fail(path, `"${text}" is not a number`)
|
|
15
|
+
: { ok: true, value: number };
|
|
16
|
+
}
|
|
17
|
+
if (def.type === "text[]") {
|
|
18
|
+
const items = text.split(/(?<!\\)\|/).map((item) => item.replaceAll("\\|", "|").trim());
|
|
19
|
+
return { ok: true, value: text === "" ? [] : items };
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, value: text };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `--set k=v` values to slot values by slot type: text/enum as is, `text[]` as `a|b|c` (backslash-pipe is a literal pipe),
|
|
25
|
+
* number as a finite number. Objects (and lists of objects) need `--slots file.json`.
|
|
26
|
+
*/
|
|
27
|
+
export function coerceSets(slots, sets) {
|
|
28
|
+
const value = {};
|
|
29
|
+
for (const set of sets) {
|
|
30
|
+
const eq = set.indexOf("=");
|
|
31
|
+
if (eq === -1)
|
|
32
|
+
return fail(`/set/${set}`, `--set needs name=value, got "${set}"`);
|
|
33
|
+
const name = set.slice(0, eq);
|
|
34
|
+
const def = slots[name];
|
|
35
|
+
if (def === undefined) {
|
|
36
|
+
return fail(`/set/${name}`, `unknown slot "${name}"`, `use one of: ${Object.keys(slots).join("|")}`);
|
|
37
|
+
}
|
|
38
|
+
const one = coerceOne(name, def, set.slice(eq + 1));
|
|
39
|
+
if (!one.ok)
|
|
40
|
+
return one;
|
|
41
|
+
value[name] = one.value;
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, value };
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Command } from "../types.ts";
|
|
2
|
+
import { type TemplateEnv } from "./env.ts";
|
|
3
|
+
/** `mk template list|show|validate|fork`; `env` is injectable for tests. */
|
|
4
|
+
export declare const makeTemplateCommand: (env?: () => TemplateEnv) => Command;
|
|
5
|
+
export declare const templateCommand: Command;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { loadTemplate, validateTemplate } from "@anvpx/motion-build";
|
|
3
|
+
import { parseFlags, requirePositional, usageError } from "../args.js";
|
|
4
|
+
import { EXIT } from "../exit-codes.js";
|
|
5
|
+
import { defaultEnv, findTemplate, formatErrors, loadOptions, platformsFor, } from "./env.js";
|
|
6
|
+
import { runFork } from "./fork.js";
|
|
7
|
+
import { formatBlueprint, formatTemplate } from "./format.js";
|
|
8
|
+
import { list } from "./list.js";
|
|
9
|
+
const USAGE = `mk template list [--for "<brief>"] [--tag a,b] [--aspect 9:16] [--sec 45] [--domain <d>] [--json]
|
|
10
|
+
mk template show <template>[/<blueprint>] [--json]
|
|
11
|
+
mk template validate <dir|id> [--json]
|
|
12
|
+
mk template fork <template>/<blueprint> --as <name> [--out <storyboard.json>] [--json]
|
|
13
|
+
|
|
14
|
+
Templates are data folders (themes, styles, blueprints, pacing, copy); a blueprint is a parameterised storyboard.
|
|
15
|
+
list one line per template: id@version | summary | tags | aspects | blueprints | source (<= 15 lines; shadowed flagged)
|
|
16
|
+
--for "<brief>" ranks blueprints (top 8, with matched words; NOT-FOR sinks); --tag a,b is an exact AND;
|
|
17
|
+
--aspect and --sec are hard filters. It filters and explains; you choose.
|
|
18
|
+
show template: use cases, good/not for, styles, themes, blueprints with slot signatures (<= 60 lines);
|
|
19
|
+
template/blueprint: slots, scenes, example and a ready mk new line (<= 40 lines)
|
|
20
|
+
validate contracts, themes (AA contrast), styles, blueprint examples; exit 0 clean, 2 on errors
|
|
21
|
+
Roots, highest precedence first: ./templates, $MK_TEMPLATES_PATH, ~/.motion-kit/templates, built-in.
|
|
22
|
+
`;
|
|
23
|
+
const FLAGS = {
|
|
24
|
+
domain: { type: "string" },
|
|
25
|
+
for: { type: "string" },
|
|
26
|
+
tag: { type: "string" },
|
|
27
|
+
aspect: { type: "string" },
|
|
28
|
+
sec: { type: "string" },
|
|
29
|
+
json: { type: "boolean" },
|
|
30
|
+
};
|
|
31
|
+
const SUBCOMMANDS = ["list", "show", "validate", "fork"];
|
|
32
|
+
async function show(target, json, env, io) {
|
|
33
|
+
const [id = "", blueprintId, ...extra] = target.split("/");
|
|
34
|
+
if (extra.length > 0)
|
|
35
|
+
return usageError("template", USAGE, `bad target "${target}"`, io).code;
|
|
36
|
+
const found = findTemplate(env, io, id);
|
|
37
|
+
if (!found.ok) {
|
|
38
|
+
io.stderr(`mk template: ${found.message}\n`);
|
|
39
|
+
return EXIT.USAGE;
|
|
40
|
+
}
|
|
41
|
+
const { template, source } = found;
|
|
42
|
+
if (blueprintId === undefined) {
|
|
43
|
+
io.stdout(json
|
|
44
|
+
? `${JSON.stringify({ ...template.manifest, source })}\n`
|
|
45
|
+
: formatTemplate(template, source));
|
|
46
|
+
return EXIT.OK;
|
|
47
|
+
}
|
|
48
|
+
const blueprint = template.blueprints.get(blueprintId);
|
|
49
|
+
if (blueprint === undefined) {
|
|
50
|
+
const known = [...template.blueprints.keys()].join("|");
|
|
51
|
+
io.stderr(`mk template: unknown blueprint "${blueprintId}"; use one of: ${known}\n`);
|
|
52
|
+
return EXIT.USAGE;
|
|
53
|
+
}
|
|
54
|
+
const previewFile = join(template.dir, "previews", `${blueprint.id}.png`);
|
|
55
|
+
const preview = (await io.host.exists(previewFile)) ? previewFile : undefined;
|
|
56
|
+
io.stdout(json ? `${JSON.stringify(blueprint)}\n` : formatBlueprint(template, blueprint, preview));
|
|
57
|
+
return EXIT.OK;
|
|
58
|
+
}
|
|
59
|
+
async function validate(target, json, env, io) {
|
|
60
|
+
const dir = resolve(target);
|
|
61
|
+
if (await io.host.exists(join(dir, "template.json"))) {
|
|
62
|
+
const loaded = loadTemplate(dir, loadOptions(env));
|
|
63
|
+
if (!loaded.ok)
|
|
64
|
+
return print(target, loaded.errors, [], json, io);
|
|
65
|
+
return validateLoaded(loaded.value, json, io, isBuiltIn(dir, env));
|
|
66
|
+
}
|
|
67
|
+
const byId = findTemplate(env, io, target);
|
|
68
|
+
if (!byId.ok) {
|
|
69
|
+
io.stderr(`mk template: ${byId.message}
|
|
70
|
+
`);
|
|
71
|
+
return EXIT.USAGE;
|
|
72
|
+
}
|
|
73
|
+
return validateLoaded(byId.template, json, io, byId.source === "built-in");
|
|
74
|
+
}
|
|
75
|
+
const isBuiltIn = (dir, env) => {
|
|
76
|
+
const inside = relative(resolve(env.builtInDir), dir);
|
|
77
|
+
return inside !== "" && !inside.startsWith("..") && !isAbsolute(inside);
|
|
78
|
+
};
|
|
79
|
+
async function validateLoaded(template, json, io, builtIn) {
|
|
80
|
+
const report = validateTemplate(template, { platforms: await platformsFor(io), builtIn });
|
|
81
|
+
const label = `${template.manifest.id}@${template.manifest.version}`;
|
|
82
|
+
return print(label, report.errors, report.warnings, json, io);
|
|
83
|
+
}
|
|
84
|
+
function print(label, errors, warnings, json, io) {
|
|
85
|
+
if (json) {
|
|
86
|
+
io.stdout(`${JSON.stringify({ ok: errors.length === 0, errors, warnings })}\n`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const warn = warnings.map((w) => `W ${w.path}: ${w.message}${w.suggestion === undefined ? "" : ` -> ${w.suggestion}`}`);
|
|
90
|
+
const lines = [...(errors.length > 0 ? [formatErrors(errors)] : []), ...warn];
|
|
91
|
+
io.stdout(`${lines.map((l) => `${l}\n`).join("")}template ${label}: ${errors.length} errors, ${warnings.length} warnings\n`);
|
|
92
|
+
}
|
|
93
|
+
return errors.length === 0 ? EXIT.OK : EXIT.USAGE;
|
|
94
|
+
}
|
|
95
|
+
const isSub = (value) => SUBCOMMANDS.some((sub) => sub === value);
|
|
96
|
+
const ASPECT = /^\d+:\d+$/;
|
|
97
|
+
const bad = (message, io) => usageError("template", USAGE, message, io).code;
|
|
98
|
+
/** The `list` flags as a request, or an exit code after a usage error. */
|
|
99
|
+
function listRequest(flags, io) {
|
|
100
|
+
const text = (name) => {
|
|
101
|
+
const value = flags[name];
|
|
102
|
+
return typeof value === "string" ? value : undefined;
|
|
103
|
+
};
|
|
104
|
+
const aspect = text("aspect");
|
|
105
|
+
if (aspect !== undefined && !ASPECT.test(aspect))
|
|
106
|
+
return bad(`--aspect ${aspect} must look like 16:9`, io);
|
|
107
|
+
const secText = text("sec");
|
|
108
|
+
const sec = secText === undefined ? undefined : Number(secText);
|
|
109
|
+
if (sec !== undefined && !(Number.isInteger(sec) && sec > 0)) {
|
|
110
|
+
return bad(`--sec ${secText} must be a positive whole number of seconds`, io);
|
|
111
|
+
}
|
|
112
|
+
const tags = (text("tag") ?? "")
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((tag) => tag.trim().toLowerCase())
|
|
115
|
+
.filter((tag) => tag !== "");
|
|
116
|
+
return {
|
|
117
|
+
domain: text("domain"),
|
|
118
|
+
query: text("for"),
|
|
119
|
+
filters: {
|
|
120
|
+
...(tags.length === 0 ? {} : { tags }),
|
|
121
|
+
...(aspect === undefined ? {} : { aspect }),
|
|
122
|
+
...(sec === undefined ? {} : { sec }),
|
|
123
|
+
},
|
|
124
|
+
json: flags.json === true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function parseRequest(args, io) {
|
|
128
|
+
const parsed = parseFlags("template", USAGE, args, FLAGS, io);
|
|
129
|
+
if (parsed.kind === "exit")
|
|
130
|
+
return parsed.code;
|
|
131
|
+
const [sub, ...rest] = parsed.positionals;
|
|
132
|
+
if (!isSub(sub))
|
|
133
|
+
return bad(`use one of: ${SUBCOMMANDS.join("|")}`, io);
|
|
134
|
+
const request = listRequest(parsed.flags, io);
|
|
135
|
+
if (typeof request === "number")
|
|
136
|
+
return request;
|
|
137
|
+
if (sub === "list") {
|
|
138
|
+
const extra = rest[0];
|
|
139
|
+
return extra === undefined
|
|
140
|
+
? { sub, target: undefined, list: request }
|
|
141
|
+
: bad(`unexpected argument "${extra}"`, io);
|
|
142
|
+
}
|
|
143
|
+
const target = requirePositional("template", USAGE, rest, "<template>", io);
|
|
144
|
+
return typeof target === "string" ? { sub, target, list: request } : target.code;
|
|
145
|
+
}
|
|
146
|
+
/** `mk template list|show|validate|fork`; `env` is injectable for tests. */
|
|
147
|
+
export const makeTemplateCommand = (env = defaultEnv) => ({
|
|
148
|
+
name: "template",
|
|
149
|
+
summary: "list, show, validate or fork templates (themes + styles + blueprints)",
|
|
150
|
+
async run(args, io) {
|
|
151
|
+
if (args[0] === "fork")
|
|
152
|
+
return runFork(args.slice(1), env(), io);
|
|
153
|
+
const request = parseRequest(args, io);
|
|
154
|
+
if (typeof request === "number")
|
|
155
|
+
return request;
|
|
156
|
+
const { sub, target = "", list: listing } = request;
|
|
157
|
+
if (sub === "list")
|
|
158
|
+
return list(listing, env(), io);
|
|
159
|
+
const json = listing.json;
|
|
160
|
+
return sub === "show" ? show(target, json, env(), io) : validate(target, json, env(), io);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
export const templateCommand = makeTemplateCommand();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type LoadedTemplate, type PlatformLookup } from "@anvpx/motion-build";
|
|
2
|
+
import type { ValidationError } from "@anvpx/motion-schema";
|
|
3
|
+
import type { Io } from "../types.ts";
|
|
4
|
+
/** Where templates come from; injectable so tests use fixtures instead of the real built-ins. */
|
|
5
|
+
export interface TemplateEnv {
|
|
6
|
+
/** The built-in templates directory (from `@anvpx/motion-templates`; `build` never imports it). */
|
|
7
|
+
builtInDir: string;
|
|
8
|
+
/** Folder behind `@shared/<name>.json` theme refs; unset means such templates fail to load. */
|
|
9
|
+
sharedThemesDir?: string | undefined;
|
|
10
|
+
/** Search stopwords; unset means the build default list. */
|
|
11
|
+
stopwords?: readonly string[] | undefined;
|
|
12
|
+
/** `MK_TEMPLATES_PATH`. */
|
|
13
|
+
envPath?: string | undefined;
|
|
14
|
+
/** Project directory whose `templates/` has top precedence. */
|
|
15
|
+
projectDir: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const defaultEnv: () => TemplateEnv;
|
|
18
|
+
export declare const loadOptions: (env: TemplateEnv) => {
|
|
19
|
+
sharedThemesDir: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
export declare const rootsOf: (env: TemplateEnv, io: Io) => import("@anvpx/motion-build").TemplateRoot[];
|
|
22
|
+
export declare const discover: (env: TemplateEnv, io: Io) => import("@anvpx/motion-build").Discovery;
|
|
23
|
+
export type Found = {
|
|
24
|
+
ok: true;
|
|
25
|
+
template: LoadedTemplate;
|
|
26
|
+
source: string;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
/** Loads a template by id from the discovery roots; a miss lists the known ids. */
|
|
32
|
+
export declare function findTemplate(env: TemplateEnv, io: Io, id: string): Found;
|
|
33
|
+
export declare const formatErrors: (errors: readonly ValidationError[]) => string;
|
|
34
|
+
export declare const platformsFor: (io: Io) => Promise<PlatformLookup>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { discoverTemplates, loadTemplate, templateRoots, } from "@anvpx/motion-build";
|
|
2
|
+
import { listNpmTemplateRoots, SHARED_THEMES_DIR, STOPWORDS, TEMPLATES_DIR, } from "@anvpx/motion-templates";
|
|
3
|
+
import { platformLookup } from "../verify/platform.js";
|
|
4
|
+
export const defaultEnv = () => ({
|
|
5
|
+
builtInDir: TEMPLATES_DIR,
|
|
6
|
+
sharedThemesDir: SHARED_THEMES_DIR,
|
|
7
|
+
stopwords: STOPWORDS,
|
|
8
|
+
envPath: process.env.MK_TEMPLATES_PATH,
|
|
9
|
+
projectDir: process.cwd(),
|
|
10
|
+
});
|
|
11
|
+
export const loadOptions = (env) => ({ sharedThemesDir: env.sharedThemesDir });
|
|
12
|
+
export const rootsOf = (env, io) => templateRoots({
|
|
13
|
+
projectDir: env.projectDir,
|
|
14
|
+
envPath: env.envPath,
|
|
15
|
+
homeDir: io.host.homeDir,
|
|
16
|
+
builtInDir: env.builtInDir,
|
|
17
|
+
});
|
|
18
|
+
export const discover = (env, io) => discoverTemplates([...rootsOf(env, io), ...listNpmTemplateRoots(env.projectDir)]);
|
|
19
|
+
/** Loads a template by id from the discovery roots; a miss lists the known ids. */
|
|
20
|
+
export function findTemplate(env, io, id) {
|
|
21
|
+
const { templates } = discover(env, io);
|
|
22
|
+
const candidate = templates.find((t) => t.id === id);
|
|
23
|
+
if (candidate === undefined) {
|
|
24
|
+
const known = templates.map((t) => t.id).join("|");
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
message: `unknown template "${id}"; use one of: ${known || "(none found)"}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const loaded = loadTemplate(candidate.dir, loadOptions(env));
|
|
31
|
+
if (!loaded.ok)
|
|
32
|
+
return { ok: false, message: formatErrors(loaded.errors) };
|
|
33
|
+
return { ok: true, template: loaded.value, source: candidate.source };
|
|
34
|
+
}
|
|
35
|
+
export const formatErrors = (errors) => errors
|
|
36
|
+
.map((e) => `E ${e.path}: ${e.message}${e.suggestion === undefined ? "" : ` -> ${e.suggestion}`}`)
|
|
37
|
+
.join("\n");
|
|
38
|
+
export const platformsFor = (io) => platformLookup(io.host);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command, Io } from "../types.ts";
|
|
2
|
+
import { type TemplateEnv } from "./env.ts";
|
|
3
|
+
export declare const FORK_USAGE = "mk template fork <template>/<blueprint> --as <name> [--out <storyboard.json>] [--json]\n\nForks a blueprint example into a user-owned storyboard you can reshape with mk scene/patch.\nWrites the storyboard, vendors the theme(s) it uses into <out dir>/themes/ and records\nprovenance (including forkedFrom <template>@<version>) in <out dir>/.mk/<name>/template.json.\nBuilt-ins are never modified; the fork is yours.\n --as <name> fork folder/name (required): letters, numbers, - and _; no slashes\n --out f storyboard path (default <as>/storyboard.json)\nSee `mk template show <template>/<blueprint>` for slots and limits.\n";
|
|
4
|
+
/** `mk template fork <template>/<blueprint> --as <name>`; `env` is injectable for tests. */
|
|
5
|
+
export declare function runFork(args: readonly string[], env: TemplateEnv, io: Io): Promise<number>;
|
|
6
|
+
/** `mk template fork` as a standalone command (wired as a `mk template` subcommand too). */
|
|
7
|
+
export declare const makeForkCommand: (env?: () => TemplateEnv) => Command;
|
|
8
|
+
export declare const forkCommand: Command;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { instantiate } from "@anvpx/motion-build";
|
|
4
|
+
import { MK_DIR, mkDir, storyboardStem } from "../approval/hash.js";
|
|
5
|
+
import { parseFlags, requirePositional, textFlag, usageError } from "../args.js";
|
|
6
|
+
import { EXIT } from "../exit-codes.js";
|
|
7
|
+
import { defaultEnv, findTemplate, formatErrors, platformsFor } from "./env.js";
|
|
8
|
+
import { TEMPLATE_PROVENANCE } from "./new.js";
|
|
9
|
+
export const FORK_USAGE = `mk template fork <template>/<blueprint> --as <name> [--out <storyboard.json>] [--json]
|
|
10
|
+
|
|
11
|
+
Forks a blueprint example into a user-owned storyboard you can reshape with mk scene/patch.
|
|
12
|
+
Writes the storyboard, vendors the theme(s) it uses into <out dir>/themes/ and records
|
|
13
|
+
provenance (including forkedFrom <template>@<version>) in <out dir>/.mk/<name>/template.json.
|
|
14
|
+
Built-ins are never modified; the fork is yours.
|
|
15
|
+
--as <name> fork folder/name (required): letters, numbers, - and _; no slashes
|
|
16
|
+
--out f storyboard path (default <as>/storyboard.json)
|
|
17
|
+
See \`mk template show <template>/<blueprint>\` for slots and limits.
|
|
18
|
+
`;
|
|
19
|
+
const FLAGS = {
|
|
20
|
+
as: { type: "string" },
|
|
21
|
+
out: { type: "string" },
|
|
22
|
+
json: { type: "boolean" },
|
|
23
|
+
};
|
|
24
|
+
const AS_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
25
|
+
const MAX_WARNING_LINES = 5;
|
|
26
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
27
|
+
/** Resolves `<template>/<blueprint>`; a miss prints the known ids and yields an exit code. */
|
|
28
|
+
function resolveTarget(target, env, io) {
|
|
29
|
+
const [templateId = "", blueprintId = "", ...extra] = target.split("/");
|
|
30
|
+
if (blueprintId === "" || extra.length > 0) {
|
|
31
|
+
return usageError("template fork", FORK_USAGE, "target must be <template>/<blueprint>", io)
|
|
32
|
+
.code;
|
|
33
|
+
}
|
|
34
|
+
const found = findTemplate(env, io, templateId);
|
|
35
|
+
if (!found.ok) {
|
|
36
|
+
io.stderr(`mk template fork: ${found.message}\n`);
|
|
37
|
+
return EXIT.USAGE;
|
|
38
|
+
}
|
|
39
|
+
const blueprint = found.template.blueprints.get(blueprintId);
|
|
40
|
+
if (blueprint === undefined) {
|
|
41
|
+
const known = [...found.template.blueprints.keys()].join("|");
|
|
42
|
+
io.stderr(`mk template fork: unknown blueprint "${blueprintId}"; use one of: ${known}\n`);
|
|
43
|
+
return EXIT.USAGE;
|
|
44
|
+
}
|
|
45
|
+
return { template: found.template, blueprintId, blueprint };
|
|
46
|
+
}
|
|
47
|
+
const asNameOf = (flags, io) => {
|
|
48
|
+
const name = textFlag(flags, "as");
|
|
49
|
+
if (name === undefined) {
|
|
50
|
+
return usageError("template fork", FORK_USAGE, "missing --as <name>", io).code;
|
|
51
|
+
}
|
|
52
|
+
if (!AS_RE.test(name)) {
|
|
53
|
+
return usageError("template fork", FORK_USAGE, `--as "${name}" must match [A-Za-z0-9_-]+ with no slashes`, io).code;
|
|
54
|
+
}
|
|
55
|
+
return name;
|
|
56
|
+
};
|
|
57
|
+
/** Writes the storyboard, the vendored theme(s) and the fork provenance; returns the storyboard path. */
|
|
58
|
+
async function writeFork(made, asName, outFlag, io) {
|
|
59
|
+
const out = resolve(outFlag ?? join(asName, "storyboard.json"));
|
|
60
|
+
const dir = dirname(out);
|
|
61
|
+
const text = `${made.text}\n`;
|
|
62
|
+
await io.host.writeText(out, text);
|
|
63
|
+
for (const theme of made.themes)
|
|
64
|
+
await io.host.writeText(join(dir, theme.file), theme.text);
|
|
65
|
+
const record = {
|
|
66
|
+
...made.provenance,
|
|
67
|
+
storyboardSha256: sha256(text),
|
|
68
|
+
forkedFrom: made.provenance.template,
|
|
69
|
+
forkName: asName,
|
|
70
|
+
};
|
|
71
|
+
await io.host.writeText(join(mkDir(out), TEMPLATE_PROVENANCE), `${JSON.stringify(record, null, 2)}\n`);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function report(made, asName, out, json, io) {
|
|
75
|
+
const { meta } = made.storyboard;
|
|
76
|
+
const overlays = made.storyboard.scenes.filter((s) => s.layer === "overlay").length;
|
|
77
|
+
const scenes = made.sceneCount - overlays;
|
|
78
|
+
const frame = { width: meta.width, height: meta.height, fps: meta.fps };
|
|
79
|
+
const files = made.themes.map((t) => t.file);
|
|
80
|
+
if (json) {
|
|
81
|
+
const { provenance, totalSec } = made;
|
|
82
|
+
io.stdout(`${JSON.stringify({
|
|
83
|
+
ok: true,
|
|
84
|
+
fork: asName,
|
|
85
|
+
storyboard: out,
|
|
86
|
+
forkedFrom: provenance.template,
|
|
87
|
+
...provenance,
|
|
88
|
+
scenes,
|
|
89
|
+
overlays,
|
|
90
|
+
totalSec,
|
|
91
|
+
frame,
|
|
92
|
+
themes: files,
|
|
93
|
+
warnings: made.warnings.length,
|
|
94
|
+
})}\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { provenance } = made;
|
|
98
|
+
const label = `${provenance.template.split("@")[0]}/${provenance.blueprint}`;
|
|
99
|
+
io.stdout(`fork ok | ${label} -> ${asName} | ${scenes} scenes + ${overlays} overlays ${made.totalSec}s ${frame.width}x${frame.height}@${frame.fps}\n` +
|
|
100
|
+
`wrote ${out} (+ ${files.join(", ")}, ${MK_DIR}/${storyboardStem(out)}/${TEMPLATE_PROVENANCE})\n` +
|
|
101
|
+
`next: mk validate ${out} && mk preview ${out}\n`);
|
|
102
|
+
}
|
|
103
|
+
/** Everything `instantiate` needs, from flags and the template example; an exit code on bad input. */
|
|
104
|
+
async function plan(args, env, io) {
|
|
105
|
+
const parsed = parseFlags("template fork", FORK_USAGE, args, FLAGS, io);
|
|
106
|
+
if (parsed.kind === "exit")
|
|
107
|
+
return parsed.code;
|
|
108
|
+
const name = requirePositional("template fork", FORK_USAGE, parsed.positionals, "<template>/<blueprint>", io);
|
|
109
|
+
if (typeof name !== "string")
|
|
110
|
+
return name.code;
|
|
111
|
+
const asName = asNameOf(parsed.flags, io);
|
|
112
|
+
if (typeof asName === "number")
|
|
113
|
+
return asName;
|
|
114
|
+
const target = resolveTarget(name, env, io);
|
|
115
|
+
if (typeof target === "number")
|
|
116
|
+
return target;
|
|
117
|
+
return {
|
|
118
|
+
target,
|
|
119
|
+
asName,
|
|
120
|
+
out: textFlag(parsed.flags, "out"),
|
|
121
|
+
json: parsed.flags.json === true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const formatWarnings = (made) => {
|
|
125
|
+
const rows = made.warnings.slice(0, MAX_WARNING_LINES);
|
|
126
|
+
return rows.map((w) => `W ${w.path}: ${w.message}\n`).join("");
|
|
127
|
+
};
|
|
128
|
+
/** `mk template fork <template>/<blueprint> --as <name>`; `env` is injectable for tests. */
|
|
129
|
+
export async function runFork(args, env, io) {
|
|
130
|
+
const planned = await plan(args, env, io);
|
|
131
|
+
if (typeof planned === "number")
|
|
132
|
+
return planned;
|
|
133
|
+
const slots = { ...planned.target.blueprint.example };
|
|
134
|
+
const result = instantiate(planned.target.template, planned.target.blueprintId, slots, {
|
|
135
|
+
platforms: await platformsFor(io),
|
|
136
|
+
});
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
io.stderr(`mk template fork: ${formatErrors(result.errors)}\n`);
|
|
139
|
+
return EXIT.USAGE;
|
|
140
|
+
}
|
|
141
|
+
const out = await writeFork(result.value, planned.asName, planned.out, io);
|
|
142
|
+
report(result.value, planned.asName, out, planned.json, io);
|
|
143
|
+
const warnings = formatWarnings(result.value);
|
|
144
|
+
if (warnings !== "")
|
|
145
|
+
io.stderr(warnings);
|
|
146
|
+
return EXIT.OK;
|
|
147
|
+
}
|
|
148
|
+
/** `mk template fork` as a standalone command (wired as a `mk template` subcommand too). */
|
|
149
|
+
export const makeForkCommand = (env = defaultEnv) => ({
|
|
150
|
+
name: "fork",
|
|
151
|
+
summary: "fork a template blueprint example into a user-owned storyboard",
|
|
152
|
+
async run(args, io) {
|
|
153
|
+
return runFork(args, env(), io);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
export const forkCommand = makeForkCommand();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LoadedTemplate } from "@anvpx/motion-build";
|
|
2
|
+
import type { BlueprintDoc, SlotDef } from "@anvpx/motion-schema";
|
|
3
|
+
/** One-line slot signature: `points* text[][3..5]<=70`; `*` required, `?` optional. */
|
|
4
|
+
export declare const slotSignature: (name: string, def: SlotDef) => string;
|
|
5
|
+
/** One line cut to `max` characters with an ellipsis. */
|
|
6
|
+
export declare const clip: (text: string, max: number) => string;
|
|
7
|
+
/** Approximate tokens of a text (4 characters each, as in the copy-guide limit). */
|
|
8
|
+
export declare const tokensOf: (text: string) => number;
|
|
9
|
+
/** Lengths as `30-60s`; `-` when unknown. */
|
|
10
|
+
export declare const lengthLabel: (lengths: {
|
|
11
|
+
min: number;
|
|
12
|
+
max: number;
|
|
13
|
+
} | undefined) => string;
|
|
14
|
+
/** The 3D honesty line, repeated wherever a storyboard may contain `scene3d`. */
|
|
15
|
+
export declare const SCENE3D_NOTE = "scene3d requires WebGL and falls back to a flat 2D canvas without it.";
|
|
16
|
+
/** `mk template show <template>`; at most 60 lines and about 600 tokens. */
|
|
17
|
+
export declare function formatTemplate(template: LoadedTemplate, source: string): string;
|
|
18
|
+
/** A ready `mk new` line built from the blueprint `example`; object slots need `--slots`. */
|
|
19
|
+
export declare const useLine: (id: string, blueprint: BlueprintDoc) => string;
|
|
20
|
+
/** `mk template show <template>/<blueprint>`; at most 40 lines and about 450 tokens (the example is the first to go, the ready `mk new` line stays). */
|
|
21
|
+
export declare function formatBlueprint(template: LoadedTemplate, blueprint: BlueprintDoc, preview?: string): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { templateLengths } from "@anvpx/motion-build";
|
|
2
|
+
const flagOf = (def) => def.required ? "*" : def.optional ? "?" : "";
|
|
3
|
+
const range = (min, max) => min === undefined && max === undefined ? "" : `[${min ?? 0}..${max ?? "n"}]`;
|
|
4
|
+
const upTo = (max) => (max === undefined ? "" : `<=${max}`);
|
|
5
|
+
const fieldsOf = (fields) => Object.entries(fields)
|
|
6
|
+
.map(([key, type]) => `${key}:${type}`)
|
|
7
|
+
.join(",");
|
|
8
|
+
const SIGNATURES = {
|
|
9
|
+
text: (name, def) => `${name}${flagOf(def)} text${upTo(def.max)}`,
|
|
10
|
+
"text[]": (name, def) => `${name}${flagOf(def)} text${range(def.min, def.max) || "[]"}${upTo(def.itemMax)}`,
|
|
11
|
+
number: (name, def) => `${name}${flagOf(def)} number${range(def.min, def.max)}`,
|
|
12
|
+
enum: (name, def) => `${name} ${def.values.join("|")}${def.default === undefined ? "" : ` (default ${def.default})`}`,
|
|
13
|
+
object: (name, def) => `${name}${flagOf(def)} ${def.list ? `{${fieldsOf(def.fields)}}[]${range(def.min, def.max)}` : `{${fieldsOf(def.fields)}}`}`,
|
|
14
|
+
};
|
|
15
|
+
/** One-line slot signature: `points* text[][3..5]<=70`; `*` required, `?` optional. */
|
|
16
|
+
export const slotSignature = (name, def) => SIGNATURES[def.type](name, def);
|
|
17
|
+
const frameOf = (template, styleId) => {
|
|
18
|
+
const frame = template.styles.get(styleId)?.frame;
|
|
19
|
+
if (frame === undefined)
|
|
20
|
+
return "";
|
|
21
|
+
return "platform" in frame
|
|
22
|
+
? `, ${frame.platform}`
|
|
23
|
+
: `, ${frame.width}x${frame.height}@${frame.fps}`;
|
|
24
|
+
};
|
|
25
|
+
const CHARS_PER_TOKEN = 4;
|
|
26
|
+
/** Keeps the first `maxLines` lines; the rest collapses into one `(+N more lines)` line. */
|
|
27
|
+
const cap = (lines, max) => lines.length <= max
|
|
28
|
+
? lines
|
|
29
|
+
: [...lines.slice(0, max - 1), `(+${lines.length - max + 1} more lines)`];
|
|
30
|
+
/** One line cut to `max` characters with an ellipsis. */
|
|
31
|
+
export const clip = (text, max) => text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
32
|
+
/** Approximate tokens of a text (4 characters each, as in the copy-guide limit). */
|
|
33
|
+
export const tokensOf = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
34
|
+
/** Lengths as `30-60s`; `-` when unknown. */
|
|
35
|
+
export const lengthLabel = (lengths) => lengths === undefined ? "-" : `${lengths.min}-${lengths.max}s`;
|
|
36
|
+
/** Slot signature without the `length`/`style` enums every blueprint repeats. */
|
|
37
|
+
const signatureLine = (blueprint) => {
|
|
38
|
+
const own = Object.entries(blueprint.slots).filter(([name]) => name !== "length" && name !== "style");
|
|
39
|
+
return own.map(([name, def]) => slotSignature(name, def)).join("; ");
|
|
40
|
+
};
|
|
41
|
+
const copyLinesWithin = (copy, budgetChars) => {
|
|
42
|
+
const lines = [];
|
|
43
|
+
let used = 0;
|
|
44
|
+
for (const line of copy
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.filter((l) => l.trim() !== "")
|
|
47
|
+
.slice(0, 14)) {
|
|
48
|
+
used += line.length + 3;
|
|
49
|
+
if (used > budgetChars)
|
|
50
|
+
break;
|
|
51
|
+
lines.push(` ${line}`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
};
|
|
55
|
+
const TEMPLATE_MAX_LINES = 60;
|
|
56
|
+
const TEMPLATE_MAX_TOKENS = 600;
|
|
57
|
+
const BLUEPRINT_MAX_LINES = 40;
|
|
58
|
+
const BLUEPRINT_MAX_TOKENS = 450;
|
|
59
|
+
/** The 3D honesty line, repeated wherever a storyboard may contain `scene3d`. */
|
|
60
|
+
export const SCENE3D_NOTE = "scene3d requires WebGL and falls back to a flat 2D canvas without it.";
|
|
61
|
+
/** Drops trailing lines until the text fits the token budget; always keeps the first. */
|
|
62
|
+
const withinTokens = (lines, maxTokens) => {
|
|
63
|
+
const out = [...lines];
|
|
64
|
+
while (out.length > 1 && tokensOf(out.join("\n")) > maxTokens)
|
|
65
|
+
out.pop();
|
|
66
|
+
return out;
|
|
67
|
+
};
|
|
68
|
+
const discoverLines = (discover, lengths) => {
|
|
69
|
+
if (discover === undefined)
|
|
70
|
+
return [];
|
|
71
|
+
const facts = [
|
|
72
|
+
`tags: ${discover.tags.join(", ")}`,
|
|
73
|
+
...(discover.audience === undefined ? [] : [`audience: ${discover.audience.join(", ")}`]),
|
|
74
|
+
...(discover.tone === undefined ? [] : [`tone: ${discover.tone.join(", ")}`]),
|
|
75
|
+
...(discover.aspects === undefined ? [] : [`aspects: ${discover.aspects.join(", ")}`]),
|
|
76
|
+
...(lengths === undefined ? [] : [`length: ${lengthLabel(lengths)}`]),
|
|
77
|
+
];
|
|
78
|
+
return [
|
|
79
|
+
`good for: ${discover.goodFor}`,
|
|
80
|
+
...(discover.notFor === undefined ? [] : [`not for: ${discover.notFor}`]),
|
|
81
|
+
"use cases:",
|
|
82
|
+
...discover.useCases.map((useCase) => ` - ${useCase}`),
|
|
83
|
+
facts.join("; "),
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
/** `mk template show <template>`; at most 60 lines and about 600 tokens. */
|
|
87
|
+
export function formatTemplate(template, source) {
|
|
88
|
+
const { manifest } = template;
|
|
89
|
+
const lines = [
|
|
90
|
+
`${manifest.id}@${manifest.version} (${source}): ${manifest.title}`,
|
|
91
|
+
manifest.summary,
|
|
92
|
+
...discoverLines(manifest.discover, templateLengths(template)),
|
|
93
|
+
`domains: ${manifest.domains.join(", ")}`,
|
|
94
|
+
`styles: ${[...template.styles.values()].map((s) => `${s.id} (theme ${s.theme}${frameOf(template, s.id)})`).join("; ")}`,
|
|
95
|
+
`themes: ${manifest.themes.map((t) => `${t.id} (${t.mode}${t.pair === undefined ? "" : `, pair ${t.pair}`})`).join("; ")}`,
|
|
96
|
+
"blueprints (mk template show <template>/<blueprint> for slots; every one also takes length, style):",
|
|
97
|
+
];
|
|
98
|
+
for (const blueprint of template.blueprints.values()) {
|
|
99
|
+
const pick = clip(blueprint.discover?.goodFor ?? blueprint.purpose, 70);
|
|
100
|
+
lines.push(` ${blueprint.id}: ${signatureLine(blueprint)} | ${lengthLabel(blueprint.discover?.lengths)} | ${pick}`);
|
|
101
|
+
}
|
|
102
|
+
if (uses3d(template))
|
|
103
|
+
lines.push(`note: ${SCENE3D_NOTE}`);
|
|
104
|
+
const { pacing } = template;
|
|
105
|
+
const ranges = Object.entries(pacing.lengths)
|
|
106
|
+
.map(([name, r]) => `${name} ${r.scenes[0]}-${r.scenes[1]} scenes, ${r.totalSec[0]}-${r.totalSec[1]}s`)
|
|
107
|
+
.join("; ");
|
|
108
|
+
lines.push(`pacing: ${pacing.wordsPerSecond} words/s; ${ranges}`);
|
|
109
|
+
const spent = tokensOf(lines.join("\n")) * CHARS_PER_TOKEN;
|
|
110
|
+
const room = TEMPLATE_MAX_TOKENS * CHARS_PER_TOKEN - spent - 16;
|
|
111
|
+
const copy = copyLinesWithin(template.copy, room);
|
|
112
|
+
const all = copy.length === 0 ? lines : [...lines, "copy guide:", ...copy];
|
|
113
|
+
return `${cap(withinTokens(all, TEMPLATE_MAX_TOKENS), TEMPLATE_MAX_LINES).join("\n")}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
117
|
+
const setOf = (name, def, value) => {
|
|
118
|
+
if (def.type === "object" || value === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const text = Array.isArray(value)
|
|
121
|
+
? value.map((item) => String(item).replaceAll("|", "\\|")).join("|")
|
|
122
|
+
: String(value);
|
|
123
|
+
return `--set ${shellQuote(`${name}=${text}`)}`;
|
|
124
|
+
};
|
|
125
|
+
/** A ready `mk new` line built from the blueprint `example`; object slots need `--slots`. */
|
|
126
|
+
export const useLine = (id, blueprint) => {
|
|
127
|
+
const example = blueprint.example;
|
|
128
|
+
const sets = Object.entries(blueprint.slots).flatMap(([name, def]) => {
|
|
129
|
+
const set = setOf(name, def, example[name]);
|
|
130
|
+
return set === undefined ? [] : [set];
|
|
131
|
+
});
|
|
132
|
+
const objects = Object.values(blueprint.slots).some((def) => def.type === "object");
|
|
133
|
+
const tail = objects ? " (object slots: --slots file.json)" : "";
|
|
134
|
+
return `use: mk new ${id} ${sets.join(" ")} --out storyboard.json${tail}`;
|
|
135
|
+
};
|
|
136
|
+
const sceneTypes = (blueprint) => [
|
|
137
|
+
...new Set(blueprint.scenes.map((entry) => ("$repeat" in entry ? entry.scene.type : entry.type))),
|
|
138
|
+
];
|
|
139
|
+
/** True when any blueprint can emit `scene3d` (the template view then repeats the honesty line). */
|
|
140
|
+
const uses3d = (template) => [...template.blueprints.values()].some((blueprint) => sceneTypes(blueprint).includes("scene3d"));
|
|
141
|
+
/** `mk template show <template>/<blueprint>`; at most 40 lines and about 450 tokens (the example is the first to go, the ready `mk new` line stays). */
|
|
142
|
+
export function formatBlueprint(template, blueprint, preview) {
|
|
143
|
+
const id = `${template.manifest.id}/${blueprint.id}`;
|
|
144
|
+
const types = sceneTypes(blueprint);
|
|
145
|
+
const lines = [
|
|
146
|
+
`${id}: ${blueprint.purpose}`,
|
|
147
|
+
...(preview === undefined ? [] : [`preview: ${preview}`]),
|
|
148
|
+
...discoverLines(blueprint.discover, blueprint.discover?.lengths),
|
|
149
|
+
"slots (* required, ? optional):",
|
|
150
|
+
];
|
|
151
|
+
for (const [name, def] of Object.entries(blueprint.slots))
|
|
152
|
+
lines.push(` ${slotSignature(name, def)}`);
|
|
153
|
+
lines.push(`styles: ${[...template.styles.keys()].join("|")}; lengths: short|standard|long`);
|
|
154
|
+
lines.push(`scenes: ${types.join(", ")}`);
|
|
155
|
+
if (types.includes("scene3d"))
|
|
156
|
+
lines.push(`note: ${SCENE3D_NOTE}`);
|
|
157
|
+
lines.push(useLine(id, blueprint));
|
|
158
|
+
lines.push(`example: ${JSON.stringify(blueprint.example)}`);
|
|
159
|
+
return `${cap(withinTokens(lines, BLUEPRINT_MAX_TOKENS), BLUEPRINT_MAX_LINES).join("\n")}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type SearchFilters } from "@anvpx/motion-build";
|
|
2
|
+
import type { Io } from "../types.ts";
|
|
3
|
+
import { type TemplateEnv } from "./env.ts";
|
|
4
|
+
export declare const LIST_MAX_LINES = 15;
|
|
5
|
+
/** Ranked or filtered blueprints shown (one header line on top: 9 lines in all). */
|
|
6
|
+
export declare const BLUEPRINT_ROWS = 8;
|
|
7
|
+
export interface ListRequest {
|
|
8
|
+
readonly domain: string | undefined;
|
|
9
|
+
/** `--for` free text. */
|
|
10
|
+
readonly query: string | undefined;
|
|
11
|
+
readonly filters: SearchFilters;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** `mk template list`: templates, or (with `--for/--tag/--aspect/--sec`) ranked or filtered blueprints. */
|
|
15
|
+
export declare function list(request: ListRequest, env: TemplateEnv, io: Io): Promise<number>;
|