@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,110 @@
1
+ import { listFlag, parseFlags, textFlag, usageError } from "../args.js";
2
+ import { editStoryboard } from "../storyboard/edit.js";
3
+ import { sceneLint, sceneNew, scenePromote } from "./author.js";
4
+ import { addScene, moveScene, removeScene } from "./ops.js";
5
+ const USAGE = `mk scene new <id> [--dir <path>] [--force]
6
+ mk scene lint <dir>
7
+ mk scene promote <dir> --as <type>
8
+ mk scene add <storyboard.json> <type> [--id x] [--after id] [--set k=v ...] [--duration s] [--over id] [--offset s]
9
+ mk scene remove <storyboard.json> <id>
10
+ mk scene move <storyboard.json> <id> (--after <id> | --first)
11
+
12
+ Edit the scene list of a storyboard; the result is validated and written atomically (never written invalid).
13
+ add insert a scene of <type> built from its minimal catalog example (see \`mk catalog <type>\`)
14
+ --id x scene id (default <type>-<n>) --after id insert after that scene (default: at the end)
15
+ --set k=v top-level prop; v is JSON when it parses (3, true, ["a","b"]), else text; repeatable
16
+ --duration s seconds (default: auto from the text)
17
+ --over id overlay types only: the base scene it covers (required), --offset s seconds into it (default 0)
18
+ remove delete a scene (an overlay still pointing at it makes the result invalid)
19
+ move reorder: land after another scene, or first
20
+ Nested prop edits: \`mk patch\`. Prints one line.
21
+ `;
22
+ const FLAGS = {
23
+ id: { type: "string" },
24
+ after: { type: "string" },
25
+ first: { type: "boolean" },
26
+ set: { type: "string", multiple: true },
27
+ duration: { type: "string" },
28
+ over: { type: "string" },
29
+ offset: { type: "string" },
30
+ };
31
+ const SUBCOMMANDS = ["add", "remove", "move"];
32
+ const isSub = (value) => SUBCOMMANDS.some((sub) => sub === value);
33
+ /** Flags each subcommand accepts; anything else is a usage error rather than silently ignored. */
34
+ const ALLOWED = {
35
+ add: ["id", "after", "set", "duration", "over", "offset"],
36
+ remove: [],
37
+ move: ["after", "first"],
38
+ };
39
+ const seconds = (flags, name) => {
40
+ const text = textFlag(flags, name);
41
+ if (text === undefined)
42
+ return undefined;
43
+ const value = Number(text);
44
+ return Number.isFinite(value) && value >= 0 ? value : "bad";
45
+ };
46
+ /** Subcommand, storyboard path, target and flags checked together; an exit code on bad usage. */
47
+ function parseTarget(args, io) {
48
+ const parsed = parseFlags("scene", USAGE, args, FLAGS, io);
49
+ if (parsed.kind === "exit")
50
+ return parsed.code;
51
+ const [sub, path, target, ...extra] = parsed.positionals;
52
+ const usage = (message) => usageError("scene", USAGE, message, io).code;
53
+ if (!isSub(sub))
54
+ return usage(`use one of: ${SUBCOMMANDS.join("|")}`);
55
+ const stray = Object.keys(parsed.flags).find((flag) => !ALLOWED[sub].includes(flag));
56
+ if (stray !== undefined)
57
+ return usage(`--${stray} does not apply to ${sub}`);
58
+ if (path === undefined)
59
+ return usage("missing <storyboard.json>");
60
+ if (target === undefined)
61
+ return usage(sub === "add" ? "missing <type>" : "missing <id>");
62
+ if (extra.length > 0)
63
+ return usage(`unexpected argument "${extra[0]}"`);
64
+ return { sub, path, target, flags: parsed.flags };
65
+ }
66
+ function runMove(request, io) {
67
+ const after = textFlag(request.flags, "after");
68
+ if ((after === undefined) === (request.flags.first !== true)) {
69
+ return usageError("scene", USAGE, "move needs exactly one of --after <id> or --first", io).code;
70
+ }
71
+ return editStoryboard("scene", request.path, io, (doc) => moveScene(doc, { id: request.target, after }));
72
+ }
73
+ function runAdd(request, io) {
74
+ const { flags } = request;
75
+ const duration = seconds(flags, "duration");
76
+ const offset = seconds(flags, "offset");
77
+ if (duration === "bad" || offset === "bad" || duration === 0) {
78
+ return usageError("scene", USAGE, "--duration and --offset need positive seconds", io).code;
79
+ }
80
+ return editStoryboard("scene", request.path, io, (doc) => addScene(doc, {
81
+ type: request.target,
82
+ id: textFlag(flags, "id"),
83
+ after: textFlag(flags, "after"),
84
+ sets: listFlag(flags, "set"),
85
+ duration,
86
+ over: textFlag(flags, "over"),
87
+ offset,
88
+ }));
89
+ }
90
+ export const sceneCommand = {
91
+ name: "scene",
92
+ summary: "add, remove, move, scaffold or lint scenes",
93
+ async run(args, io) {
94
+ const head = args[0];
95
+ if (head === "new")
96
+ return sceneNew(args.slice(1), io);
97
+ if (head === "lint")
98
+ return sceneLint(args.slice(1), io);
99
+ if (head === "promote")
100
+ return scenePromote(args.slice(1), io);
101
+ const request = parseTarget(args, io);
102
+ if (typeof request === "number")
103
+ return request;
104
+ if (request.sub === "move")
105
+ return runMove(request, io);
106
+ if (request.sub === "add")
107
+ return runAdd(request, io);
108
+ return editStoryboard("scene", request.path, io, (doc) => removeScene(doc, request.target));
109
+ },
110
+ };
@@ -0,0 +1,23 @@
1
+ import { type Edit } from "../storyboard/edit.ts";
2
+ export interface AddRequest {
3
+ type: string;
4
+ id?: string | undefined;
5
+ after?: string | undefined;
6
+ /** `k=v` pairs; v is JSON when it parses (numbers, true, [..]), else a string. */
7
+ sets: readonly string[];
8
+ duration?: number | undefined;
9
+ /** Overlay scenes only. */
10
+ over?: string | undefined;
11
+ offset?: number | undefined;
12
+ }
13
+ /** Inserts a scene built from the catalog's minimal example for `type`. */
14
+ export declare function addScene(doc: unknown, request: AddRequest): Edit;
15
+ /** Removes a scene by id; an overlay still pointing at it makes the validator refuse the result. */
16
+ export declare function removeScene(doc: unknown, id: string): Edit;
17
+ export interface MoveRequest {
18
+ id: string;
19
+ /** Scene id to land after; `undefined` moves to the start. */
20
+ after: string | undefined;
21
+ }
22
+ /** Moves a scene to follow another one (or to the start). */
23
+ export declare function moveScene(doc: unknown, request: MoveRequest): Edit;
@@ -0,0 +1,151 @@
1
+ import { sceneDocs } from "@anvpx/motion-build";
2
+ import { forbiddenKeys } from "@anvpx/motion-schema";
3
+ import { round1 } from "../storyboard/edit.js";
4
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
+ const sceneList = (doc) => {
6
+ const scenes = isRecord(doc) ? doc.scenes : undefined;
7
+ return Array.isArray(scenes) ? scenes : undefined;
8
+ };
9
+ const indexOfId = (scenes, id) => scenes.findIndex((scene) => scene.id === id);
10
+ const knownIds = (scenes) => scenes.map((s) => String(s.id)).join(", ");
11
+ const NO_SCENES = { ok: false, message: "the storyboard has no scenes array" };
12
+ const OVERLAY_SECONDS = 3;
13
+ const jsonOrText = (text) => {
14
+ try {
15
+ return JSON.parse(text);
16
+ }
17
+ catch {
18
+ return text;
19
+ }
20
+ };
21
+ /** `k=v` pairs onto the props; keys are top-level prop names (use `mk patch` for nested edits). */
22
+ function applySets(props, sets) {
23
+ for (const pair of sets) {
24
+ const cut = pair.indexOf("=");
25
+ const key = cut > 0 ? pair.slice(0, cut) : "";
26
+ if (key === "")
27
+ return `--set "${pair}" needs k=v`;
28
+ const [problem] = forbiddenKeys({ [key]: 0 }, []);
29
+ if (problem !== undefined)
30
+ return `--set ${key}: ${problem.message}`;
31
+ props[key] = jsonOrText(pair.slice(cut + 1));
32
+ }
33
+ return undefined;
34
+ }
35
+ const freeId = (type, scenes) => {
36
+ const taken = new Set(scenes.map((scene) => scene.id));
37
+ for (let n = 1;; n++)
38
+ if (!taken.has(`${type}-${n}`))
39
+ return `${type}-${n}`;
40
+ };
41
+ /** The entry's timing fields: `duration`, and `layer`/`over`/`offset` for overlays. */
42
+ function timingOf(overlay, request) {
43
+ const duration = request.duration ?? (overlay ? OVERLAY_SECONDS : undefined);
44
+ return {
45
+ ...(duration === undefined ? {} : { duration }),
46
+ ...(overlay ? { layer: "overlay", over: request.over, offset: request.offset ?? 0 } : {}),
47
+ };
48
+ }
49
+ /** The scene entry for `type`, or the reason it cannot be built. */
50
+ function buildEntry(scenes, request) {
51
+ const docs = sceneDocs();
52
+ const found = docs.find((entry) => entry.type === request.type);
53
+ if (found === undefined) {
54
+ const known = docs.map((d) => d.type).join("|");
55
+ return { ok: false, message: `unknown scene type "${request.type}"; use one of: ${known}` };
56
+ }
57
+ const id = request.id ?? freeId(request.type, scenes);
58
+ if (indexOfId(scenes, id) !== -1)
59
+ return { ok: false, message: `scene id "${id}" already exists` };
60
+ const props = structuredClone(found.example);
61
+ const problem = applySets(props, request.sets);
62
+ if (problem !== undefined)
63
+ return { ok: false, message: problem };
64
+ const overlay = found.layer === "overlay";
65
+ if (overlay && (request.over === undefined || indexOfId(scenes, request.over) === -1)) {
66
+ const known = knownIds(scenes);
67
+ return {
68
+ ok: false,
69
+ message: `${request.type} is an overlay: give --over <base scene id> (one of: ${known})`,
70
+ };
71
+ }
72
+ return { ok: true, entry: { id, type: request.type, props, ...timingOf(overlay, request) } };
73
+ }
74
+ /** Index to insert at: after `--after`, else the end; undefined when that scene does not exist. */
75
+ function insertionIndex(scenes, after) {
76
+ if (after === undefined)
77
+ return scenes.length;
78
+ const at = indexOfId(scenes, after);
79
+ return at === -1 ? undefined : at + 1;
80
+ }
81
+ /** Inserts a scene built from the catalog's minimal example for `type`. */
82
+ export function addScene(doc, request) {
83
+ const scenes = sceneList(doc);
84
+ if (scenes === undefined)
85
+ return NO_SCENES;
86
+ const built = buildEntry(scenes, request);
87
+ if (!built.ok)
88
+ return built;
89
+ const at = insertionIndex(scenes, request.after);
90
+ if (at === undefined) {
91
+ return {
92
+ ok: false,
93
+ message: `--after "${request.after}": no such scene (have: ${knownIds(scenes)})`,
94
+ };
95
+ }
96
+ const next = structuredClone(doc);
97
+ next.scenes.splice(at, 0, built.entry);
98
+ return {
99
+ ok: true,
100
+ value: next,
101
+ lines: (report) => [
102
+ `scene add ok | ${built.entry.id} (${request.type}) at ${at + 1}/${next.scenes.length} | total ${round1(report.total ?? 0)}s | ${report.errors.length} errors, ${report.warnings.length} warnings`,
103
+ ],
104
+ };
105
+ }
106
+ /** Removes a scene by id; an overlay still pointing at it makes the validator refuse the result. */
107
+ export function removeScene(doc, id) {
108
+ const scenes = sceneList(doc);
109
+ if (scenes === undefined)
110
+ return NO_SCENES;
111
+ const at = indexOfId(scenes, id);
112
+ if (at === -1)
113
+ return { ok: false, message: `no scene "${id}" (have: ${knownIds(scenes)})` };
114
+ const next = structuredClone(doc);
115
+ next.scenes.splice(at, 1);
116
+ return {
117
+ ok: true,
118
+ value: next,
119
+ lines: (report) => [
120
+ `scene remove ok | ${id} | scenes ${next.scenes.length} | total ${round1(report.total ?? 0)}s | ${report.errors.length} errors, ${report.warnings.length} warnings`,
121
+ ],
122
+ };
123
+ }
124
+ /** Moves a scene to follow another one (or to the start). */
125
+ export function moveScene(doc, request) {
126
+ const scenes = sceneList(doc);
127
+ if (scenes === undefined)
128
+ return NO_SCENES;
129
+ const from = indexOfId(scenes, request.id);
130
+ if (from === -1)
131
+ return { ok: false, message: `no scene "${request.id}" (have: ${knownIds(scenes)})` };
132
+ if (request.after === request.id)
133
+ return { ok: false, message: "cannot move a scene after itself" };
134
+ const next = structuredClone(doc);
135
+ const [moved] = next.scenes.splice(from, 1);
136
+ const anchor = request.after === undefined ? -1 : indexOfId(next.scenes, request.after);
137
+ if (request.after !== undefined && anchor === -1) {
138
+ return {
139
+ ok: false,
140
+ message: `--after "${request.after}": no such scene (have: ${knownIds(scenes)})`,
141
+ };
142
+ }
143
+ next.scenes.splice(anchor + 1, 0, moved);
144
+ return {
145
+ ok: true,
146
+ value: next,
147
+ lines: (report) => [
148
+ `scene move ok | ${request.id} now at ${anchor + 2}/${next.scenes.length} | total ${round1(report.total ?? 0)}s | ${report.errors.length} errors, ${report.warnings.length} warnings`,
149
+ ],
150
+ };
151
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../types.ts";
2
+ export declare const stateCommand: Command;
@@ -0,0 +1,128 @@
1
+ import { resolve } from "node:path";
2
+ import { parseFlags, requireStoryboard, textFlag, usageError, } from "../args.js";
3
+ import { EXIT } from "../exit-codes.js";
4
+ import { deriveState, gatherEvidence } from "./evidence.js";
5
+ const USAGE = `mk state <storyboard.json> [--out <dir>] [--render <dir>] [--assets <dir>] [--ledger <assets.json>] [--audio <manifest>] [--music <bed.wav>] [--music-volume <n>] [--music-license <file>] [--json]
6
+
7
+ Report the highest project state the artifacts prove; read-only, never writes.
8
+ --out <dir> preview pack base (default <storyboard dir>/out)
9
+ --render <dir> render output dir (default <storyboard dir>/out/project); one manifest per dir
10
+ --assets <dir> the --assets used with \`mk preview\`/\`mk approve\`
11
+ --ledger <file> the --ledger used with \`mk preview\`/\`mk approve\`
12
+ --audio <manifest> the --audio used with \`mk preview\`/\`mk approve\`
13
+ --music <bed.wav> the --music used with \`mk preview\`/\`mk approve\`
14
+ --music-volume <n> the --music-volume used with \`mk preview\`/\`mk approve\`
15
+ --music-license <file> the --music-license used with \`mk preview\`/\`mk approve\`
16
+ --json one compact line {state, next, missing, problems, evidence}
17
+ Exit 0 consistent; 1 an artifact claims more than the evidence; 2 usage, invalid storyboard or unreadable inputs.
18
+ `;
19
+ const FLAGS = {
20
+ out: { type: "string" },
21
+ render: { type: "string" },
22
+ assets: { type: "string" },
23
+ ledger: { type: "string" },
24
+ audio: { type: "string" },
25
+ music: { type: "string" },
26
+ "music-volume": { type: "string" },
27
+ "music-license": { type: "string" },
28
+ json: { type: "boolean" },
29
+ };
30
+ const MAX_PROBLEM_LINES = 3;
31
+ /** The state word, the one missing artifact, then at most three problem lines. */
32
+ function formatText(report) {
33
+ const lines = [report.state];
34
+ if (report.next !== null)
35
+ lines.push(`missing: ${report.next}`);
36
+ for (const problem of report.problems.slice(0, MAX_PROBLEM_LINES)) {
37
+ lines.push(`problem: ${problem.code} ${problem.message}`);
38
+ }
39
+ return `${lines.join("\n")}\n`;
40
+ }
41
+ function isExit(value) {
42
+ return typeof value === "object" && value !== null && value.kind === "exit";
43
+ }
44
+ /** `--music-volume` as a number in (0, 1], or the usage exit. */
45
+ function musicVolumeOf(flags, io) {
46
+ const raw = textFlag(flags, "music-volume");
47
+ if (raw === undefined)
48
+ return {};
49
+ const volume = Number(raw);
50
+ if (!Number.isFinite(volume) || volume <= 0 || volume > 1) {
51
+ return usageError("state", USAGE, "--music-volume must be within (0, 1]", io);
52
+ }
53
+ return { volume };
54
+ }
55
+ /** `--music` needs `--audio`; its volume/license need `--music`. */
56
+ function musicDependency(flags, io) {
57
+ const music = textFlag(flags, "music");
58
+ if (music === undefined) {
59
+ return textFlag(flags, "music-volume") === undefined &&
60
+ textFlag(flags, "music-license") === undefined
61
+ ? undefined
62
+ : usageError("state", USAGE, "--music-volume/--music-license need --music", io);
63
+ }
64
+ return textFlag(flags, "audio") === undefined
65
+ ? usageError("state", USAGE, "--music needs --audio", io)
66
+ : undefined;
67
+ }
68
+ /** The same approval input scope `mk approve` used; without it vendored assets cannot resolve. */
69
+ function approvalOptions(flags, io) {
70
+ const volume = musicVolumeOf(flags, io);
71
+ if (isExit(volume))
72
+ return volume;
73
+ const dependency = musicDependency(flags, io);
74
+ if (dependency !== undefined)
75
+ return dependency;
76
+ const assets = textFlag(flags, "assets");
77
+ const ledger = textFlag(flags, "ledger");
78
+ const audio = textFlag(flags, "audio");
79
+ const music = textFlag(flags, "music");
80
+ const license = textFlag(flags, "music-license");
81
+ const options = {};
82
+ if (assets !== undefined)
83
+ options.assetsDir = resolve(assets);
84
+ if (ledger !== undefined)
85
+ options.ledgerPath = ledger;
86
+ if (audio !== undefined)
87
+ options.audioManifest = audio;
88
+ if (music !== undefined)
89
+ options.music = music;
90
+ if (volume.volume !== undefined)
91
+ options.musicVolume = volume.volume;
92
+ if (license !== undefined)
93
+ options.musicLicense = license;
94
+ return options;
95
+ }
96
+ /** Unreadable inputs are undecidable (exit 2); a render overclaim is a contradiction (exit 1). */
97
+ function exitFor(report) {
98
+ if (report.problems.some((problem) => problem.code === "inputs-unreadable"))
99
+ return EXIT.USAGE;
100
+ return report.problems.length > 0 ? EXIT.ERROR : EXIT.OK;
101
+ }
102
+ export const stateCommand = {
103
+ name: "state",
104
+ summary: "report the highest project state the artifacts prove; flags overclaims",
105
+ async run(args, io) {
106
+ const parsed = parseFlags("state", USAGE, args, FLAGS, io);
107
+ if (parsed.kind === "exit")
108
+ return parsed.code;
109
+ const storyboard = requireStoryboard("state", USAGE, parsed.positionals, io);
110
+ if (isExit(storyboard))
111
+ return storyboard.code;
112
+ const approval = approvalOptions(parsed.flags, io);
113
+ if (isExit(approval))
114
+ return approval.code;
115
+ const gathered = await gatherEvidence(io, storyboard, {
116
+ out: textFlag(parsed.flags, "out"),
117
+ render: textFlag(parsed.flags, "render"),
118
+ approval,
119
+ });
120
+ if (!gathered.ok) {
121
+ io.stderr(`mk state: ${gathered.message.trimEnd()}\n`);
122
+ return EXIT.USAGE;
123
+ }
124
+ const report = deriveState(gathered.facts);
125
+ io.stdout(parsed.flags.json === true ? `${JSON.stringify(report)}\n` : formatText(report));
126
+ return exitFor(report);
127
+ },
128
+ };
@@ -0,0 +1,71 @@
1
+ import { type ApprovalState } from "../approval/approval.ts";
2
+ import type { ApprovalBuildOptions } from "../approval/hash.ts";
3
+ import type { Io } from "../types.ts";
4
+ /** The highest state a project has current evidence for. */
5
+ export type ProjectState = "storyboard" | "previewed" | "approved" | "rendered" | "verified";
6
+ /** The next missing artifact, one word. */
7
+ export type NextState = "preview" | "approval" | "render" | "verify";
8
+ export type Presence = "missing" | "stale" | "current";
9
+ export type ApprovalEvidence = ApprovalState["state"];
10
+ export type VerifyEvidence = "PASS" | "not-run" | "unknown";
11
+ export type ProblemCode = "render-output-missing" | "render-output-mismatch" | "render-unapproved" | "verify-claim-unsupported" | "inputs-unreadable";
12
+ export interface Problem {
13
+ readonly code: ProblemCode;
14
+ readonly message: string;
15
+ }
16
+ export interface Evidence {
17
+ readonly storyboard: string;
18
+ readonly preview: Presence;
19
+ readonly approval: ApprovalEvidence;
20
+ readonly render: Presence;
21
+ readonly verify: VerifyEvidence;
22
+ }
23
+ export interface StateReport {
24
+ readonly state: ProjectState;
25
+ readonly next: NextState | null;
26
+ readonly missing: readonly NextState[];
27
+ readonly problems: readonly Problem[];
28
+ readonly evidence: Evidence;
29
+ }
30
+ /** A render manifest we understand: recorded output plus what is actually on disk. */
31
+ export interface RenderFacts {
32
+ readonly file: string;
33
+ readonly recordedSha256: string;
34
+ readonly diskSha256?: string;
35
+ /** `storyboardSha256` of the build manifest; absent when it is missing or unparsable. */
36
+ readonly buildStoryboardSha256?: string;
37
+ readonly verify: string;
38
+ }
39
+ /** Already-read facts; `deriveState` is pure so every combination is testable without files. */
40
+ export interface EvidenceFacts {
41
+ readonly storyboardSha256: string;
42
+ /** The hash a build would record: `sha256(JSON.stringify(board))`, so whitespace is not identity. */
43
+ readonly storyboardCanonicalSha256: string;
44
+ /** The approval hash the current inputs resolve to; absent when they cannot be read. */
45
+ readonly currentHash?: string;
46
+ /** `inputManifestSha256` recorded by the preview pack. */
47
+ readonly previewSha256?: string;
48
+ readonly approval: ApprovalEvidence;
49
+ readonly render?: RenderFacts;
50
+ /** Short, path-free reason the approval inputs could not be resolved. */
51
+ readonly unreadable?: string;
52
+ }
53
+ /** Derives the state, the next missing artifact and every overclaim from already-read facts. */
54
+ export declare function deriveState(facts: EvidenceFacts): StateReport;
55
+ export interface StateOptions {
56
+ /** Preview pack base; default `<storyboard dir>/out`. */
57
+ readonly out?: string;
58
+ /** Render output dir; default `<out>/project`. */
59
+ readonly render?: string;
60
+ /** The same approval input scope `mk approve` used (`--assets`, `--ledger`, audio, music). */
61
+ readonly approval?: ApprovalBuildOptions;
62
+ }
63
+ export type GatherResult = {
64
+ readonly ok: true;
65
+ readonly facts: EvidenceFacts;
66
+ } | {
67
+ readonly ok: false;
68
+ readonly message: string;
69
+ };
70
+ /** Reads every artifact once; validation failures and unreadable inputs are exit-2 values. */
71
+ export declare function gatherEvidence(io: Io, storyboard: string, options?: StateOptions): Promise<GatherResult>;