@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,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>;
@@ -0,0 +1,116 @@
1
+ import { entriesOf, filterEntries, loadTemplate, rankEntries, } from "@anvpx/motion-build";
2
+ import { EXIT } from "../exit-codes.js";
3
+ import { discover, loadOptions } from "./env.js";
4
+ import { clip, lengthLabel } from "./format.js";
5
+ export const LIST_MAX_LINES = 15;
6
+ /** Ranked or filtered blueprints shown (one header line on top: 9 lines in all). */
7
+ export const BLUEPRINT_ROWS = 8;
8
+ const SUMMARY_MAX = 70;
9
+ const GOOD_FOR_MAX = 70;
10
+ const TOP_TAGS = 4;
11
+ /** `--for`, `--tag`, `--aspect` or `--sec` switch the listing from templates to blueprints. */
12
+ const wantsBlueprints = (request) => request.query !== undefined ||
13
+ (request.filters.tags?.length ?? 0) > 0 ||
14
+ request.filters.aspect !== undefined ||
15
+ request.filters.sec !== undefined;
16
+ const capLines = (lines) => lines.length <= LIST_MAX_LINES
17
+ ? lines
18
+ : [...lines.slice(0, LIST_MAX_LINES - 1), `(+${lines.length - LIST_MAX_LINES + 1} more)`];
19
+ const loadAll = (env, io) => {
20
+ const found = discover(env, io);
21
+ const loaded = found.templates.flatMap((candidate) => {
22
+ const result = loadTemplate(candidate.dir, loadOptions(env));
23
+ return result.ok ? [{ template: result.value, source: candidate.source }] : [];
24
+ });
25
+ return { found, loaded };
26
+ };
27
+ const templateRow = (template, source) => {
28
+ const { manifest } = template;
29
+ const tags = manifest.discover?.tags.slice(0, TOP_TAGS) ?? [];
30
+ return {
31
+ id: `${manifest.id}@${manifest.version}`,
32
+ summary: manifest.summary,
33
+ domains: manifest.domains,
34
+ tags,
35
+ aspects: manifest.discover?.aspects ?? [],
36
+ blueprints: manifest.blueprints.map((b) => b.id),
37
+ source,
38
+ };
39
+ };
40
+ const dash = (items) => (items.length === 0 ? "-" : items.join(","));
41
+ function listTemplates(request, env, io) {
42
+ const { found, loaded } = loadAll(env, io);
43
+ const wanted = request.domain?.toLowerCase();
44
+ const rows = loaded
45
+ .filter(({ template }) => wanted === undefined || template.manifest.domains.some((d) => d.toLowerCase() === wanted))
46
+ .map(({ template, source }) => templateRow(template, source));
47
+ const shadowed = found.shadowed.map((s) => `! ${s.id}@${s.version} (${s.source}) shadowed by ${s.reason === "root" ? "higher-precedence root" : "newer version"}: ${s.by}`);
48
+ const problems = found.problems.map((p) => `! ${p.path}: ${p.message}`);
49
+ if (request.json) {
50
+ io.stdout(`${JSON.stringify({ templates: rows, shadowed, problems })}\n`);
51
+ return EXIT.OK;
52
+ }
53
+ const lines = [
54
+ ...rows.map((r) => `${r.id} | ${clip(r.summary, SUMMARY_MAX)} | ${dash(r.tags)} | ${dash(r.aspects)} | ${r.blueprints.join(",")} | ${r.source}`),
55
+ ...shadowed,
56
+ ...problems,
57
+ ];
58
+ if (lines.length === 0) {
59
+ lines.push(wanted === undefined ? "no templates found" : `no template for domain "${request.domain}"`);
60
+ }
61
+ io.stdout(`${capLines(lines).join("\n")}\n`);
62
+ return EXIT.OK;
63
+ }
64
+ const idOf = (entry) => `${entry.template}/${entry.blueprint}`;
65
+ const pickText = (entry) => clip(entry.goodFor === "" ? entry.purpose : entry.goodFor, GOOD_FOR_MAX);
66
+ const criteria = (request) => {
67
+ const { tags = [], aspect, sec } = request.filters;
68
+ return [
69
+ request.query === undefined ? "" : `for "${request.query}"`,
70
+ tags.length === 0 ? "" : `tag ${tags.join(",")}`,
71
+ aspect === undefined ? "" : `aspect ${aspect}`,
72
+ sec === undefined ? "" : `sec ${sec}`,
73
+ ]
74
+ .filter((part) => part !== "")
75
+ .join(" ");
76
+ };
77
+ const rowOf = (entry, hit) => {
78
+ const tail = `${pickText(entry)} | ${lengthLabel(entry.lengths)} | ${dash(entry.aspects)}`;
79
+ if (hit === undefined)
80
+ return `${idOf(entry)} | ${tail}`;
81
+ const mark = hit.notFor ? `${hit.score} NOT-FOR` : String(hit.score);
82
+ return `${idOf(entry)} | ${mark} | ${tail} | why: ${hit.why.join(",")}`;
83
+ };
84
+ function listBlueprints(request, env, io) {
85
+ const { loaded } = loadAll(env, io);
86
+ const filtered = filterEntries(entriesOf(loaded.map((item) => item.template)), request.filters);
87
+ const hits = request.query === undefined ? undefined : rankEntries(filtered, request.query, env.stopwords);
88
+ const shown = hits === undefined
89
+ ? filtered.map((entry) => ({ entry, hit: undefined }))
90
+ : hits.map((hit) => ({ entry: hit.entry, hit }));
91
+ const top = shown.slice(0, BLUEPRINT_ROWS);
92
+ if (request.json) {
93
+ const blueprints = top.map(({ entry, hit }) => ({
94
+ id: idOf(entry),
95
+ goodFor: entry.goodFor,
96
+ aspects: entry.aspects,
97
+ lengths: entry.lengths ?? null,
98
+ ...(hit === undefined ? {} : { score: hit.score, notFor: hit.notFor, why: hit.why }),
99
+ }));
100
+ io.stdout(`${JSON.stringify({ total: shown.length, blueprints })}\n`);
101
+ return EXIT.OK;
102
+ }
103
+ const count = shown.length > top.length ? `top ${top.length} of ${shown.length}` : `${shown.length}`;
104
+ const header = `blueprints ${criteria(request)}: ${count}`;
105
+ const lines = top.length === 0
106
+ ? [header, "no blueprint matches; relax a filter or reword --for"]
107
+ : [header, ...top.map(({ entry, hit }) => rowOf(entry, hit))];
108
+ io.stdout(`${lines.join("\n")}\n`);
109
+ return EXIT.OK;
110
+ }
111
+ /** `mk template list`: templates, or (with `--for/--tag/--aspect/--sec`) ranked or filtered blueprints. */
112
+ export async function list(request, env, io) {
113
+ return wantsBlueprints(request)
114
+ ? listBlueprints(request, env, io)
115
+ : listTemplates(request, env, io);
116
+ }
@@ -0,0 +1,6 @@
1
+ import type { Command } from "../types.ts";
2
+ import { type TemplateEnv } from "./env.ts";
3
+ export declare const TEMPLATE_PROVENANCE = "template.json";
4
+ /** `mk new`; `env` is injectable for tests. */
5
+ export declare const makeNewCommand: (env?: () => TemplateEnv) => Command;
6
+ export declare const newCommand: Command;