@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.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/analyze/command.d.ts +2 -0
- package/dist/analyze/command.js +190 -0
- package/dist/approval/approval.d.ts +32 -0
- package/dist/approval/approval.js +141 -0
- package/dist/approval/command.d.ts +2 -0
- package/dist/approval/command.js +255 -0
- package/dist/approval/gate.d.ts +7 -0
- package/dist/approval/gate.js +23 -0
- package/dist/approval/hash.d.ts +71 -0
- package/dist/approval/hash.js +464 -0
- package/dist/args.d.ts +30 -0
- package/dist/args.js +53 -0
- package/dist/assets/command.d.ts +2 -0
- package/dist/assets/command.js +165 -0
- package/dist/captions/command.d.ts +2 -0
- package/dist/captions/command.js +341 -0
- package/dist/captions/cues.d.ts +10 -0
- package/dist/captions/cues.js +61 -0
- package/dist/catalog/command.d.ts +2 -0
- package/dist/catalog/command.js +60 -0
- package/dist/catalog/format.d.ts +13 -0
- package/dist/catalog/format.js +54 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +35 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +59 -0
- package/dist/compare/command.d.ts +4 -0
- package/dist/compare/command.js +144 -0
- package/dist/contact/argv.d.ts +24 -0
- package/dist/contact/argv.js +42 -0
- package/dist/contact/command.d.ts +2 -0
- package/dist/contact/command.js +79 -0
- package/dist/doctor/checks.d.ts +25 -0
- package/dist/doctor/checks.js +571 -0
- package/dist/doctor/command.d.ts +4 -0
- package/dist/doctor/command.js +88 -0
- package/dist/doctor/format.d.ts +7 -0
- package/dist/doctor/format.js +41 -0
- package/dist/doctor/heal.d.ts +14 -0
- package/dist/doctor/heal.js +71 -0
- package/dist/doctor/pins.d.ts +8 -0
- package/dist/doctor/pins.js +28 -0
- package/dist/doctor/skills.d.ts +15 -0
- package/dist/doctor/skills.js +58 -0
- package/dist/doctor/types.d.ts +36 -0
- package/dist/doctor/types.js +1 -0
- package/dist/doctor/vendored.d.ts +15 -0
- package/dist/doctor/vendored.js +90 -0
- package/dist/doctor/version.d.ts +8 -0
- package/dist/doctor/version.js +11 -0
- package/dist/exit-codes.d.ts +16 -0
- package/dist/exit-codes.js +15 -0
- package/dist/fit/command.d.ts +5 -0
- package/dist/fit/command.js +154 -0
- package/dist/fit/solve.d.ts +21 -0
- package/dist/fit/solve.js +80 -0
- package/dist/fit/target.d.ts +18 -0
- package/dist/fit/target.js +47 -0
- package/dist/generate3d/command.d.ts +3 -0
- package/dist/generate3d/command.js +38 -0
- package/dist/generate3d/run.d.ts +39 -0
- package/dist/generate3d/run.js +77 -0
- package/dist/icon/command.d.ts +2 -0
- package/dist/icon/command.js +113 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/install-mk.d.ts +31 -0
- package/dist/install-mk.js +168 -0
- package/dist/integration/preview.integration.d.ts +1 -0
- package/dist/integration/preview.integration.js +53 -0
- package/dist/integration/smoke.integration.d.ts +1 -0
- package/dist/integration/smoke.integration.js +91 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +4 -0
- package/dist/node-io.d.ts +14 -0
- package/dist/node-io.js +152 -0
- package/dist/optimize/command.d.ts +12 -0
- package/dist/optimize/command.js +110 -0
- package/dist/packs/command.d.ts +2 -0
- package/dist/packs/command.js +109 -0
- package/dist/patch/apply.d.ts +36 -0
- package/dist/patch/apply.js +186 -0
- package/dist/patch/command.d.ts +2 -0
- package/dist/patch/command.js +92 -0
- package/dist/plugin/install.d.ts +40 -0
- package/dist/plugin/install.js +170 -0
- package/dist/preview/command.d.ts +10 -0
- package/dist/preview/command.js +764 -0
- package/dist/preview/manifest.d.ts +94 -0
- package/dist/preview/manifest.js +19 -0
- package/dist/preview/pack.d.ts +24 -0
- package/dist/preview/pack.js +53 -0
- package/dist/preview/plan.d.ts +58 -0
- package/dist/preview/plan.js +160 -0
- package/dist/preview/provenance.d.ts +23 -0
- package/dist/preview/provenance.js +38 -0
- package/dist/publish/command.d.ts +2 -0
- package/dist/publish/command.js +70 -0
- package/dist/publish/player.d.ts +25 -0
- package/dist/publish/player.js +108 -0
- package/dist/publish/static.d.ts +16 -0
- package/dist/publish/static.js +78 -0
- package/dist/qc.d.ts +2 -0
- package/dist/qc.js +192 -0
- package/dist/scene/author.d.ts +16 -0
- package/dist/scene/author.js +279 -0
- package/dist/scene/command.d.ts +2 -0
- package/dist/scene/command.js +110 -0
- package/dist/scene/ops.d.ts +23 -0
- package/dist/scene/ops.js +151 -0
- package/dist/state/command.d.ts +2 -0
- package/dist/state/command.js +128 -0
- package/dist/state/evidence.d.ts +71 -0
- package/dist/state/evidence.js +243 -0
- package/dist/storyboard/check.d.ts +21 -0
- package/dist/storyboard/check.js +39 -0
- package/dist/storyboard/containment.d.ts +24 -0
- package/dist/storyboard/containment.js +43 -0
- package/dist/storyboard/custom.d.ts +15 -0
- package/dist/storyboard/custom.js +133 -0
- package/dist/storyboard/edit.d.ts +19 -0
- package/dist/storyboard/edit.js +39 -0
- package/dist/storyboard/load.d.ts +11 -0
- package/dist/storyboard/load.js +52 -0
- package/dist/storyboard/report.d.ts +5 -0
- package/dist/storyboard/report.js +8 -0
- package/dist/template/coerce.d.ts +6 -0
- package/dist/template/coerce.js +44 -0
- package/dist/template/command.d.ts +5 -0
- package/dist/template/command.js +163 -0
- package/dist/template/env.d.ts +34 -0
- package/dist/template/env.js +38 -0
- package/dist/template/fork.d.ts +8 -0
- package/dist/template/fork.js +156 -0
- package/dist/template/format.d.ts +21 -0
- package/dist/template/format.js +161 -0
- package/dist/template/list.d.ts +15 -0
- package/dist/template/list.js +116 -0
- package/dist/template/new.d.ts +6 -0
- package/dist/template/new.js +302 -0
- package/dist/testing/env.d.ts +8 -0
- package/dist/testing/env.js +19 -0
- package/dist/testing/fakes.d.ts +33 -0
- package/dist/testing/fakes.js +95 -0
- package/dist/testing/fixtures.d.ts +8 -0
- package/dist/testing/fixtures.js +6 -0
- package/dist/testing/probe.d.ts +15 -0
- package/dist/testing/probe.js +20 -0
- package/dist/testing/project.d.ts +22 -0
- package/dist/testing/project.js +26 -0
- package/dist/tooling/session.d.ts +40 -0
- package/dist/tooling/session.js +106 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/verify/audio-loudness.d.ts +46 -0
- package/dist/verify/audio-loudness.js +186 -0
- package/dist/verify/command.d.ts +2 -0
- package/dist/verify/command.js +161 -0
- package/dist/verify/determinism.d.ts +30 -0
- package/dist/verify/determinism.js +59 -0
- package/dist/verify/expectation.d.ts +15 -0
- package/dist/verify/expectation.js +19 -0
- package/dist/verify/media-events.d.ts +61 -0
- package/dist/verify/media-events.js +174 -0
- package/dist/verify/platform.d.ts +33 -0
- package/dist/verify/platform.js +59 -0
- package/dist/verify/probe.d.ts +14 -0
- package/dist/verify/probe.js +23 -0
- package/dist/verify/report.d.ts +15 -0
- package/dist/verify/report.js +45 -0
- package/dist/verify/run.d.ts +27 -0
- package/dist/verify/run.js +53 -0
- package/dist/voice/command.d.ts +3 -0
- package/dist/voice/command.js +49 -0
- package/dist/voice/manifest.d.ts +33 -0
- package/dist/voice/manifest.js +69 -0
- package/dist/voice/run.d.ts +27 -0
- package/dist/voice/run.js +190 -0
- package/dist/workflow/build.d.ts +2 -0
- package/dist/workflow/build.js +25 -0
- package/dist/workflow/draft.d.ts +9 -0
- package/dist/workflow/draft.js +215 -0
- package/dist/workflow/finish.d.ts +23 -0
- package/dist/workflow/finish.js +28 -0
- package/dist/workflow/project.d.ts +73 -0
- package/dist/workflow/project.js +198 -0
- package/dist/workflow/provenance.d.ts +18 -0
- package/dist/workflow/provenance.js +36 -0
- package/dist/workflow/render.d.ts +31 -0
- package/dist/workflow/render.js +721 -0
- package/dist/workflow/still.d.ts +5 -0
- package/dist/workflow/still.js +62 -0
- package/dist/workflow/validate.d.ts +2 -0
- package/dist/workflow/validate.js +49 -0
- package/package.json +38 -3
|
@@ -0,0 +1,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,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.1";
|
|
16
|
+
export declare const PINNED_NPX = "npx -y @anvpx/motion@0.1.1";
|
|
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.1";
|
|
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;
|