@anvpx/motion-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/dist/analyze/command.d.ts +2 -0
  4. package/dist/analyze/command.js +190 -0
  5. package/dist/approval/approval.d.ts +32 -0
  6. package/dist/approval/approval.js +141 -0
  7. package/dist/approval/command.d.ts +2 -0
  8. package/dist/approval/command.js +255 -0
  9. package/dist/approval/gate.d.ts +7 -0
  10. package/dist/approval/gate.js +23 -0
  11. package/dist/approval/hash.d.ts +71 -0
  12. package/dist/approval/hash.js +464 -0
  13. package/dist/args.d.ts +30 -0
  14. package/dist/args.js +53 -0
  15. package/dist/assets/command.d.ts +2 -0
  16. package/dist/assets/command.js +165 -0
  17. package/dist/captions/command.d.ts +2 -0
  18. package/dist/captions/command.js +341 -0
  19. package/dist/captions/cues.d.ts +10 -0
  20. package/dist/captions/cues.js +61 -0
  21. package/dist/catalog/command.d.ts +2 -0
  22. package/dist/catalog/command.js +60 -0
  23. package/dist/catalog/format.d.ts +13 -0
  24. package/dist/catalog/format.js +54 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.js +35 -0
  27. package/dist/commands.d.ts +3 -0
  28. package/dist/commands.js +59 -0
  29. package/dist/compare/command.d.ts +4 -0
  30. package/dist/compare/command.js +144 -0
  31. package/dist/contact/argv.d.ts +24 -0
  32. package/dist/contact/argv.js +42 -0
  33. package/dist/contact/command.d.ts +2 -0
  34. package/dist/contact/command.js +79 -0
  35. package/dist/doctor/checks.d.ts +25 -0
  36. package/dist/doctor/checks.js +571 -0
  37. package/dist/doctor/command.d.ts +4 -0
  38. package/dist/doctor/command.js +88 -0
  39. package/dist/doctor/format.d.ts +7 -0
  40. package/dist/doctor/format.js +41 -0
  41. package/dist/doctor/heal.d.ts +14 -0
  42. package/dist/doctor/heal.js +71 -0
  43. package/dist/doctor/pins.d.ts +8 -0
  44. package/dist/doctor/pins.js +28 -0
  45. package/dist/doctor/skills.d.ts +15 -0
  46. package/dist/doctor/skills.js +58 -0
  47. package/dist/doctor/types.d.ts +36 -0
  48. package/dist/doctor/types.js +1 -0
  49. package/dist/doctor/vendored.d.ts +15 -0
  50. package/dist/doctor/vendored.js +90 -0
  51. package/dist/doctor/version.d.ts +8 -0
  52. package/dist/doctor/version.js +11 -0
  53. package/dist/exit-codes.d.ts +16 -0
  54. package/dist/exit-codes.js +15 -0
  55. package/dist/fit/command.d.ts +5 -0
  56. package/dist/fit/command.js +154 -0
  57. package/dist/fit/solve.d.ts +21 -0
  58. package/dist/fit/solve.js +80 -0
  59. package/dist/fit/target.d.ts +18 -0
  60. package/dist/fit/target.js +47 -0
  61. package/dist/generate3d/command.d.ts +3 -0
  62. package/dist/generate3d/command.js +38 -0
  63. package/dist/generate3d/run.d.ts +39 -0
  64. package/dist/generate3d/run.js +77 -0
  65. package/dist/icon/command.d.ts +2 -0
  66. package/dist/icon/command.js +113 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +16 -0
  69. package/dist/install-mk.d.ts +31 -0
  70. package/dist/install-mk.js +168 -0
  71. package/dist/integration/preview.integration.d.ts +1 -0
  72. package/dist/integration/preview.integration.js +53 -0
  73. package/dist/integration/smoke.integration.d.ts +1 -0
  74. package/dist/integration/smoke.integration.js +91 -0
  75. package/dist/main.d.ts +2 -0
  76. package/dist/main.js +4 -0
  77. package/dist/node-io.d.ts +14 -0
  78. package/dist/node-io.js +152 -0
  79. package/dist/optimize/command.d.ts +12 -0
  80. package/dist/optimize/command.js +110 -0
  81. package/dist/packs/command.d.ts +2 -0
  82. package/dist/packs/command.js +109 -0
  83. package/dist/patch/apply.d.ts +36 -0
  84. package/dist/patch/apply.js +186 -0
  85. package/dist/patch/command.d.ts +2 -0
  86. package/dist/patch/command.js +92 -0
  87. package/dist/plugin/install.d.ts +40 -0
  88. package/dist/plugin/install.js +170 -0
  89. package/dist/preview/command.d.ts +10 -0
  90. package/dist/preview/command.js +764 -0
  91. package/dist/preview/manifest.d.ts +94 -0
  92. package/dist/preview/manifest.js +19 -0
  93. package/dist/preview/pack.d.ts +24 -0
  94. package/dist/preview/pack.js +53 -0
  95. package/dist/preview/plan.d.ts +58 -0
  96. package/dist/preview/plan.js +160 -0
  97. package/dist/preview/provenance.d.ts +23 -0
  98. package/dist/preview/provenance.js +38 -0
  99. package/dist/publish/command.d.ts +2 -0
  100. package/dist/publish/command.js +70 -0
  101. package/dist/publish/player.d.ts +25 -0
  102. package/dist/publish/player.js +108 -0
  103. package/dist/publish/static.d.ts +16 -0
  104. package/dist/publish/static.js +78 -0
  105. package/dist/qc.d.ts +2 -0
  106. package/dist/qc.js +192 -0
  107. package/dist/scene/author.d.ts +16 -0
  108. package/dist/scene/author.js +279 -0
  109. package/dist/scene/command.d.ts +2 -0
  110. package/dist/scene/command.js +110 -0
  111. package/dist/scene/ops.d.ts +23 -0
  112. package/dist/scene/ops.js +151 -0
  113. package/dist/state/command.d.ts +2 -0
  114. package/dist/state/command.js +128 -0
  115. package/dist/state/evidence.d.ts +71 -0
  116. package/dist/state/evidence.js +243 -0
  117. package/dist/storyboard/check.d.ts +21 -0
  118. package/dist/storyboard/check.js +39 -0
  119. package/dist/storyboard/containment.d.ts +24 -0
  120. package/dist/storyboard/containment.js +43 -0
  121. package/dist/storyboard/custom.d.ts +15 -0
  122. package/dist/storyboard/custom.js +133 -0
  123. package/dist/storyboard/edit.d.ts +19 -0
  124. package/dist/storyboard/edit.js +39 -0
  125. package/dist/storyboard/load.d.ts +11 -0
  126. package/dist/storyboard/load.js +52 -0
  127. package/dist/storyboard/report.d.ts +5 -0
  128. package/dist/storyboard/report.js +8 -0
  129. package/dist/template/coerce.d.ts +6 -0
  130. package/dist/template/coerce.js +44 -0
  131. package/dist/template/command.d.ts +5 -0
  132. package/dist/template/command.js +163 -0
  133. package/dist/template/env.d.ts +34 -0
  134. package/dist/template/env.js +38 -0
  135. package/dist/template/fork.d.ts +8 -0
  136. package/dist/template/fork.js +156 -0
  137. package/dist/template/format.d.ts +21 -0
  138. package/dist/template/format.js +161 -0
  139. package/dist/template/list.d.ts +15 -0
  140. package/dist/template/list.js +116 -0
  141. package/dist/template/new.d.ts +6 -0
  142. package/dist/template/new.js +302 -0
  143. package/dist/testing/env.d.ts +8 -0
  144. package/dist/testing/env.js +19 -0
  145. package/dist/testing/fakes.d.ts +33 -0
  146. package/dist/testing/fakes.js +95 -0
  147. package/dist/testing/fixtures.d.ts +8 -0
  148. package/dist/testing/fixtures.js +6 -0
  149. package/dist/testing/probe.d.ts +15 -0
  150. package/dist/testing/probe.js +20 -0
  151. package/dist/testing/project.d.ts +22 -0
  152. package/dist/testing/project.js +26 -0
  153. package/dist/tooling/session.d.ts +40 -0
  154. package/dist/tooling/session.js +106 -0
  155. package/dist/types.d.ts +59 -0
  156. package/dist/types.js +1 -0
  157. package/dist/verify/audio-loudness.d.ts +46 -0
  158. package/dist/verify/audio-loudness.js +186 -0
  159. package/dist/verify/command.d.ts +2 -0
  160. package/dist/verify/command.js +161 -0
  161. package/dist/verify/determinism.d.ts +30 -0
  162. package/dist/verify/determinism.js +59 -0
  163. package/dist/verify/expectation.d.ts +15 -0
  164. package/dist/verify/expectation.js +19 -0
  165. package/dist/verify/media-events.d.ts +61 -0
  166. package/dist/verify/media-events.js +174 -0
  167. package/dist/verify/platform.d.ts +33 -0
  168. package/dist/verify/platform.js +59 -0
  169. package/dist/verify/probe.d.ts +14 -0
  170. package/dist/verify/probe.js +23 -0
  171. package/dist/verify/report.d.ts +15 -0
  172. package/dist/verify/report.js +45 -0
  173. package/dist/verify/run.d.ts +27 -0
  174. package/dist/verify/run.js +53 -0
  175. package/dist/voice/command.d.ts +3 -0
  176. package/dist/voice/command.js +49 -0
  177. package/dist/voice/manifest.d.ts +33 -0
  178. package/dist/voice/manifest.js +69 -0
  179. package/dist/voice/run.d.ts +27 -0
  180. package/dist/voice/run.js +190 -0
  181. package/dist/workflow/build.d.ts +2 -0
  182. package/dist/workflow/build.js +25 -0
  183. package/dist/workflow/draft.d.ts +9 -0
  184. package/dist/workflow/draft.js +215 -0
  185. package/dist/workflow/finish.d.ts +23 -0
  186. package/dist/workflow/finish.js +28 -0
  187. package/dist/workflow/project.d.ts +73 -0
  188. package/dist/workflow/project.js +198 -0
  189. package/dist/workflow/provenance.d.ts +18 -0
  190. package/dist/workflow/provenance.js +36 -0
  191. package/dist/workflow/render.d.ts +31 -0
  192. package/dist/workflow/render.js +721 -0
  193. package/dist/workflow/still.d.ts +5 -0
  194. package/dist/workflow/still.js +62 -0
  195. package/dist/workflow/validate.d.ts +2 -0
  196. package/dist/workflow/validate.js +49 -0
  197. package/package.json +38 -3
@@ -0,0 +1,80 @@
1
+ /** Durations are whole tenths of a second internally, so the sum is exact. */
2
+ const TENTHS = 10;
3
+ const EPS = 1e-9;
4
+ const ITERATIONS = 200;
5
+ const MAX_FACTOR = 1e9;
6
+ const clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
7
+ const total = (values) => values.reduce((a, b) => a + b, 0);
8
+ const inTenths = (rows) => {
9
+ const lo = rows.map((row) => Math.ceil(row.lo * TENTHS - EPS));
10
+ return {
11
+ cur: rows.map((row) => row.cur * TENTHS),
12
+ lo,
13
+ hi: rows.map((row, i) => Math.max(lo[i] ?? 0, Math.floor(row.hi * TENTHS + EPS))),
14
+ };
15
+ };
16
+ /** Every scene times one factor, held inside its own range. */
17
+ const scaled = (t, factor) => t.cur.map((cur, i) => clamp(cur * factor, t.lo[i] ?? 0, t.hi[i] ?? 0));
18
+ /** The smallest factor whose scaled total reaches `goal` (the total never decreases with it). */
19
+ function factorFor(t, goal) {
20
+ let low = 0;
21
+ let high = 1;
22
+ while (total(scaled(t, high)) < goal && high < MAX_FACTOR)
23
+ high *= 2;
24
+ for (let step = 0; step < ITERATIONS; step++) {
25
+ const middle = (low + high) / 2;
26
+ if (total(scaled(t, middle)) < goal)
27
+ low = middle;
28
+ else
29
+ high = middle;
30
+ }
31
+ return high;
32
+ }
33
+ /** The scene's duration moved by `step` tenths, or undefined when that leaves its range. */
34
+ const shifted = (t, whole, i, step) => {
35
+ const next = (whole[i] ?? 0) + step;
36
+ return next >= (t.lo[i] ?? 0) && next <= (t.hi[i] ?? 0) ? next : undefined;
37
+ };
38
+ /** One pass over `order`, moving scenes one tenth towards the goal; returns what is still missing. */
39
+ function handOut(t, whole, order, missing) {
40
+ let left = missing;
41
+ for (const i of order) {
42
+ const next = left === 0 ? undefined : shifted(t, whole, i, Math.sign(left));
43
+ if (next === undefined)
44
+ continue;
45
+ whole[i] = next;
46
+ left -= Math.sign(left);
47
+ }
48
+ return left;
49
+ }
50
+ /**
51
+ * Rounds down to whole tenths, then hands the remainder out one tenth at a time: to the scenes
52
+ * with the largest fractional part first (ties: first scene), or taken back from the smallest.
53
+ */
54
+ function distribute(t, real, goal) {
55
+ const whole = real.map((value, i) => clamp(Math.floor(value + EPS), t.lo[i] ?? 0, t.hi[i] ?? 0));
56
+ const fraction = real.map((value, i) => value - (whole[i] ?? 0));
57
+ const byFraction = [...whole.keys()].sort((a, b) => (fraction[b] ?? 0) - (fraction[a] ?? 0) || a - b);
58
+ let missing = goal - total(whole);
59
+ const order = missing > 0 ? byFraction : [...byFraction].reverse();
60
+ for (let pass = 0; missing !== 0 && pass < real.length * 2; pass++) {
61
+ missing = handOut(t, whole, order, missing);
62
+ }
63
+ return whole;
64
+ }
65
+ /**
66
+ * Scales every scene by one common factor, clamped to each scene's [lo, hi], so the durations
67
+ * add up to `targetSum` seconds (to 0.1 s). Deterministic: bisection on the factor, then the
68
+ * rounding remainder is distributed by largest fractional part.
69
+ */
70
+ export function fitDurations(rows, targetSum) {
71
+ const t = inTenths(rows);
72
+ const goal = Math.round(targetSum * TENTHS);
73
+ const range = { min: total(t.lo) / TENTHS, max: total(t.hi) / TENTHS };
74
+ if (goal < total(t.lo))
75
+ return { ok: false, reason: "below", ...range };
76
+ if (goal > total(t.hi))
77
+ return { ok: false, reason: "above", ...range };
78
+ const whole = distribute(t, scaled(t, factorFor(t, goal)), goal);
79
+ return { ok: true, seconds: whole.map((value) => value / TENTHS) };
80
+ }
@@ -0,0 +1,18 @@
1
+ export type FitTarget = {
2
+ readonly ok: true;
3
+ readonly target: number;
4
+ readonly from: string;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly message: string;
8
+ };
9
+ /**
10
+ * The fit target in seconds: either `--seconds`, or a bar count at a tempo
11
+ * (`bars * 4 * 60 / bpm`, i.e. 4/4). Tempo is a first-class authoring unit in the
12
+ * reference corpus, so the CLI can take bars instead of doing the arithmetic by hand.
13
+ */
14
+ export declare const fitTarget: (flags: {
15
+ readonly seconds?: string;
16
+ readonly bpm?: string;
17
+ readonly bars?: string;
18
+ }) => FitTarget;
@@ -0,0 +1,47 @@
1
+ /** Beats in a bar at 4/4; the only time signature the engine assumes. */
2
+ const BEATS_PER_BAR = 4;
3
+ const SECONDS_PER_MINUTE = 60;
4
+ const positive = (text) => {
5
+ const value = Number(text);
6
+ return text !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
7
+ };
8
+ const round1 = (value) => Math.round(value * 10) / 10;
9
+ /**
10
+ * The fit target in seconds: either `--seconds`, or a bar count at a tempo
11
+ * (`bars * 4 * 60 / bpm`, i.e. 4/4). Tempo is a first-class authoring unit in the
12
+ * reference corpus, so the CLI can take bars instead of doing the arithmetic by hand.
13
+ */
14
+ export const fitTarget = (flags) => {
15
+ const seconds = positive(flags.seconds);
16
+ const bpm = positive(flags.bpm);
17
+ const bars = positive(flags.bars);
18
+ if (seconds !== undefined && (bpm !== undefined || bars !== undefined)) {
19
+ return { ok: false, message: "use --seconds or --bpm/--bars, not both" };
20
+ }
21
+ if (flags.seconds !== undefined && seconds === undefined) {
22
+ return { ok: false, message: "--seconds needs a positive number" };
23
+ }
24
+ if (seconds !== undefined)
25
+ return { ok: true, target: seconds, from: "seconds" };
26
+ if (flags.bpm !== undefined && bpm === undefined) {
27
+ return { ok: false, message: "--bpm needs a positive number" };
28
+ }
29
+ if (flags.bars !== undefined && bars === undefined) {
30
+ return { ok: false, message: "--bars needs a positive number" };
31
+ }
32
+ if (bpm === undefined && bars === undefined) {
33
+ return { ok: false, message: "--seconds needs a positive number" };
34
+ }
35
+ if (bpm === undefined || bars === undefined) {
36
+ return { ok: false, message: "--bpm and --bars go together" };
37
+ }
38
+ if (bpm < 30 || bpm > 300)
39
+ return { ok: false, message: "--bpm must be between 30 and 300" };
40
+ if (bars < 1 || bars > 400)
41
+ return { ok: false, message: "--bars must be between 1 and 400" };
42
+ return {
43
+ ok: true,
44
+ target: round1((bars * BEATS_PER_BAR * SECONDS_PER_MINUTE) / bpm),
45
+ from: `${bars} bars at ${bpm} BPM`,
46
+ };
47
+ };
@@ -0,0 +1,3 @@
1
+ import type { Command } from "../types.ts";
2
+ export declare const generate3dCommand: Command;
3
+ export declare const makeGenerate3dCommand: () => Command;
@@ -0,0 +1,38 @@
1
+ import { parseFlags, requirePositional, textFlag, usageError } from "../args.js";
2
+ import { runGenerate3d } from "./run.js";
3
+ const USAGE = `mk generate-3d <image> --out <model.glb> [--python <bin>]
4
+
5
+ Generate a 3D model from a single image via the triposr backend and validate it.
6
+ Calls the audio triposr adapter, checks the GLB with the optimize-model validator,
7
+ then writes the model plus licences-style generate-3d-provenance.json next to it.
8
+ No venv/model: fails closed with a doctor hint, never a silent fallback.
9
+ --out <model.glb> output model path (required)
10
+ --python <bin> explicit venv python (skips the probe)
11
+ `;
12
+ const FLAGS = {
13
+ out: { type: "string" },
14
+ python: { type: "string" },
15
+ };
16
+ const NAME = "generate-3d";
17
+ export const generate3dCommand = {
18
+ name: NAME,
19
+ summary: "generate a validated GLB model from an image via the triposr backend",
20
+ async run(args, io) {
21
+ const parsed = parseFlags(NAME, USAGE, args, FLAGS, io);
22
+ if (parsed.kind === "exit")
23
+ return parsed.code;
24
+ const image = requirePositional(NAME, USAGE, parsed.positionals, "<image>", io);
25
+ if (typeof image !== "string")
26
+ return image.code;
27
+ const out = textFlag(parsed.flags, "out");
28
+ if (out === undefined || out.trim() === "") {
29
+ return usageError(NAME, USAGE, "--out <model.glb> is required", io).code;
30
+ }
31
+ const python = textFlag(parsed.flags, "python");
32
+ return runGenerate3d(image, {
33
+ outPath: out,
34
+ ...(python === undefined ? {} : { python }),
35
+ }, io);
36
+ },
37
+ };
38
+ export const makeGenerate3dCommand = () => generate3dCommand;
@@ -0,0 +1,39 @@
1
+ import type { Io, Runner } from "../types.ts";
2
+ /**
3
+ * Triposr port (adapter-agnostic).
4
+ *
5
+ * Real adapter lives in the audio package and is re-exported
6
+ * from `@anvpx/motion-audio`. This file keeps the CLI-side port types against
7
+ * the agreed spec:
8
+ * `generate3d(image, outGlb) -> { status: ok|missing|unknown, hint?, glbPath? }`.
9
+ * The default wires the real adapter with a shape-preserving mapping
10
+ * (adapter owns model pins; the CLI never hardcodes model ids).
11
+ */
12
+ export type Generate3dStatus = "ok" | "missing" | "unknown";
13
+ export type Generate3dResult = {
14
+ status: "ok";
15
+ glbPath: string;
16
+ } | {
17
+ status: "missing" | "unknown";
18
+ hint: string;
19
+ };
20
+ export interface Generate3dRequest {
21
+ image: string;
22
+ outGlb: string;
23
+ python?: string;
24
+ }
25
+ export type Generate3dFn = (run: Runner, req: Generate3dRequest) => Promise<Generate3dResult>;
26
+ export interface Generate3dOptions {
27
+ outPath: string;
28
+ python?: string;
29
+ }
30
+ export interface Generate3dDeps {
31
+ generate3d?: Generate3dFn;
32
+ }
33
+ export declare const provenancePathFor: (outGlb: string) => string;
34
+ /**
35
+ * Run the triposr adapter, validate the GLB via the optimize-model validator,
36
+ * and write licences-style provenance next to the output. Resolves to the
37
+ * process exit code; fails closed (127 missing, 1 unknown/invalid).
38
+ */
39
+ export declare function runGenerate3d(image: string, options: Generate3dOptions, io: Io, deps?: Generate3dDeps): Promise<number>;
@@ -0,0 +1,77 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { generate3d as generate3dAdapter } from "@anvpx/motion-audio";
4
+ import { EXIT } from "../exit-codes.js";
5
+ import { validateGlb } from "../optimize/command.js";
6
+ const doctorHint = "Run `mk doctor` for the generate-3d setup steps.";
7
+ /** Default: real TripoSR adapter with a shape-preserving result mapping. */
8
+ const defaultGenerate3d = async (run, req) => {
9
+ const result = await generate3dAdapter(run, req);
10
+ if (result.ok)
11
+ return { status: "ok", glbPath: result.outGlb };
12
+ return { status: result.status, hint: result.hint };
13
+ };
14
+ export const provenancePathFor = (outGlb) => join(dirname(outGlb), "generate-3d-provenance.json");
15
+ const readGlbBytes = async (host, path) => {
16
+ try {
17
+ const buf = await readFile(path);
18
+ return { bytes: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) };
19
+ }
20
+ catch {
21
+ // In-memory fakes (tests) fall through to Host.
22
+ }
23
+ try {
24
+ const text = await host.readText(path);
25
+ return { bytes: Uint8Array.from(Buffer.from(text, "binary")) };
26
+ }
27
+ catch {
28
+ return { message: `cannot read "${path}"` };
29
+ }
30
+ };
31
+ /**
32
+ * Run the triposr adapter, validate the GLB via the optimize-model validator,
33
+ * and write licences-style provenance next to the output. Resolves to the
34
+ * process exit code; fails closed (127 missing, 1 unknown/invalid).
35
+ */
36
+ export async function runGenerate3d(image, options, io, deps = {}) {
37
+ const exists = await io.host.exists(image).catch(() => false);
38
+ if (!exists) {
39
+ io.stderr(`mk generate-3d: cannot read "${image}"\n`);
40
+ return EXIT.USAGE;
41
+ }
42
+ const generate3d = deps.generate3d ?? defaultGenerate3d;
43
+ const outGlb = options.outPath;
44
+ const result = await generate3d(io.run, {
45
+ image,
46
+ outGlb,
47
+ ...(options.python === undefined ? {} : { python: options.python }),
48
+ });
49
+ if (result.status === "missing") {
50
+ io.stderr(`mk generate-3d: ${result.hint}\n${doctorHint}\n`);
51
+ return EXIT.MISSING_TOOL;
52
+ }
53
+ if (result.status !== "ok") {
54
+ io.stderr(`mk generate-3d: ${result.hint}\n`);
55
+ return EXIT.ERROR;
56
+ }
57
+ const read = await readGlbBytes(io.host, outGlb);
58
+ if ("message" in read) {
59
+ io.stderr(`mk generate-3d: ${read.message}\n`);
60
+ return EXIT.ERROR;
61
+ }
62
+ const checked = validateGlb(read.bytes);
63
+ if (!checked.ok) {
64
+ io.stderr(`mk generate-3d: refusing invalid model: ${checked.message}\n`);
65
+ return EXIT.ERROR;
66
+ }
67
+ const provenance = {
68
+ tool: "generate-3d",
69
+ backend: "triposr",
70
+ image,
71
+ out: outGlb,
72
+ bytes: checked.bytes,
73
+ };
74
+ await io.host.writeTextAtomic(provenancePathFor(outGlb), `${JSON.stringify(provenance, null, 2)}\n`);
75
+ io.stdout(`generate-3d ok | ${checked.bytes} bytes | ${outGlb}\n`);
76
+ return EXIT.OK;
77
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../types.ts";
2
+ export declare const iconCommand: Command;
@@ -0,0 +1,113 @@
1
+ import { iconGroups, listIcons, searchIconNames } from "@anvpx/motion-build";
2
+ import { parseFlags, textFlag, usageError } from "../args.js";
3
+ import { EXIT } from "../exit-codes.js";
4
+ const USAGE = `mk icon search <query> | list [--tag <group|keyword>] [--json]
5
+
6
+ Icon names for the \`icon\` props of icon-grid, bullets, steps, ... (names only; unknown names fail \`mk validate\`).
7
+ search q icons whose name, keyword or group contains every word of q, ranked exact > prefix > word-part > substring, then A-Z
8
+ list the groups with their icon counts
9
+ list --tag t the names in group or keyword t (case-insensitive, trimmed)
10
+ --json one JSON line (array of names; list without --tag gives group names)
11
+ `;
12
+ const FLAGS = { tag: { type: "string" }, json: { type: "boolean" } };
13
+ const MAX_NAMES = 60;
14
+ const WIDTH = 100;
15
+ /** Tier for one query token against a lowercased icon name (lower is better). */
16
+ function tier(nameLower, token) {
17
+ if (nameLower === token)
18
+ return 0;
19
+ if (nameLower.startsWith(token))
20
+ return 1;
21
+ if (nameLower.includes(`-${token}`))
22
+ return 2;
23
+ if (nameLower.includes(token))
24
+ return 3;
25
+ return 4;
26
+ }
27
+ /** Relevance score for one icon name (lower is better). */
28
+ function relevanceScore(dashed, tokens, nameLower) {
29
+ const exact = nameLower === dashed ? 0 : 1;
30
+ const parts = tokens.reduce((sum, t) => sum + tier(nameLower, t), 0);
31
+ return exact * 100 + parts;
32
+ }
33
+ /** Re-rank library hits (alphabetical) by relevance; ties stay alphabetical. */
34
+ function rankNames(query, names) {
35
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
36
+ const dashed = tokens.join("-");
37
+ const scored = names.map((name) => ({
38
+ name,
39
+ lower: name.toLowerCase(),
40
+ score: relevanceScore(dashed, tokens, name.toLowerCase()),
41
+ }));
42
+ scored.sort((a, b) => (a.score !== b.score ? a.score - b.score : a.lower < b.lower ? -1 : 1));
43
+ return scored.map((s) => s.name);
44
+ }
45
+ /** Names joined by spaces, wrapped at WIDTH columns, at most MAX_NAMES of them. */
46
+ function wrap(names) {
47
+ const rows = [];
48
+ let row = "";
49
+ for (const name of names.slice(0, MAX_NAMES)) {
50
+ if (row !== "" && row.length + 1 + name.length > WIDTH) {
51
+ rows.push(row);
52
+ row = name;
53
+ }
54
+ else {
55
+ row = row === "" ? name : `${row} ${name}`;
56
+ }
57
+ }
58
+ if (row !== "")
59
+ rows.push(row);
60
+ if (names.length > MAX_NAMES)
61
+ rows.push(`(+${names.length - MAX_NAMES} more; narrow the query)`);
62
+ return `${rows.join("\n")}\n`;
63
+ }
64
+ const emit = (io, names, json, header) => {
65
+ io.stdout(json ? `${JSON.stringify(names)}\n` : `${header}\n${wrap(names)}`);
66
+ return EXIT.OK;
67
+ };
68
+ function search(positionals, json, io) {
69
+ const query = positionals.join(" ").trim();
70
+ if (query === "")
71
+ return usageError("icon", USAGE, "search needs a query", io).code;
72
+ const names = rankNames(query, searchIconNames(query));
73
+ if (names.length === 0 && !json) {
74
+ io.stdout(`no icon matches "${query}"; try one word, or \`mk icon list\` for the groups\n`);
75
+ return EXIT.OK;
76
+ }
77
+ return emit(io, names, json, `${names.length} icons for "${query}":`);
78
+ }
79
+ function list(tag, json, io) {
80
+ const groups = iconGroups();
81
+ const clean = tag === undefined ? undefined : tag.trim();
82
+ if (clean === undefined) {
83
+ if (json) {
84
+ io.stdout(`${JSON.stringify(groups.map((g) => g.group))}\n`);
85
+ return EXIT.OK;
86
+ }
87
+ const row = groups.map((g) => `${g.group} (${g.count})`).join(", ");
88
+ io.stdout(`groups: ${row}\nnext: mk icon list --tag <group> | mk icon search <query>\n`);
89
+ return EXIT.OK;
90
+ }
91
+ const names = listIcons(clean);
92
+ if (names.length === 0) {
93
+ const known = groups.map((g) => g.group).join("|");
94
+ return usageError("icon", USAGE, `unknown tag "${clean}"; groups: ${known}`, io).code;
95
+ }
96
+ return emit(io, names, json, `${names.length} icons tagged "${clean}":`);
97
+ }
98
+ export const iconCommand = {
99
+ name: "icon",
100
+ summary: "find icon names for scene props: search <query> | list [--tag group|keyword] [--json]",
101
+ async run(args, io) {
102
+ const parsed = parseFlags("icon", USAGE, args, FLAGS, io);
103
+ if (parsed.kind === "exit")
104
+ return parsed.code;
105
+ const [action, ...rest] = parsed.positionals;
106
+ const json = parsed.flags.json === true;
107
+ if (action === "search")
108
+ return search(rest, json, io);
109
+ if (action === "list")
110
+ return list(textFlag(parsed.flags, "tag"), json, io);
111
+ return usageError("icon", USAGE, "action must be search|list", io).code;
112
+ },
113
+ };
@@ -0,0 +1,18 @@
1
+ export { approveCommand } from "./approval/command.ts";
2
+ export { catalogCommand } from "./catalog/command.ts";
3
+ export { runCli } from "./cli.ts";
4
+ export { COMMANDS } from "./commands.ts";
5
+ export { contactCommand } from "./contact/command.ts";
6
+ export { CHECKS } from "./doctor/checks.ts";
7
+ export { doctorCommand } from "./doctor/command.ts";
8
+ export { PIN_KEYS, readPins } from "./doctor/pins.ts";
9
+ export type { Check, CheckResult, CheckStatus, Pins } from "./doctor/types.ts";
10
+ export { previewCommand } from "./preview/command.ts";
11
+ export { templateCommand } from "./template/command.ts";
12
+ export { newCommand } from "./template/new.ts";
13
+ export type { Command, CommandResult, Host, Io, Runner, RunOptions } from "./types.ts";
14
+ export { buildCommand } from "./workflow/build.ts";
15
+ export { draftCommand } from "./workflow/draft.ts";
16
+ export { renderCommand } from "./workflow/render.ts";
17
+ export { stillCommand } from "./workflow/still.ts";
18
+ export { validateCommand } from "./workflow/validate.ts";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export { approveCommand } from "./approval/command.js";
2
+ export { catalogCommand } from "./catalog/command.js";
3
+ export { runCli } from "./cli.js";
4
+ export { COMMANDS } from "./commands.js";
5
+ export { contactCommand } from "./contact/command.js";
6
+ export { CHECKS } from "./doctor/checks.js";
7
+ export { doctorCommand } from "./doctor/command.js";
8
+ export { PIN_KEYS, readPins } from "./doctor/pins.js";
9
+ export { previewCommand } from "./preview/command.js";
10
+ export { templateCommand } from "./template/command.js";
11
+ export { newCommand } from "./template/new.js";
12
+ export { buildCommand } from "./workflow/build.js";
13
+ export { draftCommand } from "./workflow/draft.js";
14
+ export { renderCommand } from "./workflow/render.js";
15
+ export { stillCommand } from "./workflow/still.js";
16
+ export { validateCommand } from "./workflow/validate.js";
@@ -0,0 +1,31 @@
1
+ import type { Command } from "./types.ts";
2
+ /** Default install directory for the empty-consumer proof (no workspace fallback). */
3
+ export declare const INSTALL_MK_DEFAULT_OUT = "motion-kit-pack";
4
+ /** Manifest written beside the copied pack so consumers can verify bytes. */
5
+ export declare const INSTALL_MK_MANIFEST = "install.json";
6
+ export type InstallMkSource = {
7
+ kind: "file-path";
8
+ path: string;
9
+ } | {
10
+ kind: "registry";
11
+ name: "current";
12
+ };
13
+ export interface InstallMkFailure {
14
+ error: string;
15
+ }
16
+ /** File-path first, registry fallback stub. Only the unversioned current name is accepted. */
17
+ export declare function resolveInstallMkSource(opts: {
18
+ from?: string;
19
+ name?: string;
20
+ }): InstallMkSource | InstallMkFailure;
21
+ /**
22
+ * Normalize-and-constrain traversal guard: reject NUL bytes and any `..`
23
+ * path segment before touching the host, so `../evil` can never escape the
24
+ * install directory. Both the source path and the destination directory are
25
+ * constrained; the copied file name is always the source basename joined
26
+ * under the normalized out dir.
27
+ */
28
+ export declare function isTraversalPath(value: string): boolean;
29
+ /** File-path installs copy + manifest + SHA-verify; registry name stubs fail closed with a hint. */
30
+ export declare const makeInstallMkCommand: () => Command;
31
+ export declare const installMkCommand: Command;
@@ -0,0 +1,168 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join, normalize } from "node:path";
3
+ import { parseFlags, textFlag, usageError } from "./args.js";
4
+ import { EXIT } from "./exit-codes.js";
5
+ const USAGE = `mk install-mk [--from <path>] [--out <dir>] [current]
6
+
7
+ Install the smallest single-scene current pack.
8
+ --from <path> file path to a pack storyboard (takes priority)
9
+ --out <dir> install directory (default motion-kit-pack)
10
+ current registry stub name (unavailable in skeleton; use --from <path>)
11
+ Alias: mk install [--from <path>] [--out <dir>] behaves the same as mk install-mk.
12
+ `;
13
+ const FLAGS = { from: { type: "string" }, out: { type: "string" } };
14
+ /** Default install directory for the empty-consumer proof (no workspace fallback). */
15
+ export const INSTALL_MK_DEFAULT_OUT = "motion-kit-pack";
16
+ /** Manifest written beside the copied pack so consumers can verify bytes. */
17
+ export const INSTALL_MK_MANIFEST = "install.json";
18
+ /** File-path first, registry fallback stub. Only the unversioned current name is accepted. */
19
+ export function resolveInstallMkSource(opts) {
20
+ if (opts.from !== undefined && opts.from !== "") {
21
+ if (opts.from.includes("@"))
22
+ return { error: "versioned names not supported (current only)" };
23
+ return { kind: "file-path", path: opts.from };
24
+ }
25
+ const name = opts.name;
26
+ if (name === undefined || name === "" || name === "current")
27
+ return { kind: "registry", name: "current" };
28
+ if (name.includes("@"))
29
+ return { error: "versioned names not supported (current only)" };
30
+ return {
31
+ error: `unknown pack "${name}" (only "current" supported; use --from <path> for file paths)`,
32
+ };
33
+ }
34
+ function isSingleScenePack(value) {
35
+ if (typeof value !== "object" || value === null)
36
+ return false;
37
+ const scenes = value.scenes;
38
+ return Array.isArray(scenes) && scenes.length === 1;
39
+ }
40
+ /**
41
+ * Normalize-and-constrain traversal guard: reject NUL bytes and any `..`
42
+ * path segment before touching the host, so `../evil` can never escape the
43
+ * install directory. Both the source path and the destination directory are
44
+ * constrained; the copied file name is always the source basename joined
45
+ * under the normalized out dir.
46
+ */
47
+ export function isTraversalPath(value) {
48
+ if (value.includes("\0"))
49
+ return true;
50
+ return value.split(/[\\/]/).some((segment) => segment === "..");
51
+ }
52
+ function safeBasename(path) {
53
+ const base = path
54
+ .split(/[\\/]/)
55
+ .filter((part) => part !== "")
56
+ .pop();
57
+ if (base === undefined || base === "" || base === "." || base === "..")
58
+ return undefined;
59
+ if (base.includes("\0"))
60
+ return undefined;
61
+ return base;
62
+ }
63
+ async function installFromPath(io, path, outDir) {
64
+ if (isTraversalPath(path)) {
65
+ io.stderr(`mk install-mk: refusing traversal path at ${path}\n`);
66
+ return EXIT.ERROR;
67
+ }
68
+ if (isTraversalPath(outDir)) {
69
+ io.stderr(`mk install-mk: refusing traversal output directory at ${outDir}\n`);
70
+ return EXIT.ERROR;
71
+ }
72
+ const base = safeBasename(path);
73
+ if (base === undefined) {
74
+ io.stderr(`mk install-mk: refusing traversal path at ${path}\n`);
75
+ return EXIT.ERROR;
76
+ }
77
+ if (!(await io.host.exists(path))) {
78
+ io.stderr(`mk install-mk: cannot read pack file at ${path}\n`);
79
+ return EXIT.ERROR;
80
+ }
81
+ let text;
82
+ try {
83
+ text = await io.host.readText(path);
84
+ }
85
+ catch {
86
+ io.stderr(`mk install-mk: cannot read pack file at ${path}\n`);
87
+ return EXIT.ERROR;
88
+ }
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(text);
92
+ }
93
+ catch {
94
+ io.stderr("mk install-mk: pack file is not valid JSON\n");
95
+ return EXIT.ERROR;
96
+ }
97
+ if (!isSingleScenePack(parsed)) {
98
+ io.stderr("mk install-mk: expected the smallest single-scene current pack\n");
99
+ return EXIT.ERROR;
100
+ }
101
+ const sha256 = createHash("sha256").update(text, "utf8").digest("hex");
102
+ const destDir = normalize(outDir);
103
+ const destFile = join(destDir, base);
104
+ const manifestPath = join(destDir, INSTALL_MK_MANIFEST);
105
+ try {
106
+ await io.host.copyFile(path, destFile);
107
+ }
108
+ catch {
109
+ io.stderr(`mk install-mk: cannot copy pack file to ${destFile}\n`);
110
+ return EXIT.ERROR;
111
+ }
112
+ const manifest = `${JSON.stringify({
113
+ command: "install-mk",
114
+ alias: "mk install",
115
+ source: path,
116
+ file: destFile,
117
+ sha256,
118
+ scenes: 1,
119
+ name: "current",
120
+ }, null, 2)}\n`;
121
+ try {
122
+ await io.host.writeTextAtomic(manifestPath, manifest);
123
+ }
124
+ catch {
125
+ io.stderr(`mk install-mk: cannot write manifest at ${manifestPath}\n`);
126
+ return EXIT.ERROR;
127
+ }
128
+ const copiedSha = await io.host.sha256File(destFile);
129
+ if (copiedSha !== sha256) {
130
+ io.stderr(`mk install-mk: SHA mismatch after copy to ${destFile}\n`);
131
+ return EXIT.ERROR;
132
+ }
133
+ io.stdout(`install-mk 1 scene -> ${destFile} (sha256 ${sha256.slice(0, 12)})\n`);
134
+ return EXIT.OK;
135
+ }
136
+ function toInstallTarget(positionals, flags, io) {
137
+ const [name, ...extra] = positionals;
138
+ if (extra.length > 0)
139
+ return { exit: usageError("install-mk", USAGE, `unexpected argument "${extra[0]}"`, io).code };
140
+ const resolved = resolveInstallMkSource({ from: textFlag(flags, "from"), name });
141
+ if ("error" in resolved)
142
+ return { exit: usageError("install-mk", USAGE, resolved.error, io).code };
143
+ if (resolved.kind === "registry")
144
+ return { registry: true };
145
+ const outFlag = textFlag(flags, "out");
146
+ if (outFlag !== undefined && outFlag === "")
147
+ return { exit: usageError("install-mk", USAGE, "--out needs a directory", io).code };
148
+ return { path: resolved.path, outDir: outFlag ?? INSTALL_MK_DEFAULT_OUT };
149
+ }
150
+ /** File-path installs copy + manifest + SHA-verify; registry name stubs fail closed with a hint. */
151
+ export const makeInstallMkCommand = () => ({
152
+ name: "install-mk",
153
+ summary: "install the smallest single-scene current pack from a file path (alias: mk install)",
154
+ async run(args, io) {
155
+ const parsed = parseFlags("install-mk", USAGE, args, FLAGS, io);
156
+ if (parsed.kind === "exit")
157
+ return parsed.code;
158
+ const target = toInstallTarget(parsed.positionals, parsed.flags, io);
159
+ if ("exit" in target)
160
+ return target.exit;
161
+ if ("registry" in target) {
162
+ io.stderr('mk install-mk: registry "current" unavailable in skeleton; provide --from <path>\n');
163
+ return EXIT.ERROR;
164
+ }
165
+ return installFromPath(io, target.path, target.outDir);
166
+ },
167
+ });
168
+ export const installMkCommand = makeInstallMkCommand();
@@ -0,0 +1 @@
1
+ export {};