@anvpx/motion-cli 0.0.0-stage → 0.1.1

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.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/dist/analyze/command.d.ts +2 -0
  4. package/dist/analyze/command.js +190 -0
  5. package/dist/approval/approval.d.ts +32 -0
  6. package/dist/approval/approval.js +141 -0
  7. package/dist/approval/command.d.ts +2 -0
  8. package/dist/approval/command.js +255 -0
  9. package/dist/approval/gate.d.ts +7 -0
  10. package/dist/approval/gate.js +23 -0
  11. package/dist/approval/hash.d.ts +71 -0
  12. package/dist/approval/hash.js +464 -0
  13. package/dist/args.d.ts +30 -0
  14. package/dist/args.js +53 -0
  15. package/dist/assets/command.d.ts +2 -0
  16. package/dist/assets/command.js +165 -0
  17. package/dist/captions/command.d.ts +2 -0
  18. package/dist/captions/command.js +341 -0
  19. package/dist/captions/cues.d.ts +10 -0
  20. package/dist/captions/cues.js +61 -0
  21. package/dist/catalog/command.d.ts +2 -0
  22. package/dist/catalog/command.js +60 -0
  23. package/dist/catalog/format.d.ts +13 -0
  24. package/dist/catalog/format.js +54 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.js +35 -0
  27. package/dist/commands.d.ts +3 -0
  28. package/dist/commands.js +59 -0
  29. package/dist/compare/command.d.ts +4 -0
  30. package/dist/compare/command.js +144 -0
  31. package/dist/contact/argv.d.ts +24 -0
  32. package/dist/contact/argv.js +42 -0
  33. package/dist/contact/command.d.ts +2 -0
  34. package/dist/contact/command.js +79 -0
  35. package/dist/doctor/checks.d.ts +25 -0
  36. package/dist/doctor/checks.js +571 -0
  37. package/dist/doctor/command.d.ts +4 -0
  38. package/dist/doctor/command.js +88 -0
  39. package/dist/doctor/format.d.ts +7 -0
  40. package/dist/doctor/format.js +41 -0
  41. package/dist/doctor/heal.d.ts +14 -0
  42. package/dist/doctor/heal.js +71 -0
  43. package/dist/doctor/pins.d.ts +8 -0
  44. package/dist/doctor/pins.js +28 -0
  45. package/dist/doctor/skills.d.ts +15 -0
  46. package/dist/doctor/skills.js +58 -0
  47. package/dist/doctor/types.d.ts +36 -0
  48. package/dist/doctor/types.js +1 -0
  49. package/dist/doctor/vendored.d.ts +15 -0
  50. package/dist/doctor/vendored.js +90 -0
  51. package/dist/doctor/version.d.ts +8 -0
  52. package/dist/doctor/version.js +11 -0
  53. package/dist/exit-codes.d.ts +16 -0
  54. package/dist/exit-codes.js +15 -0
  55. package/dist/fit/command.d.ts +5 -0
  56. package/dist/fit/command.js +154 -0
  57. package/dist/fit/solve.d.ts +21 -0
  58. package/dist/fit/solve.js +80 -0
  59. package/dist/fit/target.d.ts +18 -0
  60. package/dist/fit/target.js +47 -0
  61. package/dist/generate3d/command.d.ts +3 -0
  62. package/dist/generate3d/command.js +38 -0
  63. package/dist/generate3d/run.d.ts +39 -0
  64. package/dist/generate3d/run.js +77 -0
  65. package/dist/icon/command.d.ts +2 -0
  66. package/dist/icon/command.js +113 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +16 -0
  69. package/dist/install-mk.d.ts +31 -0
  70. package/dist/install-mk.js +168 -0
  71. package/dist/integration/preview.integration.d.ts +1 -0
  72. package/dist/integration/preview.integration.js +53 -0
  73. package/dist/integration/smoke.integration.d.ts +1 -0
  74. package/dist/integration/smoke.integration.js +91 -0
  75. package/dist/main.d.ts +2 -0
  76. package/dist/main.js +4 -0
  77. package/dist/node-io.d.ts +14 -0
  78. package/dist/node-io.js +152 -0
  79. package/dist/optimize/command.d.ts +12 -0
  80. package/dist/optimize/command.js +110 -0
  81. package/dist/packs/command.d.ts +2 -0
  82. package/dist/packs/command.js +109 -0
  83. package/dist/patch/apply.d.ts +36 -0
  84. package/dist/patch/apply.js +186 -0
  85. package/dist/patch/command.d.ts +2 -0
  86. package/dist/patch/command.js +92 -0
  87. package/dist/plugin/install.d.ts +40 -0
  88. package/dist/plugin/install.js +170 -0
  89. package/dist/preview/command.d.ts +10 -0
  90. package/dist/preview/command.js +764 -0
  91. package/dist/preview/manifest.d.ts +94 -0
  92. package/dist/preview/manifest.js +19 -0
  93. package/dist/preview/pack.d.ts +24 -0
  94. package/dist/preview/pack.js +53 -0
  95. package/dist/preview/plan.d.ts +58 -0
  96. package/dist/preview/plan.js +160 -0
  97. package/dist/preview/provenance.d.ts +23 -0
  98. package/dist/preview/provenance.js +38 -0
  99. package/dist/publish/command.d.ts +2 -0
  100. package/dist/publish/command.js +70 -0
  101. package/dist/publish/player.d.ts +25 -0
  102. package/dist/publish/player.js +108 -0
  103. package/dist/publish/static.d.ts +16 -0
  104. package/dist/publish/static.js +78 -0
  105. package/dist/qc.d.ts +2 -0
  106. package/dist/qc.js +192 -0
  107. package/dist/scene/author.d.ts +16 -0
  108. package/dist/scene/author.js +279 -0
  109. package/dist/scene/command.d.ts +2 -0
  110. package/dist/scene/command.js +110 -0
  111. package/dist/scene/ops.d.ts +23 -0
  112. package/dist/scene/ops.js +151 -0
  113. package/dist/state/command.d.ts +2 -0
  114. package/dist/state/command.js +128 -0
  115. package/dist/state/evidence.d.ts +71 -0
  116. package/dist/state/evidence.js +243 -0
  117. package/dist/storyboard/check.d.ts +21 -0
  118. package/dist/storyboard/check.js +39 -0
  119. package/dist/storyboard/containment.d.ts +24 -0
  120. package/dist/storyboard/containment.js +43 -0
  121. package/dist/storyboard/custom.d.ts +15 -0
  122. package/dist/storyboard/custom.js +133 -0
  123. package/dist/storyboard/edit.d.ts +19 -0
  124. package/dist/storyboard/edit.js +39 -0
  125. package/dist/storyboard/load.d.ts +11 -0
  126. package/dist/storyboard/load.js +52 -0
  127. package/dist/storyboard/report.d.ts +5 -0
  128. package/dist/storyboard/report.js +8 -0
  129. package/dist/template/coerce.d.ts +6 -0
  130. package/dist/template/coerce.js +44 -0
  131. package/dist/template/command.d.ts +5 -0
  132. package/dist/template/command.js +163 -0
  133. package/dist/template/env.d.ts +34 -0
  134. package/dist/template/env.js +38 -0
  135. package/dist/template/fork.d.ts +8 -0
  136. package/dist/template/fork.js +156 -0
  137. package/dist/template/format.d.ts +21 -0
  138. package/dist/template/format.js +161 -0
  139. package/dist/template/list.d.ts +15 -0
  140. package/dist/template/list.js +116 -0
  141. package/dist/template/new.d.ts +6 -0
  142. package/dist/template/new.js +302 -0
  143. package/dist/testing/env.d.ts +8 -0
  144. package/dist/testing/env.js +19 -0
  145. package/dist/testing/fakes.d.ts +33 -0
  146. package/dist/testing/fakes.js +95 -0
  147. package/dist/testing/fixtures.d.ts +8 -0
  148. package/dist/testing/fixtures.js +6 -0
  149. package/dist/testing/probe.d.ts +15 -0
  150. package/dist/testing/probe.js +20 -0
  151. package/dist/testing/project.d.ts +22 -0
  152. package/dist/testing/project.js +26 -0
  153. package/dist/tooling/session.d.ts +40 -0
  154. package/dist/tooling/session.js +106 -0
  155. package/dist/types.d.ts +59 -0
  156. package/dist/types.js +1 -0
  157. package/dist/verify/audio-loudness.d.ts +46 -0
  158. package/dist/verify/audio-loudness.js +186 -0
  159. package/dist/verify/command.d.ts +2 -0
  160. package/dist/verify/command.js +161 -0
  161. package/dist/verify/determinism.d.ts +30 -0
  162. package/dist/verify/determinism.js +59 -0
  163. package/dist/verify/expectation.d.ts +15 -0
  164. package/dist/verify/expectation.js +19 -0
  165. package/dist/verify/media-events.d.ts +61 -0
  166. package/dist/verify/media-events.js +174 -0
  167. package/dist/verify/platform.d.ts +33 -0
  168. package/dist/verify/platform.js +59 -0
  169. package/dist/verify/probe.d.ts +14 -0
  170. package/dist/verify/probe.js +23 -0
  171. package/dist/verify/report.d.ts +15 -0
  172. package/dist/verify/report.js +45 -0
  173. package/dist/verify/run.d.ts +27 -0
  174. package/dist/verify/run.js +53 -0
  175. package/dist/voice/command.d.ts +3 -0
  176. package/dist/voice/command.js +49 -0
  177. package/dist/voice/manifest.d.ts +33 -0
  178. package/dist/voice/manifest.js +69 -0
  179. package/dist/voice/run.d.ts +27 -0
  180. package/dist/voice/run.js +190 -0
  181. package/dist/workflow/build.d.ts +2 -0
  182. package/dist/workflow/build.js +25 -0
  183. package/dist/workflow/draft.d.ts +9 -0
  184. package/dist/workflow/draft.js +215 -0
  185. package/dist/workflow/finish.d.ts +23 -0
  186. package/dist/workflow/finish.js +28 -0
  187. package/dist/workflow/project.d.ts +73 -0
  188. package/dist/workflow/project.js +198 -0
  189. package/dist/workflow/provenance.d.ts +18 -0
  190. package/dist/workflow/provenance.js +36 -0
  191. package/dist/workflow/render.d.ts +31 -0
  192. package/dist/workflow/render.js +721 -0
  193. package/dist/workflow/still.d.ts +5 -0
  194. package/dist/workflow/still.js +62 -0
  195. package/dist/workflow/validate.d.ts +2 -0
  196. package/dist/workflow/validate.js +49 -0
  197. package/package.json +38 -3
@@ -0,0 +1,302 @@
1
+ import { createHash } from "node:crypto";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { instantiate, resolveTimeline, sceneDocs, validateStoryboardFull, } from "@anvpx/motion-build";
4
+ import { canonicalJson, LENGTH_CLASSES, } from "@anvpx/motion-schema";
5
+ import { MK_DIR, mkDir, storyboardStem } from "../approval/hash.js";
6
+ import { listFlag, parseFlags, textFlag, usageError } from "../args.js";
7
+ import { EXIT } from "../exit-codes.js";
8
+ import { readJsonFile } from "../storyboard/load.js";
9
+ import { formatFindings } from "../storyboard/report.js";
10
+ import { coerceSets } from "./coerce.js";
11
+ import { defaultEnv, findTemplate, formatErrors, platformsFor } from "./env.js";
12
+ export const TEMPLATE_PROVENANCE = "template.json";
13
+ /** Bare `mk new --demo` target: the most familiar blueprint. */
14
+ const DEFAULT_DEMO = "explain/architecture";
15
+ const USAGE = `mk new [<template>/<blueprint>] [--demo] [--with scene[,scene] ...] [--set k=v ...] [--slots file.json] [--style s] [--length l] [--out storyboard.json] [--json]
16
+
17
+ Instantiate a blueprint into a plain, validated storyboard. Writes the storyboard, vendors the theme(s) it uses
18
+ into <out dir>/themes/ (the project builds without the template installed) and records provenance in <out dir>/.mk/<name>/template.json.
19
+ --demo start from the blueprint's own example; --slots/--set still win. Without a target, demo uses ${DEFAULT_DEMO}.
20
+ --with t[,t] append one base scene per type after the blueprint; props come from the catalog example
21
+ merged with slots["$with"]["<type>"] (same --slots file), then the whole storyboard validates.
22
+ --set k=v one slot; lists as k=a|b|c, backslash-pipe for a literal pipe (repeatable; wins over --slots)
23
+ --slots f JSON object of slots (needed for object slots); may carry a "$with" object for --with props
24
+ --style s style id (default: the blueprint's default)
25
+ --length l short|standard|long (default: the blueprint's default)
26
+ --out f storyboard path (default storyboard.json)
27
+ See \`mk template show <template>/<blueprint>\` for slots and limits.
28
+ `;
29
+ const FLAGS = {
30
+ demo: { type: "boolean" },
31
+ with: { type: "string", multiple: true },
32
+ set: { type: "string", multiple: true },
33
+ slots: { type: "string" },
34
+ style: { type: "string" },
35
+ length: { type: "string" },
36
+ out: { type: "string" },
37
+ json: { type: "boolean" },
38
+ };
39
+ /** Reserved slots key holding per-scene props for `--with`; stripped before slot resolution. */
40
+ const WITH_KEY = "$with";
41
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
42
+ const MAX_WARNING_LINES = 5;
43
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
44
+ async function slotsFile(path, io) {
45
+ if (path === undefined)
46
+ return { slots: {}, withProps: {} };
47
+ const loaded = await readJsonFile(path, io.host);
48
+ if (!loaded.ok) {
49
+ io.stderr(`mk new: ${loaded.message}\n`);
50
+ return EXIT.USAGE;
51
+ }
52
+ const value = loaded.value;
53
+ if (!isRecord(value)) {
54
+ io.stderr(`mk new: ${path} must hold a JSON object of slots\n`);
55
+ return EXIT.USAGE;
56
+ }
57
+ const { [WITH_KEY]: withRaw, ...slots } = value;
58
+ if (withRaw !== undefined && !isRecord(withRaw)) {
59
+ io.stderr(`mk new: ${path} "${WITH_KEY}" must be an object keyed by scene type\n`);
60
+ return EXIT.USAGE;
61
+ }
62
+ return { slots, withProps: isRecord(withRaw) ? withRaw : {} };
63
+ }
64
+ /** Resolves `<template>/<blueprint>`; a miss prints the known ids and yields an exit code. */
65
+ function resolveTarget(target, env, io) {
66
+ const [templateId = "", blueprintId = "", ...extra] = target.split("/");
67
+ if (blueprintId === "" || extra.length > 0) {
68
+ return usageError("new", USAGE, "target must be <template>/<blueprint>", io).code;
69
+ }
70
+ const found = findTemplate(env, io, templateId);
71
+ if (!found.ok) {
72
+ io.stderr(`mk new: ${found.message}
73
+ `);
74
+ return EXIT.USAGE;
75
+ }
76
+ const blueprint = found.template.blueprints.get(blueprintId);
77
+ if (blueprint === undefined) {
78
+ const known = [...found.template.blueprints.keys()].join("|");
79
+ io.stderr(`mk new: unknown blueprint "${blueprintId}"; use one of: ${known}
80
+ `);
81
+ return EXIT.USAGE;
82
+ }
83
+ return { template: found.template, blueprintId, blueprint };
84
+ }
85
+ /** `--demo`: the blueprint's own example values, or an exit code when it has none (never invented). */
86
+ function demoSlots(target, io) {
87
+ if (Object.keys(target.blueprint.example).length > 0)
88
+ return target.blueprint.example;
89
+ io.stderr(`mk new: ${target.template.manifest.id}/${target.blueprintId} has no example slots; pass --slots or --set
90
+ `);
91
+ return EXIT.USAGE;
92
+ }
93
+ /** `--demo` example first, then `--slots`, then `--set` (which wins); an exit code on bad input. */
94
+ async function gatherSlots(flags, target, io) {
95
+ const base = flags.demo === true ? demoSlots(target, io) : {};
96
+ if (typeof base === "number")
97
+ return base;
98
+ const fromFile = await slotsFile(textFlag(flags, "slots"), io);
99
+ if (typeof fromFile === "number")
100
+ return fromFile;
101
+ const sets = coerceSets(target.blueprint.slots, listFlag(flags, "set"));
102
+ if (sets.ok) {
103
+ return {
104
+ slots: { ...base, ...fromFile.slots, ...sets.value },
105
+ withProps: fromFile.withProps,
106
+ };
107
+ }
108
+ io.stderr(`mk new: ${formatErrors(sets.errors)}
109
+ `);
110
+ return EXIT.USAGE;
111
+ }
112
+ /** Writes the storyboard, the vendored theme(s) and the provenance record; returns the storyboard path. */
113
+ async function writeProject(made, outFlag, io) {
114
+ const out = resolve(outFlag ?? "storyboard.json");
115
+ const dir = dirname(out);
116
+ const text = `${made.text}
117
+ `;
118
+ await io.host.writeText(out, text);
119
+ for (const theme of made.themes)
120
+ await io.host.writeText(join(dir, theme.file), theme.text);
121
+ const record = { ...made.provenance, storyboardSha256: sha256(text) };
122
+ await io.host.writeText(join(mkDir(out), TEMPLATE_PROVENANCE), `${JSON.stringify(record, null, 2)}
123
+ `);
124
+ return out;
125
+ }
126
+ function report(made, out, json, io) {
127
+ const { meta } = made.storyboard;
128
+ const overlays = made.storyboard.scenes.filter((s) => s.layer === "overlay").length;
129
+ const scenes = made.sceneCount - overlays;
130
+ const frame = { width: meta.width, height: meta.height, fps: meta.fps };
131
+ const files = made.themes.map((t) => t.file);
132
+ if (json) {
133
+ const { provenance, totalSec } = made;
134
+ const doc = {
135
+ ok: true,
136
+ storyboard: out,
137
+ ...provenance,
138
+ scenes,
139
+ overlays,
140
+ totalSec,
141
+ frame,
142
+ themes: files,
143
+ warnings: made.warnings.length,
144
+ };
145
+ io.stdout(`${JSON.stringify(doc)}
146
+ `);
147
+ return;
148
+ }
149
+ const { provenance } = made;
150
+ const label = `${provenance.template.split("@")[0]}/${provenance.blueprint}`;
151
+ io.stdout(`new ok | ${label} style ${provenance.style} length ${provenance.length} | ${scenes} scenes + ${overlays} overlays ${made.totalSec}s ${frame.width}x${frame.height}@${frame.fps} | ${made.warnings.length} warnings
152
+ ` +
153
+ `wrote ${out} (+ ${files.join(", ")}, ${MK_DIR}/${storyboardStem(out)}/${TEMPLATE_PROVENANCE})
154
+ ` +
155
+ `next: mk validate ${out} && mk preview ${out}
156
+ `);
157
+ }
158
+ const lengthOf = (flags, io) => {
159
+ const length = textFlag(flags, "length");
160
+ if (length === undefined)
161
+ return undefined;
162
+ if (LENGTH_CLASSES.includes(length))
163
+ return length;
164
+ return usageError("new", USAGE, `--length must be ${LENGTH_CLASSES.join("|")}`, io).code;
165
+ };
166
+ const appendScenes = (made, types, withProps) => {
167
+ if (types.length === 0)
168
+ return { ok: true, value: made };
169
+ const docs = new Map(sceneDocs().map((doc) => [doc.type, doc]));
170
+ const ids = new Set(made.storyboard.scenes.map((scene) => scene.id));
171
+ const appended = [];
172
+ for (const type of types) {
173
+ const doc = docs.get(type);
174
+ if (doc === undefined) {
175
+ return { ok: false, message: `unknown scene type "${type}"; run mk catalog` };
176
+ }
177
+ if (doc.layer === "overlay") {
178
+ return {
179
+ ok: false,
180
+ message: `"${type}" is an overlay; --with appends base scenes (use mk scene add for overlays)`,
181
+ };
182
+ }
183
+ const override = withProps[type];
184
+ if (override !== undefined && !isRecord(override)) {
185
+ return { ok: false, message: `"${WITH_KEY}.${type}" must be an object of props` };
186
+ }
187
+ const provided = isRecord(override) ? override : {};
188
+ const missing = doc.fields
189
+ .filter((field) => field.required && provided[field.name] === undefined)
190
+ .map((field) => field.name);
191
+ if (missing.length > 0) {
192
+ return {
193
+ ok: false,
194
+ message: `${type} needs ${missing.join(", ")}; add them under "${WITH_KEY}"."${type}" in --slots`,
195
+ };
196
+ }
197
+ // The slot object is the complete props value: no example defaults leak in
198
+ // (the kinetic example pins emphasis to a word index that the new words may not have).
199
+ const props = provided;
200
+ let id = type;
201
+ let suffix = 2;
202
+ while (ids.has(id)) {
203
+ id = `${type}-${suffix}`;
204
+ suffix += 1;
205
+ }
206
+ ids.add(id);
207
+ appended.push({ id, type, props });
208
+ }
209
+ const storyboard = {
210
+ ...made.storyboard,
211
+ scenes: [...made.storyboard.scenes, ...appended],
212
+ };
213
+ const valid = validateStoryboardFull(storyboard);
214
+ if (!valid.ok)
215
+ return { ok: false, message: formatErrors(valid.errors) };
216
+ const timeline = resolveTimeline(valid.value);
217
+ if (!timeline.ok)
218
+ return { ok: false, message: formatErrors(timeline.errors) };
219
+ return {
220
+ ok: true,
221
+ value: {
222
+ ...made,
223
+ storyboard: valid.value,
224
+ text: canonicalJson(valid.value),
225
+ sceneCount: valid.value.scenes.length,
226
+ totalSec: timeline.value.total,
227
+ },
228
+ };
229
+ };
230
+ /** `--with t[,t]` entries, trimmed and de-duplicated by scene type. */
231
+ const withTypesOf = (flags) => [
232
+ ...new Set(listFlag(flags, "with")
233
+ .flatMap((value) => value.split(","))
234
+ .map((value) => value.trim())
235
+ .filter((value) => value !== "")),
236
+ ];
237
+ /** Everything `instantiate` needs, from flags and the template; an exit code on bad input. */
238
+ async function plan(args, env, io) {
239
+ const parsed = parseFlags("new", USAGE, args, FLAGS, io);
240
+ if (parsed.kind === "exit")
241
+ return parsed.code;
242
+ const [positional, ...extra] = parsed.positionals;
243
+ if (extra.length > 0) {
244
+ return usageError("new", USAGE, `unexpected argument "${extra[0]}"`, io).code;
245
+ }
246
+ const demo = parsed.flags.demo === true;
247
+ if (positional === undefined && !demo) {
248
+ return usageError("new", USAGE, "missing <template>/<blueprint> (or pass --demo for the default demo)", io).code;
249
+ }
250
+ const name = positional ?? DEFAULT_DEMO;
251
+ const length = lengthOf(parsed.flags, io);
252
+ const target = typeof length === "number" ? length : resolveTarget(name, env, io);
253
+ if (typeof target === "number")
254
+ return target;
255
+ const gathered = await gatherSlots(parsed.flags, target, io);
256
+ if (typeof gathered === "number")
257
+ return gathered;
258
+ const style = textFlag(parsed.flags, "style");
259
+ const options = {
260
+ ...(style === undefined ? {} : { style }),
261
+ ...(length === undefined ? {} : { length: length }),
262
+ platforms: await platformsFor(io),
263
+ };
264
+ return {
265
+ target,
266
+ slots: gathered.slots,
267
+ withTypes: withTypesOf(parsed.flags),
268
+ withProps: gathered.withProps,
269
+ options,
270
+ out: textFlag(parsed.flags, "out"),
271
+ json: parsed.flags.json === true,
272
+ };
273
+ }
274
+ /** `mk new`; `env` is injectable for tests. */
275
+ export const makeNewCommand = (env = defaultEnv) => ({
276
+ name: "new",
277
+ summary: "instantiate a template blueprint into a validated storyboard",
278
+ async run(args, io) {
279
+ const planned = await plan(args, env(), io);
280
+ if (typeof planned === "number")
281
+ return planned;
282
+ const { target, slots, options } = planned;
283
+ const result = instantiate(target.template, target.blueprintId, slots, options);
284
+ if (!result.ok) {
285
+ const label = `${target.template.manifest.id}/${target.blueprintId}`;
286
+ io.stderr(`mk new: ${formatErrors(result.errors)}
287
+ run mk template show ${label} for the required slots (the use line is copy-pasteable)
288
+ `);
289
+ return EXIT.USAGE;
290
+ }
291
+ const appended = appendScenes(result.value, planned.withTypes, planned.withProps);
292
+ if (!appended.ok) {
293
+ io.stderr(`mk new: ${appended.message}\n`);
294
+ return EXIT.USAGE;
295
+ }
296
+ const out = await writeProject(appended.value, planned.out, io);
297
+ report(appended.value, out, planned.json, io);
298
+ io.stderr(formatFindings(appended.value.warnings.slice(0, MAX_WARNING_LINES)));
299
+ return EXIT.OK;
300
+ },
301
+ });
302
+ export const newCommand = makeNewCommand();
@@ -0,0 +1,8 @@
1
+ import { type TemplateEnv } from "../template/env.ts";
2
+ /**
3
+ * The environment the real CLI passes (`defaultEnv`: built-ins, shared themes, stopwords) pinned to a
4
+ * project directory and with `MK_TEMPLATES_PATH` cleared, so tests cannot drift from production.
5
+ */
6
+ export declare const testEnv: (projectDir: string, overrides?: Partial<TemplateEnv>) => TemplateEnv;
7
+ /** `<id>@<version>` of a built-in template, read from its manifest (never hard-coded in tests). */
8
+ export declare const builtInRef: (id: string) => string;
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { TEMPLATES_DIR } from "@anvpx/motion-templates";
4
+ import { defaultEnv } from "../template/env.js";
5
+ /**
6
+ * The environment the real CLI passes (`defaultEnv`: built-ins, shared themes, stopwords) pinned to a
7
+ * project directory and with `MK_TEMPLATES_PATH` cleared, so tests cannot drift from production.
8
+ */
9
+ export const testEnv = (projectDir, overrides = {}) => ({
10
+ ...defaultEnv(),
11
+ envPath: undefined,
12
+ projectDir,
13
+ ...overrides,
14
+ });
15
+ /** `<id>@<version>` of a built-in template, read from its manifest (never hard-coded in tests). */
16
+ export const builtInRef = (id) => {
17
+ const manifest = JSON.parse(readFileSync(join(TEMPLATES_DIR, id, "template.json"), "utf8"));
18
+ return `${id}@${manifest.version}`;
19
+ };
@@ -0,0 +1,33 @@
1
+ import type { CommandResult, Host, Io, Runner, RunOptions } from "../types.ts";
2
+ export declare const NOT_FOUND: CommandResult;
3
+ export declare function ok(stdout?: string): CommandResult;
4
+ export declare function failed(code: number, stderr: string, stdout?: string): CommandResult;
5
+ /** A canned result, or a function that may also act (e.g. create the files a tool would). */
6
+ export type Response = CommandResult | ((options: RunOptions | undefined) => CommandResult | Promise<CommandResult>);
7
+ /**
8
+ * Runner answering by exact "cmd arg arg" key; anything else is command-not-found.
9
+ * `calls` records each key, `options` the RunOptions of the same call.
10
+ */
11
+ export declare function fakeRunner(responses: Record<string, Response>, calls?: string[], options?: (RunOptions | undefined)[]): Runner;
12
+ export interface HostOptions {
13
+ platform?: NodeJS.Platform;
14
+ nodeVersion?: string;
15
+ homeDir?: string;
16
+ env?: Readonly<Record<string, string | undefined>>;
17
+ dirs?: Record<string, string[]>;
18
+ packageJson?: unknown;
19
+ /** Readable files by exact path. */
20
+ files?: Record<string, string>;
21
+ }
22
+ export interface FakeHost extends Host {
23
+ written: Record<string, string>;
24
+ removed: string[];
25
+ }
26
+ export declare function fakeHost(options?: HostOptions): FakeHost;
27
+ /** The real filesystem (real build into a temp dir) with a stubbed root package.json. */
28
+ export declare function diskHost(packageJson: unknown): Host;
29
+ export interface FakeIo extends Io {
30
+ out: string[];
31
+ err: string[];
32
+ }
33
+ export declare function fakeIo(run: Runner, host?: Host): FakeIo;
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { nodeHost } from "../node-io.js";
3
+ export const NOT_FOUND = { code: 127, stdout: "", stderr: "not found" };
4
+ export function ok(stdout = "") {
5
+ return { code: 0, stdout, stderr: "" };
6
+ }
7
+ export function failed(code, stderr, stdout = "") {
8
+ return { code, stdout, stderr };
9
+ }
10
+ /**
11
+ * Runner answering by exact "cmd arg arg" key; anything else is command-not-found.
12
+ * `calls` records each key, `options` the RunOptions of the same call.
13
+ */
14
+ export function fakeRunner(responses, calls = [], options = []) {
15
+ return async (cmd, args, runOptions) => {
16
+ const key = [cmd, ...args].join(" ");
17
+ calls.push(key);
18
+ options.push(runOptions);
19
+ const response = responses[key];
20
+ if (response === undefined)
21
+ return NOT_FOUND;
22
+ return typeof response === "function" ? response(runOptions) : response;
23
+ };
24
+ }
25
+ export function fakeHost(options = {}) {
26
+ const dirs = options.dirs ?? {};
27
+ const files = options.files ?? {};
28
+ const written = {};
29
+ const removed = [];
30
+ return {
31
+ platform: options.platform ?? "win32",
32
+ nodeVersion: options.nodeVersion ?? "24.12.0",
33
+ homeDir: options.homeDir ?? "/home/u",
34
+ env: options.env ?? {},
35
+ async listDir(path) {
36
+ const entries = dirs[path];
37
+ if (!entries)
38
+ throw new Error(`ENOENT: ${path}`);
39
+ return entries;
40
+ },
41
+ async mkdir(path) {
42
+ dirs[path] = dirs[path] ?? [];
43
+ },
44
+ async readRootPackageJson() {
45
+ return options.packageJson;
46
+ },
47
+ async readText(path) {
48
+ const text = files[path] ?? written[path];
49
+ if (text === undefined)
50
+ throw new Error(`ENOENT: ${path}`);
51
+ return text;
52
+ },
53
+ async writeText(path, text) {
54
+ written[path] = text;
55
+ },
56
+ async writeTextAtomic(path, text) {
57
+ written[path] = text;
58
+ },
59
+ async exists(path) {
60
+ return (files[path] ?? written[path]) !== undefined || path in dirs;
61
+ },
62
+ async copyFile(from, to) {
63
+ const text = files[from] ?? written[from];
64
+ if (text === undefined)
65
+ throw new Error(`ENOENT: ${from}`);
66
+ written[to] = text;
67
+ },
68
+ async renameFile(from, to) {
69
+ const text = files[from] ?? written[from];
70
+ if (text === undefined)
71
+ throw new Error(`ENOENT: ${from}`);
72
+ written[to] = text;
73
+ // Mirrors node:rename: the source is gone after the move.
74
+ delete written[from];
75
+ },
76
+ async sha256File(path) {
77
+ const text = files[path] ?? written[path];
78
+ return text === undefined ? undefined : createHash("sha256").update(text).digest("hex");
79
+ },
80
+ async remove(path) {
81
+ removed.push(path);
82
+ },
83
+ written,
84
+ removed,
85
+ };
86
+ }
87
+ /** The real filesystem (real build into a temp dir) with a stubbed root package.json. */
88
+ export function diskHost(packageJson) {
89
+ return { ...nodeHost(), readRootPackageJson: async () => packageJson };
90
+ }
91
+ export function fakeIo(run, host = fakeHost()) {
92
+ const out = [];
93
+ const err = [];
94
+ return { stdout: (t) => out.push(t), stderr: (t) => err.push(t), run, host, out, err };
95
+ }
@@ -0,0 +1,8 @@
1
+ /** The 5-scene demo storyboard, as raw text. */
2
+ export declare const demoText: () => string;
3
+ export interface LooseBoard {
4
+ scenes: Record<string, unknown>[];
5
+ [key: string]: unknown;
6
+ }
7
+ /** A fresh deep copy of the demo storyboard for mutation in tests. */
8
+ export declare const demoBoard: () => LooseBoard;
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ const DEMO = new URL("../../../build/fixtures/demo.storyboard.json", import.meta.url);
3
+ /** The 5-scene demo storyboard, as raw text. */
4
+ export const demoText = () => readFileSync(DEMO, "utf8");
5
+ /** A fresh deep copy of the demo storyboard for mutation in tests. */
6
+ export const demoBoard = () => JSON.parse(demoText());
@@ -0,0 +1,15 @@
1
+ export declare const probeKey: (file: string) => string;
2
+ export interface ProbeInit {
3
+ width?: number;
4
+ height?: number;
5
+ rate?: string;
6
+ frames?: number;
7
+ duration?: number;
8
+ codec?: string;
9
+ pixFmt?: string;
10
+ audio?: string;
11
+ }
12
+ /** ffprobe JSON for the demo storyboard's final render: 1920x1080, 30 fps, 32.5 s, 975 frames. */
13
+ export declare function probeJson(init?: ProbeInit): string;
14
+ /** The same render as a draft: 960x540 at 15 fps (488 frames). */
15
+ export declare const draftProbeJson: (init?: ProbeInit) => string;
@@ -0,0 +1,20 @@
1
+ export const probeKey = (file) => `ffprobe -v error -show_streams -show_format -of json ${file}`;
2
+ /** ffprobe JSON for the demo storyboard's final render: 1920x1080, 30 fps, 32.5 s, 975 frames. */
3
+ export function probeJson(init = {}) {
4
+ const streams = [
5
+ {
6
+ codec_type: "video",
7
+ codec_name: init.codec ?? "h264",
8
+ pix_fmt: init.pixFmt ?? "yuv420p",
9
+ width: init.width ?? 1920,
10
+ height: init.height ?? 1080,
11
+ r_frame_rate: init.rate ?? "30/1",
12
+ nb_frames: String(init.frames ?? 975),
13
+ },
14
+ ];
15
+ if (init.audio)
16
+ streams.push({ codec_type: "audio", codec_name: init.audio });
17
+ return JSON.stringify({ streams, format: { duration: String(init.duration ?? 32.5) } });
18
+ }
19
+ /** The same render as a draft: 960x540 at 15 fps (488 frames). */
20
+ export const draftProbeJson = (init = {}) => probeJson({ width: 960, height: 540, rate: "15/1", frames: 488, ...init });
@@ -0,0 +1,22 @@
1
+ export declare const PIN = "0.8.212";
2
+ export declare const PINNED_PACKAGE: {
3
+ motionKit: {
4
+ pins: {
5
+ hyperframes: string;
6
+ };
7
+ };
8
+ };
9
+ /** Fake-runner key for `node <resolved hyperframes CLI> <args>` (mirrors runInProject). */
10
+ export declare const hfKey: (...args: readonly string[]) => string;
11
+ /** Fake-runner key for the hyperframes pin check. */
12
+ export declare const VERSION_KEY: string;
13
+ export interface TempProject {
14
+ dir: string;
15
+ storyboard: string;
16
+ /** Default build output: `<dir>/out/project`. */
17
+ out: string;
18
+ }
19
+ /** A temp directory holding the demo storyboard; removed when the test finishes. */
20
+ export declare function tempProject(storyboardText?: string): TempProject;
21
+ /** Creates empty files, as a stand-in for what an external tool would produce. */
22
+ export declare function touch(dir: string, ...names: string[]): void;
@@ -0,0 +1,26 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { onTestFinished } from "vitest";
5
+ import { hyperframesCliPath } from "../tooling/session.js";
6
+ import { demoText } from "./fixtures.js";
7
+ export const PIN = "0.8.212";
8
+ export const PINNED_PACKAGE = { motionKit: { pins: { hyperframes: PIN } } };
9
+ /** Fake-runner key for `node <resolved hyperframes CLI> <args>` (mirrors runInProject). */
10
+ export const hfKey = (...args) => [process.execPath, hyperframesCliPath(), ...args].join(" ");
11
+ /** Fake-runner key for the hyperframes pin check. */
12
+ export const VERSION_KEY = hfKey("--version");
13
+ /** A temp directory holding the demo storyboard; removed when the test finishes. */
14
+ export function tempProject(storyboardText = demoText()) {
15
+ const dir = mkdtempSync(join(tmpdir(), "mk-cli-"));
16
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }));
17
+ const storyboard = join(dir, "sb.json");
18
+ writeFileSync(storyboard, storyboardText);
19
+ return { dir, storyboard, out: join(dir, "out", "project") };
20
+ }
21
+ /** Creates empty files, as a stand-in for what an external tool would produce. */
22
+ export function touch(dir, ...names) {
23
+ mkdirSync(dir, { recursive: true });
24
+ for (const name of names)
25
+ writeFileSync(join(dir, name), "x");
26
+ }
@@ -0,0 +1,40 @@
1
+ import type { CommandResult, Io } from "../types.ts";
2
+ export declare const LOG_FILE = "render-log.txt";
3
+ /**
4
+ * Absolute path of the hyperframes CLI entry point, resolved from the packages
5
+ * installed alongside this one (hyperframes is a runtime dependency of
6
+ * `@anvpx/motion`). Throws when the install cannot provide it.
7
+ */
8
+ export declare function hyperframesCliPath(): string;
9
+ /** Absolute path of the installed hyperframes manifest (doctor reports its version). */
10
+ export declare function hyperframesPackagePath(): string;
11
+ export interface StepOptions {
12
+ env?: Readonly<Record<string, string>>;
13
+ }
14
+ export interface Session {
15
+ /** Runs the resolved hyperframes CLI with node; resolves false (and records the failure) on a non-zero exit. */
16
+ hf(args: readonly string[], options?: StepOptions): Promise<boolean>;
17
+ /** Runs another program (e.g. ffmpeg) in the project directory. */
18
+ tool(cmd: string, args: readonly string[], options?: StepOptions): Promise<boolean>;
19
+ /**
20
+ * Runs another program and returns its captured output, so a caller can parse
21
+ * it (e.g. loudnorm measurement). A non-zero exit still fails the run.
22
+ */
23
+ probe(cmd: string, args: readonly string[], options?: StepOptions): Promise<CommandResult>;
24
+ }
25
+ interface PinProblem {
26
+ message: string;
27
+ code: number;
28
+ }
29
+ /**
30
+ * Why hyperframes may not be used (missing install or mismatched pin), or undefined when
31
+ * it is runnable. A pin is optional: consumer projects get it from `@anvpx/motion`, so
32
+ * no pin plus a runnable installed CLI is acceptable.
33
+ */
34
+ export declare function pinProblem(io: Io, project: string, resolveCli?: () => string): Promise<PinProblem | undefined>;
35
+ /**
36
+ * Checks the hyperframes pin, runs `steps` in the project directory, and always writes
37
+ * `render-log.txt` with every command's output. Resolves to the exit code: OK, ERROR on a failed step, MISSING_TOOL when a program cannot run.
38
+ */
39
+ export declare function runInProject(io: Io, project: string, steps: (session: Session) => Promise<void>): Promise<number>;
40
+ export {};