@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,133 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { CUSTOM_SCENE_TYPE, HELPER_LIMITS, lintCustomSource, readRelativeClosure, resolveSceneDir, scanVendorImports, } from "@anvpx/motion-build";
3
+ import { sceneContainment } from "./containment.js";
4
+ /** Files an authored scene must ship; `style.css` is optional. */
5
+ const REQUIRED = ["scene.js", "fragment.html"];
6
+ const OPTIONAL_CSS = "style.css";
7
+ const customEntries = (board) => {
8
+ const scenes = board.scenes;
9
+ if (!Array.isArray(scenes))
10
+ return [];
11
+ const entries = [];
12
+ scenes.forEach((scene, index) => {
13
+ const entry = scene;
14
+ if (entry.type !== CUSTOM_SCENE_TYPE)
15
+ return;
16
+ entries.push({
17
+ index,
18
+ id: typeof entry.id === "string" ? entry.id : `#${index}`,
19
+ dir: typeof entry.props?.dir === "string" ? entry.props.dir : "",
20
+ });
21
+ });
22
+ return entries;
23
+ };
24
+ /** True when the storyboard references an authored scene; cheap, no disk access. */
25
+ export const hasCustomScenes = (board) => customEntries(board).length > 0;
26
+ const error = (path, message, suggestion) => ({
27
+ severity: "error",
28
+ path,
29
+ message,
30
+ ...(suggestion === undefined ? {} : { suggestion }),
31
+ });
32
+ /** Runs the determinism probes on one helper, under its own scene-relative path. */
33
+ const lintHelper = (text, dirName, path) => lintCustomSource({ js: text }, dirName).map((finding) => ({
34
+ ...finding,
35
+ path: finding.path.replace(/scene\.js:/, `${path}:`),
36
+ }));
37
+ /**
38
+ * Reads every authored scene beside the storyboard, refuses directories that escape it
39
+ * (textually or through a symlink/junction), and runs the determinism lint. The build
40
+ * stays pure: this is the only place that touches disk.
41
+ */
42
+ export async function readCustomScenes(board, storyboardPath, host) {
43
+ const entries = customEntries(board);
44
+ const sources = {};
45
+ const findings = [];
46
+ const baseDir = dirname(resolve(storyboardPath));
47
+ for (const entry of entries) {
48
+ const where = `/scenes/${entry.index}/props/dir`;
49
+ const resolved = resolveSceneDir(baseDir, entry.dir);
50
+ if (!resolved.ok) {
51
+ findings.push(error(where, resolved.message));
52
+ continue;
53
+ }
54
+ const dirName = `scenes/${entry.id}`;
55
+ const missing = [];
56
+ for (const file of REQUIRED) {
57
+ if (!(await host.exists(join(resolved.dir, file))))
58
+ missing.push(file);
59
+ }
60
+ if (missing.length > 0) {
61
+ findings.push(error(where, `missing ${missing.join(", ")}`, `add them under ${entry.dir}`));
62
+ continue;
63
+ }
64
+ // Textual containment is not enough: a symlink or junction can leave the project.
65
+ const containment = sceneContainment(baseDir, resolved.dir, [...REQUIRED, OPTIONAL_CSS]);
66
+ if (!containment.dirInside) {
67
+ findings.push(error(where, "scene directory resolves outside the storyboard directory (symlink/junction)", "replace the link with a real directory inside the project"));
68
+ continue;
69
+ }
70
+ const escaped = new Set(containment.escapedFiles);
71
+ for (const file of escaped) {
72
+ findings.push(error(`${dirName}/${file}`, "scene file resolves outside the scene directory (symlink/junction)", "replace the link with a real file inside the scene directory"));
73
+ }
74
+ if (escaped.size > 0)
75
+ continue;
76
+ try {
77
+ const js = await host.readText(join(resolved.dir, "scene.js"));
78
+ const html = await host.readText(join(resolved.dir, "fragment.html"));
79
+ const cssPath = join(resolved.dir, OPTIONAL_CSS);
80
+ const css = (await host.exists(cssPath)) ? await host.readText(cssPath) : undefined;
81
+ // A bare import only resolves if the build wrote an import map entry for it.
82
+ const vendor = scanVendorImports(js);
83
+ for (const name of vendor.unknown) {
84
+ findings.push(error(`${dirName}/scene.js`, `cannot import "${name}": the emitted page vendors no such library`, "import three or postprocessing, or use a relative path (./, ../) inside the scene"));
85
+ }
86
+ // Relative helpers are part of the scene: package the closure and lint it too.
87
+ // Each helper is containment-checked before its bytes are read.
88
+ const helperEscapes = [];
89
+ const closure = await readRelativeClosure(js, async (path) => {
90
+ const check = sceneContainment(baseDir, resolved.dir, [path]);
91
+ if (!check.dirInside || check.escapedFiles.length > 0) {
92
+ helperEscapes.push(path);
93
+ return undefined;
94
+ }
95
+ const full = join(resolved.dir, path);
96
+ return (await host.exists(full)) ? host.readText(full) : undefined;
97
+ });
98
+ for (const path of helperEscapes) {
99
+ findings.push(error(`${dirName}/${path}`, "helper resolves outside the scene directory (symlink/junction)", "keep the real file inside the scene directory"));
100
+ }
101
+ for (const specifier of closure.escaping) {
102
+ findings.push(error(`${dirName}/scene.js`, `relative import "${specifier}" escapes the scene directory`, "keep helpers inside the scene directory (./lib/format.js)"));
103
+ }
104
+ for (const path of closure.missing) {
105
+ if (helperEscapes.includes(path))
106
+ continue;
107
+ findings.push(error(`${dirName}/scene.js`, `cannot import "./${path}": no such file beside the scene`, "add the file, or inline the code"));
108
+ }
109
+ if (closure.overflow) {
110
+ findings.push(error(`${dirName}/scene.js`, `scene helper closure exceeds ${HELPER_LIMITS.files} files or ${HELPER_LIMITS.chars} characters`, "keep the scene to a few small helpers"));
111
+ }
112
+ for (const file of closure.files) {
113
+ findings.push(...lintHelper(file.text, dirName, file.path));
114
+ }
115
+ // A scene whose helper cannot be read safely is not emitted at all.
116
+ if (helperEscapes.length > 0)
117
+ continue;
118
+ const source = {
119
+ js,
120
+ html,
121
+ ...(css === undefined ? {} : { css }),
122
+ ...(vendor.libs.length === 0 ? {} : { vendor: vendor.libs }),
123
+ ...(closure.files.length === 0 ? {} : { files: closure.files }),
124
+ };
125
+ findings.push(...lintCustomSource(source, dirName));
126
+ sources[entry.id] = source;
127
+ }
128
+ catch (cause) {
129
+ findings.push(error(where, `cannot read scene files: ${cause.message}`));
130
+ }
131
+ }
132
+ return { sources, findings };
133
+ }
@@ -0,0 +1,19 @@
1
+ import type { Io } from "../types.ts";
2
+ import { type CheckReport } from "./check.ts";
3
+ /** What an edit produced: the new document and how to describe it once it is written. */
4
+ export type Edit = {
5
+ ok: true;
6
+ value: unknown;
7
+ lines(report: CheckReport): string[];
8
+ } | {
9
+ ok: false;
10
+ message: string;
11
+ };
12
+ /** `scenes 5 | total 25s | 0 errors, 1 warnings`: the second line every edit prints. */
13
+ export declare const statusLine: (report: CheckReport, scenes: number) => string;
14
+ export declare const round1: (value: number) => number;
15
+ /**
16
+ * The shared edit pipeline: load, transform, validate the result with the full validator, write
17
+ * atomically. An invalid result is never written: the pointer errors are printed and the exit is 2.
18
+ */
19
+ export declare function editStoryboard(name: string, path: string, io: Io, transform: (doc: unknown) => Edit): Promise<number>;
@@ -0,0 +1,39 @@
1
+ import { EXIT } from "../exit-codes.js";
2
+ import { checkStoryboard } from "./check.js";
3
+ import { readJsonFile } from "./load.js";
4
+ import { formatFinding } from "./report.js";
5
+ const MAX_ERROR_LINES = 2;
6
+ const refusal = (name, report) => {
7
+ const shown = report.errors.slice(0, MAX_ERROR_LINES).map(formatFinding);
8
+ const more = report.errors.length - shown.length;
9
+ const head = `${name} refused, nothing written: ${report.errors.length} errors`;
10
+ const tail = more > 0 ? ` (+${more} more; run mk validate)` : "";
11
+ return `${[head, ...shown].join("\n")}${tail}\n`;
12
+ };
13
+ /** `scenes 5 | total 25s | 0 errors, 1 warnings`: the second line every edit prints. */
14
+ export const statusLine = (report, scenes) => `scenes ${scenes} | total ${round1(report.total ?? 0)}s | ${report.errors.length} errors, ${report.warnings.length} warnings`;
15
+ export const round1 = (value) => Math.round(value * 10) / 10;
16
+ /**
17
+ * The shared edit pipeline: load, transform, validate the result with the full validator, write
18
+ * atomically. An invalid result is never written: the pointer errors are printed and the exit is 2.
19
+ */
20
+ export async function editStoryboard(name, path, io, transform) {
21
+ const loaded = await readJsonFile(path, io.host);
22
+ if (!loaded.ok) {
23
+ io.stderr(`mk ${name}: ${loaded.message}\n`);
24
+ return EXIT.USAGE;
25
+ }
26
+ const edit = transform(loaded.value);
27
+ if (!edit.ok) {
28
+ io.stderr(`mk ${name}: ${edit.message}\n`);
29
+ return EXIT.USAGE;
30
+ }
31
+ const report = checkStoryboard(edit.value);
32
+ if (report.errors.length > 0) {
33
+ io.stderr(refusal(name, report));
34
+ return EXIT.USAGE;
35
+ }
36
+ await io.host.writeTextAtomic(path, `${JSON.stringify(edit.value, null, 2)}\n`);
37
+ io.stdout(`${edit.lines(report).join("\n")}\n`);
38
+ return EXIT.OK;
39
+ }
@@ -0,0 +1,11 @@
1
+ import type { Host } from "../types.ts";
2
+ export type LoadResult = {
3
+ ok: true;
4
+ value: unknown;
5
+ } | {
6
+ ok: false;
7
+ kind: "io" | "syntax";
8
+ message: string;
9
+ };
10
+ /** Reads and parses a JSON file; I/O and syntax failures are reported as values. */
11
+ export declare function readJsonFile(path: string, host: Host): Promise<LoadResult>;
@@ -0,0 +1,52 @@
1
+ const POSITION = /at position (\d+)/;
2
+ const POSITION_SUFFIX = /\s+in JSON at position \d+(?: \(line \d+ column \d+\))?/;
3
+ /** True while `text[0, length)` is still the start of some valid document. */
4
+ function isLivePrefix(text, length) {
5
+ try {
6
+ JSON.parse(text.slice(0, length));
7
+ return true;
8
+ }
9
+ catch (error) {
10
+ const message = error.message;
11
+ if (message.includes("end of JSON input"))
12
+ return true;
13
+ const position = POSITION.exec(message)?.[1];
14
+ return position !== undefined && Number(position) >= length;
15
+ }
16
+ }
17
+ /** Offset of the first character that makes the text invalid (binary search over prefixes). */
18
+ function errorOffset(text) {
19
+ let live = 0;
20
+ let dead = text.length;
21
+ while (dead - live > 1) {
22
+ const middle = Math.floor((live + dead) / 2);
23
+ if (isLivePrefix(text, middle))
24
+ live = middle;
25
+ else
26
+ dead = middle;
27
+ }
28
+ return live;
29
+ }
30
+ function syntaxMessage(text, error) {
31
+ const reason = error.message.replace(POSITION_SUFFIX, "");
32
+ const before = text.slice(0, errorOffset(text)).split("\n");
33
+ const line = before.length;
34
+ const col = (before.at(-1)?.length ?? 0) + 1;
35
+ return `invalid JSON at line ${line}, col ${col}: ${reason}`;
36
+ }
37
+ /** Reads and parses a JSON file; I/O and syntax failures are reported as values. */
38
+ export async function readJsonFile(path, host) {
39
+ let text;
40
+ try {
41
+ text = await host.readText(path);
42
+ }
43
+ catch (error) {
44
+ return { ok: false, kind: "io", message: `cannot read ${path}: ${error.message}` };
45
+ }
46
+ try {
47
+ return { ok: true, value: JSON.parse(text) };
48
+ }
49
+ catch (error) {
50
+ return { ok: false, kind: "syntax", message: syntaxMessage(text, error) };
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ import type { Finding } from "./check.ts";
2
+ /** `E /scenes/1/props/items/0: message -> suggestion`; the JSON root prints as `/`. */
3
+ export declare function formatFinding(finding: Finding): string;
4
+ export declare const formatFindings: (findings: readonly Finding[]) => string;
5
+ export declare const summaryLine: (errors: number, warnings: number) => string;
@@ -0,0 +1,8 @@
1
+ /** `E /scenes/1/props/items/0: message -> suggestion`; the JSON root prints as `/`. */
2
+ export function formatFinding(finding) {
3
+ const level = finding.severity === "error" ? "E" : "W";
4
+ const suggestion = finding.suggestion === undefined ? "" : ` -> ${finding.suggestion}`;
5
+ return `${level} ${finding.path || "/"}: ${finding.message}${suggestion}`;
6
+ }
7
+ export const formatFindings = (findings) => findings.map((finding) => `${formatFinding(finding)}\n`).join("");
8
+ export const summaryLine = (errors, warnings) => `${errors} errors, ${warnings} warnings\n`;
@@ -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;