@vosjs/cli 0.53.6 → 0.54.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.
@@ -9,6 +9,7 @@ import {
9
9
  apiError,
10
10
  apiJson,
11
11
  clientId,
12
+ configDuration,
12
13
  containerMismatch,
13
14
  deriveSlug,
14
15
  directoryKind,
@@ -16,12 +17,16 @@ import {
16
17
  formatChanges,
17
18
  hasFlag,
18
19
  launchBrowser,
20
+ loadVosConfig,
19
21
  nameForType,
20
22
  numFlag2 as numFlag,
21
23
  parseArgs2 as parseArgs,
24
+ parseTimes,
22
25
  parseVosId,
23
26
  platformOrigin,
24
27
  readSyncState,
28
+ reencodeStill,
29
+ renderStill,
25
30
  requireCredential,
26
31
  resolveCredential,
27
32
  resolveMediaType,
@@ -31,7 +36,7 @@ import {
31
36
  videoFormat,
32
37
  writeCredential,
33
38
  writeSyncState
34
- } from "./chunk-WLUWDQQO.js";
39
+ } from "./chunk-SAWRKTRX.js";
35
40
  import {
36
41
  deadLine,
37
42
  deadWarns
@@ -94,8 +99,8 @@ function landedLines(l) {
94
99
 
95
100
  // src/plugin/run.ts
96
101
  import { mkdir as mkdir11, readFile as readFile17, rm as rm5 } from "fs/promises";
97
- import { existsSync as existsSync20 } from "fs";
98
- import { join as join22, resolve as resolve10 } from "path";
102
+ import { existsSync as existsSync23 } from "fs";
103
+ import { join as join27, resolve as resolve15 } from "path";
99
104
  import {
100
105
  END_CARD_FROM as END_CARD_FROM2,
101
106
  docOutputDuration as docOutputDuration3,
@@ -2334,10 +2339,10 @@ function startTakeServer(rootDir, pages) {
2334
2339
  res.writeHead(404).end();
2335
2340
  });
2336
2341
  return new Promise(
2337
- (resolve11) => server.listen(0, () => {
2342
+ (resolve16) => server.listen(0, () => {
2338
2343
  const addr = server.address();
2339
2344
  const port = typeof addr === "object" && addr ? addr.port : 0;
2340
- resolve11({ base: `http://localhost:${port}`, close: () => server.close() });
2345
+ resolve16({ base: `http://localhost:${port}`, close: () => server.close() });
2341
2346
  })
2342
2347
  );
2343
2348
  }
@@ -3584,7 +3589,7 @@ async function momentsFor(dir, doc) {
3584
3589
  }
3585
3590
  return momentsFromDoc2(doc, planForDigest2(doc));
3586
3591
  }
3587
- function parseTimes(spec, duration) {
3592
+ function parseTimes2(spec, duration) {
3588
3593
  return spec.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
3589
3594
  if (s.endsWith("%")) {
3590
3595
  const p = Number(s.slice(0, -1));
@@ -4211,11 +4216,11 @@ var DUPLICATE_BITS = 6;
4211
4216
  var POPULATED_INK = 0.12;
4212
4217
  function pickMoments(measured, opts = {}) {
4213
4218
  const minInk = opts.minInk ?? 0.06;
4214
- const relative2 = opts.relativeInk ?? 0.4;
4219
+ const relative3 = opts.relativeInk ?? 0.4;
4215
4220
  const bits = opts.duplicateBits ?? DUPLICATE_BITS;
4216
4221
  const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
4217
4222
  const median2 = inks[inks.length >> 1] ?? 0;
4218
- const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative2));
4223
+ const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative3));
4219
4224
  const kept = [];
4220
4225
  const dropped = [];
4221
4226
  for (const m of measured) {
@@ -4846,7 +4851,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4846
4851
  const vh = meta.captureHeight ?? meta.height;
4847
4852
  const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
4848
4853
  try {
4849
- const probe2 = await framesTake(browser, dir, {
4854
+ const probe3 = await framesTake(browser, dir, {
4850
4855
  times: candidates.map((c) => c.time),
4851
4856
  width: PROBE_WIDTH,
4852
4857
  height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
@@ -4857,7 +4862,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4857
4862
  }
4858
4863
  });
4859
4864
  const byTime = /* @__PURE__ */ new Map();
4860
- for (const frame of probe2.frames) {
4865
+ for (const frame of probe3.frames) {
4861
4866
  const img = decodePng(new Uint8Array(await readFile7(frame.file)));
4862
4867
  if (!img) continue;
4863
4868
  const whole = { x: 0, y: 0, w: img.w, h: img.h };
@@ -6059,10 +6064,10 @@ function duplicateFindings(stills) {
6059
6064
  };
6060
6065
  });
6061
6066
  }
6062
- async function videoFrame(file, at2, ffmpeg) {
6067
+ async function videoFrame(file, at2, ffmpeg2) {
6063
6068
  try {
6064
6069
  const { stdout } = await execFileP(
6065
- ffmpeg,
6070
+ ffmpeg2,
6066
6071
  [
6067
6072
  "-v",
6068
6073
  "error",
@@ -6097,7 +6102,7 @@ async function pictureChecks(assets) {
6097
6102
  const findings = [];
6098
6103
  const measured = [];
6099
6104
  const stills = [];
6100
- let ffmpeg = null;
6105
+ let ffmpeg2 = null;
6101
6106
  for (const a of assets) {
6102
6107
  if (/\.png$/i.test(a.file)) {
6103
6108
  const img = decodePng(new Uint8Array(await readFile11(a.file)));
@@ -6126,8 +6131,8 @@ async function pictureChecks(assets) {
6126
6131
  continue;
6127
6132
  }
6128
6133
  if (/\.(mp4|webm|mov)$/i.test(a.file)) {
6129
- if (ffmpeg === null) ffmpeg = await onPath("ffmpeg");
6130
- if (!ffmpeg) {
6134
+ if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
6135
+ if (!ffmpeg2) {
6131
6136
  findings.push({
6132
6137
  code: "firstlast",
6133
6138
  severity: "info",
@@ -6514,10 +6519,10 @@ async function openSession(name, url, opts = { log: () => {
6514
6519
  opts.log(
6515
6520
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
6516
6521
  );
6517
- return new Promise((resolve11, reject) => {
6522
+ return new Promise((resolve16, reject) => {
6518
6523
  const child = spawn(exe, args, { stdio: "ignore" });
6519
6524
  child.on("error", reject);
6520
- child.on("exit", (code) => resolve11({ code }));
6525
+ child.on("exit", (code) => resolve16({ code }));
6521
6526
  });
6522
6527
  }
6523
6528
  async function withSession(name, fn, opts = {}) {
@@ -7339,7 +7344,7 @@ async function cmdPull(argv) {
7339
7344
  members.push({ dir, id: v.id, title: v.title, take });
7340
7345
  }
7341
7346
  const assets = payload.assets.filter((a) => a.category !== "recipe");
7342
- const summary2 = [
7347
+ const summary3 = [
7343
7348
  `Pulled ${folder.name} (${folder.slug}) \u2192 ${out}/`,
7344
7349
  ...folder.id && payload.folder.description ? [` ${payload.folder.description}`] : [],
7345
7350
  ` recipes (${recipes.length}) \u2014 read EVERY one:`,
@@ -7366,7 +7371,7 @@ async function cmdPull(argv) {
7366
7371
  assets,
7367
7372
  subfolders: payload.subfolders
7368
7373
  },
7369
- summary2
7374
+ summary3
7370
7375
  );
7371
7376
  return EXIT_OK;
7372
7377
  }
@@ -8561,9 +8566,9 @@ async function cmdLogin(argv) {
8561
8566
  return storeKey(key, origin, r);
8562
8567
  }
8563
8568
  async function storeKey(key, origin, r) {
8564
- const probe2 = await apiJson(origin, "/api/folders", { key });
8565
- if (probe2.status === 401 || probe2.status === 403) {
8566
- throw new Error(apiError("validate key", probe2));
8569
+ const probe3 = await apiJson(origin, "/api/folders", { key });
8570
+ if (probe3.status === 401 || probe3.status === 403) {
8571
+ throw new Error(apiError("validate key", probe3));
8567
8572
  }
8568
8573
  const path = writeCredential(key);
8569
8574
  r.done(
@@ -8813,20 +8818,1354 @@ async function cmdDelete(argv) {
8813
8818
  return EXIT_OK;
8814
8819
  }
8815
8820
 
8816
- // src/plugin/recipe.ts
8817
- import { readFileSync as readFileSync4 } from "fs";
8818
- import { basename as basename6 } from "path";
8821
+ // src/plugin/buildProgram.ts
8822
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
8823
+ import { basename as basename6, dirname as dirname5, join as join19, resolve as resolve7 } from "path";
8824
+ import { pathToFileURL } from "url";
8825
+ import { parse } from "acorn";
8826
+ import { fullAncestor } from "acorn-walk";
8827
+ var FUNCTION_KEYS = [
8828
+ "setup",
8829
+ "createContent",
8830
+ "createTimeline",
8831
+ "onFrame",
8832
+ "retime"
8833
+ ];
8834
+ var PAGE_GLOBALS = /* @__PURE__ */ new Set([
8835
+ "window",
8836
+ "document",
8837
+ "navigator",
8838
+ "location",
8839
+ "performance",
8840
+ "requestAnimationFrame",
8841
+ "cancelAnimationFrame",
8842
+ "devicePixelRatio",
8843
+ "Image",
8844
+ "ImageData",
8845
+ "ImageBitmap",
8846
+ "createImageBitmap",
8847
+ "OffscreenCanvas",
8848
+ "HTMLCanvasElement",
8849
+ "CanvasRenderingContext2D",
8850
+ "Path2D",
8851
+ "DOMMatrix",
8852
+ "FontFace",
8853
+ "Blob",
8854
+ "URL",
8855
+ "fetch",
8856
+ "Response",
8857
+ "TextEncoder",
8858
+ "TextDecoder",
8859
+ "AudioContext",
8860
+ "OfflineAudioContext",
8861
+ "getComputedStyle",
8862
+ "matchMedia",
8863
+ "atob",
8864
+ "btoa"
8865
+ ]);
8866
+ var JS_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
8867
+ JS_GLOBALS.add("undefined");
8868
+ JS_GLOBALS.add("arguments");
8869
+ function functionSource(fn) {
8870
+ const src = fn.toString();
8871
+ if (parsesAsExpression(src)) return src;
8872
+ const asyncMethod = /^async\s+/.test(src);
8873
+ const body = asyncMethod ? src.replace(/^async\s+/, "") : src;
8874
+ const candidate = `${asyncMethod ? "async " : ""}function ${body}`;
8875
+ if (parsesAsExpression(candidate)) return candidate;
8876
+ return src;
8877
+ }
8878
+ function parsesAsExpression(src) {
8879
+ try {
8880
+ parse(`(${src})`, { ecmaVersion: "latest" });
8881
+ return true;
8882
+ } catch {
8883
+ return false;
8884
+ }
8885
+ }
8886
+ function freeIdentifiers(src) {
8887
+ const ast = parse(`(${src})`, { ecmaVersion: "latest" });
8888
+ const declared = /* @__PURE__ */ new Set();
8889
+ const read = /* @__PURE__ */ new Set();
8890
+ const addPattern = (p) => {
8891
+ if (!p) return;
8892
+ if (p.type === "Identifier") declared.add(p.name);
8893
+ else if (p.type === "ObjectPattern")
8894
+ for (const prop of p.properties)
8895
+ addPattern(prop.type === "RestElement" ? prop.argument : prop.value);
8896
+ else if (p.type === "ArrayPattern")
8897
+ for (const el of p.elements) addPattern(el);
8898
+ else if (p.type === "RestElement") addPattern(p.argument);
8899
+ else if (p.type === "AssignmentPattern") addPattern(p.left);
8900
+ };
8901
+ fullAncestor(ast, (node, _state, ancestors) => {
8902
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
8903
+ if (node.id) declared.add(node.id.name);
8904
+ for (const p of node.params) addPattern(p);
8905
+ } else if (node.type === "VariableDeclarator") addPattern(node.id);
8906
+ else if (node.type === "ClassDeclaration" && node.id)
8907
+ declared.add(node.id.name);
8908
+ else if (node.type === "CatchClause") addPattern(node.param);
8909
+ else if (node.type === "Identifier") {
8910
+ const parent = ancestors[ancestors.length - 2];
8911
+ if (!parent) return;
8912
+ if (parent.type === "MemberExpression" && parent.property === node && !parent.computed)
8913
+ return;
8914
+ if ((parent.type === "Property" || parent.type === "MethodDefinition" || parent.type === "PropertyDefinition") && parent.key === node && !parent.computed && !(parent.type === "Property" && parent.shorthand))
8915
+ return;
8916
+ if (parent.type === "LabeledStatement" || parent.type === "BreakStatement" || parent.type === "ContinueStatement")
8917
+ return;
8918
+ if (parent.type === "MetaProperty") return;
8919
+ read.add(node.name);
8920
+ }
8921
+ });
8922
+ return [...read].filter(
8923
+ (n) => !declared.has(n) && !JS_GLOBALS.has(n) && !PAGE_GLOBALS.has(n)
8924
+ ).sort();
8925
+ }
8926
+ function buildProgram(program, file, text) {
8927
+ const problems = [];
8928
+ const sources = {};
8929
+ const stringify = (fn, name) => {
8930
+ if (fn === void 0) return void 0;
8931
+ if (typeof fn === "string") return fn;
8932
+ if (typeof fn !== "function") {
8933
+ problems.push({
8934
+ fn: name,
8935
+ message: `must be a function, got ${typeof fn}`
8936
+ });
8937
+ return void 0;
8938
+ }
8939
+ const raw = fn.toString();
8940
+ const src = functionSource(fn);
8941
+ const at2 = text.indexOf(raw);
8942
+ sources[name] = at2 >= 0 ? { file, line: text.slice(0, at2).split("\n").length } : null;
8943
+ let free;
8944
+ try {
8945
+ free = freeIdentifiers(src);
8946
+ } catch (e) {
8947
+ problems.push({
8948
+ fn: name,
8949
+ message: `does not parse as JavaScript the page can run: ${e.message}`
8950
+ });
8951
+ return src;
8952
+ }
8953
+ for (const n of free) {
8954
+ problems.push({
8955
+ fn: name,
8956
+ message: `reads \`${n}\` from outside the function; the string travels without it. Move it inside the function, or put its value in data and read ctx.data.`
8957
+ });
8958
+ }
8959
+ return src;
8960
+ };
8961
+ const out = {};
8962
+ for (const [key, value] of Object.entries(program)) {
8963
+ if (FUNCTION_KEYS.includes(key)) {
8964
+ const s = stringify(value, key);
8965
+ if (s !== void 0) out[key] = s;
8966
+ } else if (key === "stack" && Array.isArray(value)) {
8967
+ out.stack = value.map((entry, i) => {
8968
+ const e = { ...entry };
8969
+ for (const k of FUNCTION_KEYS) {
8970
+ if (k in e) {
8971
+ const s = stringify(e[k], `stack[${String(entry.id ?? i)}].${k}`);
8972
+ if (s !== void 0) e[k] = s;
8973
+ }
8974
+ }
8975
+ return e;
8976
+ });
8977
+ } else {
8978
+ out[key] = value;
8979
+ }
8980
+ }
8981
+ for (const key of Object.keys(out)) {
8982
+ if (FUNCTION_KEYS.includes(key) || key === "stack")
8983
+ continue;
8984
+ const bad = nonJson(out[key], key);
8985
+ if (bad) problems.push({ fn: key, message: bad });
8986
+ }
8987
+ return { config: out, sources, problems };
8988
+ }
8989
+ function nonJson(value, path) {
8990
+ if (value === null) return null;
8991
+ const t = typeof value;
8992
+ if (t === "function")
8993
+ return `${path} is a function; only the hooks (${FUNCTION_KEYS.join(", ")}) may be`;
8994
+ if (t === "number" && !Number.isFinite(value))
8995
+ return `${path} is ${String(value)}, which JSON cannot carry`;
8996
+ if (t === "undefined" || t === "symbol" || t === "bigint")
8997
+ return `${path} is ${t}, which JSON cannot carry`;
8998
+ if (Array.isArray(value)) {
8999
+ for (let i = 0; i < value.length; i++) {
9000
+ const r = nonJson(value[i], `${path}[${i}]`);
9001
+ if (r) return r;
9002
+ }
9003
+ return null;
9004
+ }
9005
+ if (t === "object") {
9006
+ const proto = Object.getPrototypeOf(value);
9007
+ if (proto !== Object.prototype && proto !== null)
9008
+ return `${path} is a ${proto?.constructor?.name ?? "class"} instance; use plain objects and arrays`;
9009
+ for (const [k, v] of Object.entries(value)) {
9010
+ const r = nonJson(v, `${path}.${k}`);
9011
+ if (r) return r;
9012
+ }
9013
+ }
9014
+ return null;
9015
+ }
8819
9016
  var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set(["json", "help"]);
9017
+ async function cmdBuild(argv) {
9018
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
9019
+ const source = positionals[0];
9020
+ if (!source)
9021
+ throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
9022
+ const r = createReporter(flags.json === true);
9023
+ const file = resolve7(source);
9024
+ const text = readFileSync4(file, "utf8");
9025
+ const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
9026
+ const program = mod.default ?? mod.program;
9027
+ if (!program || typeof program !== "object")
9028
+ throw new UsageError(
9029
+ `${source} exports no program: \`export default { version: 2, duration, camera, createContent, createTimeline, \u2026 }\``
9030
+ );
9031
+ const built = buildProgram(program, basename6(file), text);
9032
+ for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
9033
+ if (built.problems.length) {
9034
+ r.done(
9035
+ { ok: false, problems: built.problems },
9036
+ `${source}: ${built.problems.length} problem${built.problems.length === 1 ? "" : "s"}, nothing written`
9037
+ );
9038
+ return EXIT_ERROR;
9039
+ }
9040
+ const out = resolve7(
9041
+ strFlag(flags, "out") ?? join19(dirname5(file), "config.json")
9042
+ );
9043
+ writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
9044
+ const sidecar = out.replace(/\.json$/, "") + ".sources.json";
9045
+ writeFileSync2(sidecar, JSON.stringify(built.sources, null, 2) + "\n");
9046
+ for (const [fn, at2] of Object.entries(built.sources)) {
9047
+ if (at2) r.log(` ${fn} \u2192 ${at2.file}:${at2.line}`);
9048
+ }
9049
+ r.done(
9050
+ { ok: true, out, sources: sidecar, functions: Object.keys(built.sources) },
9051
+ `Wrote ${out} (${Object.keys(built.sources).length} functions; an engine error at fn:N is that function's line N, from the line above)`
9052
+ );
9053
+ return EXIT_OK;
9054
+ }
9055
+
9056
+ // src/plugin/compare.ts
9057
+ import { spawnSync } from "child_process";
9058
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
9059
+ import { join as join20, resolve as resolve8 } from "path";
9060
+ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set(["json", "help"]);
9061
+ function sampleTimes(duration, every) {
9062
+ const step = every > 0 ? every : 1;
9063
+ const out = [];
9064
+ for (let t = step / 2; t < duration; t += step) out.push(Number(t.toFixed(3)));
9065
+ return out.length ? out : [Number((duration / 2).toFixed(3))];
9066
+ }
9067
+ function parseSsim(stderr) {
9068
+ const m = /All:([0-9.]+)/.exec(stderr);
9069
+ return m ? Number(m[1]) : null;
9070
+ }
9071
+ function compareVerdict(scores, threshold) {
9072
+ const values = scores.map((s) => s.ssim);
9073
+ const min = values.length ? Math.min(...values) : 0;
9074
+ const mean = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
9075
+ const failing = scores.filter((s) => s.ssim < threshold).map((s) => s.t);
9076
+ return { min, mean, failing, pass: failing.length === 0 && values.length > 0 };
9077
+ }
9078
+ function ffmpeg(args) {
9079
+ const res = spawnSync("ffmpeg", ["-hide_banner", ...args], {
9080
+ encoding: "utf8"
9081
+ });
9082
+ return { ok: res.status === 0, stderr: res.stderr ?? "" };
9083
+ }
9084
+ function requireFfmpeg() {
9085
+ const res = spawnSync("ffmpeg", ["-version"], { encoding: "utf8" });
9086
+ if (res.status !== 0)
9087
+ throw new UsageError(
9088
+ "vos compare reads frames with ffmpeg, and none is on PATH: install it (brew install ffmpeg, apt install ffmpeg) and run again"
9089
+ );
9090
+ }
9091
+ function probeSize(video) {
9092
+ const res = spawnSync(
9093
+ "ffprobe",
9094
+ [
9095
+ "-v",
9096
+ "error",
9097
+ "-select_streams",
9098
+ "v:0",
9099
+ "-show_entries",
9100
+ "stream=width,height",
9101
+ "-of",
9102
+ "csv=p=0",
9103
+ video
9104
+ ],
9105
+ { encoding: "utf8" }
9106
+ );
9107
+ const m = /(\d+),(\d+)/.exec(res.stdout ?? "");
9108
+ return m ? { width: Number(m[1]), height: Number(m[2]) } : null;
9109
+ }
9110
+ async function cmdCompare(argv) {
9111
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
9112
+ const source = positionals[0];
9113
+ const against = strFlag(flags, "against");
9114
+ if (!source || !against)
9115
+ throw new UsageError(
9116
+ "vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--width W --height H] [--threshold 0.95] [--out compare] [--json]"
9117
+ );
9118
+ requireFfmpeg();
9119
+ const r = createReporter(flags.json === true);
9120
+ const ref = resolve8(against);
9121
+ const loaded = await loadVosConfig(source);
9122
+ for (const w of loaded.warnings) r.log(`note: ${w}`);
9123
+ const config = loaded.config;
9124
+ const duration = configDuration(config) ?? 5;
9125
+ const probed = probeSize(ref);
9126
+ const half = probed && probed.width > 1280 ? 2 : 1;
9127
+ const width = Number(
9128
+ strFlag(flags, "width") ?? (probed ? probed.width / half : 960)
9129
+ );
9130
+ const height = Number(
9131
+ strFlag(flags, "height") ?? (probed ? probed.height / half : 540)
9132
+ );
9133
+ const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
9134
+ const timesRaw = strFlag(flags, "times");
9135
+ const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
9136
+ const outDir = resolve8(strFlag(flags, "out") ?? "compare");
9137
+ mkdirSync2(outDir, { recursive: true });
9138
+ const browser = await launchBrowser();
9139
+ const scores = [];
9140
+ try {
9141
+ for (const t of times) {
9142
+ const tag = t.toFixed(2);
9143
+ const refPng = join20(outDir, `src-${tag}.png`);
9144
+ const vosPng = join20(outDir, `vos-${tag}.png`);
9145
+ const sheet = join20(outDir, `sheet-${tag}.png`);
9146
+ const grab = ffmpeg([
9147
+ "-v",
9148
+ "error",
9149
+ "-y",
9150
+ "-ss",
9151
+ String(t),
9152
+ "-i",
9153
+ ref,
9154
+ "-frames:v",
9155
+ "1",
9156
+ "-vf",
9157
+ `scale=${width}:${height}`,
9158
+ refPng
9159
+ ]);
9160
+ if (!grab.ok)
9161
+ throw new Error(
9162
+ `could not read ${against} at ${tag}s: ${grab.stderr.trim()}`
9163
+ );
9164
+ const still = await renderStill(browser, {
9165
+ config,
9166
+ width,
9167
+ height,
9168
+ time: t
9169
+ });
9170
+ writeFileSync3(vosPng, await reencodeStill(browser, still.bytes, "png"));
9171
+ const pair = ["-i", refPng, "-i", vosPng];
9172
+ const ssim = parseSsim(
9173
+ ffmpeg([
9174
+ ...pair,
9175
+ "-lavfi",
9176
+ "[0:v]format=rgb24[x];[1:v]format=rgb24[y];[x][y]ssim",
9177
+ "-f",
9178
+ "null",
9179
+ "-"
9180
+ ]).stderr
9181
+ );
9182
+ ffmpeg([
9183
+ "-v",
9184
+ "error",
9185
+ "-y",
9186
+ ...pair,
9187
+ "-filter_complex",
9188
+ "[0:v]format=gbrp[x];[1:v]format=gbrp[y];[x]split[x1][x2];[y]split[y1][y2];[x1][y1]blend=all_mode=difference[d];[x2][y2][d]hstack=inputs=3,format=rgb24",
9189
+ sheet
9190
+ ]);
9191
+ const score = { t, ssim: ssim ?? 0, sheet };
9192
+ scores.push(score);
9193
+ const mark = score.ssim < threshold ? " \u2190 under" : "";
9194
+ r.log(`t=${tag}s SSIM ${score.ssim.toFixed(3)} ${sheet}${mark}`);
9195
+ for (const w of still.pageErrors)
9196
+ r.log(`warn: t=${tag}s the page threw: ${w}`);
9197
+ }
9198
+ } finally {
9199
+ await browser.close();
9200
+ }
9201
+ const overview = join20(outDir, "overview.png");
9202
+ if (scores.length) {
9203
+ const inputs = scores.flatMap((s) => ["-i", s.sheet]);
9204
+ const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
9205
+ const stack = scores.map((_, i) => `[s${i}]`).join("");
9206
+ ffmpeg([
9207
+ "-v",
9208
+ "error",
9209
+ "-y",
9210
+ ...inputs,
9211
+ "-filter_complex",
9212
+ scores.length > 1 ? `${scaled};${stack}vstack=inputs=${scores.length}` : `${scaled}`.replace("[s0]", ""),
9213
+ overview
9214
+ ]);
9215
+ }
9216
+ const verdict = compareVerdict(scores, threshold);
9217
+ const report = {
9218
+ against: ref,
9219
+ width,
9220
+ height,
9221
+ threshold,
9222
+ ...verdict,
9223
+ frames: scores,
9224
+ overview
9225
+ };
9226
+ writeFileSync3(
9227
+ join20(outDir, "report.json"),
9228
+ JSON.stringify(report, null, 2) + "\n"
9229
+ );
9230
+ r.done(
9231
+ report,
9232
+ `${scores.length} frames: mean SSIM ${verdict.mean.toFixed(3)}, lowest ${verdict.min.toFixed(3)}${verdict.failing.length ? `; under ${threshold} at ${verdict.failing.map((t) => `${t}s`).join(", ")}: look at those sheets` : `; every frame at or above ${threshold}`}. Sheets (source | vos | difference) and overview.png in ${outDir}`
9233
+ );
9234
+ return verdict.pass ? EXIT_OK : EXIT_ERROR;
9235
+ }
9236
+
9237
+ // src/plugin/port.ts
9238
+ import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
9239
+ import { dirname as dirname8, join as join23, resolve as resolve11 } from "path";
9240
+ import { pathToFileURL as pathToFileURL3 } from "url";
9241
+
9242
+ // src/plugin/portInventory.ts
9243
+ import { spawnSync as spawnSync2 } from "child_process";
9244
+ import {
9245
+ existsSync as existsSync17,
9246
+ mkdirSync as mkdirSync3,
9247
+ readFileSync as readFileSync5,
9248
+ readdirSync as readdirSync4,
9249
+ statSync as statSync5
9250
+ } from "fs";
9251
+ import { basename as basename7, dirname as dirname6, join as join21, relative as relative2, resolve as resolve9 } from "path";
9252
+ import { pathToFileURL as pathToFileURL2 } from "url";
9253
+ import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
9254
+ function detectEngine(dir) {
9255
+ if (existsSync17(join21(dir, "hyperframes.json"))) return "hyperframes";
9256
+ const pkg = join21(dir, "package.json");
9257
+ if (existsSync17(pkg)) {
9258
+ try {
9259
+ const p = JSON.parse(readFileSync5(pkg, "utf8"));
9260
+ const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
9261
+ if ("remotion" in deps) return "remotion";
9262
+ if ("hyperframes" in deps) return "hyperframes";
9263
+ if (/hyperframes/.test(JSON.stringify(p.scripts ?? {})))
9264
+ return "hyperframes";
9265
+ } catch {
9266
+ }
9267
+ }
9268
+ return "page";
9269
+ }
9270
+ function sourcePage(source) {
9271
+ if (!existsSync17(source)) return null;
9272
+ if (statSync5(source).isFile()) return /\.html?$/i.test(source) ? source : null;
9273
+ const index = join21(source, "index.html");
9274
+ return existsSync17(index) ? index : null;
9275
+ }
9276
+ function findRender(dir) {
9277
+ const videos = (d) => existsSync17(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join21(d, f)) : [];
9278
+ for (const d of [join21(dir, "renders"), join21(dir, "out"), dir]) {
9279
+ const found = videos(d);
9280
+ if (found.length) return found[0];
9281
+ }
9282
+ return null;
9283
+ }
9284
+ function scriptStringsOf(code) {
9285
+ const out = /* @__PURE__ */ new Set();
9286
+ for (const m of code.matchAll(/(['"`])((?:(?!\1)[^\\\n]|\\.){2,60})\1/g)) {
9287
+ const s = m[2];
9288
+ if (!/[A-Za-z]{2}/.test(s)) continue;
9289
+ if (/["'`]/.test(s)) continue;
9290
+ if (/\b[a-zA-Z_$][\w$]*\s*:\s*[\w$.]+\s*,/.test(s)) continue;
9291
+ if (/^-?[\d.]+(px|%|ms|s|deg|em|rem|vh|vw)?$/.test(s)) continue;
9292
+ if (/^[a-z]+\/[a-z0-9.+-]+$/.test(s)) continue;
9293
+ if (/^(source|destination)-(over|in|out|atop)$|^(lighter|copy|xor|multiply|screen|overlay|darken|lighten|difference|exclusion)$/.test(
9294
+ s
9295
+ ))
9296
+ continue;
9297
+ if (/^[A-Za-z ]{4}$/.test(s) && /^(RIFF|WAVE|fmt |data|LIST)$/.test(s))
9298
+ continue;
9299
+ if (/[(){};=<>]|\$\{|^\.?\/|https?:|\.(js|css|png|jpe?g|mp[34]|wav|woff2?)$/i.test(
9300
+ s
9301
+ ))
9302
+ continue;
9303
+ if (/^[a-z][a-zA-Z0-9]*$/.test(s) && !/[A-Z]{2}/.test(s)) continue;
9304
+ if (/^(rgba?|hsla?)\(|^#[0-9a-f]{3,8}$/i.test(s)) continue;
9305
+ out.add(s);
9306
+ }
9307
+ return [...out].slice(0, 80);
9308
+ }
9309
+ function scriptScenes(ids, code, duration) {
9310
+ const found = /* @__PURE__ */ new Map();
9311
+ for (const id of ids) {
9312
+ const re = new RegExp(
9313
+ `\\.(?:set|to|fromTo)\\(\\s*["'\`]#${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'\`][^)]*?(?:opacity|autoAlpha)\\s*:\\s*1[^)]*?,\\s*([\\d.]+)\\s*\\)`
9314
+ );
9315
+ const m = re.exec(code);
9316
+ if (m) found.set(id, Number(m[1]));
9317
+ else if (!found.size && ids[0] === id) found.set(id, 0);
9318
+ }
9319
+ for (const m of code.matchAll(
9320
+ /addLabel\(\s*["'`]([^"'`]+)["'`]\s*,\s*([\d.]+)/g
9321
+ )) {
9322
+ if (!found.has(m[1])) found.set(m[1], Number(m[2]));
9323
+ }
9324
+ const sorted = [...found.entries()].sort((a, b) => a[1] - b[1]);
9325
+ return sorted.map(([name, start], i) => {
9326
+ const next = sorted[i + 1]?.[1] ?? duration;
9327
+ return {
9328
+ name,
9329
+ start,
9330
+ duration: next == null ? null : Math.round((next - start) * 1e3) / 1e3
9331
+ };
9332
+ });
9333
+ }
9334
+ function scriptFontFamilies(code) {
9335
+ const out = /* @__PURE__ */ new Set();
9336
+ const generic = /,\s*(?:sans-serif|serif|monospace|cursive|system-ui)\b/;
9337
+ const shorthand = /^(?:(?:italic|oblique|normal|bold|bolder|lighter|small-caps|\d{3})\s+)*[\d.]+(?:px|pt|em|rem|%)(?:\/[\d.]+\w*)?\s+/;
9338
+ for (const m of code.matchAll(/(['"`])((?:\\.|(?!\1).)*?)\1/g)) {
9339
+ const s = m[2];
9340
+ if (!generic.test(s)) continue;
9341
+ const first = s.replace(shorthand, "").split(",")[0].trim().replace(/^["']|["']$/g, "");
9342
+ if (first && !/^(sans-serif|serif|monospace|cursive|system-ui)$/.test(first))
9343
+ out.add(first);
9344
+ }
9345
+ return [...out];
9346
+ }
9347
+ function catalogFamily2(family) {
9348
+ const clean = family.trim().replace(/^['"]|['"]$/g, "");
9349
+ return findFontFamily3(clean)?.family ?? null;
9350
+ }
9351
+ function probe(video) {
9352
+ const res = spawnSync2(
9353
+ "ffprobe",
9354
+ [
9355
+ "-v",
9356
+ "error",
9357
+ "-select_streams",
9358
+ "v:0",
9359
+ "-show_entries",
9360
+ "stream=r_frame_rate:format=duration",
9361
+ "-of",
9362
+ "json",
9363
+ video
9364
+ ],
9365
+ { encoding: "utf8" }
9366
+ );
9367
+ try {
9368
+ const j = JSON.parse(res.stdout ?? "{}");
9369
+ const [n, d] = String(j.streams?.[0]?.r_frame_rate ?? "").split("/").map(Number);
9370
+ const fps = n && d ? Math.round(n / d * 100) / 100 : null;
9371
+ const duration = Number(j.format?.duration);
9372
+ return { fps, duration: Number.isFinite(duration) ? duration : null };
9373
+ } catch {
9374
+ return { fps: null, duration: null };
9375
+ }
9376
+ }
9377
+ var READ_PAGE = `(() => {
9378
+ const root = document.querySelector('[data-composition-id], [data-width][data-height]') || document.body
9379
+ const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
9380
+ const canvases = [...document.querySelectorAll('canvas')].map((c) => ({ width: c.width, height: c.height }))
9381
+ const biggest = canvases.slice().sort((a, b) => b.width * b.height - a.width * a.height)[0]
9382
+ const width = num(root.dataset && root.dataset.width) || (biggest && biggest.width) || window.innerWidth
9383
+ const height = num(root.dataset && root.dataset.height) || (biggest && biggest.height) || window.innerHeight
9384
+ const rootRect = root.getBoundingClientRect()
9385
+ const sx = rootRect.width ? width / rootRect.width : 1
9386
+ const sy = rootRect.height ? height / rootRect.height : 1
9387
+
9388
+ const texts = []
9389
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
9390
+ const seen = new Set()
9391
+ let n = 0
9392
+ while (walker.nextNode()) {
9393
+ const node = walker.currentNode
9394
+ const text = (node.textContent || '').replace(/\\s+/g, ' ').trim()
9395
+ const el = node.parentElement
9396
+ if (!text || !el || ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TITLE'].includes(el.tagName)) continue
9397
+ if (seen.has(el)) continue
9398
+ seen.add(el)
9399
+ const cs = getComputedStyle(el)
9400
+ const r = el.getBoundingClientRect()
9401
+ const box = r.width && r.height ? {
9402
+ x: Math.round((r.left - rootRect.left) * sx), y: Math.round((r.top - rootRect.top) * sy),
9403
+ width: Math.round(r.width * sx), height: Math.round(r.height * sy) } : null
9404
+ const ls = parseFloat(cs.letterSpacing)
9405
+ el.setAttribute('data-vos-inv', String(texts.length))
9406
+ const sceneEl = el.closest('.scene, [data-scene], section[id]')
9407
+ texts.push({
9408
+ id: el.id || (el.className && typeof el.className === 'string' ? el.className.split(' ')[0] : '') || el.tagName.toLowerCase() + '-' + (n++),
9409
+ text, tag: el.tagName.toLowerCase(), box, scene: sceneEl && sceneEl.id ? sceneEl.id : null,
9410
+ font: { family: cs.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g, ''),
9411
+ size: Math.round(parseFloat(cs.fontSize) * sy), weight: Number(cs.fontWeight) || 400,
9412
+ style: cs.fontStyle, color: cs.color, letterSpacing: Number.isFinite(ls) ? Math.round(ls * sy * 100) / 100 : 0,
9413
+ transform: cs.textTransform },
9414
+ })
9415
+ }
9416
+
9417
+ const palette = {}
9418
+ const isColor = (v) => /^(#[0-9a-f]{3,8}|rgba?\\(|hsla?\\(|oklch\\()/i.test(v)
9419
+ const scan = (style) => { for (let i = 0; i < style.length; i++) { const p = style[i]; if (p.startsWith('--')) { const v = style.getPropertyValue(p).trim(); if (isColor(v)) palette[p.slice(2)] = v } } }
9420
+ for (const sheet of document.styleSheets) {
9421
+ let rules
9422
+ try { rules = sheet.cssRules } catch (e) { continue }
9423
+ for (const rule of rules) if (rule.style) scan(rule.style)
9424
+ }
9425
+ scan(root.style)
9426
+
9427
+ const colors = new Set()
9428
+ const gaps = new Set()
9429
+ for (const el of document.querySelectorAll('body *')) {
9430
+ const cs = getComputedStyle(el)
9431
+ if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)') colors.add(cs.backgroundColor)
9432
+ if (cs.mixBlendMode && cs.mixBlendMode !== 'normal') gaps.add('mix-blend-mode: ' + cs.mixBlendMode)
9433
+ if (cs.clipPath && cs.clipPath !== 'none') gaps.add('clip-path')
9434
+ if (cs.maskImage && cs.maskImage !== 'none') gaps.add('mask')
9435
+ if (cs.overflow === 'hidden' && el.children.length && el.textContent.trim()) gaps.add('overflow: hidden (a text mask)')
9436
+ if (cs.filter && cs.filter !== 'none') gaps.add('filter: ' + cs.filter.split('(')[0])
9437
+ if (cs.backdropFilter && cs.backdropFilter !== 'none') gaps.add('backdrop-filter')
9438
+ if (cs.perspective && cs.perspective !== 'none') gaps.add('3D perspective')
9439
+ if (cs.webkitTextStrokeWidth && parseFloat(cs.webkitTextStrokeWidth) > 0) gaps.add('-webkit-text-stroke (a stroke: see the reference for its width)')
9440
+ }
9441
+ for (const t of texts) colors.add(t.font.color)
9442
+
9443
+ const media = []
9444
+ const attrs = (el) => ({ start: num(el.dataset.start), duration: num(el.dataset.duration), volume: num(el.dataset.volume), mediaStart: num(el.dataset.mediaStart) })
9445
+ for (const el of document.querySelectorAll('img')) media.push({ kind: 'image', src: el.getAttribute('src'), ...attrs(el) })
9446
+ for (const el of document.querySelectorAll('svg')) if (!el.closest('svg svg')) media.push({ kind: 'svg', src: null, ...attrs(el) })
9447
+ for (const el of document.querySelectorAll('video')) media.push({ kind: 'video', src: el.getAttribute('src') || (el.querySelector('source') && el.querySelector('source').getAttribute('src')), ...attrs(el) })
9448
+ for (const el of document.querySelectorAll('audio')) media.push({ kind: 'audio', src: el.getAttribute('src') || (el.querySelector('source') && el.querySelector('source').getAttribute('src')), ...attrs(el) })
9449
+
9450
+ const scenes = []
9451
+ for (const el of document.querySelectorAll('[data-start]')) {
9452
+ if (['AUDIO', 'VIDEO', 'IMG'].includes(el.tagName)) continue
9453
+ scenes.push({ name: el.id || el.dataset.compositionId || (typeof el.className === 'string' ? el.className.split(' ')[0] : '') || el.tagName.toLowerCase(), start: num(el.dataset.start) || 0, duration: num(el.dataset.duration) })
9454
+ }
9455
+ const sceneEls = [...document.querySelectorAll('.scene, [data-scene], section[id]')]
9456
+ .filter((el) => el.id)
9457
+ .map((el) => el.id)
9458
+ let variables = {}
9459
+ try { variables = JSON.parse(root.dataset.compositionVariables || '{}') } catch (e) {}
9460
+ const scriptText = [...document.querySelectorAll('script:not([src])')].map((s) => s.textContent).join('\\n')
9461
+ return {
9462
+ width, height, fps: num(root.dataset && root.dataset.fps), duration: num(root.dataset && root.dataset.duration),
9463
+ texts, palette, colors: [...colors], gaps: [...gaps], media, canvases, scenes, sceneEls, variables, scriptText,
9464
+ }
9465
+ })()`;
9466
+ var SEEK_AND_READ = `((args) => {
9467
+ const tls = window.__timelines ? Object.values(window.__timelines) : []
9468
+ if (!tls.length) return null
9469
+ for (const tl of tls) { try { tl.pause(); tl.seek(args.t, false) } catch (e) {} }
9470
+ const root = document.querySelector('[data-composition-id], [data-width][data-height]') || document.body
9471
+ const rr = root.getBoundingClientRect()
9472
+ const sx = rr.width ? args.width / rr.width : 1
9473
+ const sy = rr.height ? args.height / rr.height : 1
9474
+ const out = {}
9475
+ for (const i of args.indices) {
9476
+ const el = document.querySelector('[data-vos-inv="' + i + '"]')
9477
+ if (!el) continue
9478
+ const r = el.getBoundingClientRect()
9479
+ if (r.width && r.height) out[i] = { x: Math.round((r.left - rr.left) * sx), y: Math.round((r.top - rr.top) * sy), width: Math.round(r.width * sx), height: Math.round(r.height * sy) }
9480
+ }
9481
+ return out
9482
+ })`;
9483
+ async function readInventory(source, opts) {
9484
+ const abs = resolve9(source);
9485
+ const dir = existsSync17(abs) && statSync5(abs).isDirectory() ? abs : dirname6(abs);
9486
+ const engine = detectEngine(dir);
9487
+ const page = sourcePage(abs);
9488
+ const render = opts.render === void 0 ? findRender(dir) : opts.render;
9489
+ let read = {
9490
+ width: 1920,
9491
+ height: 1080,
9492
+ fps: null,
9493
+ duration: null,
9494
+ texts: [],
9495
+ palette: {},
9496
+ colors: [],
9497
+ gaps: [],
9498
+ media: [],
9499
+ canvases: [],
9500
+ scenes: [],
9501
+ variables: {},
9502
+ scriptText: ""
9503
+ };
9504
+ const fromRender = render ? probe(render) : { fps: null, duration: null };
9505
+ let fps = fromRender.fps;
9506
+ let duration = fromRender.duration;
9507
+ let settled = 0;
9508
+ if (page) {
9509
+ const browser = await launchBrowser();
9510
+ try {
9511
+ const ctx = await browser.newContext({
9512
+ viewport: { width: 1920, height: 1080 }
9513
+ });
9514
+ const p = await ctx.newPage();
9515
+ await p.addInitScript("window.__timelines = window.__timelines || {}");
9516
+ await p.goto(pathToFileURL2(page).href, { waitUntil: "load" });
9517
+ await p.evaluate("document.fonts && document.fonts.ready");
9518
+ await p.waitForTimeout(300);
9519
+ read = await p.evaluate(READ_PAGE);
9520
+ fps = read.fps ?? fps;
9521
+ duration = read.duration ?? duration;
9522
+ const scripted = scriptScenes(
9523
+ read.sceneEls ?? [],
9524
+ read.scriptText ?? "",
9525
+ duration
9526
+ );
9527
+ if (scripted.length > (read.scenes?.length ?? 0)) read.scenes = scripted;
9528
+ for (const s of read.scenes) {
9529
+ const indices = read.texts.map((t2, i) => t2.scene === s.name ? i : -1).filter((i) => i >= 0);
9530
+ if (!indices.length) continue;
9531
+ const t = s.start + (s.duration ?? 1) * 0.75;
9532
+ const boxes = await p.evaluate(
9533
+ `${SEEK_AND_READ}(${JSON.stringify({ t, indices, width: read.width, height: read.height })})`
9534
+ );
9535
+ if (!boxes) break;
9536
+ for (const [i, box] of Object.entries(boxes)) {
9537
+ read.texts[Number(i)].box = box;
9538
+ settled++;
9539
+ }
9540
+ }
9541
+ await ctx.close();
9542
+ } finally {
9543
+ await browser.close();
9544
+ }
9545
+ } else if (engine === "remotion") {
9546
+ opts.log?.(
9547
+ "a Remotion project: its words and colours live in its source (src/), which the agent reads; the inventory records the render and its stills"
9548
+ );
9549
+ }
9550
+ if (settled)
9551
+ opts.log?.(
9552
+ `${settled} words laid out where they settle in their scene (the piece's own timelines, seeked)`
9553
+ );
9554
+ const families = /* @__PURE__ */ new Map();
9555
+ for (const t of read.texts) {
9556
+ const f = families.get(t.font.family) ?? {
9557
+ family: t.font.family,
9558
+ weights: [],
9559
+ italic: false,
9560
+ catalog: catalogFamily2(t.font.family)
9561
+ };
9562
+ if (!f.weights.includes(t.font.weight)) f.weights.push(t.font.weight);
9563
+ if (t.font.style === "italic") f.italic = true;
9564
+ families.set(t.font.family, f);
9565
+ }
9566
+ for (const family of scriptFontFamilies(read.scriptText ?? "")) {
9567
+ if (!families.has(family))
9568
+ families.set(family, {
9569
+ family,
9570
+ weights: [],
9571
+ italic: false,
9572
+ catalog: catalogFamily2(family)
9573
+ });
9574
+ }
9575
+ const stills = [];
9576
+ if (render && duration) {
9577
+ const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
9578
+ if (has) {
9579
+ const refDir = join21(opts.outDir, "ref");
9580
+ mkdirSync3(refDir, { recursive: true });
9581
+ const times = read.scenes.length >= 3 ? read.scenes.map(
9582
+ (s) => s.start + (s.duration ?? 1) / 2
9583
+ ) : Array.from(
9584
+ { length: Math.max(1, Math.floor(duration)) },
9585
+ (_, i) => i + 0.5
9586
+ );
9587
+ for (const t of times.filter((t2) => t2 < duration)) {
9588
+ const file = join21(refDir, `src-${t.toFixed(2)}.png`);
9589
+ const ok = spawnSync2("ffmpeg", [
9590
+ "-v",
9591
+ "error",
9592
+ "-y",
9593
+ "-ss",
9594
+ String(t),
9595
+ "-i",
9596
+ render,
9597
+ "-frames:v",
9598
+ "1",
9599
+ "-vf",
9600
+ "scale=960:-2",
9601
+ file
9602
+ ]).status === 0;
9603
+ if (ok)
9604
+ stills.push({
9605
+ t: Number(t.toFixed(2)),
9606
+ file: relative2(opts.outDir, file)
9607
+ });
9608
+ }
9609
+ } else {
9610
+ opts.log?.(
9611
+ "no ffmpeg on PATH: no reference stills (install it to get them)"
9612
+ );
9613
+ }
9614
+ }
9615
+ const scriptStrings = scriptStringsOf(read.scriptText);
9616
+ return {
9617
+ source: relative2(process.cwd(), abs) || basename7(abs),
9618
+ engine,
9619
+ width: read.width,
9620
+ height: read.height,
9621
+ fps,
9622
+ duration,
9623
+ scenes: read.scenes,
9624
+ texts: read.texts,
9625
+ palette: read.palette,
9626
+ colors: read.colors,
9627
+ fonts: [...families.values()],
9628
+ media: read.media,
9629
+ canvases: read.canvases,
9630
+ gaps: read.gaps,
9631
+ variables: read.variables,
9632
+ scriptStrings,
9633
+ render: render ? relative2(process.cwd(), render) : null,
9634
+ stills
9635
+ };
9636
+ }
9637
+
9638
+ // src/plugin/portScaffold.ts
9639
+ import { spawnSync as spawnSync3 } from "child_process";
9640
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
9641
+ import { basename as basename8, dirname as dirname7, join as join22, resolve as resolve10 } from "path";
9642
+ import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
9643
+ var DESIGN_HEIGHT = 1080;
9644
+ function toHex(css) {
9645
+ const s = css.trim();
9646
+ if (/^#[0-9a-f]{3,8}$/i.test(s)) {
9647
+ if (s.length === 4)
9648
+ return `#${[...s.slice(1)].map((c) => c + c).join("")}`.toLowerCase();
9649
+ return s.toLowerCase();
9650
+ }
9651
+ const m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,/\s]+([\d.]+%?))?\s*\)$/i.exec(
9652
+ s
9653
+ );
9654
+ if (!m) return s;
9655
+ const hex2 = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
9656
+ let a = m[4] === void 0 ? 1 : m[4].endsWith("%") ? Number(m[4].slice(0, -1)) / 100 : Number(m[4]);
9657
+ if (!Number.isFinite(a)) a = 1;
9658
+ return `#${hex2(+m[1])}${hex2(+m[2])}${hex2(+m[3])}${a < 1 ? hex2(a * 255) : ""}`;
9659
+ }
9660
+ function keyFor(label, taken, prefix = "t") {
9661
+ const words = label.replace(/[^A-Za-z0-9]+/g, " ").trim().split(" ").filter(Boolean).slice(0, 4);
9662
+ let base = words.map(
9663
+ (w, i) => i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase()
9664
+ ).join("");
9665
+ if (!base || /^[0-9]/.test(base)) base = prefix + (base || "");
9666
+ let key = base;
9667
+ for (let n = 2; taken.has(key); n++) key = `${base}${n}`;
9668
+ taken.add(key);
9669
+ return key;
9670
+ }
9671
+ function substituteFamily(family) {
9672
+ const f = family.toLowerCase();
9673
+ if (/mono|code|courier/.test(f)) return "JetBrains Mono";
9674
+ if (/serif/.test(f) && !/sans/.test(f)) return "Source Serif 4";
9675
+ if (/condensed|compressed|black|anton|bebas|oswald/.test(f)) return "Anton";
9676
+ return "Inter";
9677
+ }
9678
+ var fmtPct = (v) => `${Math.round(v * 1e4) / 100}%`;
9679
+ function planScaffold(inv, opts) {
9680
+ const taken = /* @__PURE__ */ new Set(["scenes", "bg"]);
9681
+ const data = {};
9682
+ const report = [];
9683
+ const scale = DESIGN_HEIGHT / (inv.height || DESIGN_HEIGHT);
9684
+ const colorKey = /* @__PURE__ */ new Map();
9685
+ for (const [name, value] of Object.entries(inv.palette)) {
9686
+ const key = keyFor(name, taken, "color");
9687
+ const hex2 = toHex(value);
9688
+ data[key] = hex2;
9689
+ if (!colorKey.has(hex2)) colorKey.set(hex2, key);
9690
+ }
9691
+ const colorOf = (css) => {
9692
+ const hex2 = toHex(css);
9693
+ const known = colorKey.get(hex2);
9694
+ if (known) return known;
9695
+ const key = keyFor(`color ${colorKey.size + 1}`, taken, "color");
9696
+ data[key] = hex2;
9697
+ colorKey.set(hex2, key);
9698
+ return key;
9699
+ };
9700
+ data.bg = inv.colors.length ? toHex(inv.colors[0]) : "#000000";
9701
+ const faces = /* @__PURE__ */ new Map();
9702
+ const faceFor = (t) => {
9703
+ const hit = findFontFamily4(t.font.family);
9704
+ const family = hit?.family ?? substituteFamily(t.font.family);
9705
+ const face = faces.get(family) ?? {
9706
+ family,
9707
+ weights: /* @__PURE__ */ new Set(),
9708
+ italic: false,
9709
+ from: hit ? null : t.font.family
9710
+ };
9711
+ face.weights.add(t.font.weight);
9712
+ if (t.font.style === "italic") face.italic = true;
9713
+ faces.set(family, face);
9714
+ return family;
9715
+ };
9716
+ const elements = [];
9717
+ const textKeys = [];
9718
+ const sceneNames = new Set(inv.scenes.map((s) => s.name));
9719
+ const sceneElements = {};
9720
+ for (const t of inv.texts) {
9721
+ const generic = /^(div|span|p|h[1-6]|li|a|b|i|em|strong)-\d+$/.test(t.id);
9722
+ const key = keyFor(generic ? t.text : t.id, taken);
9723
+ const words = t.font.transform === "uppercase" ? t.text.toUpperCase() : t.font.transform === "lowercase" ? t.text.toLowerCase() : t.text;
9724
+ data[key] = words;
9725
+ const family = faceFor(t);
9726
+ elements.push({
9727
+ id: key,
9728
+ type: "text",
9729
+ content: { $data: key },
9730
+ position: t.box ? { x: fmtPct(t.box.x / inv.width), y: fmtPct(t.box.y / inv.height) } : "center",
9731
+ font: {
9732
+ family,
9733
+ size: Math.round(t.font.size * scale),
9734
+ weight: t.font.weight,
9735
+ ...t.font.style === "italic" ? { style: "italic" } : {},
9736
+ color: { $data: colorOf(t.font.color) },
9737
+ ...t.font.letterSpacing ? {
9738
+ letterSpacing: Math.round(t.font.letterSpacing * scale * 10) / 10
9739
+ } : {}
9740
+ }
9741
+ });
9742
+ textKeys.push({ key, size: t.font.size, label: words.slice(0, 40) });
9743
+ if (t.scene && sceneNames.has(t.scene))
9744
+ (sceneElements[t.scene] ??= []).push(key);
9745
+ }
9746
+ const fonts = [];
9747
+ for (const face of faces.values()) {
9748
+ const entry = findFontFamily4(face.family);
9749
+ if (!entry) continue;
9750
+ for (const w of face.weights) {
9751
+ const weight = nearestFontWeight(entry, w);
9752
+ fonts.push({
9753
+ family: entry.family,
9754
+ weight,
9755
+ url: fontFaceUrl(entry.slug, weight)
9756
+ });
9757
+ }
9758
+ if (face.italic) {
9759
+ const italics = entry.italics ?? [];
9760
+ if (italics.length) {
9761
+ const weight = italics.includes(400) ? 400 : italics[0];
9762
+ fonts.push({
9763
+ family: entry.family,
9764
+ weight,
9765
+ style: "italic",
9766
+ url: fontFaceUrl(entry.slug, weight, "italic")
9767
+ });
9768
+ }
9769
+ }
9770
+ }
9771
+ const painterFaces = [];
9772
+ for (const f of inv.fonts) {
9773
+ if (!f.catalog || faces.has(f.catalog)) continue;
9774
+ const entry = findFontFamily4(f.catalog);
9775
+ if (!entry) continue;
9776
+ for (const weight of entry.weights)
9777
+ fonts.push({
9778
+ family: entry.family,
9779
+ weight,
9780
+ url: fontFaceUrl(entry.slug, weight)
9781
+ });
9782
+ for (const weight of entry.italics ?? [])
9783
+ fonts.push({
9784
+ family: entry.family,
9785
+ weight,
9786
+ style: "italic",
9787
+ url: fontFaceUrl(entry.slug, weight, "italic")
9788
+ });
9789
+ painterFaces.push(entry.family);
9790
+ }
9791
+ for (const f of inv.fonts) {
9792
+ if (!f.catalog && !faces.has(substituteFamily(f.family)) && !inv.texts.some((t) => t.font.family === f.family))
9793
+ report.push(
9794
+ `- **${f.family}** (named in the scripts) is not in the catalog: draw with ${substituteFamily(f.family)} and say so in the push note`
9795
+ );
9796
+ }
9797
+ if (painterFaces.length)
9798
+ report.push(
9799
+ `- faces the scripts paint with, declared at every hosted weight for the painter: ${painterFaces.join(", ")}`
9800
+ );
9801
+ data.scenes = inv.scenes.map((s) => ({
9802
+ name: s.name,
9803
+ at: s.start,
9804
+ until: s.duration == null ? null : Math.round((s.start + s.duration) * 1e3) / 1e3
9805
+ }));
9806
+ data.sceneElements = sceneElements;
9807
+ const params = [];
9808
+ const paletteKeys = [...colorKey.values()].slice(0, 6);
9809
+ for (const key of paletteKeys)
9810
+ params.push({
9811
+ key,
9812
+ label: key,
9813
+ kind: "color",
9814
+ default: data[key],
9815
+ group: "Colours"
9816
+ });
9817
+ for (const t of [...textKeys].sort((a, b) => b.size - a.size)) {
9818
+ if (params.length >= 12) break;
9819
+ params.push({
9820
+ key: t.key,
9821
+ label: t.label,
9822
+ kind: "text",
9823
+ default: data[t.key],
9824
+ group: "Words"
9825
+ });
9826
+ }
9827
+ const presets = paletteKeys.length ? [
9828
+ {
9829
+ name: "Original",
9830
+ values: Object.fromEntries(paletteKeys.map((k) => [k, data[k]]))
9831
+ }
9832
+ ] : [];
9833
+ const painter = inv.canvases.length > 0 || inv.gaps.length > 0;
9834
+ const duration = Math.round((inv.duration ?? 5) * 1e3) / 1e3;
9835
+ const program = programSource({
9836
+ source: inv.source,
9837
+ duration,
9838
+ data,
9839
+ params,
9840
+ presets,
9841
+ fonts,
9842
+ elements,
9843
+ scenes: inv.scenes,
9844
+ painter
9845
+ });
9846
+ const audio = [];
9847
+ const copies = [];
9848
+ inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
9849
+ const from = resolve10(opts.sourceDir, m.src);
9850
+ const name = basename8(from);
9851
+ if (!existsSync18(from)) {
9852
+ report.push(
9853
+ `- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
9854
+ );
9855
+ return;
9856
+ }
9857
+ copies.push([from, name]);
9858
+ const fileDuration = opts.probeDuration?.(from) ?? m.duration ?? duration;
9859
+ audio.push({
9860
+ id: `score${i ? i + 1 : ""}`,
9861
+ key: name,
9862
+ name,
9863
+ start: m.start ?? 0,
9864
+ in: m.mediaStart ?? 0,
9865
+ out: Math.min(
9866
+ fileDuration,
9867
+ (m.mediaStart ?? 0) + (m.duration ?? fileDuration)
9868
+ ),
9869
+ duration: fileDuration,
9870
+ gain: m.volume ?? 1,
9871
+ fadeIn: 0,
9872
+ fadeOut: 0
9873
+ });
9874
+ });
9875
+ const short = Math.min(inv.width, inv.height);
9876
+ const doc = {
9877
+ program: {},
9878
+ audio,
9879
+ export: {
9880
+ resolution: short >= 2160 ? "4k" : short >= 1440 ? "2k" : short >= 1080 ? "1080p" : "720p",
9881
+ fps: (inv.fps ?? 30) >= 50 ? 60 : 30,
9882
+ format: "mp4"
9883
+ }
9884
+ };
9885
+ return {
9886
+ program,
9887
+ doc,
9888
+ copies,
9889
+ report: reportText(inv, {
9890
+ faces: [...faces.values()],
9891
+ elements: elements.length,
9892
+ params: params.length,
9893
+ painter,
9894
+ extra: report
9895
+ })
9896
+ };
9897
+ }
9898
+ function programSource(p) {
9899
+ const j = (v) => JSON.stringify(v, null, 2).replace(/\n/g, "\n ");
9900
+ const sceneTodos = p.scenes.length ? p.scenes.map(
9901
+ (s) => ` // TODO scene "${s.name}" (${s.start}s${s.duration ? ` to ${s.start + s.duration}s` : ""}): translate its motion onto ctx.elements.get(id).props / .segments here`
9902
+ ).join("\n") : " // TODO: translate the source's motion here, one block per scene (tl.addLabel above names them)";
9903
+ const painterContent = p.painter ? `
9904
+ // THE PAINTER (vos-port rule 6): one canvas, only for what no element can
9905
+ // say (the inventory's gaps and canvases). It reads every value from
9906
+ // ctx.data. Its plane sits above the elements; move its renderOrder
9907
+ // between two of them to paint between (SKILL.md "Layering").
9908
+ const T = ctx.THREE
9909
+ const canvas = document.createElement('canvas')
9910
+ canvas.width = ctx.resolution.width
9911
+ canvas.height = ctx.resolution.height
9912
+ const tex = new T.CanvasTexture(canvas)
9913
+ tex.colorSpace = T.SRGBColorSpace
9914
+ const plane = new T.Mesh(
9915
+ new T.PlaneGeometry(1, 1),
9916
+ new T.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false, depthWrite: false }),
9917
+ )
9918
+ plane.scale.set(ctx.resolution.width, ctx.resolution.height, 1)
9919
+ plane.renderOrder = 100.995
9920
+ plane.frustumCulled = false
9921
+ ctx.overlayScene.add(plane)
9922
+ // The starter's helpers: Remotion's interpolate, a seeded PRNG, smooth noise.
9923
+ const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v)
9924
+ const interpolate = (v, [a, b], [x, y], ease = (t) => t) =>
9925
+ x + (y - x) * ease(clamp01((v - a) / (b - a)))
9926
+ const rand = (seed) => () => {
9927
+ seed |= 0
9928
+ seed = (seed + 0x6d2b79f5) | 0
9929
+ let r = Math.imul(seed ^ (seed >>> 15), 1 | seed)
9930
+ r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r
9931
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296
9932
+ }
9933
+ const hash = (x, y) => {
9934
+ const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453
9935
+ return s - Math.floor(s)
9936
+ }
9937
+ const noise2 = (x, y) => {
9938
+ const xi = Math.floor(x), yi = Math.floor(y)
9939
+ const xf = x - xi, yf = y - yi
9940
+ const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf)
9941
+ const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1), d = hash(xi + 1, yi + 1)
9942
+ return (a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v) * 2 - 1
9943
+ }
9944
+ return { objects: [], refs: { canvas, g: canvas.getContext('2d'), tex, lib: { interpolate, rand, noise2 } } }` : `
9945
+ return { objects: [], refs: {} }`;
9946
+ const painterFrame = p.painter ? `
9947
+ // Paint the procedural layer from ctx.time and ctx.data, then upload it.
9948
+ const { canvas, g, tex } = content.refs
9949
+ g.clearRect(0, 0, canvas.width, canvas.height)
9950
+ // TODO: draw what no element can (grain, a blended HUD, noise blobs, masks)
9951
+ tex.needsUpdate = true` : "";
9952
+ return `// Scaffolded by \`vos port scaffold\` from ${p.source}.
9953
+ // Every word and colour is in \`data\`, bound to an element or read by a
9954
+ // painter; the knobs sit over them. What is left is the MOTION: translate each
9955
+ // scene where it says TODO, with the vos-port skill's tables open. Then:
9956
+ // npx vos build program.mjs && npx vos check config.json
9957
+ // npx vos compare . --against <the source's render>
9958
+ export default {
9959
+ version: 2,
9960
+ duration: ${p.duration},
9961
+ camera: { preset: 'fullscreen' },
9962
+ fonts: ${j(p.fonts)},
9963
+ data: ${j(p.data)},
9964
+ params: ${j(p.params)},
9965
+ presets: ${j(p.presets)},
9966
+ elements: ${j(p.elements)},
9967
+
9968
+ createContent(ctx) {
9969
+ ctx.scene.background = new ctx.THREE.Color(ctx.data.bg)${painterContent}
9970
+ },
9971
+
9972
+ createTimeline(ctx, content, duration) {
9973
+ const tl = ctx.gsap.timeline()
9974
+ for (const s of ctx.data.scenes) {
9975
+ tl.addLabel(s.name, s.at)
9976
+ // Each scene's words show in its window; the motion replaces this.
9977
+ for (const id of ctx.data.sceneElements[s.name] || []) {
9978
+ const p = ctx.elements.get(id).props
9979
+ if (s.at > 0) {
9980
+ tl.set(p, { opacity: 0 }, 0)
9981
+ tl.set(p, { opacity: 1 }, s.at)
9982
+ }
9983
+ if (s.until != null && s.until < duration) tl.set(p, { opacity: 0 }, s.until)
9984
+ }
9985
+ }
9986
+ ${sceneTodos}
9987
+ tl.to({}, { duration }, 0)
9988
+ return tl
9989
+ },
9990
+
9991
+ onFrame(ctx, content) {
9992
+ ctx.scene.background.set(ctx.data.bg)${painterFrame}
9993
+ },
9994
+ }
9995
+ `;
9996
+ }
9997
+ function reportText(inv, r) {
9998
+ const lines = [
9999
+ `# Port report: ${inv.source}`,
10000
+ "",
10001
+ `${inv.engine} piece, ${inv.width}\xD7${inv.height}${inv.fps ? ` at ${inv.fps} fps` : ""}, ${inv.duration ?? "?"} s. A program has no size of its own: render it with \`--width ${inv.width} --height ${inv.height}\`.`,
10002
+ "",
10003
+ "## Placed",
10004
+ "",
10005
+ `- ${r.elements} words as bound text elements, ${Object.keys(inv.palette).length} palette colours from the source's custom properties, ${r.params} knobs.`,
10006
+ `- ${inv.scenes.length} scenes as labels${inv.scenes.length ? "" : " (none declared: name them as you translate)"}.`,
10007
+ ...inv.media.some((m) => m.kind === "audio") ? ["- the score as a doc.json track (vos push uploads it)."] : [],
10008
+ "",
10009
+ "## Faces",
10010
+ "",
10011
+ ...r.faces.map(
10012
+ (f) => f.from ? `- **${f.from}** is not in the catalog: substituted with ${f.family}. Say so in the push note.` : `- ${f.family}${f.italic ? " (italic)" : ""}: from the catalog.`
10013
+ ),
10014
+ "",
10015
+ "## Needs a painter or an approximation",
10016
+ "",
10017
+ ...inv.canvases.length ? [
10018
+ `- ${inv.canvases.length} canvas${inv.canvases.length === 1 ? "" : "es"}: the source paints pixels; port those parts as the painter, with their values in data.`
10019
+ ] : [],
10020
+ ...inv.gaps.map((g) => `- ${g}`),
10021
+ ...r.painter ? [] : ["- nothing: every visible part is an element."],
10022
+ ...inv.scriptStrings.length && inv.canvases.length ? [
10023
+ "",
10024
+ "## Words the scripts paint",
10025
+ "",
10026
+ "The page draws these on its canvas, so no text node holds them. Each one a person reads becomes a data key and a bound element:",
10027
+ "",
10028
+ ...inv.scriptStrings.slice(0, 40).map((s) => `- ${JSON.stringify(s)}`)
10029
+ ] : [],
10030
+ ...inv.media.some((m) => m.kind === "image" || m.kind === "video") ? [
10031
+ "",
10032
+ "## Images and video",
10033
+ "",
10034
+ "Not placed: upload each with `vos asset upload` and add an image or video element over its hosted URL.",
10035
+ "",
10036
+ ...inv.media.filter((m) => m.kind === "image" || m.kind === "video").map((m) => `- ${m.kind} ${m.src ?? "(inline)"}`)
10037
+ ] : [],
10038
+ ...r.extra.length ? ["", "## Also", "", ...r.extra] : [],
10039
+ "",
10040
+ "## Reference",
10041
+ "",
10042
+ inv.render ? `The source's render: ${inv.render}. Stills in port/ref/. Check every scene: \`npx vos compare . --against ${inv.render}\`.` : "No render of the source was found: render it with its own tool and pass it to `vos compare --against`.",
10043
+ ""
10044
+ ];
10045
+ return lines.join("\n");
10046
+ }
10047
+ function writeScaffold(plan, outDir) {
10048
+ mkdirSync4(outDir, { recursive: true });
10049
+ writeFileSync4(join22(outDir, "program.mjs"), plan.program);
10050
+ writeFileSync4(
10051
+ join22(outDir, "doc.json"),
10052
+ JSON.stringify(plan.doc, null, 2) + "\n"
10053
+ );
10054
+ writeFileSync4(join22(dirname7(outDir), "REPORT.md"), plan.report);
10055
+ for (const [from, name] of plan.copies) copyFileSync(from, join22(outDir, name));
10056
+ }
10057
+ function probeAudioDuration(file) {
10058
+ const res = spawnSync3(
10059
+ "ffprobe",
10060
+ ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", file],
10061
+ { encoding: "utf8" }
10062
+ );
10063
+ const d = Number((res.stdout ?? "").trim());
10064
+ return Number.isFinite(d) && d > 0 ? Math.round(d * 1e3) / 1e3 : null;
10065
+ }
10066
+
10067
+ // src/plugin/port.ts
10068
+ var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
10069
+ async function cmdPort(argv) {
10070
+ const [sub, ...rest] = argv;
10071
+ if (sub === "inventory") return await inventory(rest);
10072
+ if (sub === "scaffold") return await scaffold(rest);
10073
+ throw new UsageError(
10074
+ "vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]\nvos port scaffold [--from port/inventory.json] [--out port/program] [--json]"
10075
+ );
10076
+ }
10077
+ async function inventory(argv) {
10078
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
10079
+ const source = positionals[0] ?? ".";
10080
+ const r = createReporter(flags.json === true);
10081
+ const outDir = resolve11(strFlag(flags, "out") ?? "port");
10082
+ mkdirSync5(outDir, { recursive: true });
10083
+ const renderFlag = strFlag(flags, "render");
10084
+ const inv = await readInventory(source, {
10085
+ render: renderFlag === void 0 ? void 0 : resolve11(renderFlag),
10086
+ outDir,
10087
+ log: (l) => r.log(`note: ${l}`)
10088
+ });
10089
+ const file = join23(outDir, "inventory.json");
10090
+ writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
10091
+ const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
10092
+ r.log(
10093
+ `${inv.engine}, ${inv.width}\xD7${inv.height}${inv.fps ? `@${inv.fps}` : ""}, ${inv.duration ?? "?"}s: ${inv.texts.length} words, ${Object.keys(inv.palette).length} palette colours, ${inv.fonts.length} faces${missing.length ? ` (not in the catalog: ${missing.join(", ")})` : ""}, ${inv.media.length} media, ${inv.scenes.length} scenes, ${inv.canvases.length} canvases${inv.gaps.length ? `; needs a painter or an approximation: ${inv.gaps.join(", ")}` : ""}`
10094
+ );
10095
+ r.done(
10096
+ { inventory: file, ...summary(inv) },
10097
+ `Wrote ${file}${inv.stills.length ? ` and ${inv.stills.length} reference stills` : ""}. Next: npx vos port scaffold`
10098
+ );
10099
+ return EXIT_OK;
10100
+ }
10101
+ function summary(inv) {
10102
+ return {
10103
+ engine: inv.engine,
10104
+ width: inv.width,
10105
+ height: inv.height,
10106
+ fps: inv.fps,
10107
+ duration: inv.duration,
10108
+ words: inv.texts.length,
10109
+ palette: Object.keys(inv.palette).length,
10110
+ faces: inv.fonts.map((f) => ({ family: f.family, catalog: f.catalog })),
10111
+ media: inv.media.length,
10112
+ scenes: inv.scenes.length,
10113
+ canvases: inv.canvases.length,
10114
+ gaps: inv.gaps,
10115
+ render: inv.render
10116
+ };
10117
+ }
10118
+ async function scaffold(argv) {
10119
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
10120
+ const r = createReporter(flags.json === true);
10121
+ const from = resolve11(strFlag(flags, "from") ?? join23("port", "inventory.json"));
10122
+ if (!existsSync19(from))
10123
+ throw new UsageError(
10124
+ `no inventory at ${from}: run npx vos port inventory <source> first`
10125
+ );
10126
+ const inv = JSON.parse(readFileSync6(from, "utf8"));
10127
+ const outDir = resolve11(
10128
+ strFlag(flags, "out") ?? join23(dirname8(from), "program")
10129
+ );
10130
+ const plan = planScaffold(inv, {
10131
+ sourceDir: dirname8(
10132
+ resolve11(
10133
+ inv.source.endsWith(".html") ? inv.source : join23(inv.source, "index.html")
10134
+ )
10135
+ ),
10136
+ probeDuration: probeAudioDuration
10137
+ });
10138
+ writeScaffold(plan, outDir);
10139
+ const programFile = join23(outDir, "program.mjs");
10140
+ const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
10141
+ const built = buildProgram(mod.default, "program.mjs", plan.program);
10142
+ for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
10143
+ if (built.problems.length) return EXIT_ERROR;
10144
+ writeFileSync5(
10145
+ join23(outDir, "config.json"),
10146
+ JSON.stringify(built.config, null, 2) + "\n"
10147
+ );
10148
+ r.done(
10149
+ { program: outDir, report: join23(dirname8(outDir), "REPORT.md") },
10150
+ `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join23(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
10151
+ );
10152
+ return EXIT_OK;
10153
+ }
10154
+
10155
+ // src/plugin/recipe.ts
10156
+ import { readFileSync as readFileSync7 } from "fs";
10157
+ import { basename as basename9 } from "path";
10158
+ var BOOLEAN_FLAGS8 = /* @__PURE__ */ new Set(["json", "help"]);
8820
10159
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
8821
10160
  function recipeFilename(file) {
8822
- const name = basename6(file);
10161
+ const name = basename9(file);
8823
10162
  if (!/\.md$/i.test(name)) {
8824
10163
  throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
8825
10164
  }
8826
10165
  return name;
8827
10166
  }
8828
10167
  async function cmdPush2(argv) {
8829
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10168
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
8830
10169
  const [file] = positionals;
8831
10170
  const folderRef = strFlag(flags, "folder");
8832
10171
  const assetId = strFlag(flags, "asset");
@@ -8834,7 +10173,7 @@ async function cmdPush2(argv) {
8834
10173
  throw new UsageError(USAGE);
8835
10174
  }
8836
10175
  const filename = recipeFilename(file);
8837
- const body = readFileSync4(file);
10176
+ const body = readFileSync7(file);
8838
10177
  const r = createReporter(flags.json === true);
8839
10178
  const origin = platformOrigin({
8840
10179
  origin: strFlag(flags, "origin"),
@@ -8907,7 +10246,7 @@ async function cmdRecipe(argv) {
8907
10246
 
8908
10247
  // src/plugin/brand.ts
8909
10248
  import { writeFile as writeFile12 } from "fs/promises";
8910
- import { resolve as resolve7 } from "path";
10249
+ import { resolve as resolve12 } from "path";
8911
10250
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
8912
10251
  import { lookKindForGround } from "@vosjs/studio-core";
8913
10252
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -9226,7 +10565,7 @@ async function cmdBrand(argv) {
9226
10565
  throw new UsageError(`not a URL: ${raw}`);
9227
10566
  }
9228
10567
  const r = createReporter(flags.json === true);
9229
- const out = resolve7(strFlag(flags, "out") ?? "BRAND.md");
10568
+ const out = resolve12(strFlag(flags, "out") ?? "BRAND.md");
9230
10569
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
9231
10570
  const [designText, llmsText] = await Promise.all([
9232
10571
  fetchText(`${origin}/design.md`),
@@ -9376,7 +10715,7 @@ async function recordHosted(input) {
9376
10715
 
9377
10716
  // src/plugin/agentBrowser.ts
9378
10717
  import { writeFile as writeFile13 } from "fs/promises";
9379
- import { resolve as resolve8 } from "path";
10718
+ import { resolve as resolve13 } from "path";
9380
10719
  function parseAgentBrowserLog(text) {
9381
10720
  const records = [];
9382
10721
  const problems = [];
@@ -9477,8 +10816,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
9477
10816
  function hrefSelector(href) {
9478
10817
  try {
9479
10818
  const u = new URL(href);
9480
- const relative2 = `${u.pathname}${u.search}${u.hash}`;
9481
- return `a[href=${quoteAttr(relative2)}], a[href=${quoteAttr(href)}]`;
10819
+ const relative3 = `${u.pathname}${u.search}${u.hash}`;
10820
+ return `a[href=${quoteAttr(relative3)}], a[href=${quoteAttr(href)}]`;
9482
10821
  } catch {
9483
10822
  return `a[href=${quoteAttr(href)}]`;
9484
10823
  }
@@ -9847,7 +11186,7 @@ async function cmdActions(argv) {
9847
11186
  );
9848
11187
  }
9849
11188
  const r = createReporter(flags.json === true);
9850
- const out = resolve8(strFlag(flags, "out") ?? "actions.json");
11189
+ const out = resolve13(strFlag(flags, "out") ?? "actions.json");
9851
11190
  const viewportFlag = strFlag(flags, "viewport");
9852
11191
  let viewport;
9853
11192
  if (viewportFlag) {
@@ -9856,7 +11195,7 @@ async function cmdActions(argv) {
9856
11195
  viewport = { width: Number(m[1]), height: Number(m[2]) };
9857
11196
  }
9858
11197
  const { readFile: readFile18 } = await import("fs/promises");
9859
- const text = await readFile18(resolve8(input), "utf8");
11198
+ const text = await readFile18(resolve13(input), "utf8");
9860
11199
  const { records, problems } = parseAgentBrowserLog(text);
9861
11200
  for (const p of problems) r.log(` ${p}`);
9862
11201
  const result = convertAgentBrowser(records, {
@@ -9866,7 +11205,7 @@ async function cmdActions(argv) {
9866
11205
  const errors = validateActions(result.actions);
9867
11206
  if (errors.length) {
9868
11207
  r.done(
9869
- { ok: false, errors, ...summary(result) },
11208
+ { ok: false, errors, ...summary2(result) },
9870
11209
  `${input}: no actions file written
9871
11210
  ${errors.join("\n ")}
9872
11211
  ${result.notes.join("\n ")}`
@@ -9876,14 +11215,14 @@ async function cmdActions(argv) {
9876
11215
  await writeFile13(out, `${JSON.stringify(result.actions, null, 2)}
9877
11216
  `);
9878
11217
  r.done(
9879
- { ok: true, out, url: result.actions.url ?? null, ...summary(result) },
11218
+ { ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
9880
11219
  `Wrote ${out}: ${result.steps} step${result.steps === 1 ? "" : "s"} from ${records.length} record${records.length === 1 ? "" : "s"} (${result.ignored} read/session, ${result.skipped} skipped)` + (result.notes.length ? `
9881
11220
  ${result.notes.join("\n ")}` : "") + `
9882
11221
  Then: vos record --actions ${out} --out take --strict`
9883
11222
  );
9884
11223
  return EXIT_OK;
9885
11224
  }
9886
- function summary(result) {
11225
+ function summary2(result) {
9887
11226
  return {
9888
11227
  steps: result.steps,
9889
11228
  ignored: result.ignored,
@@ -9895,7 +11234,7 @@ async function cmdActionsScript(input, flags) {
9895
11234
  const r = createReporter(flags.json === true);
9896
11235
  const { readFile: readFile18 } = await import("fs/promises");
9897
11236
  const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
9898
- const raw = JSON.parse(await readFile18(resolve8(input), "utf8"));
11237
+ const raw = JSON.parse(await readFile18(resolve13(input), "utf8"));
9899
11238
  const errors = validateActions(raw);
9900
11239
  if (errors.length) throw new UsageError(`${input}:
9901
11240
  ${errors.join("\n ")}`);
@@ -9905,9 +11244,9 @@ async function cmdActionsScript(input, flags) {
9905
11244
  }
9906
11245
 
9907
11246
  // src/plugin/ingest.ts
9908
- import { existsSync as existsSync17 } from "fs";
11247
+ import { existsSync as existsSync20 } from "fs";
9909
11248
  import { copyFile, mkdir as mkdir10, readFile as readFile16, stat as stat4 } from "fs/promises";
9910
- import { basename as basename7, join as join19, resolve as resolve9 } from "path";
11249
+ import { basename as basename10, join as join24, resolve as resolve14 } from "path";
9911
11250
  import { inflateRawSync } from "zlib";
9912
11251
  import {
9913
11252
  cursorFromPlaywrightTrace,
@@ -9927,7 +11266,7 @@ import {
9927
11266
  Output,
9928
11267
  WebMOutputFormat
9929
11268
  } from "mediabunny";
9930
- var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set(["json", "help"]);
11269
+ var BOOLEAN_FLAGS9 = /* @__PURE__ */ new Set(["json", "help"]);
9931
11270
  async function probeVideoFile(path) {
9932
11271
  const input = new Input({
9933
11272
  source: new FilePathSource(path),
@@ -9988,7 +11327,7 @@ async function copyRecording(src, dest, ext) {
9988
11327
  }
9989
11328
  }
9990
11329
  async function readTraceFile(path, offsetMs) {
9991
- const kind = traceKindOf(basename7(path));
11330
+ const kind = traceKindOf(basename10(path));
9992
11331
  if (!kind) {
9993
11332
  throw new UsageError(
9994
11333
  `${path}: not a trace the recorder reads (trace.zip, .trace, .jsonl, .json or .csv)`
@@ -10009,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
10009
11348
  return cursorFromPlaywrightTrace(text2, { offsetMs });
10010
11349
  }
10011
11350
  const text = await readFile16(path, "utf8");
10012
- return cursorFromTrace(basename7(path), text, { offsetMs });
11351
+ return cursorFromTrace(basename10(path), text, { offsetMs });
10013
11352
  }
10014
11353
  function parseViewport(v) {
10015
11354
  if (!v) return null;
@@ -10018,16 +11357,16 @@ function parseViewport(v) {
10018
11357
  return { width: Number(m[1]), height: Number(m[2]) };
10019
11358
  }
10020
11359
  async function cmdIngest(argv) {
10021
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6, MULTI_FLAGS2);
11360
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS9, MULTI_FLAGS2);
10022
11361
  const source = positionals[0];
10023
11362
  if (!source)
10024
11363
  throw new UsageError(
10025
11364
  "vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]"
10026
11365
  );
10027
- const videoPath = resolve9(source);
10028
- if (!existsSync17(videoPath)) throw new UsageError(`${source}: no such file`);
11366
+ const videoPath = resolve14(source);
11367
+ if (!existsSync20(videoPath)) throw new UsageError(`${source}: no such file`);
10029
11368
  const r = createReporter(flags.json === true);
10030
- const outDir = resolve9(strFlag(flags, "out") ?? "take");
11369
+ const outDir = resolve14(strFlag(flags, "out") ?? "take");
10031
11370
  const offsetMs = numFlag(flags, "offset", 0);
10032
11371
  const viewportFlag = parseViewport(strFlag(flags, "viewport"));
10033
11372
  const cursorPath = strFlag(flags, "cursor");
@@ -10040,7 +11379,7 @@ async function cmdIngest(argv) {
10040
11379
  "note: finished footage opens bare, so --background is not used (pass --as take for the screen-recording card and its ground)"
10041
11380
  );
10042
11381
  const backdrop = footage ? null : await takeBackdrop(flags, r);
10043
- r.log(`probing ${basename7(videoPath)}\u2026`);
11382
+ r.log(`probing ${basename10(videoPath)}\u2026`);
10044
11383
  const probed = await probeVideoFile(videoPath);
10045
11384
  if (!(probed.durationMs > 0)) {
10046
11385
  throw new UsageError(
@@ -10049,10 +11388,10 @@ async function cmdIngest(argv) {
10049
11388
  }
10050
11389
  let trace = null;
10051
11390
  if (cursorPath) {
10052
- trace = await readTraceFile(resolve9(cursorPath), offsetMs);
11391
+ trace = await readTraceFile(resolve14(cursorPath), offsetMs);
10053
11392
  for (const n of trace.notes) r.log(`note: ${n}`);
10054
11393
  }
10055
- if (existsSync17(join19(outDir, "meta.json"))) {
11394
+ if (existsSync20(join24(outDir, "meta.json"))) {
10056
11395
  const { prevDoc, kept } = await prepareReRecord(outDir);
10057
11396
  r.log(
10058
11397
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
@@ -10060,7 +11399,7 @@ async function cmdIngest(argv) {
10060
11399
  }
10061
11400
  await mkdir10(outDir, { recursive: true });
10062
11401
  const paths = await ensureTakeDir(outDir);
10063
- const recording = join19(outDir, `recording.${probed.ext}`);
11402
+ const recording = join24(outDir, `recording.${probed.ext}`);
10064
11403
  r.log("copying the recording\u2026");
10065
11404
  const remuxed = await copyRecording(videoPath, recording, probed.ext);
10066
11405
  const viewport = viewportFlag ?? trace?.viewport ?? {
@@ -10089,13 +11428,13 @@ async function cmdIngest(argv) {
10089
11428
  const clicks = events.filter((e) => e.type === "down").length;
10090
11429
  const seconds = (probed.durationMs / 1e3).toFixed(1);
10091
11430
  const facts = `${seconds}s, ${probed.width}\xD7${probed.height} @ ${meta.fps} fps${probed.hasAudio ? ", audio" : ", no audio"}${remuxed ? "" : ", copied as is"}`;
10092
- const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename7(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
11431
+ const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename10(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
10093
11432
  const opened = footage ? `opened as finished footage: no card, no browser bar, no drawn cursor, ${meta.fps > 30 ? 60 : 30} fps export (--as take for a screen recording's card)` : `opened as a screen recording: the card, its ground and the drawn cursor (--as footage to open it bare)`;
10094
11433
  const next = footage && !trace ? `Next: vos push ${outDir} --wait \u2014 it lands as it is; trim or add to it in the studio` : trace ? `Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}` : `Add --cursor <trace> recorded beside the video (a Playwright trace.zip, stamped JSON records, or a t,x,y,type CSV) to plan zooms from its clicks; else place them by eye: vos frames ${outDir} --times 0,25%,50%,75%,100%, then edit doc.json`;
10095
11434
  r.done(
10096
11435
  {
10097
11436
  out: outDir,
10098
- recording: basename7(recording),
11437
+ recording: basename10(recording),
10099
11438
  durationMs: probed.durationMs,
10100
11439
  width: probed.width,
10101
11440
  height: probed.height,
@@ -10104,7 +11443,7 @@ async function cmdIngest(argv) {
10104
11443
  hasAudio: probed.hasAudio,
10105
11444
  remuxed,
10106
11445
  cursor: trace ? {
10107
- file: basename7(cursorPath),
11446
+ file: basename10(cursorPath),
10108
11447
  kind: trace.kind,
10109
11448
  events: events.length,
10110
11449
  clicks,
@@ -10116,7 +11455,7 @@ async function cmdIngest(argv) {
10116
11455
  backdrop: plan.backdrop ?? null,
10117
11456
  as: footage ? "footage" : "take"
10118
11457
  },
10119
- `Ingested ${basename7(videoPath)} \u2192 ${outDir}/${basename7(recording)} (${facts})
11458
+ `Ingested ${basename10(videoPath)} \u2192 ${outDir}/${basename10(recording)} (${facts})
10120
11459
  ${opened}
10121
11460
  ${cursorLine}
10122
11461
  ${next}`
@@ -10125,22 +11464,22 @@ async function cmdIngest(argv) {
10125
11464
  }
10126
11465
 
10127
11466
  // src/plugin/machineCmd.ts
10128
- import { spawnSync } from "child_process";
11467
+ import { spawnSync as spawnSync4 } from "child_process";
10129
11468
  import {
10130
11469
  cpSync,
10131
- existsSync as existsSync19,
10132
- mkdirSync as mkdirSync2,
10133
- readFileSync as readFileSync6,
11470
+ existsSync as existsSync22,
11471
+ mkdirSync as mkdirSync6,
11472
+ readFileSync as readFileSync9,
10134
11473
  rmSync as rmSync3,
10135
- writeFileSync as writeFileSync2
11474
+ writeFileSync as writeFileSync6
10136
11475
  } from "fs";
10137
11476
  import { homedir as homedir2 } from "os";
10138
- import { join as join21 } from "path";
11477
+ import { join as join26 } from "path";
10139
11478
  import { chromium as chromium2 } from "playwright";
10140
11479
 
10141
11480
  // src/plugin/machine.ts
10142
- import { existsSync as existsSync18, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
10143
- import { dirname as dirname5, join as join20 } from "path";
11481
+ import { existsSync as existsSync21, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
11482
+ import { dirname as dirname9, join as join25 } from "path";
10144
11483
  var AGENT_NAMES = [
10145
11484
  "claude",
10146
11485
  "cursor",
@@ -10160,17 +11499,17 @@ var HOME_DIRS = {
10160
11499
  copilot: [".copilot"]
10161
11500
  };
10162
11501
  function detectAgents(root, opts = {}) {
10163
- const exists = opts.exists ?? existsSync18;
11502
+ const exists = opts.exists ?? existsSync21;
10164
11503
  const out = [];
10165
11504
  for (const agent of AGENT_NAMES) {
10166
11505
  for (const rel of PROJECT_DIRS[agent]) {
10167
- const dir = join20(root, rel);
11506
+ const dir = join25(root, rel);
10168
11507
  if (exists(dir)) out.push({ agent, dir, scope: "project" });
10169
11508
  }
10170
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join20(root, ".github", "copilot-instructions.md"))) {
11509
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join25(root, ".github", "copilot-instructions.md"))) {
10171
11510
  out.push({
10172
11511
  agent,
10173
- dir: join20(root, ".github", "copilot"),
11512
+ dir: join25(root, ".github", "copilot"),
10174
11513
  scope: "project"
10175
11514
  });
10176
11515
  }
@@ -10178,7 +11517,7 @@ function detectAgents(root, opts = {}) {
10178
11517
  if (opts.global && opts.home) {
10179
11518
  for (const agent of AGENT_NAMES) {
10180
11519
  for (const rel of HOME_DIRS[agent]) {
10181
- const dir = join20(opts.home, rel);
11520
+ const dir = join25(opts.home, rel);
10182
11521
  if (exists(dir)) out.push({ agent, dir, scope: "global" });
10183
11522
  }
10184
11523
  }
@@ -10196,26 +11535,26 @@ function agentsFromFlag(value, root) {
10196
11535
  return {
10197
11536
  homes: wanted.map((agent) => ({
10198
11537
  agent,
10199
- dir: join20(root, PROJECT_DIRS[agent][0]),
11538
+ dir: join25(root, PROJECT_DIRS[agent][0]),
10200
11539
  scope: "project"
10201
11540
  }))
10202
11541
  };
10203
11542
  }
10204
11543
  function skillsDirFor(home) {
10205
- return join20(home.dir, "skills");
11544
+ return join25(home.dir, "skills");
10206
11545
  }
10207
11546
  function fallbackSkillsHome(root) {
10208
- return { agent: "codex", dir: join20(root, ".agents"), scope: "project" };
11547
+ return { agent: "codex", dir: join25(root, ".agents"), scope: "project" };
10209
11548
  }
10210
- function installedSkills(dir, exists = existsSync18, list = (p) => {
11549
+ function installedSkills(dir, exists = existsSync21, list = (p) => {
10211
11550
  try {
10212
- return readdirSync4(p);
11551
+ return readdirSync5(p);
10213
11552
  } catch {
10214
11553
  return [];
10215
11554
  }
10216
11555
  }) {
10217
11556
  if (!exists(dir)) return [];
10218
- return list(dir).filter((name) => exists(join20(dir, name, "SKILL.md"))).sort();
11557
+ return list(dir).filter((name) => exists(join25(dir, name, "SKILL.md"))).sort();
10219
11558
  }
10220
11559
  var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
10221
11560
  var RULES_END = "<!-- vos:end -->";
@@ -10247,10 +11586,10 @@ ${RULES_BLOCK}
10247
11586
  }
10248
11587
  return { text, changed: text !== current };
10249
11588
  }
10250
- function rulesFileFor(root, exists = existsSync18) {
10251
- const agents = join20(root, "AGENTS.md");
11589
+ function rulesFileFor(root, exists = existsSync21) {
11590
+ const agents = join25(root, "AGENTS.md");
10252
11591
  if (exists(agents)) return agents;
10253
- const claude = join20(root, "CLAUDE.md");
11592
+ const claude = join25(root, "CLAUDE.md");
10254
11593
  if (exists(claude)) return claude;
10255
11594
  return agents;
10256
11595
  }
@@ -10259,14 +11598,14 @@ function chromeCandidates(platform, env) {
10259
11598
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
10260
11599
  if (platform === "win32")
10261
11600
  return [
10262
- join20(
11601
+ join25(
10263
11602
  env["PROGRAMFILES"] ?? "C:\\Program Files",
10264
11603
  "Google",
10265
11604
  "Chrome",
10266
11605
  "Application",
10267
11606
  "chrome.exe"
10268
11607
  ),
10269
- join20(
11608
+ join25(
10270
11609
  env["LOCALAPPDATA"] ?? "",
10271
11610
  "Google",
10272
11611
  "Chrome",
@@ -10351,20 +11690,20 @@ function doctorReport(facts) {
10351
11690
  return { ok: nodeOk && browserOk, lines, next_step, facts };
10352
11691
  }
10353
11692
  function bundledSkillsDir(from) {
10354
- let dir = dirname5(from);
11693
+ let dir = dirname9(from);
10355
11694
  for (let i = 0; i < 6; i++) {
10356
- const pkg = join20(dir, "package.json");
10357
- if (existsSync18(pkg)) {
11695
+ const pkg = join25(dir, "package.json");
11696
+ if (existsSync21(pkg)) {
10358
11697
  try {
10359
- const name = JSON.parse(readFileSync5(pkg, "utf8")).name;
11698
+ const name = JSON.parse(readFileSync8(pkg, "utf8")).name;
10360
11699
  if (name === "@vosjs/cli") {
10361
- const skills = join20(dir, "skills");
10362
- return existsSync18(skills) && statSync5(skills).isDirectory() ? skills : null;
11700
+ const skills = join25(dir, "skills");
11701
+ return existsSync21(skills) && statSync6(skills).isDirectory() ? skills : null;
10363
11702
  }
10364
11703
  } catch {
10365
11704
  }
10366
11705
  }
10367
- const up = dirname5(dir);
11706
+ const up = dirname9(dir);
10368
11707
  if (up === dir) break;
10369
11708
  dir = up;
10370
11709
  }
@@ -10372,7 +11711,7 @@ function bundledSkillsDir(from) {
10372
11711
  }
10373
11712
 
10374
11713
  // src/plugin/machineCmd.ts
10375
- var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set([
11714
+ var BOOLEAN_FLAGS10 = /* @__PURE__ */ new Set([
10376
11715
  "json",
10377
11716
  "help",
10378
11717
  "global",
@@ -10390,21 +11729,21 @@ function npx() {
10390
11729
  function bundledChromium() {
10391
11730
  try {
10392
11731
  const p = chromium2.executablePath();
10393
- return p && existsSync19(p) ? p : null;
11732
+ return p && existsSync22(p) ? p : null;
10394
11733
  } catch {
10395
11734
  return null;
10396
11735
  }
10397
11736
  }
10398
- function probe() {
11737
+ function probe2() {
10399
11738
  return probeBrowser({
10400
11739
  platform: process.platform,
10401
11740
  env: process.env,
10402
- exists: existsSync19,
11741
+ exists: existsSync22,
10403
11742
  bundled: bundledChromium
10404
11743
  });
10405
11744
  }
10406
11745
  function ffmpegVersion() {
10407
- const res = spawnSync("ffmpeg", ["-version"], { encoding: "utf8" });
11746
+ const res = spawnSync4("ffmpeg", ["-version"], { encoding: "utf8" });
10408
11747
  if (res.status !== 0 || !res.stdout) return null;
10409
11748
  const m = /ffmpeg version (\S+)/.exec(res.stdout);
10410
11749
  return m ? m[1] : "present";
@@ -10442,7 +11781,7 @@ function skillsFacts(homes) {
10442
11781
  async function doctorFacts(opts) {
10443
11782
  return {
10444
11783
  node: process.versions.node,
10445
- browser: probe(),
11784
+ browser: probe2(),
10446
11785
  ffmpeg: ffmpegVersion(),
10447
11786
  skills: skillsFacts(opts.homes),
10448
11787
  credential: resolveCredential() !== null,
@@ -10450,7 +11789,7 @@ async function doctorFacts(opts) {
10450
11789
  };
10451
11790
  }
10452
11791
  async function cmdSetup(argv) {
10453
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
11792
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
10454
11793
  const r = reporterFor(flags);
10455
11794
  const root = process.cwd();
10456
11795
  const global = flags.global === true;
@@ -10483,7 +11822,7 @@ async function cmdSetup(argv) {
10483
11822
  ...global ? ["-g"] : []
10484
11823
  ];
10485
11824
  r.log(`installing the vos skills: npx ${args.join(" ")}`);
10486
- const res = spawnSync(npx(), args, {
11825
+ const res = spawnSync4(npx(), args, {
10487
11826
  cwd: root,
10488
11827
  encoding: "utf8",
10489
11828
  stdio: ["ignore", "pipe", "pipe"],
@@ -10501,9 +11840,9 @@ async function cmdSetup(argv) {
10501
11840
  const targets = homes.length ? homes : [fallbackSkillsHome(root)];
10502
11841
  for (const home of targets) {
10503
11842
  const dest = skillsDirFor(home);
10504
- mkdirSync2(dest, { recursive: true });
11843
+ mkdirSync6(dest, { recursive: true });
10505
11844
  for (const name of installedSkills(bundled)) {
10506
- cpSync(join21(bundled, name), join21(dest, name), { recursive: true });
11845
+ cpSync(join26(bundled, name), join26(dest, name), { recursive: true });
10507
11846
  }
10508
11847
  }
10509
11848
  skillsVia = "bundled";
@@ -10520,27 +11859,27 @@ async function cmdSetup(argv) {
10520
11859
  ].sort();
10521
11860
  }
10522
11861
  r.event({ event: "skills", via: skillsVia, names: skillNames });
10523
- let browser = probe();
11862
+ let browser = probe2();
10524
11863
  if (browser.kind === "none" && !noBrowser) {
10525
11864
  r.log(
10526
11865
  "no browser found; installing Chromium: npx playwright install chromium"
10527
11866
  );
10528
- spawnSync(npx(), ["playwright", "install", "chromium"], {
11867
+ spawnSync4(npx(), ["playwright", "install", "chromium"], {
10529
11868
  cwd: root,
10530
11869
  stdio: ["ignore", "ignore", "inherit"],
10531
11870
  shell: process.platform === "win32",
10532
11871
  timeout: 6e5
10533
11872
  });
10534
- browser = probe();
11873
+ browser = probe2();
10535
11874
  }
10536
11875
  r.event({ event: "browser", kind: browser.kind, path: browser.path });
10537
11876
  r.log(`browser: ${browser.line}`);
10538
11877
  let rules = null;
10539
11878
  if (!noRules) {
10540
11879
  const file = rulesFileFor(root);
10541
- const existing = existsSync19(file) ? readFileSync6(file, "utf8") : null;
11880
+ const existing = existsSync22(file) ? readFileSync9(file, "utf8") : null;
10542
11881
  const { text, changed } = applyRulesBlock(existing);
10543
- if (changed) writeFileSync2(file, text);
11882
+ if (changed) writeFileSync6(file, text);
10544
11883
  rules = { file, changed };
10545
11884
  r.log(
10546
11885
  changed ? `rules: wrote the vosso block into ${file}` : `rules: ${file} already carries the block`
@@ -10569,7 +11908,7 @@ Next: ${report.next_step}`
10569
11908
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
10570
11909
  }
10571
11910
  async function cmdDoctor(argv) {
10572
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
11911
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
10573
11912
  const r = reporterFor(flags);
10574
11913
  const root = process.cwd();
10575
11914
  const homes = detectAgents(root, { home: homedir2(), global: true });
@@ -10588,7 +11927,7 @@ Next: ${report.next_step}`
10588
11927
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
10589
11928
  }
10590
11929
  async function cmdWhoami(argv) {
10591
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
11930
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
10592
11931
  const r = reporterFor(flags);
10593
11932
  const origin = platformOrigin({
10594
11933
  origin: strFlag(flags, "origin"),
@@ -10650,9 +11989,9 @@ async function cmdWhoami(argv) {
10650
11989
  return EXIT_OK;
10651
11990
  }
10652
11991
  function cmdLogout(argv) {
10653
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
11992
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
10654
11993
  const r = reporterFor(flags);
10655
- const had = existsSync19(CREDENTIALS_PATH);
11994
+ const had = existsSync22(CREDENTIALS_PATH);
10656
11995
  rmSync3(CREDENTIALS_PATH, { force: true });
10657
11996
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
10658
11997
  r.done(
@@ -10668,7 +12007,7 @@ function cmdLogout(argv) {
10668
12007
  }
10669
12008
 
10670
12009
  // src/plugin/run.ts
10671
- var BOOLEAN_FLAGS8 = /* @__PURE__ */ new Set([
12010
+ var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set([
10672
12011
  "json",
10673
12012
  "help",
10674
12013
  "wait",
@@ -10714,6 +12053,14 @@ Take pipeline
10714
12053
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
10715
12054
  vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--as footage|take] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
10716
12055
  a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
12056
+ vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
12057
+ a finished piece (HyperFrames, Remotion's render, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
12058
+ vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
12059
+ a data-first program from it: every word a bound element, every colour a key, knobs, a label and a TODO per scene, the score as a doc.json track, port/REPORT.md
12060
+ vos build <program.mjs> [--out config.json] [--json]
12061
+ real functions \u2192 config.json, refusing a module-scope read, syntax the page cannot run and non-JSON data; config.sources.json maps each function to its line
12062
+ vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
12063
+ per-frame SSIM and source | vos | difference sheets; exit 1 when ANY frame is under the threshold (needs ffmpeg)
10717
12064
  vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
10718
12065
  vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
10719
12066
  vos session list | rm <name> what exists and how old; delete one
@@ -11047,8 +12394,8 @@ function takeBrowserArgs(multi) {
11047
12394
  function takeStorageState(flags) {
11048
12395
  const raw = strFlag(flags, "storage-state");
11049
12396
  if (!raw) return void 0;
11050
- const path = resolve10(raw);
11051
- if (!existsSync20(path))
12397
+ const path = resolve15(raw);
12398
+ if (!existsSync23(path))
11052
12399
  throw new UsageError(`--storage-state: no such file: ${path}`);
11053
12400
  return path;
11054
12401
  }
@@ -11061,7 +12408,7 @@ function takeSessionName(flags, storageState) {
11061
12408
  throw new UsageError(
11062
12409
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
11063
12410
  );
11064
- if (!existsSync20(sessionDir(name)))
12411
+ if (!existsSync23(sessionDir(name)))
11065
12412
  throw new UsageError(
11066
12413
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
11067
12414
  );
@@ -11117,7 +12464,7 @@ function wallGate(flags, r, opts) {
11117
12464
  async function cmdRecord(argv) {
11118
12465
  const { positionals, flags, multi } = parseArgs(
11119
12466
  argv,
11120
- BOOLEAN_FLAGS8,
12467
+ BOOLEAN_FLAGS11,
11121
12468
  MULTI_FLAGS2
11122
12469
  );
11123
12470
  const actionsPath = strFlag(flags, "actions") ?? positionals[0];
@@ -11130,7 +12477,7 @@ async function cmdRecord(argv) {
11130
12477
  const url = strFlag(flags, "url") ?? actions.url;
11131
12478
  if (!url)
11132
12479
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
11133
- const outDir = resolve10(strFlag(flags, "out") ?? "take");
12480
+ const outDir = resolve15(strFlag(flags, "out") ?? "take");
11134
12481
  const storageState = takeStorageState(flags);
11135
12482
  const sessionName = takeSessionName(flags, storageState);
11136
12483
  const browserArgs = takeBrowserArgs(multi);
@@ -11284,7 +12631,7 @@ ${lines.join("\n")}
11284
12631
  }
11285
12632
  const paths = takePaths(outDir);
11286
12633
  const prepare = async () => {
11287
- if (existsSync20(join22(outDir, "meta.json"))) {
12634
+ if (existsSync23(join27(outDir, "meta.json"))) {
11288
12635
  const { prevDoc, kept } = await prepareReRecord(outDir);
11289
12636
  r.log(
11290
12637
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
@@ -11349,7 +12696,7 @@ ${lines.join("\n")}
11349
12696
  },
11350
12697
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
11351
12698
  ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
11352
- Next: edit ${join22(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
12699
+ Next: edit ${join27(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11353
12700
  );
11354
12701
  return strictFail ? EXIT_USAGE : EXIT_OK;
11355
12702
  } finally {
@@ -11359,7 +12706,7 @@ ${lines.join("\n")}
11359
12706
  async function cmdCreate2(argv) {
11360
12707
  const { positionals, flags, multi } = parseArgs(
11361
12708
  argv,
11362
- BOOLEAN_FLAGS8,
12709
+ BOOLEAN_FLAGS11,
11363
12710
  MULTI_FLAGS2
11364
12711
  );
11365
12712
  const actionsPath = strFlag(flags, "actions");
@@ -11373,9 +12720,9 @@ async function cmdCreate2(argv) {
11373
12720
  const url = strFlag(flags, "url") ?? actions.url;
11374
12721
  if (!url)
11375
12722
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
11376
- const outDir = resolve10(strFlag(flags, "out") ?? "take");
12723
+ const outDir = resolve15(strFlag(flags, "out") ?? "take");
11377
12724
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
11378
- const out = positionals[0] ?? join22(outDir, `out.${format}`);
12725
+ const out = positionals[0] ?? join27(outDir, `out.${format}`);
11379
12726
  const parallel = numFlag(flags, "parallel", 1);
11380
12727
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11381
12728
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11388,7 +12735,7 @@ async function cmdCreate2(argv) {
11388
12735
  const headers = takeHeaders(multi);
11389
12736
  const paths = takePaths(outDir);
11390
12737
  const prepare = async () => {
11391
- if (existsSync20(join22(outDir, "meta.json"))) {
12738
+ if (existsSync23(join27(outDir, "meta.json"))) {
11392
12739
  const { prevDoc, kept } = await prepareReRecord(outDir);
11393
12740
  r.log(
11394
12741
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
@@ -11485,7 +12832,7 @@ async function cmdCreate2(argv) {
11485
12832
  async function cmdPlan(argv) {
11486
12833
  const { positionals, flags, multi } = parseArgs(
11487
12834
  argv,
11488
- BOOLEAN_FLAGS8,
12835
+ BOOLEAN_FLAGS11,
11489
12836
  /* @__PURE__ */ new Set(["with"])
11490
12837
  );
11491
12838
  const dir = positionals[0];
@@ -11495,13 +12842,13 @@ async function cmdPlan(argv) {
11495
12842
  );
11496
12843
  const r = createReporter(flags.json === true);
11497
12844
  if (flags.fresh === true) {
11498
- await rm5(join22(dir, "doc.json"), { force: true });
12845
+ await rm5(join27(dir, "doc.json"), { force: true });
11499
12846
  r.log("note: --fresh discarded the existing doc.json");
11500
12847
  }
11501
12848
  let reuse;
11502
12849
  if (flags.reuse === true) {
11503
- const from = resolve10(strFlag(flags, "from") ?? join22(dir, PREV_DOC_NAME));
11504
- if (!existsSync20(from)) {
12850
+ const from = resolve15(strFlag(flags, "from") ?? join27(dir, PREV_DOC_NAME));
12851
+ if (!existsSync23(from)) {
11505
12852
  throw new UsageError(
11506
12853
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
11507
12854
  );
@@ -11512,7 +12859,7 @@ async function cmdPlan(argv) {
11512
12859
  };
11513
12860
  }
11514
12861
  const style = await resolveStyleRef(flags);
11515
- const hasDoc = existsSync20(join22(dir, "doc.json"));
12862
+ const hasDoc = existsSync23(join27(dir, "doc.json"));
11516
12863
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
11517
12864
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
11518
12865
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -11558,7 +12905,7 @@ async function cmdPlan(argv) {
11558
12905
  ${s.reuse.flagged.join("\n ")}` : "") : "";
11559
12906
  r.done(
11560
12907
  {
11561
- take: resolve10(dir),
12908
+ take: resolve15(dir),
11562
12909
  fresh: s.fresh,
11563
12910
  cursorKept: s.cursorKept,
11564
12911
  zoomAuto: s.zoomAuto,
@@ -11570,7 +12917,7 @@ async function cmdPlan(argv) {
11570
12917
  ...s.motion ? { motion: s.motion } : {},
11571
12918
  ...s.reuse ? { reuse: s.reuse } : {}
11572
12919
  },
11573
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join22(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
12920
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join27(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
11574
12921
  );
11575
12922
  return EXIT_OK;
11576
12923
  }
@@ -11683,7 +13030,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
11683
13030
  async function cmdRender(argv) {
11684
13031
  const { positionals, flags, multi } = parseArgs(
11685
13032
  argv,
11686
- BOOLEAN_FLAGS8,
13033
+ BOOLEAN_FLAGS11,
11687
13034
  MULTI_FLAGS2
11688
13035
  );
11689
13036
  const dir = positionals[0];
@@ -11693,7 +13040,7 @@ async function cmdRender(argv) {
11693
13040
  );
11694
13041
  const r = createReporter(flags.json === true);
11695
13042
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
11696
- const out = positionals[1] ?? join22(dir, `out.${format}`);
13043
+ const out = positionals[1] ?? join27(dir, `out.${format}`);
11697
13044
  const parallel = numFlag(flags, "parallel", 1);
11698
13045
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11699
13046
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11751,7 +13098,7 @@ async function cmdRender(argv) {
11751
13098
  async function cmdFrames(argv) {
11752
13099
  const { positionals, flags, multi } = parseArgs(
11753
13100
  argv,
11754
- BOOLEAN_FLAGS8,
13101
+ BOOLEAN_FLAGS11,
11755
13102
  MULTI_FLAGS2
11756
13103
  );
11757
13104
  const dir = positionals[0];
@@ -11777,7 +13124,7 @@ async function cmdFrames(argv) {
11777
13124
  const atStill = flags["at-still"] === true;
11778
13125
  let times;
11779
13126
  try {
11780
- times = frameRaw !== void 0 ? parseTimes(frameRaw, duration) : timesRaw !== void 0 ? parseTimes(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes("0,25%,50%,75%,100%", duration);
13127
+ times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
11781
13128
  } catch (e) {
11782
13129
  throw new UsageError(e instanceof Error ? e.message : String(e));
11783
13130
  }
@@ -11824,7 +13171,7 @@ async function cmdFrames(argv) {
11824
13171
  async function cmdDeliver(argv) {
11825
13172
  const { positionals, flags, multi } = parseArgs(
11826
13173
  argv,
11827
- BOOLEAN_FLAGS8,
13174
+ BOOLEAN_FLAGS11,
11828
13175
  MULTI_FLAGS2
11829
13176
  );
11830
13177
  const dir = positionals[0];
@@ -11864,7 +13211,7 @@ async function cmdDeliver(argv) {
11864
13211
  if (timesRaw !== void 0) {
11865
13212
  try {
11866
13213
  const doc = take.doc;
11867
- times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes(s, duration)[0]);
13214
+ times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
11868
13215
  } catch (e) {
11869
13216
  throw new UsageError(e instanceof Error ? e.message : String(e));
11870
13217
  }
@@ -11940,11 +13287,11 @@ async function resolveStyleRef(flags) {
11940
13287
  }
11941
13288
  async function resolveDocRef(ref, flags, what) {
11942
13289
  const styleRef = ref;
11943
- const file = existsSync20(styleRef) ? resolve10(
13290
+ const file = existsSync23(styleRef) ? resolve15(
11944
13291
  styleRef,
11945
- existsSync20(join22(styleRef, "doc.json")) ? "doc.json" : ""
13292
+ existsSync23(join27(styleRef, "doc.json")) ? "doc.json" : ""
11946
13293
  ) : null;
11947
- if (file && existsSync20(file)) {
13294
+ if (file && existsSync23(file)) {
11948
13295
  return {
11949
13296
  from: file,
11950
13297
  doc: migrateHostedDoc5(
@@ -11990,7 +13337,7 @@ async function officialTemplate(origin, ref) {
11990
13337
  }
11991
13338
  }
11992
13339
  async function cmdDigest(argv) {
11993
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13340
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
11994
13341
  const dir = positionals[0];
11995
13342
  if (!dir)
11996
13343
  throw new UsageError(
@@ -12031,7 +13378,7 @@ async function cmdDigest(argv) {
12031
13378
  kinds,
12032
13379
  frames: d.images.full,
12033
13380
  crops: d.images.crop,
12034
- sheet: d.images.sheet ? join22(result.outDir, d.images.sheet) : null,
13381
+ sheet: d.images.sheet ? join27(result.outDir, d.images.sheet) : null,
12035
13382
  scenes: kinds.scene ?? 0,
12036
13383
  sourceDuration: d.take.sourceDuration,
12037
13384
  outputDuration: d.take.outputDuration,
@@ -12054,19 +13401,19 @@ async function cmdDigest(argv) {
12054
13401
  }
12055
13402
  }
12056
13403
  async function cmdOpen(argv) {
12057
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13404
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12058
13405
  const dir = positionals[0];
12059
13406
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
12060
13407
  const r = createReporter(flags.json === true);
12061
13408
  const take = await loadTake(dir);
12062
- if (!existsSync20(take.paths.recording)) {
13409
+ if (!existsSync23(take.paths.recording)) {
12063
13410
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
12064
13411
  }
12065
13412
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
12066
13413
  /\/+$/,
12067
13414
  ""
12068
13415
  );
12069
- const server = await startTakeServer(resolve10(dir), {});
13416
+ const server = await startTakeServer(resolve15(dir), {});
12070
13417
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
12071
13418
  r.event({ event: "open", server: server.base, url });
12072
13419
  r.log(`take served at ${server.base}`);
@@ -12092,7 +13439,7 @@ async function cmdOpen(argv) {
12092
13439
  return EXIT_OK;
12093
13440
  }
12094
13441
  async function cmdJudge(argv) {
12095
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13442
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12096
13443
  const target = positionals[0];
12097
13444
  const against = strFlag(flags, "against");
12098
13445
  if (!target || !against)
@@ -12100,9 +13447,9 @@ async function cmdJudge(argv) {
12100
13447
  "vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
12101
13448
  );
12102
13449
  const r = createReporter(flags.json === true);
12103
- const kitFile = target.endsWith("kit.json") ? target : join22(target, "kit.json");
12104
- if (!existsSync20(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
12105
- if (!existsSync20(against))
13450
+ const kitFile = target.endsWith("kit.json") ? target : join27(target, "kit.json");
13451
+ if (!existsSync23(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
13452
+ if (!existsSync23(against))
12106
13453
  throw new UsageError(`${against}: no reference manifest`);
12107
13454
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
12108
13455
  const verdicts = JSON.parse(await readFile17(result.verdictFile, "utf8")).verdicts;
@@ -12121,7 +13468,7 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
12121
13468
  return EXIT_OK;
12122
13469
  }
12123
13470
  async function cmdCallout(argv) {
12124
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13471
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12125
13472
  const dir = positionals[0];
12126
13473
  const shape = positionals[1];
12127
13474
  if (!dir || !shape || !isCalloutShape(shape))
@@ -12220,11 +13567,11 @@ async function cmdCallout(argv) {
12220
13567
  return EXIT_OK;
12221
13568
  }
12222
13569
  async function cmdValidate(argv) {
12223
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13570
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12224
13571
  const target = positionals[0];
12225
13572
  if (!target) throw new UsageError("vos validate <actions.json|take>");
12226
13573
  const r = createReporter(flags.json === true);
12227
- const kitTarget = target.endsWith("kit.json") ? target : existsSync20(join22(target, "kit.json")) && !isTakeDir(target) ? join22(target, "kit.json") : null;
13574
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync23(join27(target, "kit.json")) && !isTakeDir(target) ? join27(target, "kit.json") : null;
12228
13575
  if (kitTarget) {
12229
13576
  const picture = flags.picture === true;
12230
13577
  const verdict = await validateKit(kitTarget, { picture });
@@ -12256,9 +13603,9 @@ async function cmdValidate(argv) {
12256
13603
  r.done({ valid: true, target }, `${target}: valid actions file`);
12257
13604
  return EXIT_OK;
12258
13605
  }
12259
- if (!isTakeDir(target) && existsSync20(join22(target, "config.json"))) {
13606
+ if (!isTakeDir(target) && existsSync23(join27(target, "config.json"))) {
12260
13607
  const parsed = JSON.parse(
12261
- await readFile17(join22(target, "config.json"), "utf8")
13608
+ await readFile17(join27(target, "config.json"), "utf8")
12262
13609
  );
12263
13610
  const pre = preflightConfig(parsed);
12264
13611
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -12274,9 +13621,9 @@ async function cmdValidate(argv) {
12274
13621
  ...collectConfigMediaUrls(pre.config ?? parsed),
12275
13622
  ...collectDocMediaUrls(doc)
12276
13623
  ];
12277
- const probe2 = mediaProbeLints(await probeMediaUrls([...new Set(urls)]));
12278
- problems2.push(...probe2.problems);
12279
- warnings2.push(...probe2.warnings);
13624
+ const probe3 = mediaProbeLints(await probeMediaUrls([...new Set(urls)]));
13625
+ problems2.push(...probe3.problems);
13626
+ warnings2.push(...probe3.warnings);
12280
13627
  }
12281
13628
  if (problems2.length) {
12282
13629
  r.done(
@@ -12299,7 +13646,7 @@ async function cmdValidate(argv) {
12299
13646
  const take = await loadTake(target);
12300
13647
  const problems = [];
12301
13648
  const warnings = [];
12302
- if (!existsSync20(take.paths.recording)) {
13649
+ if (!existsSync23(take.paths.recording)) {
12303
13650
  problems.push("missing recording (re-run record)");
12304
13651
  } else {
12305
13652
  const mismatch = containerMismatch(
@@ -12325,11 +13672,11 @@ async function cmdValidate(argv) {
12325
13672
  const lint = lintDoc(take.doc);
12326
13673
  problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
12327
13674
  warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
12328
- const probe2 = mediaProbeLints(
13675
+ const probe3 = mediaProbeLints(
12329
13676
  await probeMediaUrls(collectDocMediaUrls(take.doc))
12330
13677
  );
12331
- problems.push(...probe2.problems);
12332
- warnings.push(...probe2.warnings);
13678
+ problems.push(...probe3.problems);
13679
+ warnings.push(...probe3.warnings);
12333
13680
  if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
12334
13681
  const doc = take.doc;
12335
13682
  const times = [
@@ -12382,7 +13729,7 @@ async function cmdValidate(argv) {
12382
13729
  async function cmdPush3(argv) {
12383
13730
  const { positionals, flags, multi } = parseArgs(
12384
13731
  argv,
12385
- BOOLEAN_FLAGS8,
13732
+ BOOLEAN_FLAGS11,
12386
13733
  MULTI_FLAGS2
12387
13734
  );
12388
13735
  const dir = positionals[0];
@@ -12392,7 +13739,7 @@ async function cmdPush3(argv) {
12392
13739
  );
12393
13740
  const refusal = takePushRefusal({
12394
13741
  vos: strFlag(flags, "vos"),
12395
- trackedVosId: readSyncState(resolve10(dir))?.vosId ?? null,
13742
+ trackedVosId: readSyncState(resolve15(dir))?.vosId ?? null,
12396
13743
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
12397
13744
  (name) => hasFlag(flags, name)
12398
13745
  )
@@ -12401,7 +13748,7 @@ async function cmdPush3(argv) {
12401
13748
  const r = createReporter(flags.json === true);
12402
13749
  const overrides = multi.override;
12403
13750
  const result = await pushTake(
12404
- resolve10(dir),
13751
+ resolve15(dir),
12405
13752
  {
12406
13753
  key: strFlag(flags, "key"),
12407
13754
  api: strFlag(flags, "api"),
@@ -12446,11 +13793,11 @@ ${landedLines(landed)}` : "")
12446
13793
  return EXIT_OK;
12447
13794
  }
12448
13795
  async function cmdPull2(argv) {
12449
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8, MULTI_FLAGS2);
13796
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11, MULTI_FLAGS2);
12450
13797
  const dir = positionals[0] ?? ".";
12451
13798
  const r = createReporter(flags.json === true);
12452
13799
  const result = await pullTake(
12453
- resolve10(dir),
13800
+ resolve15(dir),
12454
13801
  {
12455
13802
  key: strFlag(flags, "key"),
12456
13803
  api: strFlag(flags, "api"),
@@ -12559,6 +13906,12 @@ async function run(argv) {
12559
13906
  }
12560
13907
  case "delete":
12561
13908
  return await cmdDelete(rest);
13909
+ case "build":
13910
+ return await cmdBuild(rest);
13911
+ case "compare":
13912
+ return await cmdCompare(rest);
13913
+ case "port":
13914
+ return await cmdPort(rest);
12562
13915
  case "folder":
12563
13916
  return await cmdFolder(rest);
12564
13917
  case "asset":
@@ -12605,7 +13958,7 @@ async function run(argv) {
12605
13958
  }
12606
13959
  }
12607
13960
  async function cmdSession(argv) {
12608
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
13961
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12609
13962
  const [sub, arg] = positionals;
12610
13963
  const r = createReporter(flags.json === true);
12611
13964
  const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
@@ -12624,38 +13977,38 @@ async function cmdSession(argv) {
12624
13977
  if (!/^https?:$/.test(url.protocol))
12625
13978
  throw new UsageError("vos session open wants an http(s) URL");
12626
13979
  await openSession(name, url.href, { log: r.log });
12627
- const summary2 = await summarizeSession(name);
13980
+ const summary3 = await summarizeSession(name);
12628
13981
  r.done(
12629
13982
  {
12630
- name: summary2.name,
12631
- origins: summary2.origins,
12632
- newestExpires: summary2.newestExpires,
12633
- sessionOnly: summary2.sessionOnly
13983
+ name: summary3.name,
13984
+ origins: summary3.origins,
13985
+ newestExpires: summary3.newestExpires,
13986
+ sessionOnly: summary3.sessionOnly
12634
13987
  },
12635
- `${summaryLine(summary2)}
13988
+ `${summaryLine(summary3)}
12636
13989
  Record on it: vos record --actions actions.json --out take --session ${name} --strict`
12637
13990
  );
12638
- return summary2.origins.length ? EXIT_OK : EXIT_ERROR;
13991
+ return summary3.origins.length ? EXIT_OK : EXIT_ERROR;
12639
13992
  }
12640
13993
  case "check": {
12641
13994
  if (!arg) throw new UsageError(usage);
12642
13995
  const bad = sessionNameVerdict(arg);
12643
13996
  if (bad) throw new UsageError(bad);
12644
- if (!existsSync20(sessionDir(arg)))
13997
+ if (!existsSync23(sessionDir(arg)))
12645
13998
  throw new UsageError(`no session "${arg}" (vos session list)`);
12646
- const summary2 = await summarizeSession(arg);
13999
+ const summary3 = await summarizeSession(arg);
12647
14000
  const url = strFlag(flags, "url");
12648
14001
  if (!url) {
12649
14002
  r.done(
12650
14003
  {
12651
14004
  name: arg,
12652
- origins: summary2.origins,
12653
- newestExpires: summary2.newestExpires,
12654
- sessionOnly: summary2.sessionOnly
14005
+ origins: summary3.origins,
14006
+ newestExpires: summary3.newestExpires,
14007
+ sessionOnly: summary3.sessionOnly
12655
14008
  },
12656
- summaryLine(summary2)
14009
+ summaryLine(summary3)
12657
14010
  );
12658
- return summary2.origins.length ? EXIT_OK : EXIT_WALL;
14011
+ return summary3.origins.length ? EXIT_OK : EXIT_WALL;
12659
14012
  }
12660
14013
  const verdict = await withSession(arg, async (ctx) => {
12661
14014
  const page = await ctx.newPage();
@@ -12676,8 +14029,8 @@ async function cmdSession(argv) {
12676
14029
  name: arg,
12677
14030
  ok: false,
12678
14031
  wall: verdict,
12679
- origins: summary2.origins,
12680
- newestExpires: summary2.newestExpires
14032
+ origins: summary3.origins,
14033
+ newestExpires: summary3.newestExpires
12681
14034
  },
12682
14035
  `session "${arg}" no longer opens ${url}: ${verdict.message.split(". ")[0]}. It expired, or the sign-in never landed. Sign in again: vos session open ${url} --name ${arg}`
12683
14036
  );
@@ -12687,10 +14040,10 @@ async function cmdSession(argv) {
12687
14040
  {
12688
14041
  name: arg,
12689
14042
  ok: true,
12690
- origins: summary2.origins,
12691
- newestExpires: summary2.newestExpires
14043
+ origins: summary3.origins,
14044
+ newestExpires: summary3.newestExpires
12692
14045
  },
12693
- `session "${arg}" opens ${url} signed in. ${summaryLine(summary2)}`
14046
+ `session "${arg}" opens ${url} signed in. ${summaryLine(summary3)}`
12694
14047
  );
12695
14048
  return EXIT_OK;
12696
14049
  }
@@ -12745,4 +14098,4 @@ export {
12745
14098
  verbHelp,
12746
14099
  run
12747
14100
  };
12748
- //# sourceMappingURL=chunk-MJYXP4AA.js.map
14101
+ //# sourceMappingURL=chunk-M22MAUMZ.js.map