@anvpx/motion-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +451 -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 +758 -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 +260 -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 +21 -0
  180. package/dist/voice/run.js +181 -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 +201 -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 +705 -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,186 @@
1
+ import { canonicalJson, forbiddenKeys } from "@anvpx/motion-schema";
2
+ const OPS = ["add", "replace", "remove", "move", "test"];
3
+ const KEYS = {
4
+ add: ["op", "path", "value"],
5
+ replace: ["op", "path", "value"],
6
+ test: ["op", "path", "value"],
7
+ remove: ["op", "path"],
8
+ move: ["op", "path", "from"],
9
+ };
10
+ const INDEX = /^(0|[1-9]\d*)$/;
11
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12
+ const pointerProblem = (field, pointer) => {
13
+ if (typeof pointer !== "string")
14
+ return `${field} must be a string`;
15
+ if (pointer === "")
16
+ return `${field} must not be empty (the document root cannot be patched)`;
17
+ return pointer.startsWith("/") ? undefined : `${field} must start with "/"`;
18
+ };
19
+ const keysProblem = (raw, op) => {
20
+ const unknown = Object.keys(raw).find((key) => !KEYS[op].includes(key));
21
+ return unknown === undefined ? undefined : `unknown key "${unknown}" for ${op}`;
22
+ };
23
+ const pointersProblem = (raw, op) => {
24
+ if (op === "move" && typeof raw.from !== "string")
25
+ return "move needs from";
26
+ const from = op === "move" ? pointerProblem("from", raw.from) : undefined;
27
+ return from ?? pointerProblem("path", raw.path);
28
+ };
29
+ const opProblem = (raw) => {
30
+ if (!isRecord(raw))
31
+ return "must be an object";
32
+ const known = OPS.find((op) => op === raw.op);
33
+ if (known === undefined)
34
+ return `unsupported op ${JSON.stringify(raw.op)}; use ${OPS.join("|")}`;
35
+ const needsValue = KEYS[known].includes("value") && !Object.hasOwn(raw, "value");
36
+ return (keysProblem(raw, known) ??
37
+ pointersProblem(raw, known) ??
38
+ (needsValue ? `${known} needs a value` : undefined));
39
+ };
40
+ /** Checks the shape of an ops document; every message names the op index. */
41
+ export function parseOps(input) {
42
+ if (!Array.isArray(input))
43
+ return { ok: false, message: "ops must be a JSON array of operations" };
44
+ if (input.length === 0)
45
+ return { ok: false, message: "ops needs at least one operation" };
46
+ for (const [index, raw] of input.entries()) {
47
+ const problem = opProblem(raw);
48
+ if (problem !== undefined)
49
+ return { ok: false, message: `op ${index}: ${problem}` };
50
+ }
51
+ return { ok: true, ops: input };
52
+ }
53
+ class PatchError extends Error {
54
+ }
55
+ const fail = (message) => {
56
+ throw new PatchError(message);
57
+ };
58
+ const tokensOf = (pointer) => pointer
59
+ .slice(1)
60
+ .split("/")
61
+ .map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
62
+ const pointerOf = (tokens) => tokens.map((token) => `/${token.replaceAll("~", "~0").replaceAll("/", "~1")}`).join("");
63
+ /** Prototype-polluting and execution-shaped keys are refused in every path segment. */
64
+ const checkSegments = (tokens) => {
65
+ for (const token of tokens) {
66
+ const [problem] = forbiddenKeys({ [token]: 0 }, []);
67
+ if (problem !== undefined)
68
+ fail(`path segment "${token}": ${problem.message}`);
69
+ }
70
+ };
71
+ const isContainer = (value) => typeof value === "object" && value !== null;
72
+ const hasChild = (container, token) => Array.isArray(container)
73
+ ? INDEX.test(token) && Number(token) < container.length
74
+ : Object.hasOwn(container, token);
75
+ const childOf = (container, token) => Array.isArray(container) ? container[Number(token)] : container[token];
76
+ /** The container holding the last token; fails on the first missing prefix. */
77
+ function parentOf(root, tokens) {
78
+ let node = root;
79
+ for (const [depth, token] of tokens.slice(0, -1).entries()) {
80
+ const prefix = pointerOf(tokens.slice(0, depth + 1));
81
+ if (!isContainer(node) || !hasChild(node, token))
82
+ return fail(`parent "${prefix}" does not exist`);
83
+ node = childOf(node, token);
84
+ if (!isContainer(node))
85
+ return fail(`parent "${prefix}" is not an object or array`);
86
+ }
87
+ return isContainer(node) ? node : fail("document root is not an object or array");
88
+ }
89
+ const existing = (parent, tokens, pointer) => {
90
+ const last = tokens.at(-1) ?? "";
91
+ if (!hasChild(parent, last))
92
+ fail(`target "${pointer}" does not exist`);
93
+ return last;
94
+ };
95
+ function insert(parent, token, value) {
96
+ if (!Array.isArray(parent)) {
97
+ parent[token] = value;
98
+ return;
99
+ }
100
+ if (token === "-") {
101
+ parent.push(value);
102
+ return;
103
+ }
104
+ if (!INDEX.test(token))
105
+ fail(`"${token}" is not an array index`);
106
+ if (Number(token) > parent.length)
107
+ fail(`index ${token} is out of range (length ${parent.length})`);
108
+ parent.splice(Number(token), 0, value);
109
+ }
110
+ function drop(parent, token) {
111
+ if (Array.isArray(parent))
112
+ parent.splice(Number(token), 1);
113
+ else
114
+ delete parent[token];
115
+ }
116
+ function overwrite(parent, token, value) {
117
+ if (Array.isArray(parent))
118
+ parent[Number(token)] = value;
119
+ else
120
+ parent[token] = value;
121
+ }
122
+ const sameValue = (a, b) => canonicalJson(a) === canonicalJson(b);
123
+ function valueProblem(value) {
124
+ const [problem] = forbiddenKeys(value, []);
125
+ if (problem !== undefined) {
126
+ fail(`value ${pointerOf(problem.path.map(String)) || "/"}: ${problem.message}`);
127
+ }
128
+ }
129
+ function applyMove(root, op, changed) {
130
+ const from = tokensOf(op.from);
131
+ const tokens = tokensOf(op.path);
132
+ checkSegments(from);
133
+ checkSegments(tokens);
134
+ if (op.path === op.from)
135
+ return;
136
+ if (op.path.startsWith(`${op.from}/`))
137
+ fail("cannot move a value into its own child");
138
+ const source = parentOf(root, from);
139
+ const sourceToken = existing(source, from, op.from);
140
+ const moved = childOf(source, sourceToken);
141
+ drop(source, sourceToken);
142
+ insert(parentOf(root, tokens), tokens.at(-1) ?? "", moved);
143
+ changed.push(op.from, op.path);
144
+ }
145
+ function applyAt(root, op, changed) {
146
+ const tokens = tokensOf(op.path);
147
+ checkSegments(tokens);
148
+ const parent = parentOf(root, tokens);
149
+ if (op.op === "remove") {
150
+ drop(parent, existing(parent, tokens, op.path));
151
+ changed.push(op.path);
152
+ return;
153
+ }
154
+ valueProblem(op.value);
155
+ if (op.op === "add") {
156
+ insert(parent, tokens.at(-1) ?? "", op.value);
157
+ changed.push(op.path);
158
+ }
159
+ else if (op.op === "replace") {
160
+ overwrite(parent, existing(parent, tokens, op.path), op.value);
161
+ changed.push(op.path);
162
+ }
163
+ else if (!sameValue(childOf(parent, existing(parent, tokens, op.path)), op.value)) {
164
+ fail(`test failed at "${op.path}": value differs`);
165
+ }
166
+ }
167
+ const applyOne = (root, op, changed) => op.op === "move" ? applyMove(root, op, changed) : applyAt(root, op, changed);
168
+ /**
169
+ * Applies ops in order to a copy of `doc`; the input is never mutated. Stops at the first op that
170
+ * cannot apply and names its index. Pure: validation of the result is the caller's job.
171
+ */
172
+ export function applyPatch(doc, ops) {
173
+ const root = structuredClone(doc);
174
+ const changed = [];
175
+ for (const [index, op] of ops.entries()) {
176
+ try {
177
+ applyOne(root, op, changed);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof PatchError)
181
+ return { ok: false, index, message: error.message };
182
+ throw error;
183
+ }
184
+ }
185
+ return { ok: true, value: root, changed };
186
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../types.ts";
2
+ export declare const patchCommand: Command;
@@ -0,0 +1,92 @@
1
+ import { listFlag, parseFlags, requireStoryboard, textFlag, usageError } from "../args.js";
2
+ import { EXIT } from "../exit-codes.js";
3
+ import { editStoryboard, statusLine } from "../storyboard/edit.js";
4
+ import { readJsonFile } from "../storyboard/load.js";
5
+ import { formatFinding } from "../storyboard/report.js";
6
+ import { applyPatch, parseOps } from "./apply.js";
7
+ const USAGE = `mk patch <storyboard.json> [--ops file.json] [--op <json> ...]
8
+
9
+ Apply an RFC 6902 JSON Patch (subset: add, replace, remove, move, test) to a storyboard, validate the result
10
+ with the full validator and write it atomically. An invalid result is never written; the pointer errors are printed.
11
+ --ops f JSON array of operations: [{"op":"replace","path":"/scenes/0/props/title","value":"New"}]
12
+ --op j one operation as JSON (repeatable; applied after --ops)
13
+ Paths follow RFC 6902: "-" appends to an array, ~0 and ~1 escape ~ and /. A path into a missing parent is an error.
14
+ __proto__, constructor, prototype and command-shaped keys are refused. Any edit changes the approval hash.
15
+ Prints <= 3 lines: changed paths; scene count, total seconds, errors and warnings.
16
+ `;
17
+ const FLAGS = { ops: { type: "string" }, op: { type: "string", multiple: true } };
18
+ const MAX_PATHS = 4;
19
+ async function gatherOps(opsFile, inline, io) {
20
+ const raw = [];
21
+ if (opsFile !== undefined) {
22
+ const loaded = await readJsonFile(opsFile, io.host);
23
+ if (!loaded.ok) {
24
+ io.stderr(`mk patch: ${loaded.message}\n`);
25
+ return EXIT.USAGE;
26
+ }
27
+ if (!Array.isArray(loaded.value)) {
28
+ io.stderr(`mk patch: ${opsFile} must hold a JSON array of operations\n`);
29
+ return EXIT.USAGE;
30
+ }
31
+ raw.push(...loaded.value);
32
+ }
33
+ for (const text of inline) {
34
+ try {
35
+ raw.push(JSON.parse(text));
36
+ }
37
+ catch {
38
+ io.stderr(`mk patch: --op: invalid JSON ${text.slice(0, 40)}\n`);
39
+ return EXIT.USAGE;
40
+ }
41
+ }
42
+ const parsed = parseOps(raw);
43
+ if (parsed.ok)
44
+ return parsed.ops;
45
+ io.stderr(`mk patch: ${parsed.message}\n`);
46
+ return EXIT.USAGE;
47
+ }
48
+ const changedLine = (ops, changed) => {
49
+ const unique = [...new Set(changed)];
50
+ const shown = unique.slice(0, MAX_PATHS).join(", ");
51
+ const more = unique.length > MAX_PATHS ? ` (+${unique.length - MAX_PATHS} more)` : "";
52
+ const label = `${ops} op${ops === 1 ? "" : "s"}`;
53
+ return `patch ok | ${label} | changed ${shown || "nothing"}${more}`;
54
+ };
55
+ export const patchCommand = {
56
+ name: "patch",
57
+ summary: "apply a JSON Patch to a storyboard; validated, atomic, never writes invalid",
58
+ async run(args, io) {
59
+ const parsed = parseFlags("patch", USAGE, args, FLAGS, io);
60
+ if (parsed.kind === "exit")
61
+ return parsed.code;
62
+ const path = requireStoryboard("patch", USAGE, parsed.positionals, io);
63
+ if (typeof path !== "string")
64
+ return path.code;
65
+ const inline = listFlag(parsed.flags, "op");
66
+ const opsFile = textFlag(parsed.flags, "ops");
67
+ if (opsFile === undefined && inline.length === 0) {
68
+ return usageError("patch", USAGE, "give --ops <file> or --op <json>", io).code;
69
+ }
70
+ const ops = await gatherOps(opsFile, inline, io);
71
+ if (typeof ops === "number")
72
+ return ops;
73
+ return editStoryboard("patch", path, io, (doc) => {
74
+ const result = applyPatch(doc, ops);
75
+ if (!result.ok) {
76
+ const failed = ops[result.index];
77
+ const where = failed === undefined ? "" : ` (${failed.op} ${failed.path})`;
78
+ return { ok: false, message: `op ${result.index}${where}: ${result.message}` };
79
+ }
80
+ const scenes = result.value.scenes?.length ?? 0;
81
+ return {
82
+ ok: true,
83
+ value: result.value,
84
+ lines: (report) => [
85
+ changedLine(ops.length, result.changed),
86
+ statusLine(report, scenes),
87
+ ...report.warnings.slice(0, 1).map(formatFinding),
88
+ ],
89
+ };
90
+ });
91
+ },
92
+ };
@@ -0,0 +1,40 @@
1
+ import type { Command } from "../types.ts";
2
+ /** Pinned publishable core; every command wrapper invokes exactly this, never an unpinned npx.
3
+ *
4
+ * Version strategy (MK-080): the pin tracks `@anvpx/motion` manifest
5
+ * `version` exactly (enforced by `plugin.test.ts` "matches @anvpx/motion
6
+ * version", which also pins `.claude-plugin/plugin.json`). `0.0.0` is the
7
+ * pre-publish placeholder: an isolated `npm install` of the packed tarball
8
+ * rewrites internal `workspace:*` to `0.0.0`, which is private/unpublished,
9
+ * so `npx -y @anvpx/motion@0.0.0` 404s in isolation (qualify-pack exits 2
10
+ * BLOCKED with `unpublishedInternals`). Do NOT unpin or float the range to
11
+ * work around it. Publish path is choiceA (publish internal @anvpx/motion-*
12
+ * at real versions) or choiceB (bundle into one tarball); either lands with
13
+ * a version bump of core manifest + this pin + plugin.json together.
14
+ */
15
+ export declare const PINNED_CORE_VERSION = "0.1.0";
16
+ export declare const PINNED_NPX = "npx -y @anvpx/motion@0.1.0";
17
+ /** Skill file whose sha256 must survive `plugin install` byte for byte. */
18
+ export declare const PLUGIN_SKILL: string;
19
+ export interface PluginEnv {
20
+ sourceDir: string;
21
+ }
22
+ export declare const defaultPluginEnv: () => PluginEnv;
23
+ /** Resolves a package id (e.g. `@anvpx/motion/package.json`) to a file path. */
24
+ export type CoreResolver = (id: string) => string;
25
+ /** Path existence probe; injectable so tests can model an install layout. */
26
+ export type PathExists = (path: string) => boolean;
27
+ /** The repo checkout copy (`<repo>/plugins/motion-kit`), used for development runs. */
28
+ export declare const repoPluginDir: () => string;
29
+ /**
30
+ * Resolve the bundled plugin dir, in order: (a) `@anvpx/motion/package.json`
31
+ * -> `<core>/plugins/motion-kit` (the installed layout), (b) the repo checkout
32
+ * `<repo>/plugins/motion-kit` for development. Never returns a path that does
33
+ * not exist: when neither candidate is present it throws naming
34
+ * `@anvpx/motion` and every attempted path. An injected resolver models a
35
+ * layout without touching the filesystem; pass `exists` to force the probe.
36
+ */
37
+ export declare const resolvePluginSourceDir: (resolveCorePkg?: CoreResolver, exists?: PathExists) => string;
38
+ /** `mk plugin install`; `env` is injectable so tests use a fake source tree. */
39
+ export declare const makePluginCommand: (env?: () => PluginEnv) => Command;
40
+ export declare const pluginCommand: Command;
@@ -0,0 +1,170 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseFlags, textFlag, usageError } from "../args.js";
6
+ import { EXIT } from "../exit-codes.js";
7
+ /** Pinned publishable core; every command wrapper invokes exactly this, never an unpinned npx.
8
+ *
9
+ * Version strategy (MK-080): the pin tracks `@anvpx/motion` manifest
10
+ * `version` exactly (enforced by `plugin.test.ts` "matches @anvpx/motion
11
+ * version", which also pins `.claude-plugin/plugin.json`). `0.0.0` is the
12
+ * pre-publish placeholder: an isolated `npm install` of the packed tarball
13
+ * rewrites internal `workspace:*` to `0.0.0`, which is private/unpublished,
14
+ * so `npx -y @anvpx/motion@0.0.0` 404s in isolation (qualify-pack exits 2
15
+ * BLOCKED with `unpublishedInternals`). Do NOT unpin or float the range to
16
+ * work around it. Publish path is choiceA (publish internal @anvpx/motion-*
17
+ * at real versions) or choiceB (bundle into one tarball); either lands with
18
+ * a version bump of core manifest + this pin + plugin.json together.
19
+ */
20
+ export const PINNED_CORE_VERSION = "0.1.0";
21
+ export const PINNED_NPX = `npx -y @anvpx/motion@${PINNED_CORE_VERSION}`;
22
+ /** Skill file whose sha256 must survive `plugin install` byte for byte. */
23
+ export const PLUGIN_SKILL = join("skills", "motion-kit", "SKILL.md");
24
+ const USAGE = `mk plugin install [--out <dir>]
25
+
26
+ Copies the bundled Claude Code plugin (plugins/motion-kit) to <dir> (default motion-kit-plugin).
27
+ Verifies skills/motion-kit/SKILL.md copied byte for byte (sha256 match).
28
+ `;
29
+ const FLAGS = { out: { type: "string" } };
30
+ export const defaultPluginEnv = () => ({
31
+ sourceDir: resolvePluginSourceDir(),
32
+ });
33
+ const defaultResolve = (id) => createRequire(import.meta.url).resolve(id);
34
+ /** `plugins/motion-kit` inside a resolved `@anvpx/motion` package manifest path. */
35
+ const pluginDirIn = (corePkgPath) => join(dirname(corePkgPath), "plugins", "motion-kit");
36
+ /** The repo checkout copy (`<repo>/plugins/motion-kit`), used for development runs. */
37
+ export const repoPluginDir = () => fileURLToPath(new URL("../../../../plugins/motion-kit", import.meta.url));
38
+ const MISSING_SOURCE = "cannot find the bundled plugin; install @anvpx/motion (it ships plugins/motion-kit)";
39
+ /**
40
+ * Resolve the bundled plugin dir, in order: (a) `@anvpx/motion/package.json`
41
+ * -> `<core>/plugins/motion-kit` (the installed layout), (b) the repo checkout
42
+ * `<repo>/plugins/motion-kit` for development. Never returns a path that does
43
+ * not exist: when neither candidate is present it throws naming
44
+ * `@anvpx/motion` and every attempted path. An injected resolver models a
45
+ * layout without touching the filesystem; pass `exists` to force the probe.
46
+ */
47
+ export const resolvePluginSourceDir = (resolveCorePkg, exists) => {
48
+ const probe = exists ?? (resolveCorePkg === undefined ? existsSync : () => true);
49
+ const searched = [];
50
+ try {
51
+ const resolve = resolveCorePkg ?? defaultResolve;
52
+ const installed = pluginDirIn(resolve("@anvpx/motion/package.json"));
53
+ searched.push(installed);
54
+ if (probe(installed))
55
+ return installed;
56
+ }
57
+ catch {
58
+ // Not installed, or an older exports map hides ./package.json.
59
+ }
60
+ const repo = repoPluginDir();
61
+ searched.push(repo);
62
+ if (probe(repo))
63
+ return repo;
64
+ throw new Error(`${MISSING_SOURCE}; searched: ${searched.join(", ")}`);
65
+ };
66
+ function assertSafeEntry(entry) {
67
+ if (entry === "" || entry.includes("/") || entry.includes("\\") || entry.includes("..")) {
68
+ throw new Error(`unsafe plugin entry: ${entry}`);
69
+ }
70
+ }
71
+ async function copyTree(host, from, to) {
72
+ const entries = await host.listDir(from);
73
+ let count = 0;
74
+ for (const entry of entries) {
75
+ assertSafeEntry(entry);
76
+ const src = join(from, entry);
77
+ const dest = join(to, entry);
78
+ count += (await isDir(host, src))
79
+ ? await copyTree(host, src, dest)
80
+ : await copyOne(host, src, dest);
81
+ }
82
+ return count;
83
+ }
84
+ async function isDir(host, path) {
85
+ try {
86
+ await host.listDir(path);
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ async function copyOne(host, from, to) {
94
+ await host.copyFile(from, to);
95
+ return 1;
96
+ }
97
+ async function readSource(host, sourceDir) {
98
+ try {
99
+ await host.listDir(sourceDir);
100
+ return true;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ async function skillMatches(host, sourceDir, dest) {
107
+ const before = await host.sha256File(join(sourceDir, PLUGIN_SKILL));
108
+ const after = await host.sha256File(join(dest, PLUGIN_SKILL));
109
+ return before !== undefined && before === after;
110
+ }
111
+ function parseInstall(args, io) {
112
+ const bad = (message) => usageError("plugin", USAGE, message, io).code;
113
+ const parsed = parseFlags("plugin", USAGE, args, FLAGS, io);
114
+ if (parsed.kind === "exit")
115
+ return parsed.code;
116
+ const [sub, destPos, ...extra] = parsed.positionals;
117
+ if (sub !== "install")
118
+ return bad("use one of: install");
119
+ if (extra.length > 0)
120
+ return bad(`unexpected argument "${extra[0]}"`);
121
+ const outFlag = textFlag(parsed.flags, "out");
122
+ if (outFlag !== undefined && destPos !== undefined)
123
+ return bad("use --out or <dir>, not both");
124
+ const dest = outFlag ?? destPos ?? "motion-kit-plugin";
125
+ if (dest === "")
126
+ return bad("--out needs a directory");
127
+ return { dest };
128
+ }
129
+ /** `mk plugin install`; `env` is injectable so tests use a fake source tree. */
130
+ export const makePluginCommand = (env = defaultPluginEnv) => ({
131
+ name: "plugin",
132
+ summary: "install the Claude Code plugin (copies plugins/motion-kit)",
133
+ async run(args, io) {
134
+ const request = parseInstall(args, io);
135
+ if (typeof request === "number")
136
+ return request;
137
+ let sourceDir;
138
+ try {
139
+ sourceDir = env().sourceDir;
140
+ }
141
+ catch (error) {
142
+ const detail = error instanceof Error ? error.message : String(error);
143
+ io.stderr(`mk plugin: ${detail}\n`);
144
+ return EXIT.ERROR;
145
+ }
146
+ return installSource(io, sourceDir, request.dest);
147
+ },
148
+ });
149
+ async function installSource(io, sourceDir, dest) {
150
+ if (!(await readSource(io.host, sourceDir))) {
151
+ io.stderr(`mk plugin: cannot read plugin source at ${sourceDir}\n`);
152
+ return EXIT.ERROR;
153
+ }
154
+ let count;
155
+ try {
156
+ count = await copyTree(io.host, sourceDir, dest);
157
+ }
158
+ catch (error) {
159
+ const detail = error instanceof Error ? error.message : error;
160
+ io.stderr(`mk plugin: refusing unsafe entry (${detail})\n`);
161
+ return EXIT.ERROR;
162
+ }
163
+ if (!(await skillMatches(io.host, sourceDir, dest))) {
164
+ io.stderr("mk plugin: SKILL.md hash mismatch after copy\n");
165
+ return EXIT.ERROR;
166
+ }
167
+ io.stdout(`plugin ${count} files -> ${dest}\n`);
168
+ return EXIT.OK;
169
+ }
170
+ export const pluginCommand = makePluginCommand();
@@ -0,0 +1,10 @@
1
+ import type { Storyboard } from "@anvpx/motion-schema";
2
+ import { type TemplateEnv } from "../template/env.ts";
3
+ import type { Command, Io } from "../types.ts";
4
+ /**
5
+ * Copies authored scene directories beside a derived storyboard, so a relative
6
+ * `props.dir` still resolves after the storyboard is rewritten into a scratch dir.
7
+ */
8
+ export declare function copyCustomScenes(host: Io["host"], board: Storyboard, origin: string, targetDir: string): Promise<void>;
9
+ export declare const makePreviewCommand: (env?: () => TemplateEnv, clock?: () => number) => Command;
10
+ export declare const previewCommand: Command;