@vosjs/cli 0.77.1 → 0.78.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,12 +9,14 @@ import {
9
9
  reencodeStill,
10
10
  renderStill,
11
11
  videoFormat
12
- } from "./chunk-KDIRW72G.js";
12
+ } from "./chunk-JYCW4EOA.js";
13
13
  import {
14
14
  PREV_DOC_NAME,
15
15
  RECORDING_NAME,
16
16
  docLayerRefs,
17
17
  docMediaRefs,
18
+ docObjectRefs,
19
+ docOverlayRefs,
18
20
  dropScreencastFrames,
19
21
  ensureTakeDir,
20
22
  fileSha256,
@@ -37,7 +39,7 @@ import {
37
39
  uploadDocRefs,
38
40
  uploadManifest,
39
41
  writeJson
40
- } from "./chunk-UY2N42P2.js";
42
+ } from "./chunk-ZSJEISKL.js";
41
43
  import {
42
44
  CREDENTIALS_PATH,
43
45
  MEDIA_HEAD_BYTES,
@@ -62,6 +64,7 @@ import {
62
64
  programAssets,
63
65
  promptHint,
64
66
  readSyncState,
67
+ refuseSymlink,
65
68
  requireCredential,
66
69
  resolveCredential,
67
70
  resolveMediaType,
@@ -133,12 +136,12 @@ function landedLines(l) {
133
136
  }
134
137
 
135
138
  // src/plugin/run.ts
136
- import { mkdir as mkdir8, readFile as readFile13, rm as rm2 } from "fs/promises";
137
- import { existsSync as existsSync23 } from "fs";
138
- import { join as join25, resolve as resolve14 } from "path";
139
+ import { mkdir as mkdir13, readFile as readFile15, rm as rm5 } from "fs/promises";
140
+ import { existsSync as existsSync27 } from "fs";
141
+ import { join as join29, resolve as resolve18 } from "path";
139
142
  import {
140
143
  END_CARD_FROM as END_CARD_FROM2,
141
- docOutputDuration as docOutputDuration3,
144
+ docOutputDuration as docOutputDuration4,
142
145
  migrateHostedDoc as migrateHostedDoc4
143
146
  } from "@vosjs/studio-core";
144
147
 
@@ -215,7 +218,10 @@ import {
215
218
  docCardLayout,
216
219
  docZoomTrack,
217
220
  findStep,
221
+ formatElapsed,
222
+ hasElapsed,
218
223
  outputEnd,
224
+ resolveAnchor,
219
225
  pinBox,
220
226
  pinCandidates,
221
227
  pinRectOnScreen,
@@ -451,6 +457,7 @@ function lintDoc(docIn) {
451
457
  const doc = docIn;
452
458
  const problems = [];
453
459
  const warnings = [];
460
+ const notes = [];
454
461
  const recording = isObj(doc.source);
455
462
  const source = isObj(doc.source) ? doc.source : {};
456
463
  const meta = isObj(source.meta) ? source.meta : {};
@@ -1047,6 +1054,15 @@ function lintDoc(docIn) {
1047
1054
  footageEnd = null;
1048
1055
  }
1049
1056
  }
1057
+ let anchorPieces = null;
1058
+ if (recording) {
1059
+ try {
1060
+ anchorPieces = ratedSegments(doc);
1061
+ } catch {
1062
+ anchorPieces = null;
1063
+ }
1064
+ }
1065
+ const captionTimes = [];
1050
1066
  for (let i = 0; i < (overlays?.length ?? 0); i++) {
1051
1067
  const o = overlays[i];
1052
1068
  const name = `overlays[${i}]`;
@@ -1054,6 +1070,24 @@ function lintDoc(docIn) {
1054
1070
  problems.push(`${name} must be an object`);
1055
1071
  continue;
1056
1072
  }
1073
+ const playsAt = checkOverlayAnchor(
1074
+ o,
1075
+ name,
1076
+ docIn,
1077
+ recording,
1078
+ anchorPieces,
1079
+ problems,
1080
+ warnings,
1081
+ notes
1082
+ );
1083
+ if (o.kind === "text" && o.preset === "caption" && o.pin === void 0 && isNum(playsAt) && isNum(o.duration) && o.duration > 0) {
1084
+ const label = typeof o.id === "string" ? `${name} (${o.id})` : name;
1085
+ captionTimes.push({
1086
+ name: label,
1087
+ start: playsAt,
1088
+ end: playsAt + o.duration
1089
+ });
1090
+ }
1057
1091
  if (o.kind !== "text" && o.kind !== "image" && o.kind !== "video" && o.kind !== "html") {
1058
1092
  problems.push(
1059
1093
  `${name}.kind must be "text" | "image" | "video" | "html" (got ${String(o.kind)})`
@@ -1064,14 +1098,14 @@ function lintDoc(docIn) {
1064
1098
  if (!isNum(o.duration) || o.duration <= 0) {
1065
1099
  problems.push(`${name}.duration must be > 0 (seconds)`);
1066
1100
  }
1067
- if (footageEnd !== null && footageEnd > 0 && isNum(o.start) && isNum(o.duration)) {
1068
- const over = o.start + o.duration - footageEnd;
1101
+ if (footageEnd !== null && footageEnd > 0 && isNum(playsAt) && isNum(o.duration)) {
1102
+ const over = playsAt + o.duration - footageEnd;
1069
1103
  const frameAnim = isObj(docIn) && isObj(docIn.frame) && isObj(docIn.frame.anim) ? docIn.frame.anim : null;
1070
1104
  const exit = frameAnim ? isObj(frameAnim.exit) ? frameAnim.exit.kind : frameAnim.exit : void 0;
1071
1105
  const cardLeaves = exit !== void 0 && exit !== "none";
1072
1106
  if (over > 0.05 && !cardLeaves) {
1073
1107
  warnings.push(
1074
- o.start >= footageEnd ? `${name} starts at ${o.start.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays over the card's held last frame and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the card holds its last frame under it. Shorten it, put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')`
1108
+ playsAt >= footageEnd ? `${name} starts at ${playsAt.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays over the card's held last frame and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the card holds its last frame under it. Shorten it, put a freeze under it, or, for a closing card on the ground, let the card leave (frame.anim.exit: 'fade')`
1075
1109
  );
1076
1110
  }
1077
1111
  }
@@ -1616,6 +1650,17 @@ function lintDoc(docIn) {
1616
1650
  }
1617
1651
  }
1618
1652
  }
1653
+ captionTimes.sort((a, b) => a.start - b.start || a.end - b.end);
1654
+ for (let i = 0; i < captionTimes.length; i++) {
1655
+ for (let j = i + 1; j < captionTimes.length; j++) {
1656
+ const a = captionTimes[i];
1657
+ const b = captionTimes[j];
1658
+ if (b.start >= a.end - EPS) break;
1659
+ warnings.push(
1660
+ `${a.name} (${a.start.toFixed(2)}\u2013${a.end.toFixed(2)}s) and ${b.name} (${b.start.toFixed(2)}\u2013${b.end.toFixed(2)}s) are captions on screen at the same time; shorten the first, move one (an anchor's offset), or drop one`
1661
+ );
1662
+ }
1663
+ }
1619
1664
  if (isObj(doc.export)) {
1620
1665
  const resolution = doc.export.resolution;
1621
1666
  const validResolution = EXPORT_RESOLUTION_OPTIONS.includes(resolution);
@@ -1643,7 +1688,66 @@ function lintDoc(docIn) {
1643
1688
  }
1644
1689
  if (recording)
1645
1690
  framingWarnings(docIn, doc, zoom, overlays ?? [], warnings);
1646
- return { problems, warnings };
1691
+ return { problems, warnings, ...notes.length ? { notes } : {} };
1692
+ }
1693
+ function checkOverlayAnchor(o, name, docIn, recording, pieces, problems, warnings, notes) {
1694
+ const fallback = isNum(o.start) ? o.start : null;
1695
+ const words2 = o.kind === "text" ? o.text : void 0;
1696
+ const elapsed = o.kind === "text" && hasElapsed(words2);
1697
+ if (o.anchor === void 0) {
1698
+ if (elapsed)
1699
+ warnings.push(
1700
+ `${name}: its words carry {elapsed}, which fills only from an anchored step's off-camera wait; anchor it (anchor: { step }) or write the duration`
1701
+ );
1702
+ return fallback;
1703
+ }
1704
+ const a = o.anchor;
1705
+ if (!isObj(a)) {
1706
+ problems.push(`${name}.anchor must be { step, media?, at?, offset? }`);
1707
+ return fallback;
1708
+ }
1709
+ let shaped = true;
1710
+ if (typeof a.step !== "string" && !(isNum(a.step) && Number.isInteger(a.step) && a.step >= 0)) {
1711
+ problems.push(
1712
+ `${name}.anchor.step must be a step id (string) or its index (integer \u2265 0)`
1713
+ );
1714
+ shaped = false;
1715
+ }
1716
+ if (a.media !== void 0 && typeof a.media !== "string") {
1717
+ problems.push(`${name}.anchor.media must be a media[].id (string)`);
1718
+ shaped = false;
1719
+ }
1720
+ if (a.at !== void 0 && a.at !== "start" && a.at !== "end") {
1721
+ problems.push(`${name}.anchor.at must be "start" or "end"`);
1722
+ shaped = false;
1723
+ }
1724
+ if (a.offset !== void 0 && !isNum(a.offset)) {
1725
+ problems.push(`${name}.anchor.offset must be a number (OUTPUT seconds)`);
1726
+ shaped = false;
1727
+ }
1728
+ if (!shaped || !recording || !pieces) return fallback;
1729
+ const r = resolveAnchor(
1730
+ docIn,
1731
+ a,
1732
+ pieces
1733
+ );
1734
+ const step = `step ${JSON.stringify(a.step)}${typeof a.media === "string" ? ` of media ${a.media}` : ""}`;
1735
+ const at2 = fallback === null ? "nowhere" : `${fallback.toFixed(2)}s`;
1736
+ if (r.start === null) {
1737
+ const why = r.miss === "media" ? `media "${String(a.media)}" is not one of media[].id` : r.miss === "step" ? `${step} is not in its meta.steps` : r.miss === "skipped" ? `${step} was skipped by the recorder` : `the cut leaves ${step} out`;
1738
+ warnings.push(
1739
+ `${name}.anchor: ${why}, so it plays at its fallback start (${at2}); re-anchor it, or keep the footage that step plays in`
1740
+ );
1741
+ }
1742
+ if (elapsed && r.elapsedFrom && r.elapsedMs !== void 0)
1743
+ notes.push(
1744
+ `${name}: {elapsed} reads the off-camera wait of step ${JSON.stringify(r.elapsedFrom.id ?? r.elapsedFrom.step)} (${formatElapsed(r.elapsedMs)}), the nearest one before ${step}, which waited for nothing itself`
1745
+ );
1746
+ if (elapsed && r.elapsedMs === void 0 && r.miss !== "media" && r.miss !== "step")
1747
+ warnings.push(
1748
+ `${name}: {elapsed} stays as written, because neither ${step} nor any step before it recorded an off-camera wait (no meta.skips entry names one, and none has an offCameraMs)`
1749
+ );
1750
+ return r.start ?? fallback;
1647
1751
  }
1648
1752
  function clicksWithRects(source) {
1649
1753
  const cursor = Array.isArray(source.cursor) ? source.cursor : [];
@@ -1759,15 +1863,33 @@ function checkPin(o, name, docIn, recording, problems, warnings) {
1759
1863
  }
1760
1864
  function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1761
1865
  try {
1762
- const source = isObj(doc.source) ? doc.source : {};
1763
- const meta = isObj(source.meta) ? source.meta : {};
1764
- const w = isNum(meta.width) ? meta.width : 0;
1765
- const h = isNum(meta.height) ? meta.height : 0;
1766
- if (!(w > 0) || !(h > 0)) return;
1767
- const clicks = clicksWithRects(source);
1768
- if (!clicks.length) return;
1866
+ const evidence = /* @__PURE__ */ new Map();
1867
+ const others = Array.isArray(doc.media) ? doc.media : [];
1868
+ for (const [key, src] of [
1869
+ ["", doc.source],
1870
+ ...others.map((m) => [isObj(m) ? m.id : void 0, m])
1871
+ ]) {
1872
+ if (typeof key !== "string" || !isObj(src)) continue;
1873
+ const meta = isObj(src.meta) ? src.meta : {};
1874
+ const w = isNum(meta.width) ? meta.width : 0;
1875
+ const h = isNum(meta.height) ? meta.height : 0;
1876
+ if (!(w > 0) || !(h > 0)) continue;
1877
+ const clicks = clicksWithRects(src);
1878
+ if (!clicks.length) continue;
1879
+ let layout2;
1880
+ try {
1881
+ layout2 = docCardLayout(docIn, key || null);
1882
+ } catch {
1883
+ continue;
1884
+ }
1885
+ evidence.set(key, { w, h, clicks, layout: layout2 });
1886
+ }
1887
+ if (!evidence.size) return;
1769
1888
  const layout = docCardLayout(docIn);
1770
1889
  for (const z of zoom) {
1890
+ const ev = evidence.get(typeof z.media === "string" ? z.media : "");
1891
+ if (!ev) continue;
1892
+ const { w, h, clicks } = ev;
1771
1893
  if (!isNum(z.in) || !isNum(z.out) || !isNum(z.level) || !isNum(z.cx) || !isNum(z.cy) || z.level <= 1.001)
1772
1894
  continue;
1773
1895
  const zin = z.in;
@@ -1794,7 +1916,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1794
1916
  focusMode: z.focusMode
1795
1917
  },
1796
1918
  rect,
1797
- layout,
1919
+ ev.layout,
1798
1920
  0.02,
1799
1921
  cameraModel(docIn.frame)
1800
1922
  )) {
@@ -1897,19 +2019,22 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1897
2019
  } catch {
1898
2020
  return;
1899
2021
  }
1900
- for (const c of clicks) {
1901
- const out = spanOutputExtent(rated, c.t, c.t + 1e-3);
2022
+ const presses = [...evidence.entries()].flatMap(
2023
+ ([key, ev]) => ev.clicks.map((c) => ({ c, key, ev }))
2024
+ );
2025
+ for (const { c, key, ev } of presses) {
2026
+ const out = spanOutputExtent(rated, c.t, c.t + 1e-3, key || void 0);
1902
2027
  if (!out || out.start < o.start || out.start > o.start + o.duration)
1903
2028
  continue;
1904
2029
  const target = pinRectOnScreen(
1905
2030
  {
1906
- x: c.rect.x / w,
1907
- y: c.rect.y / h,
1908
- w: c.rect.w / w,
1909
- h: c.rect.h / h
2031
+ x: c.rect.x / ev.w,
2032
+ y: c.rect.y / ev.h,
2033
+ w: c.rect.w / ev.w,
2034
+ h: c.rect.h / ev.h
1910
2035
  },
1911
2036
  out.start,
1912
- layout,
2037
+ ev.layout,
1913
2038
  camera,
1914
2039
  zoomTrack
1915
2040
  );
@@ -2017,7 +2142,7 @@ function mediaProbeLints(results) {
2017
2142
  }
2018
2143
 
2019
2144
  // src/plugin/framesTake.ts
2020
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
2145
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2021
2146
  import { existsSync as existsSync3 } from "fs";
2022
2147
  import { join as join3, resolve as resolve3 } from "path";
2023
2148
  import { compileVosConfig } from "@vosjs/core";
@@ -3279,6 +3404,55 @@ var VIDEO_TOKEN = "__VOS_CLI_VIDEO__";
3279
3404
  function stillSupersample(w, h) {
3280
3405
  return Math.min(3, Math.max(1, Math.ceil(1800 / Math.max(1, w, h))));
3281
3406
  }
3407
+ function zoomApexes(doc) {
3408
+ const rated = ratedSegments2(doc);
3409
+ const media = new Set((doc.media ?? []).map((m) => m.id));
3410
+ const apexes = [];
3411
+ const skipped = [];
3412
+ for (const z of doc.zoom) {
3413
+ const of = z.media ? { media: z.media } : {};
3414
+ if (z.media && !media.has(z.media)) {
3415
+ skipped.push({
3416
+ id: z.id,
3417
+ ...of,
3418
+ in: z.in,
3419
+ out: z.out,
3420
+ reason: `names media "${z.media}", which the take does not carry`
3421
+ });
3422
+ continue;
3423
+ }
3424
+ const ext = spanOutputExtent2(rated, z.in, z.out, z.media);
3425
+ if (!ext) {
3426
+ skipped.push({
3427
+ id: z.id,
3428
+ ...of,
3429
+ in: z.in,
3430
+ out: z.out,
3431
+ reason: "the cut keeps none of it, so it never plays"
3432
+ });
3433
+ continue;
3434
+ }
3435
+ apexes.push({
3436
+ id: z.id,
3437
+ ...of,
3438
+ time: (ext.start + ext.end) / 2,
3439
+ start: ext.start,
3440
+ end: ext.end
3441
+ });
3442
+ }
3443
+ return { apexes, skipped };
3444
+ }
3445
+ var ZOOM_UNRENDERED_BELOW = 4e-3;
3446
+ var SIGNATURE_LONG_SIDE = 96;
3447
+ function signatureDifference(a, b) {
3448
+ if (a.length !== b.length || a.length === 0) return 1;
3449
+ let sum = 0;
3450
+ for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
3451
+ return sum / a.length / 255;
3452
+ }
3453
+ function unrenderedZoomLine(f) {
3454
+ return `zoom ${f.zoomId ?? "?"} at ${f.time.toFixed(2)}s did not render (its frame matches the zoom-off frame${f.twin ? `: ${f.twin}` : ""})`;
3455
+ }
3282
3456
  async function stillsInPage(opts) {
3283
3457
  const w = window;
3284
3458
  const RW = opts.W * opts.ss;
@@ -3287,11 +3461,9 @@ async function stillsInPage(opts) {
3287
3461
  const vblob = await (await fetch(opts.videoUrl)).blob();
3288
3462
  const vurl = URL.createObjectURL(vblob);
3289
3463
  const code = opts.animationCode.split(opts.token).join(vurl);
3290
- const mod = await import(
3291
- /* @vite-ignore */
3292
- URL.createObjectURL(
3293
- new Blob([code], { type: "text/javascript" })
3294
- )
3464
+ const load = new Function("u", "return import(u)");
3465
+ const mod = await load(
3466
+ URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
3295
3467
  );
3296
3468
  w.__vos__ = w.__vos__ || {};
3297
3469
  w.__vos__.isPaused = true;
@@ -3367,6 +3539,25 @@ async function stillsInPage(opts) {
3367
3539
  }
3368
3540
  }
3369
3541
  };
3542
+ const sigCanvas = document.createElement("canvas");
3543
+ sigCanvas.width = opts.sigW;
3544
+ sigCanvas.height = opts.sigH;
3545
+ const signature = () => {
3546
+ const g = sigCanvas.getContext("2d", { willReadFrequently: true });
3547
+ g.imageSmoothingEnabled = true;
3548
+ g.imageSmoothingQuality = "high";
3549
+ g.clearRect(0, 0, opts.sigW, opts.sigH);
3550
+ g.drawImage(canvas, 0, 0, opts.sigW, opts.sigH);
3551
+ const px = g.getImageData(0, 0, opts.sigW, opts.sigH).data;
3552
+ const out2 = new Array(opts.sigW * opts.sigH);
3553
+ for (let p = 0, q = 0; p < px.length; p += 4, q++)
3554
+ out2[q] = Math.round(
3555
+ 0.299 * px[p] + 0.587 * px[p + 1] + 0.114 * px[p + 2]
3556
+ );
3557
+ return out2;
3558
+ };
3559
+ const sigWanted = new Set(opts.sigNames);
3560
+ const sigs = {};
3370
3561
  for (let i = 0; i < opts.shots.length; i++) {
3371
3562
  const shot = opts.shots[i];
3372
3563
  timeline.seek(shot.time, false);
@@ -3378,14 +3569,56 @@ async function stillsInPage(opts) {
3378
3569
  }
3379
3570
  await waitVideosSettled(8e3);
3380
3571
  const png = await toPng();
3572
+ if (sigWanted.has(shot.name)) sigs[shot.name] = signature();
3381
3573
  await fetch("/save?name=" + shot.name, { method: "POST", body: png });
3382
3574
  w.__progress = (i + 1) / opts.shots.length;
3383
3575
  }
3384
- w.__done = { ok: true, count: opts.shots.length };
3576
+ w.__done = { ok: true, count: opts.shots.length, sigs };
3385
3577
  } catch (e) {
3386
3578
  w.__error = String(e instanceof Error && e.stack || e);
3387
3579
  }
3388
3580
  }
3581
+ async function captureShots(context2, base, opts) {
3582
+ const long = Math.max(opts.width, opts.height);
3583
+ const side = (n) => Math.max(1, Math.round(n / long * SIGNATURE_LONG_SIDE));
3584
+ const page = await context2.newPage();
3585
+ try {
3586
+ page.on("console", (m) => {
3587
+ if (m.type() === "error")
3588
+ process.stderr.write(` [stills page] ${m.text()}
3589
+ `);
3590
+ });
3591
+ await page.addInitScript(() => {
3592
+ ;
3593
+ globalThis.__name = (f) => f;
3594
+ });
3595
+ await page.goto(`${base}/render.html`);
3596
+ await page.waitForFunction("window.__pageReady__ === true");
3597
+ void page.evaluate(stillsInPage, {
3598
+ animationCode: opts.animationCode,
3599
+ token: VIDEO_TOKEN,
3600
+ videoUrl: opts.videoUrl,
3601
+ W: opts.width,
3602
+ H: opts.height,
3603
+ ss: opts.ss,
3604
+ shots: opts.shots,
3605
+ sigNames: opts.sigNames,
3606
+ sigW: side(opts.width),
3607
+ sigH: side(opts.height)
3608
+ }).catch(() => {
3609
+ });
3610
+ const done = await waitForPageDone(
3611
+ page,
3612
+ "stills",
3613
+ () => {
3614
+ },
3615
+ 12e4 + opts.shots.length * 4e3
3616
+ );
3617
+ return done.sigs ?? {};
3618
+ } finally {
3619
+ await page.close();
3620
+ }
3621
+ }
3389
3622
  async function framesTake(browser, dir, opts) {
3390
3623
  const take = await loadTake(dir);
3391
3624
  const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
@@ -3408,13 +3641,17 @@ async function framesTake(browser, dir, opts) {
3408
3641
  time: clamp(t),
3409
3642
  kind: "time"
3410
3643
  }));
3644
+ let skippedZooms;
3411
3645
  if (opts.atZooms) {
3412
- const rated = ratedSegments2(doc);
3413
- for (const z of doc.zoom) {
3414
- const ext = spanOutputExtent2(rated, z.in, z.out);
3415
- if (ext)
3416
- shots.push({ time: clamp((ext.start + ext.end) / 2), kind: "zoom" });
3417
- }
3646
+ const { apexes, skipped } = zoomApexes(doc);
3647
+ for (const a of apexes)
3648
+ shots.push({
3649
+ time: clamp(a.time),
3650
+ kind: "zoom",
3651
+ zoomId: a.id,
3652
+ ...a.media ? { media: a.media } : {}
3653
+ });
3654
+ skippedZooms = skipped;
3418
3655
  }
3419
3656
  if (opts.atMoments) {
3420
3657
  for (const m of await momentsFor(dir, doc)) {
@@ -3425,16 +3662,19 @@ async function framesTake(browser, dir, opts) {
3425
3662
  if (opts.atStill)
3426
3663
  shots.push({ time: clamp(docStillTime(doc)), kind: "still" });
3427
3664
  shots.sort((a, b) => a.time - b.time);
3428
- if (!shots.length)
3665
+ if (!shots.length && !skippedZooms?.length)
3429
3666
  throw new Error(
3430
3667
  "no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
3431
3668
  );
3432
3669
  const outDir = resolve3(opts.outDir ?? join3(dir, "stills"));
3433
3670
  await mkdir2(outDir, { recursive: true });
3671
+ const stamp = (i, t) => `${String(i).padStart(2, "0")}-${t.toFixed(2)}s.png`;
3434
3672
  const named = shots.map((s, i) => ({
3435
3673
  ...s,
3436
- name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${String(i).padStart(2, "0")}-${s.time.toFixed(2)}s.png`
3674
+ name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${stamp(i, s.time)}`,
3675
+ twinName: s.kind === "zoom" ? `zoomoff-${stamp(i, s.time)}` : void 0
3437
3676
  }));
3677
+ const zoomShots = named.filter((n) => n.kind === "zoom");
3438
3678
  const server = await startTakeServer(dir, {
3439
3679
  "/render.html": renderPageHtml()
3440
3680
  });
@@ -3442,49 +3682,73 @@ async function framesTake(browser, dir, opts) {
3442
3682
  const context2 = await browser.newContext({
3443
3683
  viewport: { width: width * ss, height: height * ss }
3444
3684
  });
3685
+ const videoUrl = `/${take.paths.recordingName}`;
3445
3686
  try {
3446
- const page = await context2.newPage();
3447
- page.on("console", (m) => {
3448
- if (m.type() === "error")
3449
- process.stderr.write(` [stills page] ${m.text()}
3450
- `);
3451
- });
3452
- await page.addInitScript(() => {
3453
- ;
3454
- globalThis.__name = (f) => f;
3455
- });
3456
- await page.goto(`${server.base}/render.html`);
3457
- await page.waitForFunction("window.__pageReady__ === true");
3458
- void page.evaluate(stillsInPage, {
3687
+ const sigs = named.length ? await captureShots(context2, server.base, {
3459
3688
  animationCode,
3460
- token: VIDEO_TOKEN,
3461
- videoUrl: `/${take.paths.recordingName}`,
3462
- W: width,
3463
- H: height,
3689
+ videoUrl,
3690
+ width,
3691
+ height,
3464
3692
  ss,
3465
- shots: named.map(({ time, name }) => ({ time, name }))
3466
- }).catch(() => {
3467
- });
3468
- await waitForPageDone(
3469
- page,
3470
- "stills",
3471
- () => {
3472
- },
3473
- 12e4 + named.length * 4e3
3474
- );
3693
+ shots: named.map(({ time, name }) => ({ time, name })),
3694
+ sigNames: zoomShots.map((n) => n.name)
3695
+ }) : {};
3696
+ let twinSigs = {};
3697
+ if (zoomShots.length) {
3698
+ const twinDoc = structuredClone(doc);
3699
+ applyDocOverrides(twinDoc, { set: ["zoom=[]"] });
3700
+ const twinLowered = lowerToComposition(twinDoc);
3701
+ twinSigs = await captureShots(context2, server.base, {
3702
+ animationCode: compileVosConfig(twinLowered.config, {
3703
+ tweenEngine: "vos"
3704
+ }),
3705
+ videoUrl,
3706
+ width,
3707
+ height,
3708
+ ss,
3709
+ shots: zoomShots.map((n) => ({ time: n.time, name: n.twinName })),
3710
+ sigNames: zoomShots.map((n) => n.twinName)
3711
+ });
3712
+ }
3475
3713
  const frames = [];
3476
3714
  for (const n of named) {
3477
3715
  const from = join3(dir, n.name);
3478
3716
  const to = join3(outDir, n.name);
3479
3717
  if (from !== to) await rename2(from, to);
3480
- frames.push({
3718
+ const frame = {
3481
3719
  file: to,
3482
3720
  time: n.time,
3483
3721
  kind: n.kind,
3484
- ...n.momentId ? { momentId: n.momentId } : {}
3485
- });
3722
+ ...n.momentId ? { momentId: n.momentId } : {},
3723
+ ...n.zoomId ? { zoomId: n.zoomId } : {},
3724
+ ...n.media ? { media: n.media } : {}
3725
+ };
3726
+ if (n.twinName) {
3727
+ const a = sigs[n.name];
3728
+ const b = twinSigs[n.twinName];
3729
+ const twinFrom = join3(dir, n.twinName);
3730
+ if (a && b) {
3731
+ const difference = signatureDifference(a, b);
3732
+ frame.rendered = difference >= ZOOM_UNRENDERED_BELOW;
3733
+ frame.difference = Math.round(difference * 1e5) / 1e5;
3734
+ if (!frame.rendered) {
3735
+ const twinTo = join3(outDir, n.twinName);
3736
+ if (twinFrom !== twinTo) await rename2(twinFrom, twinTo);
3737
+ frame.twin = twinTo;
3738
+ }
3739
+ }
3740
+ if (!frame.twin) await rm2(twinFrom, { force: true });
3741
+ }
3742
+ frames.push(frame);
3486
3743
  }
3487
- return { frames, width, height, duration, outDir };
3744
+ return {
3745
+ frames,
3746
+ width,
3747
+ height,
3748
+ duration,
3749
+ outDir,
3750
+ ...skippedZooms ? { skippedZooms } : {}
3751
+ };
3488
3752
  } finally {
3489
3753
  await context2.close();
3490
3754
  server.close();
@@ -3522,7 +3786,8 @@ async function writeIndexJson(result) {
3522
3786
  width: result.width,
3523
3787
  height: result.height,
3524
3788
  duration: result.duration,
3525
- frames: result.frames
3789
+ frames: result.frames,
3790
+ ...result.skippedZooms?.length ? { skippedZooms: result.skippedZooms } : {}
3526
3791
  },
3527
3792
  null,
3528
3793
  2
@@ -3670,8 +3935,8 @@ import {
3670
3935
  // src/plugin/moments.ts
3671
3936
  import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3672
3937
  var STEP_SETTLE_SECONDS = 0.4;
3673
- function stepOutputTime(rated, step, settle) {
3674
- const src = step.tEnd + settle;
3938
+ function stepOutputTime(rated, step, settle2) {
3939
+ const src = step.tEnd + settle2;
3675
3940
  const ext = spanOutputExtent3(rated, src, src + 1e-3);
3676
3941
  if (!ext) return null;
3677
3942
  return ext.start;
@@ -3687,6 +3952,20 @@ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
3687
3952
  var BED_ID = "bed";
3688
3953
  var CLICK_ID_PREFIX = "click-";
3689
3954
  var CAPTION_ID_PREFIX = "caption-";
3955
+ function houseCaption(id, text, start, duration) {
3956
+ return {
3957
+ id,
3958
+ kind: "text",
3959
+ text,
3960
+ preset: "caption",
3961
+ start,
3962
+ duration,
3963
+ transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3964
+ anim: { enter: "rise", exit: "fade" },
3965
+ align: "center",
3966
+ box: { color: "rgba(17,17,17,0.72)" }
3967
+ };
3968
+ }
3690
3969
  var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3691
3970
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
3692
3971
  var TRANSITION_CLIP_MIN = 1;
@@ -3767,7 +4046,7 @@ function templateWords(words2) {
3767
4046
  if (sub && sub !== headline) out["endcard-sub"] = sub;
3768
4047
  return out;
3769
4048
  }
3770
- function resolveAnchor(doc, at2) {
4049
+ function resolveAnchor2(doc, at2) {
3771
4050
  if (typeof at2 !== "object") return at2;
3772
4051
  const steps = doc.source.meta.steps ?? [];
3773
4052
  const step = steps.find((s) => s.id === at2.step || String(s.step) === at2.step);
@@ -3812,7 +4091,7 @@ function proposeMotion(input, opts) {
3812
4091
  const tWords = templateWords(words2);
3813
4092
  const keys = opts.mark ? { "stage-mark": opts.mark.key, "endcard-markimg": opts.mark.key } : void 0;
3814
4093
  for (const t of templates) {
3815
- const at2 = resolveAnchor(doc, t.at);
4094
+ const at2 = resolveAnchor2(doc, t.at);
3816
4095
  if (at2 === null) {
3817
4096
  skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
3818
4097
  continue;
@@ -3893,18 +4172,14 @@ function proposeMotion(input, opts) {
3893
4172
  if (!step || step.skipped) continue;
3894
4173
  const t = stepOutputTime(rated, step, 0.2);
3895
4174
  if (t === null || t < 0 || t > footage - 1) continue;
3896
- captionClips.push({
3897
- id: `${CAPTION_ID_PREFIX}${c.step}`,
3898
- kind: "text",
3899
- text: c.caption,
3900
- preset: "caption",
3901
- start: +t.toFixed(3),
3902
- duration: Math.min(3.5, Math.max(2.5, footage - t - 0.2)),
3903
- transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3904
- anim: { enter: "rise", exit: "fade" },
3905
- align: "center",
3906
- box: { color: "rgba(17,17,17,0.72)" }
3907
- });
4175
+ captionClips.push(
4176
+ houseCaption(
4177
+ `${CAPTION_ID_PREFIX}${c.step}`,
4178
+ c.caption,
4179
+ +t.toFixed(3),
4180
+ Math.min(3.5, Math.max(2.5, footage - t - 0.2))
4181
+ )
4182
+ );
3908
4183
  }
3909
4184
  if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
3910
4185
  }
@@ -5332,8 +5607,24 @@ async function validateKit(kitPath, opts = {}) {
5332
5607
 
5333
5608
  // src/plugin/recorder.ts
5334
5609
  import { writeFileSync } from "fs";
5610
+ import { mkdir as mkdir5 } from "fs/promises";
5335
5611
  import { join as join8 } from "path";
5336
- import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
5612
+ import {
5613
+ fileNameProblem,
5614
+ recordTake as recordTakeCore
5615
+ } from "@vosjs/render-core/record";
5616
+ function outputsSink(dir) {
5617
+ const outDir = join8(dir, "outputs");
5618
+ return {
5619
+ save: async (name, bytes) => {
5620
+ const problem = fileNameProblem(name);
5621
+ if (problem) throw new Error(`outputs/${name}: ${problem}`);
5622
+ await refuseSymlink(outDir);
5623
+ await mkdir5(outDir, { recursive: true });
5624
+ await writeFileSafe(join8(outDir, name), bytes);
5625
+ }
5626
+ };
5627
+ }
5337
5628
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
5338
5629
  const dry = opts.dryRun === true;
5339
5630
  const result = await recordTakeCore(
@@ -5348,6 +5639,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5348
5639
  log,
5349
5640
  {
5350
5641
  ...opts,
5642
+ outputs: outputsSink(paths.dir),
5351
5643
  env: process.env,
5352
5644
  platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
5353
5645
  }
@@ -5361,6 +5653,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5361
5653
  return result;
5362
5654
  }
5363
5655
 
5656
+ // src/plugin/run.ts
5657
+ import {
5658
+ RecordRefusedError,
5659
+ guardVerdict,
5660
+ guardedSteps,
5661
+ hostedRefusals
5662
+ } from "@vosjs/render-core/record";
5663
+
5364
5664
  // src/plugin/wall.ts
5365
5665
  import {
5366
5666
  WALL_PROBE,
@@ -5455,10 +5755,10 @@ async function openSession(name, url, opts = { log: () => {
5455
5755
  opts.log(
5456
5756
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
5457
5757
  );
5458
- return new Promise((resolve15, reject) => {
5758
+ return new Promise((resolve19, reject) => {
5459
5759
  const child = spawn(exe, args, { stdio: "ignore" });
5460
5760
  child.on("error", reject);
5461
- child.on("exit", (code) => resolve15({ code }));
5761
+ child.on("exit", (code) => resolve19({ code }));
5462
5762
  });
5463
5763
  }
5464
5764
  async function withSession(name, fn, opts = {}) {
@@ -5607,9 +5907,14 @@ import {
5607
5907
  withBackdrop,
5608
5908
  migrateMotion as migrateMotion2
5609
5909
  } from "@vosjs/studio-core";
5910
+ import { splitSegmentsAtSkips } from "@vosjs/render-core/record";
5610
5911
 
5611
5912
  // src/plugin/reuse.ts
5612
- import { docFreezes, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
5913
+ import {
5914
+ docFreezes,
5915
+ ratedSegments as ratedSegments6,
5916
+ resolveAnchor as resolveLayerAnchor
5917
+ } from "@vosjs/studio-core";
5613
5918
  var MIN_SPAN = 0.15;
5614
5919
  var AFTER_STEP = 1;
5615
5920
  var LAYER_AFTER_STEP = 5;
@@ -5685,7 +5990,7 @@ function buildStepMap(oldSteps, newSteps, oldDuration, newDuration) {
5685
5990
  };
5686
5991
  return { map, unmatched, matched };
5687
5992
  }
5688
- function resolveAnchor2(anchor, newSteps) {
5993
+ function resolveAnchor3(anchor, newSteps) {
5689
5994
  const step = stepByRef(newSteps, anchor.step);
5690
5995
  if (!step || step.skipped) return null;
5691
5996
  const base = anchor.at === "end" ? step.tEnd : step.tStart;
@@ -5705,7 +6010,7 @@ function framed(span) {
5705
6010
  }
5706
6011
  function placeMoment(t, anchor, ctx) {
5707
6012
  if (anchor) {
5708
- const at2 = resolveAnchor2(anchor, ctx.newSteps);
6013
+ const at2 = resolveAnchor3(anchor, ctx.newSteps);
5709
6014
  if (at2 === null) return { gone: String(anchor.step) };
5710
6015
  ctx.report.anchored++;
5711
6016
  return { at: at2, via: "anchor" };
@@ -5873,6 +6178,17 @@ function retimeOverlays(prev, next, report) {
5873
6178
  return { ...o, start: +start.toFixed(3) };
5874
6179
  };
5875
6180
  return overlays.map((o) => {
6181
+ if (o.anchor && typeof o.anchor === "object") {
6182
+ const r = resolveLayerAnchor(next, o.anchor, newPieces);
6183
+ if (r.start === null) {
6184
+ report.flagged.push(
6185
+ `overlay ${o.id}: anchored to step ${String(o.anchor.step)}, which is missing, skipped or cut in the new recording \u2014 it plays at its fallback ${o.start.toFixed(2)}s; re-anchor or re-place it`
6186
+ );
6187
+ return o;
6188
+ }
6189
+ report.anchored++;
6190
+ return { ...o, start: r.start };
6191
+ }
5876
6192
  const pin = o.pin;
5877
6193
  if (pin?.step !== void 0) {
5878
6194
  const was = stepByRef(oldSteps, pin.step);
@@ -6003,7 +6319,7 @@ async function planTake(dir, opts = {}) {
6003
6319
  doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6004
6320
  doc = copyStyle(prev, doc);
6005
6321
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6006
- doc.segments = rt.segments;
6322
+ doc.segments = splitSegmentsAtSkips(rt.segments, meta.skips);
6007
6323
  if (rt.freeze.length) doc.freeze = rt.freeze;
6008
6324
  if (rt.rejected.length) doc.rejected = rt.rejected;
6009
6325
  const manualZoom = rt.zoom;
@@ -6101,6 +6417,7 @@ async function planTake(dir, opts = {}) {
6101
6417
  meta
6102
6418
  };
6103
6419
  doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6420
+ doc.segments = splitSegmentsAtSkips(doc.segments, meta.skips);
6104
6421
  if (opts.style) {
6105
6422
  const applied = applyStyle(opts.style.doc, doc, opts);
6106
6423
  doc = applied.doc;
@@ -6212,7 +6529,7 @@ async function writeScriptBeside(dir, doc) {
6212
6529
  }
6213
6530
 
6214
6531
  // src/plugin/folder.ts
6215
- import { mkdir as mkdir5 } from "fs/promises";
6532
+ import { mkdir as mkdir6 } from "fs/promises";
6216
6533
  import { join as join12 } from "path";
6217
6534
  import { recipeHints } from "@vosjs/shared/frontmatter";
6218
6535
  import { migrateHostedDoc } from "@vosjs/studio-core";
@@ -6421,7 +6738,7 @@ async function cmdPull(argv) {
6421
6738
  const payload = res.body;
6422
6739
  const safeName = (v, id) => safePathName((v ?? "").toLowerCase(), id);
6423
6740
  const out = strFlag(flags, "out") ?? safeName(folder.slug, folder.id);
6424
- await mkdir5(join12(out, "recipes"), { recursive: true });
6741
+ await mkdir6(join12(out, "recipes"), { recursive: true });
6425
6742
  const lines = [];
6426
6743
  const recipes = [];
6427
6744
  for (const rec of payload.recipes) {
@@ -6431,7 +6748,7 @@ async function cmdPull(argv) {
6431
6748
  }
6432
6749
  for (const rec of payload.inheritedRecipes) {
6433
6750
  const from = safeName(rec.folderSlug ?? "inherited", "inherited");
6434
- await mkdir5(join12(out, "recipes", "_inherited", from), { recursive: true });
6751
+ await mkdir6(join12(out, "recipes", "_inherited", from), { recursive: true });
6435
6752
  const path = join12(
6436
6753
  out,
6437
6754
  "recipes",
@@ -6448,7 +6765,7 @@ async function cmdPull(argv) {
6448
6765
  const members = [];
6449
6766
  for (const v of payload.voses) {
6450
6767
  const dir = join12(out, "members", safeName(v.slug || v.title, v.id));
6451
- await mkdir5(dir, { recursive: true });
6768
+ await mkdir6(dir, { recursive: true });
6452
6769
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
6453
6770
  if (cfg.status !== 200) {
6454
6771
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
@@ -6998,7 +7315,7 @@ async function pushTakeClaimable(dir, flags, r) {
6998
7315
 
6999
7316
  // src/plugin/program.ts
7000
7317
  import { existsSync as existsSync13 } from "fs";
7001
- import { mkdir as mkdir6, readFile as readFile11 } from "fs/promises";
7318
+ import { mkdir as mkdir7, readFile as readFile11 } from "fs/promises";
7002
7319
  import { createInterface as createInterface2 } from "readline/promises";
7003
7320
  import { basename as basename3, dirname as dirname2, join as join14 } from "path";
7004
7321
  import {
@@ -7217,7 +7534,7 @@ async function cmdFetch(argv) {
7217
7534
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
7218
7535
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
7219
7536
  const out = strFlag(flags, "out") ?? safePathName(slug, vosId);
7220
- await mkdir6(out, { recursive: true });
7537
+ await mkdir7(out, { recursive: true });
7221
7538
  await writeFileSafe(
7222
7539
  join14(out, "config.json"),
7223
7540
  JSON.stringify(cfg.body.config, null, 2)
@@ -10871,13 +11188,13 @@ function parseLlmsTxt(text) {
10871
11188
  const h1 = /^#\s+(.+)$/m.exec(text)?.[1]?.trim() ?? null;
10872
11189
  const block = /^>\s*.+(?:\n>\s*.*)*/m.exec(text)?.[0];
10873
11190
  const joined = block ? block.split("\n").map((line) => line.replace(/^>\s*/, "").trim()).filter(Boolean).join(" ") : "";
10874
- const quote = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
11191
+ const quote2 = joined ? /^(.*?[.!?])(?:\s|$)/.exec(joined)?.[1] ?? joined : null;
10875
11192
  let name = h1;
10876
11193
  if (h1) {
10877
11194
  const split = /^(.+?)\s+[—–-]\s+(.+)$/.exec(h1);
10878
11195
  if (split) name = split[1].trim();
10879
11196
  }
10880
- return { name, claim: quote ?? (h1 && h1 !== name ? h1 : null) };
11197
+ return { name, claim: quote2 ?? (h1 && h1 !== name ? h1 : null) };
10881
11198
  }
10882
11199
  var WITNESS_IN_PAGE = `(() => {
10883
11200
  const cs = (el) => el ? getComputedStyle(el) : null
@@ -11238,7 +11555,7 @@ async function recordHosted(input) {
11238
11555
  }
11239
11556
 
11240
11557
  // src/plugin/run.ts
11241
- import { urlSchemeProblem } from "@vosjs/render-core/record";
11558
+ import { urlSchemeProblem as urlSchemeProblem2 } from "@vosjs/render-core/record";
11242
11559
 
11243
11560
  // src/plugin/agentBrowser.ts
11244
11561
  import { writeFile as writeFile9 } from "fs/promises";
@@ -11288,18 +11605,18 @@ function parseAgentBrowserLog(text) {
11288
11605
  function splitCommand(command) {
11289
11606
  const out = [];
11290
11607
  let cur = "";
11291
- let quote = null;
11608
+ let quote2 = null;
11292
11609
  let has = false;
11293
11610
  for (let i = 0; i < command.length; i++) {
11294
11611
  const c = command[i];
11295
- if (quote) {
11296
- if (c === quote) quote = null;
11612
+ if (quote2) {
11613
+ if (c === quote2) quote2 = null;
11297
11614
  else if (c === "\\" && i + 1 < command.length) cur += command[++i];
11298
11615
  else cur += c;
11299
11616
  continue;
11300
11617
  }
11301
11618
  if (c === '"' || c === "'") {
11302
- quote = c;
11619
+ quote2 = c;
11303
11620
  has = true;
11304
11621
  continue;
11305
11622
  }
@@ -11343,8 +11660,8 @@ var quoteAttr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
11343
11660
  function hrefSelector(href) {
11344
11661
  try {
11345
11662
  const u = new URL(href);
11346
- const relative4 = `${u.pathname}${u.search}${u.hash}`;
11347
- return `a[href=${quoteAttr(relative4)}], a[href=${quoteAttr(href)}]`;
11663
+ const relative6 = `${u.pathname}${u.search}${u.hash}`;
11664
+ return `a[href=${quoteAttr(relative6)}], a[href=${quoteAttr(href)}]`;
11348
11665
  } catch {
11349
11666
  return `a[href=${quoteAttr(href)}]`;
11350
11667
  }
@@ -11721,8 +12038,8 @@ async function cmdActions(argv) {
11721
12038
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
11722
12039
  viewport = { width: Number(m[1]), height: Number(m[2]) };
11723
12040
  }
11724
- const { readFile: readFile14 } = await import("fs/promises");
11725
- const text = await readFile14(resolve12(input), "utf8");
12041
+ const { readFile: readFile16 } = await import("fs/promises");
12042
+ const text = await readFile16(resolve12(input), "utf8");
11726
12043
  const { records, problems } = parseAgentBrowserLog(text);
11727
12044
  for (const p of problems) r.log(` ${p}`);
11728
12045
  const result = convertAgentBrowser(records, {
@@ -11759,9 +12076,9 @@ function summary2(result) {
11759
12076
  }
11760
12077
  async function cmdActionsScript(input, flags) {
11761
12078
  const r = createReporter(flags.json === true);
11762
- const { readFile: readFile14 } = await import("fs/promises");
11763
- const { shotList, formatShotList } = await import("./shotList-MAQVLZBY.js");
11764
- const raw = JSON.parse(await readFile14(resolve12(input), "utf8"));
12079
+ const { readFile: readFile16 } = await import("fs/promises");
12080
+ const { shotList, formatShotList } = await import("./shotList-SCA234OS.js");
12081
+ const raw = JSON.parse(await readFile16(resolve12(input), "utf8"));
11765
12082
  const errors = validateActions(raw);
11766
12083
  if (errors.length) throw new UsageError(`${input}:
11767
12084
  ${errors.join("\n ")}`);
@@ -11772,7 +12089,7 @@ async function cmdActionsScript(input, flags) {
11772
12089
 
11773
12090
  // src/plugin/ingest.ts
11774
12091
  import { existsSync as existsSync20 } from "fs";
11775
- import { copyFile, mkdir as mkdir7, readFile as readFile12, stat as stat2 } from "fs/promises";
12092
+ import { copyFile, mkdir as mkdir8, readFile as readFile12, stat as stat2 } from "fs/promises";
11776
12093
  import { basename as basename9, join as join22, resolve as resolve13 } from "path";
11777
12094
 
11778
12095
  // src/plugin/traceInflate.ts
@@ -11874,7 +12191,11 @@ async function copyRecording(src, dest, ext) {
11874
12191
  target
11875
12192
  });
11876
12193
  try {
11877
- const conversion = await Conversion.init({ input, output });
12194
+ const conversion = await Conversion.init({
12195
+ input,
12196
+ output,
12197
+ showWarnings: false
12198
+ });
11878
12199
  if (!conversion.isValid || conversion.discardedTracks.length > 0) {
11879
12200
  await conversion.cancel?.();
11880
12201
  await copyFile(src, dest);
@@ -11960,7 +12281,7 @@ async function cmdIngest(argv) {
11960
12281
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
11961
12282
  );
11962
12283
  }
11963
- await mkdir7(outDir, { recursive: true });
12284
+ await mkdir8(outDir, { recursive: true });
11964
12285
  const paths = await ensureTakeDir(outDir);
11965
12286
  const recording = join22(outDir, `recording.${probed.ext}`);
11966
12287
  r.log("copying the recording\u2026");
@@ -12026,115 +12347,2465 @@ async function cmdIngest(argv) {
12026
12347
  return EXIT_OK;
12027
12348
  }
12028
12349
 
12029
- // src/plugin/machineCmd.ts
12030
- import { spawnSync as spawnSync4 } from "child_process";
12031
- import {
12032
- cpSync,
12033
- existsSync as existsSync22,
12034
- mkdirSync as mkdirSync6,
12035
- readFileSync as readFileSync11,
12036
- rmSync as rmSync3,
12037
- writeFileSync as writeFileSync6
12038
- } from "fs";
12039
- import { homedir as homedir2 } from "os";
12040
- import { join as join24 } from "path";
12041
- import { chromium as chromium2 } from "playwright";
12350
+ // src/plugin/checkVideo.ts
12351
+ import { createRequire as createRequire2 } from "module";
12352
+ import { existsSync as existsSync21 } from "fs";
12353
+ import { mkdir as mkdir9, readFile as readFile13, stat as stat3, writeFile as writeFile10 } from "fs/promises";
12354
+ import { basename as basename10, dirname as dirname8, extname as extname4, join as join23, resolve as resolve14 } from "path";
12355
+ import { ALL_FORMATS as ALL_FORMATS2, FilePathSource as FilePathSource2, Input as Input2 } from "mediabunny";
12042
12356
 
12043
- // src/plugin/machine.ts
12044
- import { existsSync as existsSync21, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12045
- import { dirname as dirname8, join as join23 } from "path";
12046
- var AGENT_NAMES = [
12047
- "claude",
12048
- "cursor",
12049
- "codex",
12050
- "copilot"
12051
- ];
12052
- var PROJECT_DIRS = {
12053
- claude: [".claude"],
12054
- cursor: [".cursor"],
12055
- codex: [".codex", ".agents"],
12056
- copilot: [".github/copilot"]
12057
- };
12058
- var HOME_DIRS = {
12059
- claude: [".claude"],
12060
- cursor: [".cursor"],
12061
- codex: [".codex", ".agents"],
12062
- copilot: [".copilot"]
12063
- };
12064
- function detectAgents(root, opts = {}) {
12065
- const exists = opts.exists ?? existsSync21;
12357
+ // src/plugin/videoChecks.ts
12358
+ var SCENE_CHANGE_AT = 0.12;
12359
+ var CELL_CHANGED_AT = 0.03;
12360
+ var SCENE_WINDOW = 5;
12361
+ var UNIFORM_BELOW = 2;
12362
+ var QUIET_BELOW_LUFS = -23;
12363
+ var LOUD_ABOVE_LUFS = -9;
12364
+ var SUB_BLOCK_SECONDS = 0.1;
12365
+ var SHORT_TERM_SECONDS = 3;
12366
+ var SCENE_SPIKE = 2.5;
12367
+ var ISOLATED_CHANGE_AT = 0.03;
12368
+ var ISOLATED_SPIKE = 8;
12369
+ var ISOLATED_COVERAGE = 0.25;
12370
+ function sceneChanges2(times, changes, coverage) {
12066
12371
  const out = [];
12067
- for (const agent of AGENT_NAMES) {
12068
- for (const rel of PROJECT_DIRS[agent]) {
12069
- const dir = join23(root, rel);
12070
- if (exists(dir)) out.push({ agent, dir, scope: "project" });
12071
- }
12072
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join23(root, ".github", "copilot-instructions.md"))) {
12073
- out.push({
12074
- agent,
12075
- dir: join23(root, ".github", "copilot"),
12076
- scope: "project"
12077
- });
12078
- }
12372
+ for (let i = 1; i < changes.length; i++) {
12373
+ const c = changes[i];
12374
+ if (c < Math.min(SCENE_CHANGE_AT, ISOLATED_CHANGE_AT)) continue;
12375
+ if (i > 1 && changes[i - 1] > c) continue;
12376
+ if (i + 1 < changes.length && changes[i + 1] >= c) continue;
12377
+ const around = [i - 2, i - 1, i + 1, i + 2].filter((k) => k >= 1 && k < changes.length).map((k) => changes[k]).sort((a, b) => a - b);
12378
+ const mid = around.length ? around.length % 2 ? around[(around.length - 1) / 2] : (around[around.length / 2 - 1] + around[around.length / 2]) / 2 : 0;
12379
+ const most = around.length ? around[around.length - 1] : 0;
12380
+ const share = coverage ? coverage[i] : 1;
12381
+ const standsOut = c >= SCENE_CHANGE_AT && c >= SCENE_SPIKE * mid;
12382
+ const standsAlone = c >= ISOLATED_CHANGE_AT && share >= ISOLATED_COVERAGE && c >= ISOLATED_SPIKE * most;
12383
+ if (!standsOut && !standsAlone) continue;
12384
+ out.push({
12385
+ time: times[i],
12386
+ change: Math.round(c * 1e4) / 1e4,
12387
+ ...coverage ? { coverage: Math.round(share * 1e3) / 1e3 } : {}
12388
+ });
12079
12389
  }
12080
- if (opts.global && opts.home) {
12081
- for (const agent of AGENT_NAMES) {
12082
- for (const rel of HOME_DIRS[agent]) {
12083
- const dir = join23(opts.home, rel);
12084
- if (exists(dir)) out.push({ agent, dir, scope: "global" });
12085
- }
12086
- }
12390
+ return out;
12391
+ }
12392
+ function windowCounts(cuts, duration, window2 = SCENE_WINDOW) {
12393
+ const n = Math.max(1, Math.ceil(duration / window2 - 1e-9));
12394
+ const out = Array.from({ length: n }, (_, k) => ({
12395
+ start: k * window2,
12396
+ count: 0
12397
+ }));
12398
+ for (const c of cuts) {
12399
+ const k = Math.min(n - 1, Math.max(0, Math.floor(c.time / window2)));
12400
+ out[k].count++;
12087
12401
  }
12088
12402
  return out;
12089
12403
  }
12090
- function agentsFromFlag(value, root) {
12091
- const wanted = value.trim() === "all" ? [...AGENT_NAMES] : value.split(",").map((s) => s.trim()).filter(Boolean);
12092
- const bad = wanted.filter((w) => !AGENT_NAMES.includes(w));
12093
- if (bad.length)
12404
+ function crowdedRuns(cuts, window2 = SCENE_WINDOW) {
12405
+ const runs = [];
12406
+ let run2 = [];
12407
+ for (const c of cuts) {
12408
+ if (run2.length && c.time - run2[run2.length - 1].time < window2) run2.push(c);
12409
+ else {
12410
+ if (run2.length > 1) runs.push(run2);
12411
+ run2 = [c];
12412
+ }
12413
+ }
12414
+ if (run2.length > 1) runs.push(run2);
12415
+ return runs;
12416
+ }
12417
+ function createLoudnessMeter(sampleRate, channelCount, subBlockSeconds) {
12418
+ const shelf = (() => {
12419
+ const f0 = 1681.974450955533;
12420
+ const G = 3.999843853973347;
12421
+ const Q = 0.7071752369554196;
12422
+ const K = Math.tan(Math.PI * f0 / sampleRate);
12423
+ const Vh = Math.pow(10, G / 20);
12424
+ const Vb = Math.pow(Vh, 0.4996667741545416);
12425
+ const a0 = 1 + K / Q + K * K;
12094
12426
  return {
12095
- error: `--agent takes ${AGENT_NAMES.join(", ")} or all, not "${bad.join(", ")}"`
12427
+ b0: (Vh + Vb * K / Q + K * K) / a0,
12428
+ b1: 2 * (K * K - Vh) / a0,
12429
+ b2: (Vh - Vb * K / Q + K * K) / a0,
12430
+ a1: 2 * (K * K - 1) / a0,
12431
+ a2: (1 - K / Q + K * K) / a0
12096
12432
  };
12097
- if (!wanted.length) return { error: "--agent expects a name" };
12433
+ })();
12434
+ const highpass = (() => {
12435
+ const f0 = 38.13547087602444;
12436
+ const Q = 0.5003270373238773;
12437
+ const K = Math.tan(Math.PI * f0 / sampleRate);
12438
+ const a0 = 1 + K / Q + K * K;
12439
+ return {
12440
+ b0: 1,
12441
+ b1: -2,
12442
+ b2: 1,
12443
+ a1: 2 * (K * K - 1) / a0,
12444
+ a2: (1 - K / Q + K * K) / a0
12445
+ };
12446
+ })();
12447
+ const per = Math.max(1, Math.round(sampleRate * subBlockSeconds));
12448
+ const state = Array.from({ length: channelCount }, () => ({
12449
+ // Direct form I history for each stage.
12450
+ x1: 0,
12451
+ x2: 0,
12452
+ y1: 0,
12453
+ y2: 0,
12454
+ u1: 0,
12455
+ u2: 0,
12456
+ w1: 0,
12457
+ w2: 0,
12458
+ sum: 0,
12459
+ count: 0,
12460
+ energies: []
12461
+ }));
12462
+ let peak = 0;
12463
+ let samples = 0;
12098
12464
  return {
12099
- homes: wanted.map((agent) => ({
12100
- agent,
12101
- dir: join23(root, PROJECT_DIRS[agent][0]),
12102
- scope: "project"
12103
- }))
12465
+ push(channels) {
12466
+ for (let c = 0; c < channelCount; c++) {
12467
+ const data = channels[c];
12468
+ const s = state[c];
12469
+ if (!data) continue;
12470
+ for (let i = 0; i < data.length; i++) {
12471
+ const x = data[i];
12472
+ const ax = x < 0 ? -x : x;
12473
+ if (ax > peak) peak = ax;
12474
+ const y = shelf.b0 * x + shelf.b1 * s.x1 + shelf.b2 * s.x2 - shelf.a1 * s.y1 - shelf.a2 * s.y2;
12475
+ s.x2 = s.x1;
12476
+ s.x1 = x;
12477
+ s.y2 = s.y1;
12478
+ s.y1 = y;
12479
+ const w = highpass.b0 * y + highpass.b1 * s.u1 + highpass.b2 * s.u2 - highpass.a1 * s.w1 - highpass.a2 * s.w2;
12480
+ s.u2 = s.u1;
12481
+ s.u1 = y;
12482
+ s.w2 = s.w1;
12483
+ s.w1 = w;
12484
+ s.sum += w * w;
12485
+ s.count++;
12486
+ if (s.count === per) {
12487
+ s.energies.push(s.sum / per);
12488
+ s.sum = 0;
12489
+ s.count = 0;
12490
+ }
12491
+ }
12492
+ if (c === 0) samples += data.length;
12493
+ }
12494
+ },
12495
+ finish() {
12496
+ return {
12497
+ energies: state.map(
12498
+ (s) => s.count > 0 ? [...s.energies, s.sum / s.count] : s.energies
12499
+ ),
12500
+ peak,
12501
+ samples
12502
+ };
12503
+ }
12104
12504
  };
12105
12505
  }
12106
- function skillsDirFor(home) {
12107
- return join23(home.dir, "skills");
12108
- }
12109
- function fallbackSkillsHome(root) {
12110
- return { agent: "codex", dir: join23(root, ".agents"), scope: "project" };
12506
+ function channelWeights(count) {
12507
+ if (count === 6) return [1, 1, 1, 0, 1.41, 1.41];
12508
+ return Array.from({ length: count }, (_, i) => i < 3 ? 1 : 1.41);
12111
12509
  }
12112
- function installedSkills(dir, exists = existsSync21, list = (p) => {
12113
- try {
12114
- return readdirSync5(p);
12115
- } catch {
12116
- return [];
12510
+ var LOUDNESS_OFFSET = -0.691;
12511
+ var toLufs = (power) => power > 0 ? LOUDNESS_OFFSET + 10 * Math.log10(power) : null;
12512
+ function windowPower(energies, weights, from, count) {
12513
+ let total = 0;
12514
+ for (let c = 0; c < energies.length; c++) {
12515
+ const e = energies[c];
12516
+ let sum = 0;
12517
+ let n = 0;
12518
+ for (let k = from; k < from + count && k < e.length; k++) {
12519
+ sum += e[k];
12520
+ n++;
12521
+ }
12522
+ if (n) total += (weights[c] ?? 1) * (sum / n);
12523
+ }
12524
+ return total;
12525
+ }
12526
+ var round1 = (n) => n === null ? null : Math.round(n * 10) / 10;
12527
+ function loudnessFromEnergies(energies, peak, subBlock = SUB_BLOCK_SECONDS) {
12528
+ const weights = channelWeights(energies.length);
12529
+ const pieces = Math.max(0, ...energies.map((e) => e.length));
12530
+ const blockLen = Math.max(1, Math.round(0.4 / subBlock));
12531
+ const blocks = [];
12532
+ for (let k = 0; k + blockLen <= pieces; k++)
12533
+ blocks.push(windowPower(energies, weights, k, blockLen));
12534
+ if (!blocks.length && pieces > 0)
12535
+ blocks.push(windowPower(energies, weights, 0, pieces));
12536
+ const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
12537
+ const loud = (p) => toLufs(p) ?? -Infinity;
12538
+ const absolute2 = blocks.filter((p) => loud(p) > -70);
12539
+ let integrated = null;
12540
+ if (absolute2.length) {
12541
+ const gate = loud(mean(absolute2)) - 10;
12542
+ const relative6 = absolute2.filter((p) => loud(p) > gate);
12543
+ integrated = toLufs(mean(relative6));
12544
+ }
12545
+ const stLen = Math.max(1, Math.round(SHORT_TERM_SECONDS / subBlock));
12546
+ const shortTerm = [];
12547
+ for (let k = 0; k < pieces; k += stLen) {
12548
+ if (pieces - k < blockLen && k > 0) break;
12549
+ shortTerm.push({
12550
+ time: Math.round(k * subBlock * 100) / 100,
12551
+ loudness: round1(toLufs(windowPower(energies, weights, k, stLen)))
12552
+ });
12117
12553
  }
12118
- }) {
12119
- if (!exists(dir)) return [];
12120
- return list(dir).filter((name) => exists(join23(dir, name, "SKILL.md"))).sort();
12554
+ const st = shortTerm.map((s) => s.loudness).filter((l) => l !== null);
12555
+ return {
12556
+ unit: "LUFS",
12557
+ integrated: round1(integrated),
12558
+ shortTerm,
12559
+ maxShortTerm: st.length ? Math.max(...st) : null,
12560
+ samplePeakDb: peak > 0 ? round1(20 * Math.log10(peak)) : null
12561
+ };
12121
12562
  }
12122
- var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
12123
- var RULES_END = "<!-- vos:end -->";
12124
- var RULES_BODY = `## Product videos (vosso)
12125
- - Skill: product-video (npx skills add vosjs/skills) records the real product from actions.json
12126
- - Look before you cut: vos frames take --at-zooms; edit take/doc.json and re-render, never re-record
12127
- - Hand back: vos open take serves the take into the studio at vos.so, doc.json intact
12128
- - Local by default: vos push only when asked for a link, the shelf or a cloud render
12129
- - Deleting: vos delete moves a vos to Trash and vos restore brings it back; if you deleted by mistake, restore it and say so. Only the person can delete forever, at vos.so/app/trash
12130
- - Credentials: VOS_API_KEY, then ~/.config/vos/credentials, then vos login; never print one
12131
- - Reference: https://vos.so/llms-full.txt`;
12132
- var RULES_BLOCK = `${RULES_BEGIN}
12133
- ${RULES_BODY}
12134
- ${RULES_END}`;
12135
- function applyRulesBlock(existing) {
12136
- const current = existing ?? "";
12137
- const begin = current.indexOf(RULES_BEGIN);
12563
+ function loudnessWarnings(l) {
12564
+ if (l.integrated === null)
12565
+ return [
12566
+ "the sound is silent: no 400 ms block reaches -70 LUFS, the floor loudness is measured from"
12567
+ ];
12568
+ if (l.integrated < QUIET_BELOW_LUFS)
12569
+ return [
12570
+ `integrated loudness ${l.integrated} LUFS is below ${QUIET_BELOW_LUFS}: too quiet for a social feed, where a viewer turns sound up once and keeps it`
12571
+ ];
12572
+ if (l.integrated > LOUD_ABOVE_LUFS)
12573
+ return [
12574
+ `integrated loudness ${l.integrated} LUFS is above ${LOUD_ABOVE_LUFS}: too loud, a platform turns it down and a peak may clip`
12575
+ ];
12576
+ return [];
12577
+ }
12578
+ function sceneWarnings(cuts) {
12579
+ return crowdedRuns(cuts).map(
12580
+ (run2) => `${run2.length} full-frame changes within ${SCENE_WINDOW} s (at ${run2.map((c) => `${c.time.toFixed(2)}s`).join(", ")}): more than one cut in ${SCENE_WINDOW} s is hard to follow`
12581
+ );
12582
+ }
12583
+ function posterWarning(which, deviation) {
12584
+ if (deviation >= UNIFORM_BELOW) return null;
12585
+ return which === "first" ? `the first frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video opens on an empty or faded frame, which is what a feed shows before it plays` : `the last frame is near-uniform (deviation ${deviation.toFixed(2)} of 255): the video ends on an empty or faded frame, which is what a player rests on`;
12586
+ }
12587
+
12588
+ // src/plugin/checkVideo.ts
12589
+ var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set(["json", "help", "strict"]);
12590
+ var THUMB_LONG_SIDE = 64;
12591
+ async function probeVideoFacts(file) {
12592
+ const input = new Input2({
12593
+ source: new FilePathSource2(file),
12594
+ formats: ALL_FORMATS2
12595
+ });
12596
+ try {
12597
+ const video = await input.getPrimaryVideoTrack();
12598
+ if (!video) throw new UsageError(`${file} has no video track`);
12599
+ const [format, duration, stats, audio, bytes] = await Promise.all([
12600
+ input.getFormat(),
12601
+ input.computeDuration(),
12602
+ video.computePacketStats(240),
12603
+ input.getPrimaryAudioTrack(),
12604
+ stat3(file).then((s) => s.size)
12605
+ ]);
12606
+ return {
12607
+ file,
12608
+ bytes,
12609
+ duration: Math.round(duration * 1e3) / 1e3,
12610
+ width: video.displayWidth,
12611
+ height: video.displayHeight,
12612
+ fps: Math.round(stats.averagePacketRate * 100) / 100,
12613
+ container: format.mimeType,
12614
+ video: {
12615
+ codec: video.codec,
12616
+ codecString: await video.getCodecParameterString()
12617
+ },
12618
+ audio: audio ? {
12619
+ codec: audio.codec,
12620
+ codecString: await audio.getCodecParameterString(),
12621
+ sampleRate: audio.sampleRate,
12622
+ channels: audio.numberOfChannels
12623
+ } : null
12624
+ };
12625
+ } finally {
12626
+ input.dispose();
12627
+ }
12628
+ }
12629
+ async function mediabunnyBundle() {
12630
+ const entry = createRequire2(import.meta.url).resolve("mediabunny");
12631
+ const bundle = join23(dirname8(entry), "mediabunny.min.mjs");
12632
+ if (existsSync21(bundle)) return readFile13(bundle, "utf8");
12633
+ return readFile13(
12634
+ join23(entry.split("/dist/")[0], "dist/bundles/mediabunny.min.mjs"),
12635
+ "utf8"
12636
+ );
12637
+ }
12638
+ async function decodeInPage(opts) {
12639
+ const w = window;
12640
+ try {
12641
+ const load = new Function("u", "return import(u)");
12642
+ const MB = await load(
12643
+ URL.createObjectURL(new Blob([opts.mbCode], { type: "text/javascript" }))
12644
+ );
12645
+ const input = new MB.Input({
12646
+ source: new MB.UrlSource(opts.videoUrl),
12647
+ formats: MB.ALL_FORMATS
12648
+ });
12649
+ const track = await input.getPrimaryVideoTrack();
12650
+ if (!track) throw new Error("no video track");
12651
+ if (!await track.canDecode())
12652
+ throw new Error(`this browser cannot decode ${String(track.codec)}`);
12653
+ const pixels = (canvas) => {
12654
+ const g = canvas.getContext("2d", {
12655
+ willReadFrequently: true
12656
+ });
12657
+ const px = g.getImageData(0, 0, canvas.width, canvas.height).data;
12658
+ const out = new Array(px.length / 4 * 3);
12659
+ for (let p = 0, q = 0; p < px.length; p += 4, q += 3) {
12660
+ out[q] = px[p];
12661
+ out[q + 1] = px[p + 1];
12662
+ out[q + 2] = px[p + 2];
12663
+ }
12664
+ return out;
12665
+ };
12666
+ const deviation = (a) => {
12667
+ let total = 0;
12668
+ for (let c = 0; c < 3; c++) {
12669
+ let s = 0;
12670
+ let n = 0;
12671
+ for (let i = c; i < a.length; i += 3) {
12672
+ s += a[i];
12673
+ n++;
12674
+ }
12675
+ const m = s / Math.max(1, n);
12676
+ let q = 0;
12677
+ for (let i = c; i < a.length; i += 3) q += (a[i] - m) * (a[i] - m);
12678
+ total += Math.sqrt(q / Math.max(1, n));
12679
+ }
12680
+ return total / 3;
12681
+ };
12682
+ const difference = (a, b) => {
12683
+ let s = 0;
12684
+ for (let i = 0; i < a.length; i++) s += Math.abs(a[i] - b[i]);
12685
+ return s / Math.max(1, a.length) / 255;
12686
+ };
12687
+ const coverageOf = (a, b) => {
12688
+ const W = opts.thumbW;
12689
+ const H = opts.thumbH;
12690
+ let cells = 0;
12691
+ let changed = 0;
12692
+ for (let cy = 0; cy < H; cy += 4)
12693
+ for (let cx = 0; cx < W; cx += 4) {
12694
+ let s = 0;
12695
+ let n = 0;
12696
+ for (let y = cy; y < Math.min(H, cy + 4); y++)
12697
+ for (let x = cx; x < Math.min(W, cx + 4); x++) {
12698
+ const k = (y * W + x) * 3;
12699
+ s += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
12700
+ n += 3;
12701
+ }
12702
+ cells++;
12703
+ if (s / Math.max(1, n) / 255 >= opts.cellChanged) changed++;
12704
+ }
12705
+ return changed / Math.max(1, cells);
12706
+ };
12707
+ const small = document.createElement("canvas");
12708
+ small.width = opts.thumbW;
12709
+ small.height = opts.thumbH;
12710
+ const sctx = small.getContext("2d", { willReadFrequently: true });
12711
+ sctx.imageSmoothingEnabled = true;
12712
+ sctx.imageSmoothingQuality = "high";
12713
+ const samples = new MB.VideoSampleSink(track);
12714
+ const step = 1 / opts.sampleFps;
12715
+ const times = [];
12716
+ const changes = [];
12717
+ const coverage = [];
12718
+ let next = -Infinity;
12719
+ let prev = null;
12720
+ let first = null;
12721
+ let firstTime = 0;
12722
+ let lastTime = 0;
12723
+ let lastKept = -1;
12724
+ let lastGray = null;
12725
+ let tail = null;
12726
+ const duration = await input.computeDuration();
12727
+ for await (const sample2 of samples.samples()) {
12728
+ const t = sample2.timestamp;
12729
+ sctx.clearRect(0, 0, opts.thumbW, opts.thumbH);
12730
+ sample2.draw(sctx, 0, 0, opts.thumbW, opts.thumbH);
12731
+ sample2.close();
12732
+ lastTime = t;
12733
+ if (t + step / 2 < next) {
12734
+ tail = pixels(small);
12735
+ continue;
12736
+ }
12737
+ tail = null;
12738
+ const g = pixels(small);
12739
+ if (!first) {
12740
+ first = g;
12741
+ firstTime = t;
12742
+ }
12743
+ times.push(t);
12744
+ changes.push(prev ? difference(prev, g) : 0);
12745
+ coverage.push(prev ? coverageOf(prev, g) : 0);
12746
+ prev = g;
12747
+ lastKept = t;
12748
+ lastGray = g;
12749
+ next = next === -Infinity ? t + step : next + step;
12750
+ if (next <= t) next = t + step;
12751
+ w.__progress = duration > 0 ? Math.min(0.9, t / duration * 0.9) : 0;
12752
+ }
12753
+ if (!first || !lastGray) throw new Error("no video frame decoded");
12754
+ if (tail && lastTime > lastKept) {
12755
+ times.push(lastTime);
12756
+ changes.push(prev ? difference(prev, tail) : 0);
12757
+ coverage.push(prev ? coverageOf(prev, tail) : 0);
12758
+ lastGray = tail;
12759
+ }
12760
+ const toBase64 = (bytes) => {
12761
+ let s = "";
12762
+ for (let i = 0; i < bytes.length; i += 32768)
12763
+ s += String.fromCharCode(...bytes.subarray(i, i + 32768));
12764
+ return btoa(s);
12765
+ };
12766
+ const full = new MB.CanvasSink(track);
12767
+ const png = async (t) => {
12768
+ const wc = await full.getCanvas(t);
12769
+ if (!wc) return "";
12770
+ const c = wc.canvas;
12771
+ const blob = "convertToBlob" in c ? await c.convertToBlob({ type: "image/png" }) : await new Promise(
12772
+ (res, rej) => c.toBlob(
12773
+ (b) => b ? res(b) : rej(new Error("toBlob failed")),
12774
+ "image/png"
12775
+ )
12776
+ );
12777
+ return toBase64(new Uint8Array(await blob.arrayBuffer()));
12778
+ };
12779
+ const firstPng = await png(firstTime);
12780
+ const lastPng = await png(lastTime);
12781
+ let audio = null;
12782
+ const at2 = await input.getPrimaryAudioTrack();
12783
+ if (at2 && await at2.canDecode()) {
12784
+ const make = new Function(`return (${opts.meterSrc})`)();
12785
+ const meter = make(at2.sampleRate, at2.numberOfChannels, opts.subBlock);
12786
+ const sink = new MB.AudioBufferSink(at2);
12787
+ for await (const wb of sink.buffers()) {
12788
+ const b = wb.buffer;
12789
+ const chans = [];
12790
+ for (let c = 0; c < at2.numberOfChannels; c++)
12791
+ chans.push(
12792
+ c < b.numberOfChannels ? b.getChannelData(c) : new Float32Array(0)
12793
+ );
12794
+ meter.push(chans);
12795
+ }
12796
+ audio = {
12797
+ sampleRate: at2.sampleRate,
12798
+ channels: at2.numberOfChannels,
12799
+ ...meter.finish()
12800
+ };
12801
+ }
12802
+ input.dispose();
12803
+ w.__done = {
12804
+ times,
12805
+ changes,
12806
+ coverage,
12807
+ firstDeviation: deviation(first),
12808
+ lastDeviation: deviation(lastGray),
12809
+ firstTime,
12810
+ lastTime,
12811
+ firstPng,
12812
+ lastPng,
12813
+ audio
12814
+ };
12815
+ } catch (e) {
12816
+ w.__error = String(e instanceof Error && e.stack || e);
12817
+ }
12818
+ }
12819
+ async function checkVideo(browser, file, outDir) {
12820
+ const facts = await probeVideoFacts(file);
12821
+ const ceiling = facts.duration > 120 ? 10 : 30;
12822
+ const sampleFps = Math.max(
12823
+ Math.min(10, facts.fps || 10),
12824
+ Math.min(facts.fps || 30, ceiling)
12825
+ );
12826
+ const long = Math.max(facts.width, facts.height, 1);
12827
+ const side = (n) => Math.max(1, Math.round(n / long * THUMB_LONG_SIDE));
12828
+ const server = await startTakeServer(dirname8(resolve14(file)), {
12829
+ "/check.html": '<!doctype html><html><head><meta charset="utf-8"><link rel="icon" href="data:,"></head><body></body></html>'
12830
+ });
12831
+ const page = await browser.newPage();
12832
+ let done;
12833
+ try {
12834
+ await page.addInitScript(() => {
12835
+ ;
12836
+ globalThis.__name = (f) => f;
12837
+ });
12838
+ await page.goto(`${server.base}/check.html`);
12839
+ void page.evaluate(decodeInPage, {
12840
+ mbCode: await mediabunnyBundle(),
12841
+ meterSrc: String(createLoudnessMeter),
12842
+ videoUrl: `/${encodeURIComponent(basename10(file))}`,
12843
+ sampleFps,
12844
+ thumbW: side(facts.width),
12845
+ thumbH: side(facts.height),
12846
+ subBlock: SUB_BLOCK_SECONDS,
12847
+ cellChanged: CELL_CHANGED_AT
12848
+ }).catch(() => {
12849
+ });
12850
+ done = await waitForPageDone(
12851
+ page,
12852
+ "check-video",
12853
+ () => {
12854
+ },
12855
+ 12e4 + facts.duration * 4e3
12856
+ );
12857
+ } finally {
12858
+ await page.close();
12859
+ server.close();
12860
+ }
12861
+ await mkdir9(outDir, { recursive: true });
12862
+ const firstFile = join23(outDir, "first.png");
12863
+ const lastFile = join23(outDir, "last.png");
12864
+ await writeFile10(firstFile, Buffer.from(done.firstPng, "base64"));
12865
+ await writeFile10(lastFile, Buffer.from(done.lastPng, "base64"));
12866
+ const changes = sceneChanges2(done.times, done.changes, done.coverage);
12867
+ const loudness = done.audio ? loudnessFromEnergies(done.audio.energies, done.audio.peak) : null;
12868
+ const round2 = (n) => Math.round(n * 100) / 100;
12869
+ const posters = {
12870
+ first: {
12871
+ file: firstFile,
12872
+ time: round2(done.firstTime),
12873
+ deviation: round2(done.firstDeviation),
12874
+ uniform: posterWarning("first", done.firstDeviation) !== null
12875
+ },
12876
+ last: {
12877
+ file: lastFile,
12878
+ time: round2(done.lastTime),
12879
+ deviation: round2(done.lastDeviation),
12880
+ uniform: posterWarning("last", done.lastDeviation) !== null
12881
+ }
12882
+ };
12883
+ const warnings = [
12884
+ ...sceneWarnings(changes),
12885
+ ...loudness ? loudnessWarnings(loudness) : [],
12886
+ ...[
12887
+ posterWarning("first", done.firstDeviation),
12888
+ posterWarning("last", done.lastDeviation)
12889
+ ].filter((w) => w !== null)
12890
+ ];
12891
+ const result = {
12892
+ ...facts,
12893
+ scenes: {
12894
+ sampleFps: round2(sampleFps),
12895
+ threshold: SCENE_CHANGE_AT,
12896
+ isolatedThreshold: ISOLATED_CHANGE_AT,
12897
+ window: SCENE_WINDOW,
12898
+ changes: changes.map((c) => ({ ...c, time: round2(c.time) })),
12899
+ perWindow: windowCounts(changes, facts.duration)
12900
+ },
12901
+ loudness,
12902
+ posters,
12903
+ warnings,
12904
+ outDir
12905
+ };
12906
+ await writeFile10(
12907
+ join23(outDir, "check.json"),
12908
+ `${JSON.stringify(result, null, 2)}
12909
+ `
12910
+ );
12911
+ return result;
12912
+ }
12913
+ var mb = (n) => n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
12914
+ function checkVideoLines(r) {
12915
+ const codecs = [
12916
+ r.video.codecString ?? r.video.codec ?? "unknown",
12917
+ ...r.audio ? [r.audio.codecString ?? r.audio.codec ?? "unknown"] : []
12918
+ ].join(" + ");
12919
+ const cuts = r.scenes.changes;
12920
+ const l = r.loudness;
12921
+ const st = l?.shortTerm.map(
12922
+ (s) => s.loudness === null ? "-" : String(s.loudness)
12923
+ );
12924
+ return [
12925
+ `Checked ${r.file}: ${r.duration.toFixed(2)}s, ${r.width}x${r.height} @ ${r.fps} fps, ${mb(r.bytes)}, ${codecs}`,
12926
+ ` scene changes: ${cuts.length}${cuts.length ? ` (${cuts.map((c) => `${c.time.toFixed(2)}s`).join(", ")})` : ""}; per ${r.scenes.window} s: ${r.scenes.perWindow.map((w) => w.count).join(", ")}`,
12927
+ r.audio ? ` loudness: ${l?.integrated ?? "silent"}${l?.integrated !== null ? " LUFS" : ""} integrated; short-term every 3 s: ${st?.join(", ")}; sample peak ${l?.samplePeakDb ?? "-inf"} dBFS` : " loudness: no audio track",
12928
+ ` posters: ${r.posters.first.file} (deviation ${r.posters.first.deviation}), ${r.posters.last.file} (deviation ${r.posters.last.deviation})`,
12929
+ ` ${r.warnings.length ? `${r.warnings.length} warning${r.warnings.length === 1 ? "" : "s"}` : "no warnings"}; ${join23(r.outDir, "check.json")}`
12930
+ ].join("\n");
12931
+ }
12932
+ async function cmdCheckVideo(argv) {
12933
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS11);
12934
+ const file = positionals[0];
12935
+ if (!file)
12936
+ throw new UsageError(
12937
+ "vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]"
12938
+ );
12939
+ if (!existsSync21(file)) throw new UsageError(`${file} does not exist`);
12940
+ const r = createReporter(flags.json === true);
12941
+ const strict = flags.strict === true;
12942
+ const outDir = resolve14(
12943
+ strFlag(flags, "out") ?? join23(dirname8(file), `${basename10(file, extname4(file))}-check`)
12944
+ );
12945
+ const browser = await launchBrowser();
12946
+ try {
12947
+ r.event({ event: "phase", phase: "check-video" });
12948
+ const result = await checkVideo(browser, file, outDir);
12949
+ for (const w of result.warnings) r.warn(w);
12950
+ r.done({ ...result }, checkVideoLines(result));
12951
+ return result.warnings.length && strict ? EXIT_USAGE : EXIT_OK;
12952
+ } finally {
12953
+ await browser.close();
12954
+ }
12955
+ }
12956
+
12957
+ // src/plugin/join.ts
12958
+ import { existsSync as existsSync22, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12959
+ import { copyFile as copyFile2, mkdir as mkdir10, rm as rm3 } from "fs/promises";
12960
+ import {
12961
+ basename as basename11,
12962
+ dirname as dirname9,
12963
+ isAbsolute as isAbsolute3,
12964
+ join as join24,
12965
+ relative as relative4,
12966
+ resolve as resolve15,
12967
+ sep as sep2
12968
+ } from "path";
12969
+ import {
12970
+ mediaKey as mediaKey2,
12971
+ mediaRefId,
12972
+ migrateMotion as migrateMotion3,
12973
+ projectFromArtifact as projectFromArtifact2,
12974
+ ratedSegments as ratedSegments8,
12975
+ resolveAnchor as resolveAnchor5,
12976
+ segmentOutputExtents as segmentOutputExtents3
12977
+ } from "@vosjs/studio-core";
12978
+ import { totalDuration as totalDuration2 } from "@vosjs/timeline";
12979
+
12980
+ // src/plugin/stepCaptions.ts
12981
+ import {
12982
+ docMediaList,
12983
+ findStep as findStep2,
12984
+ mediaKey,
12985
+ ratedSegments as ratedSegments7,
12986
+ resolveAnchor as resolveAnchor4,
12987
+ segmentOutputExtents as segmentOutputExtents2
12988
+ } from "@vosjs/studio-core";
12989
+ import { totalDuration } from "@vosjs/timeline";
12990
+ var STEP_CAPTIONS_FROM = "steps";
12991
+ var STEP_CAPTION_OFFSET = 0.2;
12992
+ var CAPTION_MAX = 3;
12993
+ var CAPTION_MIN = 1.2;
12994
+ var CAPTION_GAP = 0.1;
12995
+ function captionOf(step, actions, script) {
12996
+ const own = step.caption;
12997
+ if (typeof own === "string" && own.trim()) return own.trim();
12998
+ for (const list of [
12999
+ actions?.steps,
13000
+ Array.isArray(script?.steps) ? script.steps : void 0
13001
+ ]) {
13002
+ if (!Array.isArray(list)) continue;
13003
+ const byId = step.id !== void 0 ? list.find((s) => s?.id === step.id) : void 0;
13004
+ const raw = byId ?? list[step.step];
13005
+ if (typeof raw?.caption === "string" && raw.caption.trim())
13006
+ return raw.caption.trim();
13007
+ }
13008
+ return null;
13009
+ }
13010
+ var safeId = (v) => String(v).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "step";
13011
+ function landStepCaptions(doc, actions) {
13012
+ const report = { landed: [], skipped: [], uncaptioned: [] };
13013
+ const kept = (doc.overlays ?? []).filter((o) => o.from !== STEP_CAPTIONS_FROM);
13014
+ const rated = ratedSegments7(doc);
13015
+ const footageEnd = totalDuration(rated);
13016
+ const docSegments = doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }];
13017
+ const extents = segmentOutputExtents2(doc);
13018
+ const clipEnd = (start, media) => {
13019
+ for (let i = 0; i < extents.length; i++) {
13020
+ const e = extents[i];
13021
+ if (mediaKey(docSegments[i]?.media) === media && start >= e.start - 1e-6 && start < e.end - 1e-6)
13022
+ return e.end;
13023
+ }
13024
+ return footageEnd;
13025
+ };
13026
+ const ends = /* @__PURE__ */ new Map();
13027
+ const spoken = (media, step, steps) => kept.some((o) => {
13028
+ if (o.kind !== "text" || o.preset !== "caption") return false;
13029
+ if (o.anchor && mediaKey(o.anchor.media) === media)
13030
+ return findStep2(steps, o.anchor.step) === step;
13031
+ return media === "" && o.id === `${CAPTION_ID_PREFIX}${step.step}`;
13032
+ });
13033
+ const fresh = [];
13034
+ const taken = new Set(kept.map((o) => o.id));
13035
+ for (const { id: media, source } of docMediaList(doc)) {
13036
+ const steps = source.meta.steps ?? [];
13037
+ let captioned = 0;
13038
+ for (const step of steps) {
13039
+ const caption = captionOf(step, actions.get(media), source.script);
13040
+ if (!caption) continue;
13041
+ captioned++;
13042
+ const ref = step.id ?? step.step;
13043
+ const label = `step ${JSON.stringify(ref)}${media ? ` of ${media}` : ""}`;
13044
+ if (step.skipped) {
13045
+ report.skipped.push(`${label}: the recorder skipped it`);
13046
+ continue;
13047
+ }
13048
+ if (spoken(media, step, steps)) {
13049
+ report.skipped.push(`${label}: a caption is already anchored to it`);
13050
+ continue;
13051
+ }
13052
+ const anchor = {
13053
+ step: ref,
13054
+ ...media ? { media } : {},
13055
+ at: "end",
13056
+ offset: STEP_CAPTION_OFFSET
13057
+ };
13058
+ const placed = resolveAnchor4(doc, anchor, rated);
13059
+ if (placed.start === null) {
13060
+ report.skipped.push(`${label}: the cut leaves it out`);
13061
+ continue;
13062
+ }
13063
+ const end = clipEnd(placed.start, media);
13064
+ if (placed.start >= end - CAPTION_MIN) {
13065
+ report.skipped.push(`${label}: it plays in the last second of its clip`);
13066
+ continue;
13067
+ }
13068
+ const base = `step-${media ? `${media}-` : ""}${safeId(ref)}`;
13069
+ let id = base;
13070
+ for (let n = 2; taken.has(id); n++) id = `${base}-${n}`;
13071
+ taken.add(id);
13072
+ const clip = {
13073
+ ...houseCaption(id, caption, placed.start, CAPTION_MAX),
13074
+ anchor,
13075
+ from: STEP_CAPTIONS_FROM
13076
+ };
13077
+ ends.set(clip, end);
13078
+ fresh.push(clip);
13079
+ report.landed.push({
13080
+ id,
13081
+ step: ref,
13082
+ ...media ? { media } : {},
13083
+ start: placed.start
13084
+ });
13085
+ }
13086
+ if (steps.length && !captioned) report.uncaptioned.push(media);
13087
+ }
13088
+ fresh.sort((a, b) => a.start - b.start);
13089
+ fresh.forEach((c, i) => {
13090
+ const next = fresh[i + 1];
13091
+ const clipRoom = (ends.get(c) ?? footageEnd) - c.start;
13092
+ const nextRoom = next ? next.start - CAPTION_GAP - c.start : Infinity;
13093
+ c.duration = +Math.min(
13094
+ CAPTION_MAX,
13095
+ clipRoom,
13096
+ Math.max(CAPTION_MIN, nextRoom)
13097
+ ).toFixed(3);
13098
+ });
13099
+ const overlays = [...kept, ...fresh];
13100
+ if (overlays.length) doc.overlays = overlays;
13101
+ else delete doc.overlays;
13102
+ return report;
13103
+ }
13104
+ function bareCaptionsFlag(argv) {
13105
+ return argv.map(
13106
+ (a, i) => a === "--captions" && (i + 1 >= argv.length || argv[i + 1].startsWith("--")) ? "--captions=steps" : a
13107
+ );
13108
+ }
13109
+
13110
+ // src/plugin/join.ts
13111
+ var FULL_BLEED_FRAME = {
13112
+ inset: { top: 0, right: 0, bottom: 0, left: 0 },
13113
+ radius: 0,
13114
+ shadow: 0,
13115
+ shadowContact: 0,
13116
+ border: 0,
13117
+ fit: "cover"
13118
+ };
13119
+ var round32 = (v) => Math.round(v * 1e3) / 1e3;
13120
+ var FILE_SOUND_FADE = 0.15;
13121
+ var prefixed = (id, key) => `${id}/${takeRelativeFile(key)}`;
13122
+ function fileDoc(key, probed, t0 = 0) {
13123
+ const meta = {
13124
+ dpr: 1,
13125
+ zoom: 1,
13126
+ t0,
13127
+ durationMs: probed.durationMs,
13128
+ width: probed.width,
13129
+ height: probed.height,
13130
+ captureWidth: probed.width,
13131
+ captureHeight: probed.height,
13132
+ fps: probed.fps > 0 ? probed.fps : 30,
13133
+ hasAudio: probed.hasAudio,
13134
+ producer: "ingest"
13135
+ };
13136
+ const doc = projectFromArtifact2({ videoKey: key, cursor: [], meta }, key, {
13137
+ footage: true
13138
+ }).doc;
13139
+ doc.segments = [{ in: 0, out: round32(probed.durationMs / 1e3) }];
13140
+ return doc;
13141
+ }
13142
+ function endingLayers(doc) {
13143
+ const pieces = ratedSegments8(doc);
13144
+ const end = totalDuration2(pieces);
13145
+ if (!(end > 0)) return [];
13146
+ return (doc.overlays ?? []).filter((o) => {
13147
+ const start = o.anchor ? resolveAnchor5(doc, o.anchor, pieces).start ?? o.start : o.start;
13148
+ return start >= end - 1e-3;
13149
+ });
13150
+ }
13151
+ function dropEnding(d, label, notes) {
13152
+ const ending = endingLayers(d);
13153
+ const legacy = d.endCard !== void 0;
13154
+ if (!ending.length && !legacy) return;
13155
+ const gone = new Set(ending);
13156
+ d.overlays = (d.overlays ?? []).filter((o) => !gone.has(o));
13157
+ if (!d.overlays.length) delete d.overlays;
13158
+ if (legacy) delete d.endCard;
13159
+ const named = [
13160
+ ...ending.map((o) => o.id),
13161
+ ...legacy ? ["its endCard"] : []
13162
+ ].join(", ");
13163
+ notes.push(
13164
+ `${label}: dropped ${named}, which played after its footage and would land over the next input (only the last input's ending stays, at the end of the film)`
13165
+ );
13166
+ }
13167
+ function segs0(d) {
13168
+ return d.segments.length ? d.segments[0] : { in: 0, out: round32(d.source.meta.durationMs / 1e3) };
13169
+ }
13170
+ function joinDocs(parts) {
13171
+ if (parts.length < 2)
13172
+ throw new UsageError("vos join needs two inputs or more");
13173
+ const [first, ...rest] = parts;
13174
+ const doc = structuredClone(first.doc);
13175
+ const notes = [];
13176
+ dropEnding(doc, first.label, notes);
13177
+ const copies = /* @__PURE__ */ new Map();
13178
+ const copy = (c) => {
13179
+ if (!copies.has(c.to)) copies.set(c.to, c);
13180
+ };
13181
+ const actions = /* @__PURE__ */ new Map([["", first.actions]]);
13182
+ if (first.file) {
13183
+ copy({
13184
+ from: first.file.path,
13185
+ to: takeRelativeFile(doc.source.videoKey),
13186
+ remux: first.file.ext
13187
+ });
13188
+ } else {
13189
+ const refs = [
13190
+ ...isTakeRelativeKey(doc.source.videoKey) ? [{ where: "recording", key: doc.source.videoKey, set: () => {
13191
+ } }] : [],
13192
+ ...sourceSidecarRefs(doc),
13193
+ ...docMediaRefs(doc)
13194
+ ];
13195
+ for (const ref of refs)
13196
+ copy({
13197
+ from: join24(first.dir, takeRelativeFile(ref.key)),
13198
+ to: takeRelativeFile(ref.key),
13199
+ base: first.dir
13200
+ });
13201
+ }
13202
+ if (!doc.segments.length)
13203
+ doc.segments = [{ in: 0, out: round32(doc.source.meta.durationMs / 1e3) }];
13204
+ const taken = new Set((doc.media ?? []).map((m) => m.id));
13205
+ const allocate = () => {
13206
+ let n = 1;
13207
+ while (taken.has(`m${n}`)) n++;
13208
+ taken.add(`m${n}`);
13209
+ return `m${n}`;
13210
+ };
13211
+ const media = [...doc.media ?? []];
13212
+ const joined = [];
13213
+ const placed = [];
13214
+ for (const part of rest) {
13215
+ const d = structuredClone(migrateMotion3(part.doc));
13216
+ if (part !== rest[rest.length - 1]) dropEnding(d, part.label, notes);
13217
+ const pid = allocate();
13218
+ const idMap = /* @__PURE__ */ new Map([["", pid]]);
13219
+ for (const m of d.media ?? []) idMap.set(m.id, allocate());
13220
+ const mapMedia = (k) => idMap.get(mediaKey2(k)) ?? pid;
13221
+ joined.push({ label: part.label, id: pid });
13222
+ actions.set(pid, part.actions);
13223
+ if (part.file) {
13224
+ const to = `${pid}/recording.${part.file.ext}`;
13225
+ copy({ from: part.file.path, to, remux: part.file.ext });
13226
+ d.source.videoKey = to;
13227
+ } else {
13228
+ const refs = [
13229
+ ...isTakeRelativeKey(d.source.videoKey) ? [
13230
+ {
13231
+ where: "recording",
13232
+ key: d.source.videoKey,
13233
+ set: (next) => {
13234
+ d.source.videoKey = next;
13235
+ }
13236
+ }
13237
+ ] : [],
13238
+ ...sourceSidecarRefs(d),
13239
+ ...(d.media ?? []).flatMap(
13240
+ (m) => [
13241
+ ["videoKey", m.videoKey],
13242
+ ["micKey", m.micKey],
13243
+ ["camKey", m.camKey]
13244
+ ].filter(([, k]) => isTakeRelativeKey(k)).map(([field, k]) => ({
13245
+ where: `media ${m.id}`,
13246
+ key: k,
13247
+ set: (next) => {
13248
+ ;
13249
+ m[field] = next;
13250
+ }
13251
+ }))
13252
+ ),
13253
+ ...docOverlayRefs(d),
13254
+ ...docObjectRefs(d)
13255
+ ];
13256
+ for (const ref of refs) {
13257
+ const to = prefixed(pid, ref.key);
13258
+ copy({
13259
+ from: join24(part.dir, takeRelativeFile(ref.key)),
13260
+ to,
13261
+ base: part.dir
13262
+ });
13263
+ ref.set(to);
13264
+ }
13265
+ }
13266
+ media.push({
13267
+ ...d.source,
13268
+ id: pid,
13269
+ ...part.kind === "file" ? { frame: { ...FULL_BLEED_FRAME } } : {}
13270
+ });
13271
+ for (const m of d.media ?? []) media.push({ ...m, id: idMap.get(m.id) });
13272
+ const segs = d.segments.length ? d.segments : [{ in: 0, out: round32(d.source.meta.durationMs / 1e3) }];
13273
+ const at2 = doc.segments.length;
13274
+ doc.segments.push(...segs.map((s) => ({ ...s, media: mapMedia(s.media) })));
13275
+ const ns = (id) => `${pid}-${id}`;
13276
+ const tag = (s) => ({
13277
+ ...s,
13278
+ id: ns(s.id),
13279
+ media: mapMedia(s.media)
13280
+ });
13281
+ doc.zoom = [...doc.zoom, ...d.zoom.map(tag)];
13282
+ if (d.speed?.length) doc.speed = [...doc.speed ?? [], ...d.speed.map(tag)];
13283
+ if (d.tilt?.length) doc.tilt = [...doc.tilt ?? [], ...d.tilt.map(tag)];
13284
+ if (d.camMotion?.length)
13285
+ doc.camMotion = [...doc.camMotion ?? [], ...d.camMotion.map(tag)];
13286
+ if (d.freeze?.length)
13287
+ doc.freeze = [...doc.freeze ?? [], ...d.freeze.map(tag)];
13288
+ if (d.rejected?.length)
13289
+ doc.rejected = [...doc.rejected ?? [], ...d.rejected.map(tag)];
13290
+ if (d.audio.length)
13291
+ notes.push(
13292
+ `${part.label}: its ${d.audio.length} sound clip${d.audio.length === 1 ? "" : "s"} stay behind (the film's sound is the first input's; add a score with vos score)`
13293
+ );
13294
+ placed.push({ part, d, pid, firstSegment: at2, mapMedia });
13295
+ }
13296
+ if (media.length) doc.media = media;
13297
+ const extents = segmentOutputExtents3(doc);
13298
+ for (const { part, d, pid, firstSegment, mapMedia } of placed) {
13299
+ const offset = extents[firstSegment]?.start ?? 0;
13300
+ const ownPieces = ratedSegments8(d);
13301
+ const ns = (id) => `${pid}-${id}`;
13302
+ let dropped = 0;
13303
+ for (const o of d.overlays ?? []) {
13304
+ const next = { ...o, id: ns(o.id) };
13305
+ if (next.kind !== "text" && next.kind !== "html") {
13306
+ const ref = mediaRefId(next.key);
13307
+ if (ref !== null) next.key = `media:${mapMedia(ref)}`;
13308
+ }
13309
+ if (o.anchor) {
13310
+ next.anchor = { ...o.anchor, media: mapMedia(o.anchor.media) };
13311
+ } else if (o.pin?.step !== void 0) {
13312
+ const r = resolveAnchor5(d, { step: o.pin.step }, ownPieces);
13313
+ if (r.start !== null) {
13314
+ next.anchor = {
13315
+ step: o.pin.step,
13316
+ media: pid,
13317
+ offset: round32(o.start - r.start)
13318
+ };
13319
+ } else {
13320
+ next.start = round32(o.start + offset);
13321
+ }
13322
+ delete next.pin;
13323
+ dropped++;
13324
+ } else {
13325
+ if (o.pin) {
13326
+ delete next.pin;
13327
+ dropped++;
13328
+ }
13329
+ next.start = round32(o.start + offset);
13330
+ }
13331
+ ;
13332
+ (doc.overlays ??= []).push(next);
13333
+ }
13334
+ if (dropped)
13335
+ notes.push(
13336
+ `${part.label}: ${dropped} pinned layer${dropped === 1 ? "" : "s"} kept ${dropped === 1 ? "its" : "their"} time and lost the pin (a pin places a layer on the first input's footage only)`
13337
+ );
13338
+ if (part.file?.hasAudio) {
13339
+ const seg = segs0(d);
13340
+ const at2 = extents[firstSegment];
13341
+ if (part.mute) {
13342
+ notes.push(`${part.label}: its own sound is left out (--mute)`);
13343
+ } else if (seg && at2) {
13344
+ const len = Math.max(0, seg.out - seg.in);
13345
+ const fade = round32(Math.min(FILE_SOUND_FADE, len / 4));
13346
+ const id = `${pid}-sound`;
13347
+ doc.audio = [
13348
+ ...doc.audio,
13349
+ {
13350
+ id,
13351
+ key: d.source.videoKey,
13352
+ name: basename11(part.file.path),
13353
+ start: round32(at2.start),
13354
+ in: round32(seg.in),
13355
+ out: round32(seg.out),
13356
+ duration: round32(d.source.meta.durationMs / 1e3),
13357
+ gain: 1,
13358
+ fadeIn: fade,
13359
+ fadeOut: fade
13360
+ }
13361
+ ];
13362
+ notes.push(
13363
+ `${part.label}: its own sound plays as audio clip ${id}, ${at2.start.toFixed(2)}..${(at2.start + len).toFixed(2)}s (--mute ${part.label} to leave it out)`
13364
+ );
13365
+ }
13366
+ }
13367
+ for (const ob of d.objects ?? []) {
13368
+ ;
13369
+ (doc.objects ??= []).push({
13370
+ ...ob,
13371
+ id: ns(ob.id),
13372
+ ...ob.span ? { span: { ...ob.span, start: round32(ob.span.start + offset) } } : {}
13373
+ });
13374
+ }
13375
+ }
13376
+ for (const part of rest)
13377
+ if (part.mute && !part.file?.hasAudio)
13378
+ notes.push(
13379
+ `${part.label}: --mute leaves nothing out (${part.file ? "the file carries no sound" : "a take's own footage sound is its recording's; its added sound stays behind anyway"})`
13380
+ );
13381
+ if (first.mute)
13382
+ notes.push(
13383
+ `${first.label}: --mute leaves nothing out (the first input's sound is the film's own)`
13384
+ );
13385
+ const pieces = ratedSegments8(doc);
13386
+ for (const o of doc.overlays ?? []) {
13387
+ if (!o.anchor) continue;
13388
+ const r = resolveAnchor5(doc, o.anchor, pieces);
13389
+ if (r.start !== null) o.start = r.start;
13390
+ }
13391
+ return { doc, copies: [...copies.values()], media: joined, actions, notes };
13392
+ }
13393
+ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set(["json", "help", "force", "captions"]);
13394
+ var MULTI = /* @__PURE__ */ new Set(["mute"]);
13395
+ var USAGE2 = "vos join <take> <take|file.mp4>\u2026 --out <dir> [--force] [--captions] [--mute <file>]... [--json]";
13396
+ var within = (inner, outer) => {
13397
+ const rel = relative4(outer, inner);
13398
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute3(rel);
13399
+ };
13400
+ async function readPart(arg) {
13401
+ const path = resolve15(arg);
13402
+ if (!existsSync22(path)) throw new UsageError(`${arg}: no such take or file`);
13403
+ if (statSync7(path).isDirectory()) {
13404
+ if (!existsSync22(join24(path, "meta.json")))
13405
+ throw new UsageError(
13406
+ `${arg}: not a take directory (no meta.json); a video file is joined by its path`
13407
+ );
13408
+ const take = await loadTake(path);
13409
+ const doc2 = take.doc ?? projectFromArtifact2(
13410
+ {
13411
+ videoKey: take.paths.recordingName,
13412
+ cursor: take.cursor,
13413
+ meta: take.meta
13414
+ },
13415
+ take.paths.recordingName
13416
+ ).doc;
13417
+ return { label: arg, kind: "take", doc: doc2, dir: path, actions: take.actions };
13418
+ }
13419
+ const probed = await probeVideoFile(path);
13420
+ if (!(probed.durationMs > 0))
13421
+ throw new UsageError(`${arg}: the file reports no duration`);
13422
+ const doc = fileDoc(
13423
+ `recording.${probed.ext}`,
13424
+ probed,
13425
+ Math.round(statSync7(path).mtimeMs)
13426
+ );
13427
+ return {
13428
+ label: arg,
13429
+ kind: "file",
13430
+ doc,
13431
+ dir: dirname9(path),
13432
+ actions: null,
13433
+ file: { path, ext: probed.ext, hasAudio: probed.hasAudio }
13434
+ };
13435
+ }
13436
+ async function cmdJoin(argv) {
13437
+ const { positionals, flags, multi } = parseArgs(argv, BOOLEAN_FLAGS12, MULTI);
13438
+ const r = createReporter(flags.json === true);
13439
+ const outArg = strFlag(flags, "out");
13440
+ const force = flags.force === true;
13441
+ const withCaptions = flags.captions === true;
13442
+ if (positionals.length < 2 || !outArg) throw new UsageError(USAGE2);
13443
+ const out = resolve15(outArg);
13444
+ const inputs = positionals.map((p) => resolve15(p));
13445
+ const muted = /* @__PURE__ */ new Set();
13446
+ for (const m of multi.mute ?? []) {
13447
+ const i = inputs.indexOf(resolve15(m));
13448
+ if (i < 0)
13449
+ throw new UsageError(
13450
+ `--mute ${m}: not one of the inputs; name a file as it is given to vos join`
13451
+ );
13452
+ muted.add(i);
13453
+ }
13454
+ for (const [i, input] of inputs.entries()) {
13455
+ if (within(out, input) || within(input, out))
13456
+ throw new UsageError(
13457
+ `--out ${outArg} overlaps the input ${positionals[i]}; write the joined take somewhere of its own`
13458
+ );
13459
+ }
13460
+ if (existsSync22(out)) {
13461
+ if (!force)
13462
+ throw new UsageError(
13463
+ `--out ${outArg} exists; pass --force to replace it, or name a new directory`
13464
+ );
13465
+ const isTake = existsSync22(join24(out, "doc.json")) || existsSync22(join24(out, "meta.json"));
13466
+ if (!isTake && readdirSync5(out).length > 0)
13467
+ throw new UsageError(
13468
+ `--force replaces a take directory, and ${outArg} holds other files; name a new directory`
13469
+ );
13470
+ if (within(process.cwd(), out))
13471
+ throw new UsageError(
13472
+ `--out ${outArg} is the working directory or above it; name a new directory`
13473
+ );
13474
+ }
13475
+ const parts = [];
13476
+ for (const [i, p] of positionals.entries()) {
13477
+ r.log(`reading ${p}\u2026`);
13478
+ parts.push({
13479
+ ...await readPart(p),
13480
+ ...muted.has(i) ? { mute: true } : {}
13481
+ });
13482
+ }
13483
+ const plan = joinDocs(parts);
13484
+ let captions = null;
13485
+ if (withCaptions) {
13486
+ captions = landStepCaptions(plan.doc, plan.actions);
13487
+ const labelOf = new Map([
13488
+ ["", positionals[0]],
13489
+ ...plan.media.map((m) => [m.id, m.label])
13490
+ ]);
13491
+ for (const k of captions.uncaptioned)
13492
+ plan.notes.push(
13493
+ `no step in ${labelOf.get(k) ?? `media ${k}`} carries a caption`
13494
+ );
13495
+ }
13496
+ const lint = lintDoc(plan.doc);
13497
+ if (lint.problems.length) {
13498
+ r.log(
13499
+ `the joined document does not validate:
13500
+ ${lint.problems.join("\n ")}`
13501
+ );
13502
+ return EXIT_ERROR;
13503
+ }
13504
+ for (const c of plan.copies) {
13505
+ if (!c.base) continue;
13506
+ const found = localFile(relative4(c.base, c.from), c.base);
13507
+ if ("refused" in found)
13508
+ throw new Error(
13509
+ `${relative4(process.cwd(), c.from) || c.from}, which an input's doc.json names, ${found.refused}; bring its media home first (vos fetch --media) or fix the key`
13510
+ );
13511
+ c.from = found.file;
13512
+ }
13513
+ if (existsSync22(out)) await rm3(out, { recursive: true, force: true });
13514
+ await mkdir10(out, { recursive: true });
13515
+ for (const c of plan.copies) {
13516
+ const to = join24(out, c.to);
13517
+ await mkdir10(dirname9(to), { recursive: true });
13518
+ if (c.remux) {
13519
+ const remuxed = await copyRecording(c.from, to, c.remux);
13520
+ if (!remuxed && c.remux !== "mov")
13521
+ plan.notes.push(
13522
+ `could not read ${basename11(c.from)}'s audio in Node, so it was copied as it is; its length comes from the container`
13523
+ );
13524
+ } else await copyFile2(c.from, to);
13525
+ }
13526
+ const doc = plan.doc;
13527
+ await writeJson(join24(out, "meta.json"), doc.source.meta, true);
13528
+ if (doc.source.cursor.length)
13529
+ await writeJson(join24(out, "cursor.json"), doc.source.cursor);
13530
+ if (parts[0].actions)
13531
+ await writeJson(join24(out, "actions.json"), parts[0].actions, true);
13532
+ await writeJson(join24(out, "doc.json"), doc, true);
13533
+ const extents = segmentOutputExtents3(doc);
13534
+ const length = extents.length ? extents[extents.length - 1].end : 0;
13535
+ for (const n of plan.notes) r.log(`note: ${n}`);
13536
+ for (const w of lint.warnings) r.log(`warning: ${w}`);
13537
+ const captionLine = captions ? `
13538
+ ${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} landed` + (captions.skipped.length ? `; not landed: ${captions.skipped.join("; ")}` : "") : "";
13539
+ r.done(
13540
+ {
13541
+ out,
13542
+ primary: positionals[0],
13543
+ media: plan.media,
13544
+ segments: doc.segments.length,
13545
+ seconds: round32(length),
13546
+ files: plan.copies.map((c) => c.to),
13547
+ notes: plan.notes,
13548
+ warnings: lint.warnings,
13549
+ ...captions ? { captions } : {}
13550
+ },
13551
+ `Joined ${parts.length} inputs \u2192 ${out} (${length.toFixed(1)}s, ${doc.segments.length} segments)
13552
+ ${positionals[0]} is the primary; ${plan.media.map((m) => `${m.label} \u2192 ${m.id}`).join(", ")}${captionLine}
13553
+ Next: vos frames ${outArg} --times 0,25%,50%,75%,100%; vos render ${outArg}`
13554
+ );
13555
+ return EXIT_OK;
13556
+ }
13557
+
13558
+ // src/plugin/score.ts
13559
+ import { existsSync as existsSync23 } from "fs";
13560
+ import {
13561
+ copyFile as copyFile3,
13562
+ mkdir as mkdir11,
13563
+ mkdtemp,
13564
+ readFile as readFile14,
13565
+ rm as rm4,
13566
+ writeFile as writeFile11
13567
+ } from "fs/promises";
13568
+ import { tmpdir } from "os";
13569
+ import { basename as basename12, isAbsolute as isAbsolute4, join as join25, relative as relative5, resolve as resolve16, sep as sep3 } from "path";
13570
+ import {
13571
+ docMediaList as docMediaList2,
13572
+ docOutputDuration as docOutputDuration3,
13573
+ findStep as findStep3,
13574
+ ratedSegments as ratedSegments9,
13575
+ resolveAnchor as resolveAnchor6
13576
+ } from "@vosjs/studio-core";
13577
+ var SCORE_ID = "score";
13578
+ var MID_SONG_FADE_IN = 0.8;
13579
+ var BLOCK_SECONDS = 0.1;
13580
+ var WINDOW_SECONDS = 1;
13581
+ var HOP_SECONDS = 0.5;
13582
+ var FLOOR_DB = -70;
13583
+ function kWeighting(sampleRate) {
13584
+ const shelf = (() => {
13585
+ const f0 = 1681.974450955533;
13586
+ const G = 3.999843853973347;
13587
+ const Q = 0.7071752369554196;
13588
+ const K = Math.tan(Math.PI * f0 / sampleRate);
13589
+ const Vh = Math.pow(10, G / 20);
13590
+ const Vb = Math.pow(Vh, 0.4996667741545416);
13591
+ const a0 = 1 + K / Q + K * K;
13592
+ return {
13593
+ b0: (Vh + Vb * K / Q + K * K) / a0,
13594
+ b1: 2 * (K * K - Vh) / a0,
13595
+ b2: (Vh - Vb * K / Q + K * K) / a0,
13596
+ a1: 2 * (K * K - 1) / a0,
13597
+ a2: (1 - K / Q + K * K) / a0
13598
+ };
13599
+ })();
13600
+ const highPass = (() => {
13601
+ const f0 = 38.13547087602444;
13602
+ const Q = 0.5003270373238773;
13603
+ const K = Math.tan(Math.PI * f0 / sampleRate);
13604
+ const a0 = 1 + K / Q + K * K;
13605
+ return {
13606
+ b0: 1,
13607
+ b1: -2,
13608
+ b2: 1,
13609
+ a1: 2 * (K * K - 1) / a0,
13610
+ a2: (1 - K / Q + K * K) / a0
13611
+ };
13612
+ })();
13613
+ return [shelf, highPass];
13614
+ }
13615
+ var LoudnessMeter = class {
13616
+ constructor(sampleRate, channels) {
13617
+ this.sampleRate = sampleRate;
13618
+ this.channels = channels;
13619
+ this.blockLen = Math.max(1, Math.round(sampleRate * BLOCK_SECONDS));
13620
+ const weighted = channels >= 1 && channels <= 2 && sampleRate >= 8e3;
13621
+ this.unit = weighted ? "LUFS" : "dBFS";
13622
+ this.filters = weighted ? kWeighting(sampleRate) : null;
13623
+ this.state = Array.from({ length: channels }, () => [
13624
+ [0, 0, 0, 0],
13625
+ [0, 0, 0, 0]
13626
+ ]);
13627
+ this.acc = new Array(channels).fill(0);
13628
+ }
13629
+ sampleRate;
13630
+ channels;
13631
+ unit;
13632
+ blockLen;
13633
+ filters;
13634
+ /** Per channel, per stage: [x1, x2, y1, y2]. */
13635
+ state;
13636
+ acc;
13637
+ filled = 0;
13638
+ blocks = [];
13639
+ /** One chunk: a Float32Array per channel, all the same length. */
13640
+ push(planes) {
13641
+ const n = planes[0]?.length ?? 0;
13642
+ for (let i = 0; i < n; i++) {
13643
+ for (let c = 0; c < this.channels; c++) {
13644
+ let v = planes[c]?.[i] ?? 0;
13645
+ if (this.filters) {
13646
+ for (let k = 0; k < 2; k++) {
13647
+ const f = this.filters[k];
13648
+ const s = this.state[c][k];
13649
+ const y = f.b0 * v + f.b1 * s[0] + f.b2 * s[1] - f.a1 * s[2] - f.a2 * s[3];
13650
+ s[1] = s[0];
13651
+ s[0] = v;
13652
+ s[3] = s[2];
13653
+ s[2] = y;
13654
+ v = y;
13655
+ }
13656
+ }
13657
+ this.acc[c] += v * v;
13658
+ }
13659
+ if (++this.filled === this.blockLen) this.flush();
13660
+ }
13661
+ }
13662
+ flush() {
13663
+ let power = 0;
13664
+ for (let c = 0; c < this.channels; c++) {
13665
+ power += this.acc[c] / this.filled;
13666
+ this.acc[c] = 0;
13667
+ }
13668
+ if (this.unit === "dBFS") power /= Math.max(1, this.channels);
13669
+ this.blocks.push(power);
13670
+ this.filled = 0;
13671
+ }
13672
+ /** The blocks, the last partial one included. */
13673
+ finish() {
13674
+ if (this.filled > this.blockLen / 2) this.flush();
13675
+ return this.blocks;
13676
+ }
13677
+ };
13678
+ function levelCurve(blocks, unit) {
13679
+ const per = Math.round(WINDOW_SECONDS / BLOCK_SECONDS);
13680
+ const hop = Math.round(HOP_SECONDS / BLOCK_SECONDS);
13681
+ const out = [];
13682
+ for (let i = 0; i + per <= blocks.length; i += hop) {
13683
+ let sum = 0;
13684
+ for (let j = i; j < i + per; j++) sum += blocks[j];
13685
+ const mean = sum / per;
13686
+ const db = mean > 0 ? (unit === "LUFS" ? -0.691 : 0) + 10 * Math.log10(mean) : -Infinity;
13687
+ out.push({
13688
+ t: +((i + per / 2) * BLOCK_SECONDS).toFixed(3),
13689
+ level: +Math.max(FLOOR_DB, db).toFixed(2)
13690
+ });
13691
+ }
13692
+ return out;
13693
+ }
13694
+ var SWELL_LOOKBACK = 15;
13695
+ var SWELL_SUSTAIN = 1.5;
13696
+ var SWELL_HOLD_DB = 3;
13697
+ var SWELL_ARRIVE_DB = 1;
13698
+ var SWELL_MIN_RISE = 3;
13699
+ var SWELL_FLOOR_DB = 12;
13700
+ var SWELL_SETTLE = 5;
13701
+ function findSwell(curve) {
13702
+ const step = HOP_SECONDS;
13703
+ const back = Math.round(SWELL_LOOKBACK / step);
13704
+ const hold = Math.round(SWELL_SUSTAIN / step);
13705
+ const floor = median2(curve.map((p) => p.level)) - SWELL_FLOOR_DB;
13706
+ const begun = curve.findIndex((p) => p.level >= floor);
13707
+ if (begun < 0) return null;
13708
+ const settled = begun + Math.round(SWELL_SETTLE / step);
13709
+ const levelAt = (j) => Math.max(curve[j].level, floor);
13710
+ let best = null;
13711
+ for (let i = settled + 1; i < curve.length; i++) {
13712
+ const p = curve[i];
13713
+ let held = true;
13714
+ for (let k = i + 1; k <= i + hold; k++) {
13715
+ if (k >= curve.length || curve[k].level < p.level - SWELL_HOLD_DB) {
13716
+ held = false;
13717
+ break;
13718
+ }
13719
+ }
13720
+ if (!held) continue;
13721
+ let lo = i;
13722
+ for (let j = Math.max(settled, i - back); j < i; j++)
13723
+ if (levelAt(j) < levelAt(lo)) lo = j;
13724
+ const rise = p.level - levelAt(lo);
13725
+ if (rise < SWELL_MIN_RISE) continue;
13726
+ if (i + 1 < curve.length && curve[i + 1].level > p.level) continue;
13727
+ if (best && rise <= best.rise + 1e-9) continue;
13728
+ let arrive = i;
13729
+ for (let k = lo; k <= i; k++) {
13730
+ if (curve[k].level >= p.level - SWELL_ARRIVE_DB) {
13731
+ arrive = k;
13732
+ break;
13733
+ }
13734
+ }
13735
+ best = {
13736
+ at: curve[arrive].t,
13737
+ from: curve[lo].t,
13738
+ peak: p.t,
13739
+ rise: +rise.toFixed(2),
13740
+ peakLevel: p.level
13741
+ };
13742
+ }
13743
+ return best;
13744
+ }
13745
+ var median2 = (xs) => {
13746
+ if (!xs.length) return FLOOR_DB;
13747
+ const s = [...xs].sort((a, b) => a - b);
13748
+ const m = Math.floor(s.length / 2);
13749
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
13750
+ };
13751
+ function firstAboveMedian(curve) {
13752
+ const m = median2(curve.map((p) => p.level));
13753
+ const hit = curve.find((p) => p.level > m);
13754
+ return hit ? Math.max(0, +(hit.t - WINDOW_SECONDS / 2).toFixed(3)) : 0;
13755
+ }
13756
+ var r3 = (v) => Math.round(v * 1e3) / 1e3;
13757
+ function planScore(input) {
13758
+ const { trackDuration, film, swell } = input;
13759
+ const notes = [];
13760
+ let start;
13761
+ if (input.at !== null) {
13762
+ if (!swell) {
13763
+ notes.push(
13764
+ "the track has no swell (no sustained rise of 3 dB or more); it starts at its first rise instead"
13765
+ );
13766
+ start = firstAboveMedian(input.curve);
13767
+ } else {
13768
+ start = swell.at - input.at;
13769
+ if (start < 0) {
13770
+ notes.push(
13771
+ `the swell (${swell.at.toFixed(1)}s into the track) comes before ${input.at.toFixed(1)}s of film can play ahead of it; the track starts at its top and the swell lands at ${swell.at.toFixed(1)}s`
13772
+ );
13773
+ start = 0;
13774
+ }
13775
+ }
13776
+ } else {
13777
+ start = firstAboveMedian(input.curve);
13778
+ }
13779
+ if (trackDuration < film) {
13780
+ notes.push(
13781
+ `the track (${trackDuration.toFixed(1)}s) is shorter than the film (${film.toFixed(1)}s); it cannot cover it, so it starts at its top and ends early`
13782
+ );
13783
+ start = 0;
13784
+ } else if (start + film > trackDuration) {
13785
+ const fit = Math.max(0, trackDuration - film);
13786
+ notes.push(
13787
+ `from ${start.toFixed(1)}s the track ends before the film does; it starts at ${fit.toFixed(1)}s instead, so the film is covered${swell && input.at !== null ? ` and the swell lands at ${(swell.at - fit).toFixed(1)}s` : ""}`
13788
+ );
13789
+ start = fit;
13790
+ }
13791
+ const out = Math.min(trackDuration, start + film);
13792
+ const swellAt = swell && swell.at >= start && swell.at <= out ? r3(swell.at - start) : null;
13793
+ return { in: r3(start), out: r3(out), swellAt, notes };
13794
+ }
13795
+ function sparkline(curve, width = 64) {
13796
+ if (!curve.length) return "";
13797
+ const glyphs = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
13798
+ const step = Math.max(1, Math.ceil(curve.length / width));
13799
+ const pts = [];
13800
+ for (let i = 0; i < curve.length; i += step) {
13801
+ let m = -Infinity;
13802
+ for (let j = i; j < Math.min(curve.length, i + step); j++)
13803
+ m = Math.max(m, curve[j].level);
13804
+ pts.push(m);
13805
+ }
13806
+ const hi = Math.max(...pts);
13807
+ const lo = Math.max(Math.min(...pts), hi - 24);
13808
+ return pts.map(
13809
+ (v) => glyphs[hi > lo ? Math.max(
13810
+ 0,
13811
+ Math.min(
13812
+ glyphs.length - 1,
13813
+ Math.floor((v - lo) / (hi - lo) * glyphs.length)
13814
+ )
13815
+ ) : 0]
13816
+ ).join("");
13817
+ }
13818
+ async function measureInNode(path) {
13819
+ const MB = await import("mediabunny");
13820
+ const input = new MB.Input({
13821
+ formats: MB.ALL_FORMATS,
13822
+ source: new MB.FilePathSource(path)
13823
+ });
13824
+ try {
13825
+ const track = await input.getPrimaryAudioTrack();
13826
+ if (!track) throw new UsageError(`${path} has no audio track`);
13827
+ if (!await track.canDecode()) return null;
13828
+ const meter = new LoudnessMeter(track.sampleRate, track.numberOfChannels);
13829
+ const sink = new MB.AudioSampleSink(track);
13830
+ let frames = 0;
13831
+ for await (const sample2 of sink.samples()) {
13832
+ const planes = [];
13833
+ for (let c = 0; c < track.numberOfChannels; c++) {
13834
+ const a = new Float32Array(sample2.numberOfFrames);
13835
+ sample2.copyTo(a, { planeIndex: c, format: "f32-planar" });
13836
+ planes.push(a);
13837
+ }
13838
+ frames += sample2.numberOfFrames;
13839
+ sample2.close();
13840
+ meter.push(planes);
13841
+ }
13842
+ return {
13843
+ sampleRate: track.sampleRate,
13844
+ channels: track.numberOfChannels,
13845
+ duration: frames / track.sampleRate,
13846
+ unit: meter.unit,
13847
+ blocks: meter.finish(),
13848
+ via: "node"
13849
+ };
13850
+ } finally {
13851
+ input.dispose();
13852
+ }
13853
+ }
13854
+ var PAGE_CHUNK_SECONDS = 10;
13855
+ async function measureInBrowser(path) {
13856
+ const bytes = await readFile14(path);
13857
+ const browser = await launchBrowser();
13858
+ try {
13859
+ const page = await browser.newPage();
13860
+ const info = await page.evaluate(`(async () => {
13861
+ const b64 = ${JSON.stringify(bytes.toString("base64"))}
13862
+ const bin = atob(b64)
13863
+ const u8 = new Uint8Array(bin.length)
13864
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i)
13865
+ const ctx = new OfflineAudioContext(1, 1, 48000)
13866
+ const buf = await ctx.decodeAudioData(u8.buffer)
13867
+ window.__vosScore = buf
13868
+ return { sampleRate: buf.sampleRate, channels: buf.numberOfChannels, length: buf.length }
13869
+ })()`);
13870
+ const meter = new LoudnessMeter(info.sampleRate, info.channels);
13871
+ const chunk = Math.round(info.sampleRate * PAGE_CHUNK_SECONDS);
13872
+ for (let from = 0; from < info.length; from += chunk) {
13873
+ const parts = await page.evaluate(`(() => {
13874
+ const buf = window.__vosScore
13875
+ const out = []
13876
+ for (let c = 0; c < buf.numberOfChannels; c++) {
13877
+ const a = buf.getChannelData(c).subarray(${from}, ${from + chunk})
13878
+ const u8 = new Uint8Array(a.buffer, a.byteOffset, a.byteLength)
13879
+ let s = ''
13880
+ for (let i = 0; i < u8.length; i += 0x8000)
13881
+ s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000))
13882
+ out.push(btoa(s))
13883
+ }
13884
+ return out
13885
+ })()`);
13886
+ meter.push(
13887
+ parts.map((p) => {
13888
+ const b = Buffer.from(p, "base64");
13889
+ return new Float32Array(b.buffer, b.byteOffset, b.byteLength / 4);
13890
+ })
13891
+ );
13892
+ }
13893
+ return {
13894
+ sampleRate: info.sampleRate,
13895
+ channels: info.channels,
13896
+ duration: info.length / info.sampleRate,
13897
+ unit: meter.unit,
13898
+ blocks: meter.finish(),
13899
+ via: "browser"
13900
+ };
13901
+ } finally {
13902
+ await browser.close();
13903
+ }
13904
+ }
13905
+ async function measureTrack(path) {
13906
+ return await measureInNode(path) ?? await measureInBrowser(path);
13907
+ }
13908
+ function resolveAt(doc, raw) {
13909
+ const trimmed = raw.trim();
13910
+ if (/^\d+(\.\d+)?s?$/.test(trimmed)) {
13911
+ const at2 = parseFloat(trimmed);
13912
+ return { at: at2, via: `${at2}s` };
13913
+ }
13914
+ const v = trimmed.startsWith("step:") ? trimmed.slice(5) : trimmed;
13915
+ const pieces = ratedSegments9(doc);
13916
+ const colon = v.indexOf(":");
13917
+ const named = colon > 0 ? { media: v.slice(0, colon), step: v.slice(colon + 1) } : null;
13918
+ const candidates = named ? [named] : docMediaList2(doc).filter(({ source }) => findStep3(source.meta.steps ?? [], v)).map(({ id }) => ({ media: id, step: v }));
13919
+ if (!candidates.length)
13920
+ throw new UsageError(
13921
+ `--at ${raw}: no step by that id or index in this take (and not a number of seconds)`
13922
+ );
13923
+ if (candidates.length > 1)
13924
+ throw new UsageError(
13925
+ `--at ${raw}: steps of several media carry it; say which, as <media>:<step> (${candidates.map((c2) => `${c2.media || "primary"}`).join(", ")})`
13926
+ );
13927
+ const c = candidates[0];
13928
+ const r = resolveAnchor6(
13929
+ doc,
13930
+ { step: c.step, ...c.media ? { media: c.media } : {} },
13931
+ pieces
13932
+ );
13933
+ if (r.start === null)
13934
+ throw new UsageError(
13935
+ `--at ${raw}: that step ${r.miss === "cut" ? "is cut out of the film" : r.miss === "skipped" ? "was skipped by the recorder" : "is not in the take"}`
13936
+ );
13937
+ return { at: r.start, via: `step ${v} (${r.start.toFixed(2)}s)` };
13938
+ }
13939
+ async function resolveTrack(dir, ref, flags, dryRun) {
13940
+ const local = resolve16(ref);
13941
+ if (existsSync23(local)) {
13942
+ const rel = relative5(resolve16(dir), local);
13943
+ const inside = !!rel && !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute4(rel);
13944
+ const key = inside ? rel.split(sep3).join("/") : `music/${basename12(local)}`;
13945
+ if (!inside && !dryRun) {
13946
+ await mkdir11(join25(dir, "music"), { recursive: true });
13947
+ await copyFile3(local, join25(dir, key));
13948
+ }
13949
+ return { path: local, key, name: basename12(local) };
13950
+ }
13951
+ const origin = platformOrigin({
13952
+ origin: strFlag(flags, "origin"),
13953
+ api: strFlag(flags, "api")
13954
+ });
13955
+ const catalog = await fetchMusicCatalog(origin);
13956
+ const track = catalog.tracks.find((t) => t.slug === ref);
13957
+ if (!track)
13958
+ throw new UsageError(
13959
+ `--track ${ref}: no such file, and no catalog track by that slug (some: ${catalog.tracks.slice(0, 6).map((t) => t.slug).join(", ")}; the list is GET ${origin}/api/music)`
13960
+ );
13961
+ const res = await fetch(track.url);
13962
+ if (!res.ok) throw new Error(`GET ${track.url} answered ${res.status}`);
13963
+ const tmp = await mkdtemp(join25(tmpdir(), "vos-score-"));
13964
+ const path = join25(tmp, basename12(new URL(track.url).pathname) || "track");
13965
+ await writeFile11(path, new Uint8Array(await res.arrayBuffer()));
13966
+ return {
13967
+ path,
13968
+ key: track.url,
13969
+ name: track.title,
13970
+ cleanup: () => rm4(tmp, { recursive: true, force: true })
13971
+ };
13972
+ }
13973
+ var BOOLEAN_FLAGS13 = /* @__PURE__ */ new Set(["json", "help", "dry-run"]);
13974
+ var USAGE3 = "vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]";
13975
+ async function cmdScore(argv) {
13976
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS13);
13977
+ const dir = positionals[0];
13978
+ const trackRef = strFlag(flags, "track");
13979
+ if (!dir || !trackRef) throw new UsageError(USAGE3);
13980
+ const r = createReporter(flags.json === true);
13981
+ const dryRun = flags["dry-run"] === true;
13982
+ const gain = numFlag(flags, "gain", 0.5);
13983
+ if (!(gain >= 0 && gain <= 1))
13984
+ throw new UsageError("--gain expects a number from 0 to 1");
13985
+ const fadeOut = numFlag(flags, "fade-out", 1.5);
13986
+ if (!(fadeOut >= 0)) throw new UsageError("--fade-out expects seconds \u2265 0");
13987
+ const fadeInFlag = strFlag(flags, "fade-in");
13988
+ const fadeInAsked = numFlag(flags, "fade-in", MID_SONG_FADE_IN);
13989
+ if (!(fadeInAsked >= 0)) throw new UsageError("--fade-in expects seconds \u2265 0");
13990
+ const take = await loadTake(dir);
13991
+ if (!take.doc)
13992
+ throw new UsageError(`${dir} has no doc.json; run vos plan first`);
13993
+ const doc = take.doc;
13994
+ const atRaw = strFlag(flags, "at");
13995
+ const at2 = atRaw !== void 0 ? resolveAt(doc, atRaw) : null;
13996
+ const track = await resolveTrack(dir, trackRef, flags, dryRun);
13997
+ let measured;
13998
+ try {
13999
+ r.log(`measuring ${track.name}\u2026`);
14000
+ measured = await measureTrack(track.path);
14001
+ } finally {
14002
+ await track.cleanup?.();
14003
+ }
14004
+ const curve = levelCurve(measured.blocks, measured.unit);
14005
+ const swell = findSwell(curve);
14006
+ const film = docOutputDuration3(doc);
14007
+ const plan = planScore({
14008
+ trackDuration: measured.duration,
14009
+ film,
14010
+ curve,
14011
+ swell,
14012
+ at: at2 ? at2.at : null
14013
+ });
14014
+ const clip = {
14015
+ id: SCORE_ID,
14016
+ key: track.key,
14017
+ name: track.name,
14018
+ start: 0,
14019
+ in: plan.in,
14020
+ out: plan.out,
14021
+ duration: r3(measured.duration),
14022
+ gain,
14023
+ // A track entered mid-song opens on a fade, never a cut into the bar;
14024
+ // one that starts at its own top needs none unless asked.
14025
+ fadeIn: Math.min(
14026
+ fadeInFlag !== void 0 || plan.in > 0 ? fadeInAsked : 0,
14027
+ Math.max(0, plan.out - plan.in)
14028
+ ),
14029
+ fadeOut: Math.min(fadeOut, Math.max(0, plan.out - plan.in)),
14030
+ ...doc.source.micKey ? { duck: true } : {}
14031
+ };
14032
+ const levels = curve.map((p) => p.level);
14033
+ const envelope = {
14034
+ unit: measured.unit,
14035
+ weighted: measured.unit === "LUFS",
14036
+ decoder: measured.via,
14037
+ sampleRate: measured.sampleRate,
14038
+ channels: measured.channels,
14039
+ min: levels.length ? Math.min(...levels) : null,
14040
+ median: levels.length ? median2(levels) : null,
14041
+ max: levels.length ? Math.max(...levels) : null,
14042
+ points: curve
14043
+ };
14044
+ const others = doc.audio.filter((a) => a.id !== SCORE_ID && a.start < 0.05);
14045
+ if (others.length)
14046
+ plan.notes.push(
14047
+ `${others.map((a) => a.id).join(", ")} also play${others.length === 1 ? "s" : ""} from the top; two tracks under one film rarely mean to`
14048
+ );
14049
+ if (!dryRun) {
14050
+ doc.audio = [...doc.audio.filter((a) => a.id !== SCORE_ID), clip];
14051
+ await writeJson(take.paths.doc, doc, true);
14052
+ }
14053
+ const label = measured.unit === "LUFS" ? "LUFS, K-weighted" : "dBFS RMS";
14054
+ const swellLine = swell ? `swell: +${swell.rise.toFixed(1)} dB from ${swell.from.toFixed(1)}s, arriving ${swell.at.toFixed(1)}s into the track (peak ${swell.peakLevel.toFixed(1)} ${measured.unit} at ${swell.peak.toFixed(1)}s)` : "swell: none (no sustained rise of 3 dB or more)";
14055
+ const placeLine = at2 && plan.swellAt !== null ? `the track starts at ${plan.in.toFixed(2)}s, so the swell lands at ${plan.swellAt.toFixed(2)}s of film (asked: ${at2.via})` : `the track starts at ${plan.in.toFixed(2)}s${at2 ? ` (asked: ${at2.via})` : ", its first rise above its median level"}`;
14056
+ r.done(
14057
+ {
14058
+ take: resolve16(dir),
14059
+ dryRun,
14060
+ track: { key: track.key, name: track.name, duration: clip.duration },
14061
+ film: r3(film),
14062
+ swell,
14063
+ at: at2?.at ?? null,
14064
+ clip,
14065
+ notes: plan.notes,
14066
+ envelope
14067
+ },
14068
+ `${dryRun ? "Would score" : "Scored"} ${dir} with ${track.name} (${measured.duration.toFixed(1)}s, ${label})
14069
+ ${sparkline(curve)}
14070
+ ${swellLine}
14071
+ ${placeLine}; it plays ${plan.in.toFixed(2)}..${plan.out.toFixed(2)} under the ${film.toFixed(1)}s film at gain ${gain}${plan.notes.length ? `
14072
+ note: ${plan.notes.join("\n note: ")}` : ""}${dryRun ? "\n (dry run: doc.json untouched)" : `
14073
+ audio clip "${SCORE_ID}" ${doc.audio.length > 1 ? "written beside the others" : "written"} in doc.json`}`
14074
+ );
14075
+ return EXIT_OK;
14076
+ }
14077
+
14078
+ // src/plugin/look.ts
14079
+ import { mkdir as mkdir12 } from "fs/promises";
14080
+ import { existsSync as existsSync24 } from "fs";
14081
+ import { join as join26, resolve as resolve17 } from "path";
14082
+ import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
14083
+ function quote(value) {
14084
+ return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
14085
+ }
14086
+ var NAME_MAX = 80;
14087
+ function scopeSelector(s, kind) {
14088
+ if (s.testId) return `[${s.testId.attr}=${quote(s.testId.value)}]`;
14089
+ let base;
14090
+ if (s.role) base = `[role=${quote(s.role)}]`;
14091
+ else if (kind === "dialog" && s.tag === "dialog") base = "dialog[open]";
14092
+ else if (kind === "dialog") base = '[aria-modal="true"]';
14093
+ else base = s.tag;
14094
+ return s.label ? `${base}[aria-label=${quote(s.label)}]` : base;
14095
+ }
14096
+ function stableId(id) {
14097
+ if (!id || !/^[A-Za-z][\w-]*$/.test(id)) return false;
14098
+ if (/^(radix|headlessui|mui|react|ember|ext|rc|el|v)-/i.test(id)) return false;
14099
+ return !/\d{2,}/.test(id);
14100
+ }
14101
+ function selectorCandidates(c) {
14102
+ const out = [];
14103
+ const add = (selector, basis) => {
14104
+ if (!out.some((x) => x.selector === selector)) out.push({ selector, basis });
14105
+ };
14106
+ const dialog = c.dialog ? scopeSelector(c.dialog, "dialog") : null;
14107
+ const landmark = c.landmark ? scopeSelector(c.landmark, "landmark") : null;
14108
+ const css = (sel) => dialog ? `${dialog} ${sel}` : sel;
14109
+ const chain = (sel) => dialog ? `${dialog} >> ${sel}` : sel;
14110
+ if (c.testId) {
14111
+ const sel = `[${c.testId.attr}=${quote(c.testId.value)}]`;
14112
+ add(sel, "testid");
14113
+ if (dialog) add(`${dialog} ${sel}`, "testid");
14114
+ }
14115
+ if (c.ariaLabel && c.ariaLabel.length <= NAME_MAX)
14116
+ add(css(`[aria-label=${quote(c.ariaLabel)}]`), "aria-label");
14117
+ if (c.tag === "a" && c.hrefAttr && c.hrefAttr.length <= 200 && !/^\s*javascript:/i.test(c.hrefAttr) && c.hrefAttr.trim() !== "#")
14118
+ add(css(`a[href=${quote(c.hrefAttr)}]`), "href");
14119
+ if (c.role && c.name && c.name.length <= NAME_MAX) {
14120
+ const sel = `role=${c.role}[name=${quote(c.name)}]`;
14121
+ add(chain(sel), "role");
14122
+ if (!dialog && landmark) add(`${landmark} >> ${sel}`, "role");
14123
+ }
14124
+ const form = /^(input|textarea|select)$/.test(c.tag);
14125
+ if (form && c.nameAttr)
14126
+ add(css(`${c.tag}[name=${quote(c.nameAttr)}]`), "form");
14127
+ if (form && c.placeholder && c.placeholder.length <= NAME_MAX)
14128
+ add(css(`${c.tag}[placeholder=${quote(c.placeholder)}]`), "form");
14129
+ if (stableId(c.id)) add(`#${c.id}`, "id");
14130
+ if (!form && c.text && c.text.length <= 60) {
14131
+ const sel = `text=${quote(c.text)}`;
14132
+ if (dialog) add(`${dialog} >> ${sel}`, "text");
14133
+ else if (landmark) add(`${landmark} >> ${sel}`, "text");
14134
+ else add(sel, "text");
14135
+ }
14136
+ return out;
14137
+ }
14138
+ function judgeMatch(index, reached) {
14139
+ if (!reached.includes(String(index))) return "miss";
14140
+ return reached.length === 1 ? "unique" : "ambiguous";
14141
+ }
14142
+ function chooseProposal(index, candidates, reached) {
14143
+ let fallback = null;
14144
+ for (let i = 0; i < candidates.length; i++) {
14145
+ const verdict = judgeMatch(index, reached[i] ?? []);
14146
+ if (verdict === "unique")
14147
+ return {
14148
+ selector: candidates[i].selector,
14149
+ basis: candidates[i].basis,
14150
+ match: "unique",
14151
+ matches: 1
14152
+ };
14153
+ if (verdict === "ambiguous" && !fallback)
14154
+ fallback = { c: candidates[i], n: reached[i].length };
14155
+ }
14156
+ if (fallback)
14157
+ return {
14158
+ selector: fallback.c.selector,
14159
+ basis: fallback.c.basis,
14160
+ match: "ambiguous",
14161
+ matches: fallback.n
14162
+ };
14163
+ return { selector: null, basis: null, match: "none", matches: 0 };
14164
+ }
14165
+ var SPEND_STEMS = [
14166
+ "delet",
14167
+ "remov",
14168
+ "revok",
14169
+ "eras",
14170
+ "discard",
14171
+ "publish",
14172
+ "deploy",
14173
+ "purchas",
14174
+ "subscri",
14175
+ "unsubscri",
14176
+ "pay",
14177
+ "buy",
14178
+ "generat",
14179
+ "submit",
14180
+ "send",
14181
+ "transfer",
14182
+ "upgrad",
14183
+ "confirm",
14184
+ "leav",
14185
+ "destroy"
14186
+ ];
14187
+ var SPEND_PHRASES = [
14188
+ "move to trash",
14189
+ "empty trash",
14190
+ "cancel subscription",
14191
+ "cancel plan",
14192
+ "close account"
14193
+ ];
14194
+ var SPEND = new RegExp(
14195
+ `\\b(?:${SPEND_PHRASES.map((p) => p.replace(/ /g, "\\s+")).join("|")}|(?:${SPEND_STEMS.join("|")})[a-z]*)`,
14196
+ "i"
14197
+ );
14198
+ function spendWord(words2) {
14199
+ for (const w of words2) {
14200
+ const m = w ? SPEND.exec(w.replace(/\s+/g, " ")) : null;
14201
+ if (m) return m[0].toLowerCase();
14202
+ }
14203
+ return null;
14204
+ }
14205
+ function refusalLine(selector, label, word) {
14206
+ return `refused: --click ${selector} reaches "${label}": "${word}" spends, sends or destroys, and vos look never presses one. Look at the page before it; a flow that needs the press has it as a step in actions.json, for vos record`;
14207
+ }
14208
+ var ENGINES = /^(css|xpath|text|role|id|nth|visible|data-testid|data-test-id|data-test|internal:[a-z-]+)$/;
14209
+ function splitTypeFlag(value) {
14210
+ let depth = 0;
14211
+ let inQuote = null;
14212
+ let segment = 0;
14213
+ for (let i = 0; i < value.length; i++) {
14214
+ const ch = value[i];
14215
+ if (inQuote) {
14216
+ if (ch === "\\") i++;
14217
+ else if (ch === inQuote) inQuote = null;
14218
+ continue;
14219
+ }
14220
+ if (ch === '"' || ch === "'") inQuote = ch;
14221
+ else if (ch === "[" || ch === "(") depth++;
14222
+ else if ((ch === "]" || ch === ")") && depth > 0) depth--;
14223
+ else if (ch === ">" && value[i + 1] === ">") {
14224
+ segment = i + 2;
14225
+ i++;
14226
+ } else if (ch === "=" && depth === 0) {
14227
+ if (ENGINES.test(value.slice(segment, i).trim())) continue;
14228
+ const selector = value.slice(0, i).trim();
14229
+ if (!selector) break;
14230
+ return { selector, text: value.slice(i + 1) };
14231
+ }
14232
+ }
14233
+ throw new UsageError(
14234
+ `--type expects <selector>=<text>, got "${value}" (the text follows the first = outside the selector's brackets and quotes)`
14235
+ );
14236
+ }
14237
+ function lookSteps(argv) {
14238
+ const steps = [];
14239
+ for (let i = 0; i < argv.length; i++) {
14240
+ const arg = argv[i];
14241
+ if (arg === "--") break;
14242
+ const m = /^--(click|type)(?:=(.*))?$/s.exec(arg);
14243
+ if (!m) continue;
14244
+ let value = m[2];
14245
+ if (value === void 0) {
14246
+ value = argv[i + 1];
14247
+ if (value === void 0 || value.startsWith("--"))
14248
+ throw new UsageError(`--${m[1]} expects a value`);
14249
+ i++;
14250
+ }
14251
+ if (m[1] === "click") {
14252
+ if (!value.trim()) throw new UsageError("--click expects a selector");
14253
+ steps.push({ do: "click", selector: value });
14254
+ } else steps.push({ do: "type", ...splitTypeFlag(value) });
14255
+ }
14256
+ return steps;
14257
+ }
14258
+ function parseViewport2(value) {
14259
+ if (!value) return { width: 1280, height: 720 };
14260
+ const m = /^(\d+)x(\d+)$/i.exec(value);
14261
+ if (!m) throw new UsageError("--viewport expects WxH, like 1280x720");
14262
+ const width = Number(m[1]);
14263
+ const height = Number(m[2]);
14264
+ if (width < 200 || height < 200 || width > 3840 || height > 2160)
14265
+ throw new UsageError("--viewport wants a size from 200x200 to 3840x2160");
14266
+ return { width, height };
14267
+ }
14268
+ function displayName(c) {
14269
+ const words2 = c.name || c.text || c.placeholder || "";
14270
+ return words2.length > 60 ? `${words2.slice(0, 59).trimEnd()}\u2026` : words2;
14271
+ }
14272
+ function controlLine(c) {
14273
+ const what = c.role && c.role !== c.tag ? `${c.tag} (${c.role})` : c.tag;
14274
+ const name = c.redacted?.length ? ` [${c.redacted.join(", ")} withheld]` : c.name ? ` ${JSON.stringify(c.name)}` : "";
14275
+ const flags = [
14276
+ c.inDialog ? "in dialog" : "",
14277
+ c.disabled ? "disabled" : "",
14278
+ c.covered ? "covered" : ""
14279
+ ].filter(Boolean);
14280
+ const proposal = c.selector ? `${c.selector} ${c.match === "unique" ? "UNIQUE" : `AMBIGUOUS (${c.matches})`}` : "no stable selector (use its rect)";
14281
+ const r = c.rect;
14282
+ return ` ${what}${name}${flags.length ? ` [${flags.join(", ")}]` : ""}
14283
+ ${proposal} @ ${r.x},${r.y} ${r.w}\xD7${r.h}`;
14284
+ }
14285
+ var CONTROL_LIMIT = 150;
14286
+ var LOOK_PROBE = `(() => {
14287
+ const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
14288
+ const vw = innerWidth, vh = innerHeight
14289
+ const CONTROLS = 'button, a, [role=button], [role=tab], [role=menuitem], [role=menuitemcheckbox], [role=menuitemradio], [role=checkbox], [role=switch], [role=option], [role=radio], [role=link], input, textarea, select, [contenteditable], [data-testid], [data-test-id], [data-test], [data-cy]'
14290
+ const TEST_IDS = ['data-testid', 'data-test-id', 'data-test', 'data-cy']
14291
+ const DIALOGS = 'dialog[open], [role=dialog], [role=alertdialog], [aria-modal=true]'
14292
+ const LANDMARKS = 'nav, header, main, aside, footer, [role=navigation], [role=banner], [role=main], [role=complementary], [role=contentinfo], [role=search]'
14293
+ const squash = (s) => (s || '').replace(/\\s+/g, ' ').trim()
14294
+ const textOf = (el) => squash(typeof el.innerText === 'string' ? el.innerText : el.textContent)
14295
+ const shown = (el) => {
14296
+ const r = el.getBoundingClientRect()
14297
+ if (r.width < 2 || r.height < 2) return null
14298
+ if (el.checkVisibility && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return null
14299
+ const s = getComputedStyle(el)
14300
+ if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
14301
+ return r
14302
+ }
14303
+ const testIdOf = (el) => {
14304
+ for (const attr of TEST_IDS) { const value = el.getAttribute(attr); if (value) return { attr, value } }
14305
+ return null
14306
+ }
14307
+ const implicitRole = (el) => {
14308
+ const t = el.tagName
14309
+ if (t === 'BUTTON') return 'button'
14310
+ if (t === 'A') return el.hasAttribute('href') ? 'link' : ''
14311
+ if (t === 'TEXTAREA') return 'textbox'
14312
+ if (t === 'SELECT') return el.multiple || el.size > 1 ? 'listbox' : 'combobox'
14313
+ if (t === 'INPUT') {
14314
+ const type = (el.getAttribute('type') || 'text').toLowerCase()
14315
+ if (type === 'checkbox' || type === 'radio') return type
14316
+ if (type === 'range') return 'slider'
14317
+ if (type === 'number') return 'spinbutton'
14318
+ if (/^(button|submit|reset|image)$/.test(type)) return 'button'
14319
+ if (/^(text|email|tel|url|search)$/.test(type)) return el.hasAttribute('list') ? 'combobox' : type === 'search' ? 'searchbox' : 'textbox'
14320
+ return ''
14321
+ }
14322
+ if (el.isContentEditable && el.hasAttribute('contenteditable')) return 'textbox'
14323
+ return ''
14324
+ }
14325
+ const roleOf = (el) => (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || implicitRole(el)
14326
+ const nameOf = (el) => {
14327
+ const aria = squash(el.getAttribute('aria-label'))
14328
+ if (aria) return aria
14329
+ const by = el.getAttribute('aria-labelledby')
14330
+ if (by) {
14331
+ const t = squash(by.split(/\\s+/).map((id) => document.getElementById(id)).filter(Boolean).map(textOf).join(' '))
14332
+ if (t) return t
14333
+ }
14334
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)) {
14335
+ const type = (el.getAttribute('type') || '').toLowerCase()
14336
+ if (/^(button|submit|reset)$/.test(type)) return squash(el.value) || (type === 'submit' ? 'Submit' : type === 'reset' ? 'Reset' : '')
14337
+ if (type === 'image') return squash(el.getAttribute('alt'))
14338
+ const label = el.labels ? squash([...el.labels].map(textOf).join(' ')) : ''
14339
+ return label || squash(el.getAttribute('title')) || squash(el.getAttribute('placeholder'))
14340
+ }
14341
+ const text = textOf(el)
14342
+ if (text) return text
14343
+ const img = el.querySelector('img[alt]')
14344
+ return squash(img && img.getAttribute('alt')) || squash(el.getAttribute('title'))
14345
+ }
14346
+ const scopeOf = (el) => el ? {
14347
+ tag: el.tagName.toLowerCase(),
14348
+ role: (el.getAttribute('role') || '').trim().split(/\\s+/)[0] || '',
14349
+ label: squash(el.getAttribute('aria-label')) || null,
14350
+ testId: testIdOf(el),
14351
+ } : null
14352
+ const hrefOf = (el) => {
14353
+ if (el.tagName !== 'A' || !el.hasAttribute('href')) return null
14354
+ try { const u = new URL(el.href, location.href); const at = u.pathname + u.search; return u.origin === location.origin ? at : u.origin + at } catch { return null }
14355
+ }
14356
+ const out = []
14357
+ let offscreen = 0, more = 0
14358
+ for (const el of document.querySelectorAll(CONTROLS)) {
14359
+ if (el.tagName === 'INPUT' && (el.getAttribute('type') || '').toLowerCase() === 'hidden') continue
14360
+ if (el.getAttribute('contenteditable') === 'false') continue
14361
+ if (el.tagName === 'A' && !el.hasAttribute('href') && !el.getAttribute('role') && !testIdOf(el)) continue
14362
+ const r = shown(el)
14363
+ if (!r) continue
14364
+ if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) { offscreen++; continue }
14365
+ if (out.length >= ${CONTROL_LIMIT}) { more++; continue }
14366
+ const dlg = el.closest(DIALOGS)
14367
+ const lm = el.closest(LANDMARKS)
14368
+ let name = nameOf(el), text = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? '' : textOf(el)
14369
+ let ariaLabel = squash(el.getAttribute('aria-label')) || null
14370
+ let placeholder = squash(el.getAttribute('placeholder')) || null
14371
+ let hrefAttr = el.tagName === 'A' ? el.getAttribute('href') : null, href = hrefOf(el)
14372
+ const kinds = [...new Set([...kindsIn(name), ...kindsIn(text), ...kindsIn(ariaLabel || ''), ...kindsIn(placeholder || ''), ...kindsIn(hrefAttr || '')])]
14373
+ if (kinds.length) { name = ''; text = ''; ariaLabel = null; placeholder = null; hrefAttr = null; href = null }
14374
+ const cx = Math.min(vw - 1, Math.max(0, r.left + r.width / 2)), cy = Math.min(vh - 1, Math.max(0, r.top + r.height / 2))
14375
+ const hit = document.elementFromPoint(cx, cy)
14376
+ const covered = !!hit && !(hit === el || el.contains(hit) || (el.labels && [...el.labels].some((l) => l.contains(hit))))
14377
+ const index = out.length
14378
+ out.push({
14379
+ index,
14380
+ tag: el.tagName.toLowerCase(),
14381
+ role: roleOf(el),
14382
+ name,
14383
+ text,
14384
+ ariaLabel,
14385
+ testId: testIdOf(el),
14386
+ id: el.id || null,
14387
+ nameAttr: /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) ? el.getAttribute('name') || null : null,
14388
+ placeholder,
14389
+ href,
14390
+ hrefAttr,
14391
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
14392
+ disabled: el.matches(':disabled') || el.getAttribute('aria-disabled') === 'true',
14393
+ covered,
14394
+ dialog: dlg && shown(dlg) ? scopeOf(dlg) : null,
14395
+ landmark: lm ? scopeOf(lm) : null,
14396
+ ...(kinds.length ? { redacted: kinds } : {}),
14397
+ })
14398
+ el.setAttribute('data-vos-look', String(index))
14399
+ }
14400
+ return { controls: out, offscreen, more }
14401
+ })()`;
14402
+ var UNSTAMP = `(() => { for (const el of document.querySelectorAll('[data-vos-look]')) el.removeAttribute('data-vos-look') })()`;
14403
+ function settleScript(quietMs, capMs) {
14404
+ return `new Promise((resolve) => {
14405
+ let last = performance.now()
14406
+ const start = last
14407
+ const mo = new MutationObserver(() => { last = performance.now() })
14408
+ mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true })
14409
+ const tick = () => {
14410
+ const now = performance.now()
14411
+ if (now - last >= ${quietMs} || now - start >= ${capMs}) {
14412
+ mo.disconnect()
14413
+ resolve(now - last >= ${quietMs})
14414
+ } else setTimeout(tick, 50)
14415
+ }
14416
+ tick()
14417
+ })`;
14418
+ }
14419
+ var QUIET_MS = 800;
14420
+ var SETTLE_CAP_MS = 6e3;
14421
+ var STEP_TIMEOUT_MS = 8e3;
14422
+ async function settle(page) {
14423
+ const started = Date.now();
14424
+ for (let tries = 0; tries < 3; tries++) {
14425
+ await page.waitForLoadState("load", { timeout: 15e3 }).catch(() => {
14426
+ });
14427
+ const left = Math.max(QUIET_MS, SETTLE_CAP_MS - (Date.now() - started));
14428
+ try {
14429
+ return await page.evaluate(settleScript(QUIET_MS, left));
14430
+ } catch {
14431
+ }
14432
+ }
14433
+ return false;
14434
+ }
14435
+ var LookRefusal = class extends Error {
14436
+ constructor(message) {
14437
+ super(message);
14438
+ this.name = "LookRefusal";
14439
+ }
14440
+ };
14441
+ var place = (u) => {
14442
+ try {
14443
+ const x = new URL(u);
14444
+ return `${x.origin}${x.pathname}`;
14445
+ } catch {
14446
+ return u;
14447
+ }
14448
+ };
14449
+ function shownUrl(u) {
14450
+ try {
14451
+ const x = new URL(u);
14452
+ if (!x.username && !x.password) return u;
14453
+ x.username = "";
14454
+ x.password = "";
14455
+ return x.href;
14456
+ } catch {
14457
+ return u;
14458
+ }
14459
+ }
14460
+ async function lookAt(page, url, opts) {
14461
+ const log = opts.log ?? (() => {
14462
+ });
14463
+ log(`goto ${shownUrl(url)}`);
14464
+ const response = await page.goto(url, { waitUntil: "load", timeout: 45e3 }).catch((e) => {
14465
+ if (e instanceof Error && /net::ERR_/.test(e.message))
14466
+ throw new Error(
14467
+ `could not open ${shownUrl(url)}: ${(e.message.split("\n")[0] ?? "").split(url).join(shownUrl(url))}`
14468
+ );
14469
+ log(" (the load event never fired; looking anyway)");
14470
+ return null;
14471
+ });
14472
+ let settled = await settle(page);
14473
+ const seen = await page.evaluate(WALL_PROBE).catch(() => null);
14474
+ const wall = wallVerdict({
14475
+ askedUrl: url,
14476
+ landedUrl: page.url(),
14477
+ ...response ? { status: response.status() } : {},
14478
+ passwordFields: seen?.passwordFields ?? 0,
14479
+ newPasswordField: seen?.newPasswordField ?? false,
14480
+ oneTimeCodeField: seen?.oneTimeCodeField ?? false
14481
+ });
14482
+ const ran = [];
14483
+ if (!wall || wall.level === "soft") {
14484
+ for (const step2 of opts.steps ?? []) {
14485
+ const all = page.locator(step2.selector);
14486
+ const target = all.first();
14487
+ try {
14488
+ await target.waitFor({ state: "visible", timeout: STEP_TIMEOUT_MS });
14489
+ } catch {
14490
+ throw new UsageError(
14491
+ `--${step2.do} ${step2.selector}: no visible element matches it after ${STEP_TIMEOUT_MS / 1e3} s. Look without the step and take a selector from the list`
14492
+ );
14493
+ }
14494
+ const count = await all.count();
14495
+ const note = count > 1 ? `the first of ${count} matches` : void 0;
14496
+ if (step2.do === "click") {
14497
+ const words2 = await target.evaluate(
14498
+ (el) => [
14499
+ el.getAttribute("aria-label"),
14500
+ el.innerText || el.textContent,
14501
+ el.value,
14502
+ el.getAttribute("title"),
14503
+ el.getAttribute("alt")
14504
+ ].map(
14505
+ (s) => typeof s === "string" ? s.replace(/\s+/g, " ").trim() : ""
14506
+ )
14507
+ ).catch(() => []);
14508
+ const word = spendWord(words2);
14509
+ if (word) {
14510
+ const label = words2.find((w) => spendWord([w])) ?? word;
14511
+ throw new LookRefusal(
14512
+ refusalLine(step2.selector, label.slice(0, 60), word)
14513
+ );
14514
+ }
14515
+ log(`click ${step2.selector}${note ? ` (${note})` : ""}`);
14516
+ await target.click({ timeout: STEP_TIMEOUT_MS });
14517
+ } else {
14518
+ log(
14519
+ `type ${step2.text.length} chars into ${step2.selector}${note ? ` (${note})` : ""}`
14520
+ );
14521
+ await target.fill(step2.text, { timeout: STEP_TIMEOUT_MS });
14522
+ }
14523
+ ran.push({
14524
+ do: step2.do,
14525
+ selector: step2.selector,
14526
+ ...note ? { note } : {}
14527
+ });
14528
+ settled = await settle(page);
14529
+ }
14530
+ }
14531
+ if (opts.waitMs && opts.waitMs > 0) await page.waitForTimeout(opts.waitMs);
14532
+ await mkdir12(opts.outDir, { recursive: true });
14533
+ const screenshot = join26(opts.outDir, "look.png");
14534
+ await page.screenshot({ path: screenshot });
14535
+ const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
14536
+ const step = ran.length ? ran.length - 1 : -1;
14537
+ const exposures = (scan?.exposures ?? []).map((e) => ({
14538
+ step,
14539
+ ...e,
14540
+ seen: 1
14541
+ }));
14542
+ const probe3 = await page.evaluate(LOOK_PROBE);
14543
+ const controls = [];
14544
+ try {
14545
+ for (const raw of probe3.controls) {
14546
+ const candidates = selectorCandidates(raw);
14547
+ const reached = [];
14548
+ for (const c of candidates) {
14549
+ const stamps = await page.locator(c.selector).evaluateAll(
14550
+ (els) => els.map((el) => el.getAttribute("data-vos-look"))
14551
+ ).catch(() => []);
14552
+ reached.push(stamps);
14553
+ if (judgeMatch(raw.index, stamps) === "unique") break;
14554
+ }
14555
+ const proposal = chooseProposal(raw.index, candidates, reached);
14556
+ controls.push({
14557
+ tag: raw.tag,
14558
+ role: raw.role,
14559
+ name: displayName(raw),
14560
+ testId: raw.testId?.value ?? null,
14561
+ href: raw.href,
14562
+ rect: raw.rect,
14563
+ disabled: raw.disabled,
14564
+ inDialog: raw.dialog !== null,
14565
+ covered: raw.covered,
14566
+ ...proposal,
14567
+ ...raw.redacted ? { redacted: raw.redacted } : {}
14568
+ });
14569
+ }
14570
+ } finally {
14571
+ await page.evaluate(UNSTAMP).catch(() => {
14572
+ });
14573
+ }
14574
+ return {
14575
+ url: shownUrl(url),
14576
+ landed: place(page.url()),
14577
+ screenshot,
14578
+ viewport: page.viewportSize() ?? { width: 0, height: 0 },
14579
+ controls,
14580
+ offscreen: probe3.offscreen,
14581
+ more: probe3.more,
14582
+ exposures,
14583
+ wall,
14584
+ steps: ran,
14585
+ settled
14586
+ };
14587
+ }
14588
+ var USAGE4 = "vos look <url> [--session <name> | --storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--out <dir>] [--json]";
14589
+ function lookReport(r) {
14590
+ const inDialog = r.controls.filter((c) => c.inDialog).length;
14591
+ const head = `Looked at ${r.landed} (${r.viewport.width}\xD7${r.viewport.height}): ${r.screenshot}${r.settled ? "" : "\n note: the page never held still for 0.8 s; looked at it as it was"}`;
14592
+ const steps = r.steps.length ? `
14593
+ after ${r.steps.map((s) => `${s.do} ${s.selector}${s.note ? ` (${s.note})` : ""}`).join(", then ")}` : "";
14594
+ const wall = r.wall ? `
14595
+ ${r.wall.level === "hard" ? "WALL" : "WARNING"}: ${r.wall.message}${r.wall.level === "hard" ? "\n The steps did not run; this is the page in front of the one asked for." : ""}` : "";
14596
+ const count = `
14597
+ ${r.controls.length} control${r.controls.length === 1 ? "" : "s"} in view${inDialog ? ` (${inDialog} in an open dialog)` : ""}${r.offscreen ? `, ${r.offscreen} more outside it (scroll, or a taller --viewport)` : ""}${r.more ? `, ${r.more} past the listing's limit` : ""}:`;
14598
+ const list = r.controls.map(controlLine).join("\n");
14599
+ const exposed = r.exposures.length ? `
14600
+ EXPOSED in the frame (${r.exposures.length}):
14601
+ ${r.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
14602
+ ${EXPOSURE_ADVICE}` : "";
14603
+ const next = `
14604
+ Take a UNIQUE selector into actions.json as it is; an AMBIGUOUS one reaches several and the recorder takes the first. Then rehearse: vos record --actions actions.json --dry-run`;
14605
+ return `${head}${steps}${wall}${count}
14606
+ ${list}${exposed}${next}`;
14607
+ }
14608
+ async function cmdLook(argv) {
14609
+ const steps = lookSteps(argv);
14610
+ const { positionals, flags, multi } = parseArgs(
14611
+ argv,
14612
+ /* @__PURE__ */ new Set(["json", "headed"]),
14613
+ /* @__PURE__ */ new Set(["click", "type", "header", "browser-arg"])
14614
+ );
14615
+ void multi.click;
14616
+ void multi.type;
14617
+ const url = positionals[0];
14618
+ if (!url) throw new UsageError(USAGE4);
14619
+ const scheme = urlSchemeProblem(url);
14620
+ if (scheme) throw new UsageError(scheme);
14621
+ const r = createReporter(flags.json === true);
14622
+ const viewport = parseViewport2(strFlag(flags, "viewport"));
14623
+ const outDir = resolve17(strFlag(flags, "out") ?? "look");
14624
+ const headed = flags.headed === true;
14625
+ const waitRaw = strFlag(flags, "wait");
14626
+ const waitMs = waitRaw === void 0 ? 0 : Number(waitRaw);
14627
+ if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 6e4)
14628
+ throw new UsageError("--wait expects milliseconds, 0 to 60000");
14629
+ const storageRaw = strFlag(flags, "storage-state");
14630
+ const storageState = storageRaw ? resolve17(storageRaw) : void 0;
14631
+ if (storageState && !existsSync24(storageState))
14632
+ throw new UsageError(`--storage-state: no such file: ${storageState}`);
14633
+ const sessionName = strFlag(flags, "session");
14634
+ if (sessionName) {
14635
+ const bad = sessionNameVerdict(sessionName);
14636
+ if (bad) throw new UsageError(bad);
14637
+ if (storageState)
14638
+ throw new UsageError(
14639
+ "--session and --storage-state are two doors to one page: a persistent profile cannot take a storage state. Pass one"
14640
+ );
14641
+ if (!existsSync24(sessionDir(sessionName)))
14642
+ throw new UsageError(
14643
+ `no session "${sessionName}". Make one: vos session open <url> --name ${sessionName} (a Chrome window opens; sign in and close it)`
14644
+ );
14645
+ }
14646
+ let headers;
14647
+ try {
14648
+ headers = parseHeaders(multi.header);
14649
+ } catch (e) {
14650
+ throw new UsageError(e instanceof Error ? e.message : String(e));
14651
+ }
14652
+ const browserArgs = (multi["browser-arg"] ?? []).filter(Boolean);
14653
+ const inContext = async (ctx) => {
14654
+ if (Object.keys(headers).length)
14655
+ await ctx.route("**/*", async (route) => {
14656
+ const req = route.request();
14657
+ const scoped = headersFor(req.url(), url, headers);
14658
+ await route.continue(
14659
+ scoped ? { headers: { ...req.headers(), ...scoped } } : void 0
14660
+ );
14661
+ });
14662
+ const page = ctx.pages()[0] ?? await ctx.newPage();
14663
+ await page.setViewportSize(viewport);
14664
+ return lookAt(page, url, { steps, waitMs, outDir, log: r.log });
14665
+ };
14666
+ let result;
14667
+ try {
14668
+ if (sessionName) {
14669
+ result = await withSession(sessionName, inContext, {
14670
+ headless: !headed,
14671
+ viewport
14672
+ });
14673
+ } else {
14674
+ const browser = await launchBrowser(browserArgs, { headless: !headed });
14675
+ try {
14676
+ const ctx = await browser.newContext({
14677
+ viewport,
14678
+ deviceScaleFactor: 1,
14679
+ ...storageState ? { storageState } : {}
14680
+ });
14681
+ result = await inContext(ctx);
14682
+ } finally {
14683
+ await browser.close();
14684
+ }
14685
+ }
14686
+ } catch (e) {
14687
+ if (e instanceof LookRefusal) {
14688
+ r.done({ ok: false, refused: true, error: e.message }, e.message);
14689
+ return EXIT_USAGE;
14690
+ }
14691
+ throw e;
14692
+ }
14693
+ r.done(
14694
+ { ok: !result.wall || result.wall.level === "soft", ...result },
14695
+ lookReport(result)
14696
+ );
14697
+ return result.wall?.level === "hard" ? EXIT_WALL : EXIT_OK;
14698
+ }
14699
+
14700
+ // src/plugin/machineCmd.ts
14701
+ import { spawnSync as spawnSync4 } from "child_process";
14702
+ import {
14703
+ cpSync,
14704
+ existsSync as existsSync26,
14705
+ mkdirSync as mkdirSync6,
14706
+ readFileSync as readFileSync11,
14707
+ rmSync as rmSync3,
14708
+ writeFileSync as writeFileSync6
14709
+ } from "fs";
14710
+ import { homedir as homedir2 } from "os";
14711
+ import { join as join28 } from "path";
14712
+ import { chromium as chromium2 } from "playwright";
14713
+
14714
+ // src/plugin/machine.ts
14715
+ import { existsSync as existsSync25, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
14716
+ import { dirname as dirname10, join as join27 } from "path";
14717
+ var AGENT_NAMES = [
14718
+ "claude",
14719
+ "cursor",
14720
+ "codex",
14721
+ "copilot"
14722
+ ];
14723
+ var PROJECT_DIRS = {
14724
+ claude: [".claude"],
14725
+ cursor: [".cursor"],
14726
+ codex: [".codex", ".agents"],
14727
+ copilot: [".github/copilot"]
14728
+ };
14729
+ var HOME_DIRS = {
14730
+ claude: [".claude"],
14731
+ cursor: [".cursor"],
14732
+ codex: [".codex", ".agents"],
14733
+ copilot: [".copilot"]
14734
+ };
14735
+ function detectAgents(root, opts = {}) {
14736
+ const exists = opts.exists ?? existsSync25;
14737
+ const out = [];
14738
+ for (const agent of AGENT_NAMES) {
14739
+ for (const rel of PROJECT_DIRS[agent]) {
14740
+ const dir = join27(root, rel);
14741
+ if (exists(dir)) out.push({ agent, dir, scope: "project" });
14742
+ }
14743
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join27(root, ".github", "copilot-instructions.md"))) {
14744
+ out.push({
14745
+ agent,
14746
+ dir: join27(root, ".github", "copilot"),
14747
+ scope: "project"
14748
+ });
14749
+ }
14750
+ }
14751
+ if (opts.global && opts.home) {
14752
+ for (const agent of AGENT_NAMES) {
14753
+ for (const rel of HOME_DIRS[agent]) {
14754
+ const dir = join27(opts.home, rel);
14755
+ if (exists(dir)) out.push({ agent, dir, scope: "global" });
14756
+ }
14757
+ }
14758
+ }
14759
+ return out;
14760
+ }
14761
+ function agentsFromFlag(value, root) {
14762
+ const wanted = value.trim() === "all" ? [...AGENT_NAMES] : value.split(",").map((s) => s.trim()).filter(Boolean);
14763
+ const bad = wanted.filter((w) => !AGENT_NAMES.includes(w));
14764
+ if (bad.length)
14765
+ return {
14766
+ error: `--agent takes ${AGENT_NAMES.join(", ")} or all, not "${bad.join(", ")}"`
14767
+ };
14768
+ if (!wanted.length) return { error: "--agent expects a name" };
14769
+ return {
14770
+ homes: wanted.map((agent) => ({
14771
+ agent,
14772
+ dir: join27(root, PROJECT_DIRS[agent][0]),
14773
+ scope: "project"
14774
+ }))
14775
+ };
14776
+ }
14777
+ function skillsDirFor(home) {
14778
+ return join27(home.dir, "skills");
14779
+ }
14780
+ function fallbackSkillsHome(root) {
14781
+ return { agent: "codex", dir: join27(root, ".agents"), scope: "project" };
14782
+ }
14783
+ function installedSkills(dir, exists = existsSync25, list = (p) => {
14784
+ try {
14785
+ return readdirSync6(p);
14786
+ } catch {
14787
+ return [];
14788
+ }
14789
+ }) {
14790
+ if (!exists(dir)) return [];
14791
+ return list(dir).filter((name) => exists(join27(dir, name, "SKILL.md"))).sort();
14792
+ }
14793
+ var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
14794
+ var RULES_END = "<!-- vos:end -->";
14795
+ var RULES_BODY = `## Product videos (vosso)
14796
+ - Skill: product-video (npx skills add vosjs/skills) records the real product from actions.json
14797
+ - Look before you cut: vos frames take --at-zooms; edit take/doc.json and re-render, never re-record
14798
+ - Hand back: vos open take serves the take into the studio at vos.so, doc.json intact
14799
+ - Local by default: vos push only when asked for a link, the shelf or a cloud render
14800
+ - Deleting: vos delete moves a vos to Trash and vos restore brings it back; if you deleted by mistake, restore it and say so. Only the person can delete forever, at vos.so/app/trash
14801
+ - Credentials: VOS_API_KEY, then ~/.config/vos/credentials, then vos login; never print one
14802
+ - Reference: https://vos.so/llms-full.txt`;
14803
+ var RULES_BLOCK = `${RULES_BEGIN}
14804
+ ${RULES_BODY}
14805
+ ${RULES_END}`;
14806
+ function applyRulesBlock(existing) {
14807
+ const current = existing ?? "";
14808
+ const begin = current.indexOf(RULES_BEGIN);
12138
14809
  const end = current.indexOf(RULES_END);
12139
14810
  let text;
12140
14811
  if (begin !== -1 && end !== -1 && end > begin) {
@@ -12150,10 +14821,10 @@ ${RULES_BLOCK}
12150
14821
  }
12151
14822
  return { text, changed: text !== current };
12152
14823
  }
12153
- function rulesFileFor(root, exists = existsSync21) {
12154
- const agents = join23(root, "AGENTS.md");
14824
+ function rulesFileFor(root, exists = existsSync25) {
14825
+ const agents = join27(root, "AGENTS.md");
12155
14826
  if (exists(agents)) return agents;
12156
- const claude = join23(root, "CLAUDE.md");
14827
+ const claude = join27(root, "CLAUDE.md");
12157
14828
  if (exists(claude)) return claude;
12158
14829
  return agents;
12159
14830
  }
@@ -12162,14 +14833,14 @@ function chromeCandidates(platform, env) {
12162
14833
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
12163
14834
  if (platform === "win32")
12164
14835
  return [
12165
- join23(
14836
+ join27(
12166
14837
  env["PROGRAMFILES"] ?? "C:\\Program Files",
12167
14838
  "Google",
12168
14839
  "Chrome",
12169
14840
  "Application",
12170
14841
  "chrome.exe"
12171
14842
  ),
12172
- join23(
14843
+ join27(
12173
14844
  env["LOCALAPPDATA"] ?? "",
12174
14845
  "Google",
12175
14846
  "Chrome",
@@ -12255,27 +14926,27 @@ function doctorReport(facts) {
12255
14926
  }
12256
14927
  function bundledSkillsVersion(dir) {
12257
14928
  try {
12258
- const v = readFileSync10(join23(dir, "VERSION"), "utf8").trim();
14929
+ const v = readFileSync10(join27(dir, "VERSION"), "utf8").trim();
12259
14930
  return /^\d+\.\d+\.\d+$/.test(v) ? v : null;
12260
14931
  } catch {
12261
14932
  return null;
12262
14933
  }
12263
14934
  }
12264
14935
  function bundledSkillsDir(from) {
12265
- let dir = dirname8(from);
14936
+ let dir = dirname10(from);
12266
14937
  for (let i = 0; i < 6; i++) {
12267
- const pkg = join23(dir, "package.json");
12268
- if (existsSync21(pkg)) {
14938
+ const pkg = join27(dir, "package.json");
14939
+ if (existsSync25(pkg)) {
12269
14940
  try {
12270
14941
  const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
12271
14942
  if (name === "@vosjs/cli") {
12272
- const skills = join23(dir, "skills");
12273
- return existsSync21(skills) && statSync7(skills).isDirectory() ? skills : null;
14943
+ const skills = join27(dir, "skills");
14944
+ return existsSync25(skills) && statSync8(skills).isDirectory() ? skills : null;
12274
14945
  }
12275
14946
  } catch {
12276
14947
  }
12277
14948
  }
12278
- const up = dirname8(dir);
14949
+ const up = dirname10(dir);
12279
14950
  if (up === dir) break;
12280
14951
  dir = up;
12281
14952
  }
@@ -12283,7 +14954,7 @@ function bundledSkillsDir(from) {
12283
14954
  }
12284
14955
 
12285
14956
  // src/plugin/machineCmd.ts
12286
- var BOOLEAN_FLAGS11 = /* @__PURE__ */ new Set([
14957
+ var BOOLEAN_FLAGS14 = /* @__PURE__ */ new Set([
12287
14958
  "json",
12288
14959
  "help",
12289
14960
  "global",
@@ -12306,7 +14977,7 @@ function npx() {
12306
14977
  function bundledChromium() {
12307
14978
  try {
12308
14979
  const p = chromium2.executablePath();
12309
- return p && existsSync22(p) ? p : null;
14980
+ return p && existsSync26(p) ? p : null;
12310
14981
  } catch {
12311
14982
  return null;
12312
14983
  }
@@ -12315,7 +14986,7 @@ function probe2() {
12315
14986
  return probeBrowser({
12316
14987
  platform: process.platform,
12317
14988
  env: process.env,
12318
- exists: existsSync22,
14989
+ exists: existsSync26,
12319
14990
  bundled: bundledChromium
12320
14991
  });
12321
14992
  }
@@ -12366,7 +15037,7 @@ async function doctorFacts(opts) {
12366
15037
  };
12367
15038
  }
12368
15039
  async function cmdSetup(argv) {
12369
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15040
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12370
15041
  const r = reporterFor(flags);
12371
15042
  const root = process.cwd();
12372
15043
  const global = flags.global === true;
@@ -12400,7 +15071,7 @@ async function cmdSetup(argv) {
12400
15071
  const dest = skillsDirFor(home);
12401
15072
  mkdirSync6(dest, { recursive: true });
12402
15073
  for (const name of installedSkills(dir)) {
12403
- cpSync(join24(dir, name), join24(dest, name), { recursive: true });
15074
+ cpSync(join28(dir, name), join28(dest, name), { recursive: true });
12404
15075
  }
12405
15076
  }
12406
15077
  };
@@ -12468,7 +15139,7 @@ async function cmdSetup(argv) {
12468
15139
  let rules = null;
12469
15140
  if (!noRules) {
12470
15141
  const file = rulesFileFor(root);
12471
- const existing = existsSync22(file) ? readFileSync11(file, "utf8") : null;
15142
+ const existing = existsSync26(file) ? readFileSync11(file, "utf8") : null;
12472
15143
  const { text, changed } = applyRulesBlock(existing);
12473
15144
  if (changed) writeFileSync6(file, text);
12474
15145
  rules = { file, changed };
@@ -12499,7 +15170,7 @@ Next: ${report.next_step}`
12499
15170
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
12500
15171
  }
12501
15172
  async function cmdDoctor(argv) {
12502
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15173
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12503
15174
  const r = reporterFor(flags);
12504
15175
  const root = process.cwd();
12505
15176
  const homes = detectAgents(root, { home: homedir2(), global: true });
@@ -12518,7 +15189,7 @@ Next: ${report.next_step}`
12518
15189
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
12519
15190
  }
12520
15191
  async function cmdWhoami(argv) {
12521
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15192
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12522
15193
  const r = reporterFor(flags);
12523
15194
  const origin = platformOrigin({
12524
15195
  origin: strFlag(flags, "origin"),
@@ -12580,9 +15251,9 @@ async function cmdWhoami(argv) {
12580
15251
  return EXIT_OK;
12581
15252
  }
12582
15253
  function cmdLogout(argv) {
12583
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
15254
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
12584
15255
  const r = reporterFor(flags);
12585
- const had = existsSync22(CREDENTIALS_PATH);
15256
+ const had = existsSync26(CREDENTIALS_PATH);
12586
15257
  rmSync3(CREDENTIALS_PATH, { force: true });
12587
15258
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
12588
15259
  r.done(
@@ -12598,7 +15269,7 @@ function cmdLogout(argv) {
12598
15269
  }
12599
15270
 
12600
15271
  // src/plugin/run.ts
12601
- var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
15272
+ var BOOLEAN_FLAGS15 = /* @__PURE__ */ new Set([
12602
15273
  "json",
12603
15274
  "help",
12604
15275
  "wait",
@@ -12622,19 +15293,29 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
12622
15293
  "allow-wall",
12623
15294
  "hosted",
12624
15295
  "claimable",
12625
- "share"
15296
+ "share",
15297
+ "verbose"
15298
+ ]);
15299
+ var MULTI_FLAGS2 = /* @__PURE__ */ new Set([
15300
+ "set",
15301
+ "override",
15302
+ "browser-arg",
15303
+ "header",
15304
+ "confirm"
12626
15305
  ]);
12627
- var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
12628
15306
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
12629
15307
 
12630
15308
  Take pipeline
12631
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
12632
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
15309
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
15310
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
12633
15311
  vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
12634
15312
  the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
12635
- vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
15313
+ vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions|--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
12636
15314
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
12637
- vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
15315
+ vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--strict] [--json]
15316
+ --at-zooms stills every zoom span's apex on every media of the take and renders it again with the zooms off: a zoom whose frame matches its zoom-off twin did not render, and says so (exit 2 under --strict)
15317
+ vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]
15318
+ the rendered file itself, no ffmpeg: duration, size, codecs, resolution, frame rate; full-frame scene changes (a warning when a 5 s window holds more than one); loudness (short-term every 3 s, integrated, sample peak; a warning below -23 or above -9 LUFS); the first and last frames as PNGs (a warning when either is near-uniform). Exit 2 on any warning under --strict
12638
15319
  vos deliver <take> --to youtube,x,shorts,github (or all) [--look plate|gradient|dark|none] [--brand BRAND.md] [--set path=value] [--release v2.1] [--out dir] [--range a..b] [--parallel N] [--json]
12639
15320
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
12640
15321
  vos brand <url> [--out BRAND.md] [--json]
@@ -12644,6 +15325,10 @@ Take pipeline
12644
15325
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
12645
15326
  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]
12646
15327
  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
15328
+ vos join <take> <take|file.mp4>... --out <dir> [--force] [--captions] [--mute <file>]... [--json]
15329
+ several takes (and plain video files) as ONE take, cut in argument order: the first is the primary (its frame, cursor, export and sound), each next one a media m1, m2, \u2026 with its footage copied, its spans tagged and renamed, its layers moved to where it now starts; a file opens full-bleed and its own sound plays as an audio clip (--mute <file> leaves it out). --captions lands every step's caption anchored to its step, each ending with its clip. The result must pass vos validate before anything is written
15330
+ vos score <take> --track <slug|file> [--at <step id|output seconds>] [--gain 0..1] [--fade-in <s>] [--fade-out <s>] [--dry-run] [--json]
15331
+ the film's music as one audio clip (id score): reads the track's loudness, finds its strongest swell and offsets the track so the swell lands at --at (a step, resolved through the cut, or output seconds); without --at it starts where the track first rises above its median level
12647
15332
  vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
12648
15333
  a finished piece (a HyperFrames composition, a Remotion project bundled with its own bundler, 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
12649
15334
  vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
@@ -12652,6 +15337,8 @@ Take pipeline
12652
15337
  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
12653
15338
  vos compare <program dir|config.json> --against <their render.mp4> [--times a,b,50%] [--every 1] [--threshold 0.95] [--out compare] [--json]
12654
15339
  per-frame SSIM and source | vos | difference sheets; exit 1 when ANY frame is under the threshold (needs ffmpeg)
15340
+ vos look <url> [--session <name>|--storage-state <file>] [--viewport WxH] [--headed] [--click <selector>]... [--type <selector>=<text>]... [--wait <ms>] [--header name=value]... [--browser-arg=<switch>]... [--out look] [--json]
15341
+ see a page before scripting it, signed in the way vos record would be: <out>/look.png and every control in view with ONE proposed selector (test id, aria-label, role + name, then words in its dialog or landmark), UNIQUE or AMBIGUOUS as the recorder's own engine counts it, plus what the frame exposes. --click/--type run first, in order; a click on a spend or delete word is refused. Exit 4 at a sign-in wall
12655
15342
  vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
12656
15343
  vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
12657
15344
  vos session list | rm <name> what exists and how old; delete one
@@ -12800,11 +15487,25 @@ reuse block NAMES whatever could not follow \u2014 fix by exception, never
12800
15487
  re-cut from scratch. Give steps ids in actions.json so re-timing survives
12801
15488
  script edits.
12802
15489
 
12803
- record --strict exits 2 when any selector was skipped, the page never
12804
- reached networkidle, or the take hit --max-duration (the done event lists
12805
- skipped[] and capped either way). --max-duration defaults to the hosted
12806
- recording cap, read live from GET /api/limits (30 min when the origin is
12807
- unreachable): the capture stops there and says so in one line.
15490
+ record --strict exits 2 when any selector was skipped, an expect never
15491
+ held, a click landed on a disabled control, an await timed out, or the
15492
+ take hit --max-duration (the done event lists skipped[], disabled[] and
15493
+ capped either way). A first load that never reaches networkidle (a page
15494
+ holding a socket open) is a warning, navTimeout in the done event, never
15495
+ a strict failure. --max-duration defaults to
15496
+ the hosted recording cap, read live from GET /api/limits (30 min when the
15497
+ origin is unreachable): the capture stops there and says so in one line.
15498
+ Three steps beyond the gestures: expect checks the page on camera
15499
+ (selector, text or gone; enabled; timeout 5 s); await waits OFF camera
15500
+ (until {selector|text|gone}, timeout 600 s, poll 2 s, reload, onTimeout
15501
+ fail|continue), so the footage has no gap and the cut lands in
15502
+ meta.skips[] and becomes a segment boundary; download keeps a file the page
15503
+ makes in the take's outputs/ (selector: the download a click starts; src:
15504
+ the currentSrc of a video, audio or img; as: the file name), listed in
15505
+ meta.outputs[]. A click, press or type with "guard": "spend" costs
15506
+ something real: --dry-run hovers it and never presses, and record presses
15507
+ it only with --confirm <id> (repeatable), refusing before any capture
15508
+ otherwise. --hosted refuses guarded and download steps.
12808
15509
  frames captures PNG stills at OUTPUT times (seconds or % of duration);
12809
15510
  --at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
12810
15511
  the agent's eyes (--size WxH for an exact size). render
@@ -12922,7 +15623,7 @@ lint-gated, so a bad override fails like a bad doc.json):
12922
15623
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
12923
15624
  `;
12924
15625
  async function loadActions(file) {
12925
- const raw = JSON.parse(await readFile13(file, "utf8"));
15626
+ const raw = JSON.parse(await readFile15(file, "utf8"));
12926
15627
  const errors = validateActions(raw);
12927
15628
  if (errors.length)
12928
15629
  throw new UsageError(`invalid actions file:
@@ -12951,7 +15652,10 @@ async function maxDuration(flags, r) {
12951
15652
  }
12952
15653
  function strictReason(rec) {
12953
15654
  const parts = [`${rec.skipped.length} skipped step(s)`];
12954
- if (rec.navTimeout) parts.push("networkidle timeout");
15655
+ if (rec.disabled?.length)
15656
+ parts.push(`${rec.disabled.length} click(s) on a disabled control`);
15657
+ const timedOut = timedOutAwaits(rec);
15658
+ if (timedOut) parts.push(`${timedOut} await(s) that timed out`);
12955
15659
  if (rec.capped) parts.push("stopped at --max-duration");
12956
15660
  const missed = maskMisses(rec);
12957
15661
  if (missed.length)
@@ -12960,6 +15664,15 @@ function strictReason(rec) {
12960
15664
  );
12961
15665
  return parts.join(" + ");
12962
15666
  }
15667
+ function timedOutAwaits(rec) {
15668
+ return (rec.meta?.steps ?? []).filter((s) => s.do === "await" && s.timedOut).length;
15669
+ }
15670
+ function strictFails(rec) {
15671
+ return rec.skipped.length > 0 || rec.disabled.length > 0 || timedOutAwaits(rec) > 0 || rec.capped || maskMisses(rec).length > 0;
15672
+ }
15673
+ function navTimeoutNote(rec) {
15674
+ return rec.navTimeout ? "\n note: the first load never reached networkidle (a page holding a socket open never does); recorded anyway" : "";
15675
+ }
12963
15676
  function maskMisses(rec) {
12964
15677
  return (rec.masks ?? []).filter((m) => m.hits === 0).map((m) => m.selector);
12965
15678
  }
@@ -12970,6 +15683,53 @@ function exposureNote(rec) {
12970
15683
  ${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
12971
15684
  ${EXPOSURE_ADVICE}`;
12972
15685
  }
15686
+ function takeConfirm(multi, actions, opts) {
15687
+ const given = (multi.confirm ?? []).filter(Boolean);
15688
+ if (opts.dryRun) return given;
15689
+ const verdict = guardVerdict(actions, given);
15690
+ if (!verdict.ok) {
15691
+ const guarded = guardedSteps(actions);
15692
+ for (const id of verdict.unconfirmed)
15693
+ opts.r.event({
15694
+ event: "refused",
15695
+ reason: verdict.message,
15696
+ step: guarded.find((g) => g.id === id)?.step ?? null,
15697
+ id
15698
+ });
15699
+ for (const id of verdict.unknown)
15700
+ opts.r.event({
15701
+ event: "refused",
15702
+ reason: verdict.message,
15703
+ step: null,
15704
+ id
15705
+ });
15706
+ throw new UsageError(verdict.message);
15707
+ }
15708
+ return given;
15709
+ }
15710
+ function stepReportNote(rec) {
15711
+ const lines = [];
15712
+ if (rec.disabled.length)
15713
+ lines.push(
15714
+ `DISABLED when clicked: ${rec.disabled.map((d) => `#${d.step}${d.id ? ` ${d.id}` : ""} ${d.selector}`).join(", ")} (the press likely did nothing; an expect with enabled: true before it waits for the control)`
15715
+ );
15716
+ if (rec.skips.length)
15717
+ lines.push(
15718
+ `off camera: ${rec.skips.map((s) => `#${s.step}${s.id ? ` ${s.id}` : ""} ${(s.ms / 1e3).toFixed(1)} s, cut at ${s.at.toFixed(2)}s${s.timedOut ? " (timed out)" : ""}`).join(", ")}`
15719
+ );
15720
+ if (rec.outputs.length)
15721
+ lines.push(
15722
+ `outputs/: ${rec.outputs.map((o) => `${o.name} (${o.bytes} bytes)`).join(", ")}`
15723
+ );
15724
+ if (rec.confirmed.length)
15725
+ lines.push(`pressed, as --confirm said: ${rec.confirmed.join(", ")}`);
15726
+ if (rec.stopped) lines.push(`STOPPED: ${rec.stopped.reason}`);
15727
+ return lines.map((l) => `
15728
+ ${l}`).join("");
15729
+ }
15730
+ function skippedLine(s) {
15731
+ return `#${s.step}${s.id ? ` ${s.id}` : ""} ${s.do}${s.reason ? `: ${s.reason}` : ` ${s.selector}`}`;
15732
+ }
12973
15733
  function takeHeaders(multi) {
12974
15734
  try {
12975
15735
  return parseHeaders(multi.header);
@@ -12984,8 +15744,8 @@ function takeBrowserArgs(multi) {
12984
15744
  function takeStorageState(flags) {
12985
15745
  const raw = strFlag(flags, "storage-state");
12986
15746
  if (!raw) return void 0;
12987
- const path = resolve14(raw);
12988
- if (!existsSync23(path))
15747
+ const path = resolve18(raw);
15748
+ if (!existsSync27(path))
12989
15749
  throw new UsageError(`--storage-state: no such file: ${path}`);
12990
15750
  return path;
12991
15751
  }
@@ -12998,7 +15758,7 @@ function takeSessionName(flags, storageState) {
12998
15758
  throw new UsageError(
12999
15759
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
13000
15760
  );
13001
- if (!existsSync23(sessionDir(name)))
15761
+ if (!existsSync27(sessionDir(name)))
13002
15762
  throw new UsageError(
13003
15763
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
13004
15764
  );
@@ -13054,7 +15814,7 @@ function wallGate(flags, r, opts) {
13054
15814
  async function cmdRecord(argv) {
13055
15815
  const { positionals, flags, multi } = parseArgs(
13056
15816
  argv,
13057
- BOOLEAN_FLAGS12,
15817
+ BOOLEAN_FLAGS15,
13058
15818
  MULTI_FLAGS2
13059
15819
  );
13060
15820
  const actionsPath = strFlag(flags, "actions") ?? positionals[0];
@@ -13067,14 +15827,18 @@ async function cmdRecord(argv) {
13067
15827
  const url = strFlag(flags, "url") ?? actions.url;
13068
15828
  if (!url)
13069
15829
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13070
- const scheme = urlSchemeProblem(url);
15830
+ const scheme = urlSchemeProblem2(url);
13071
15831
  if (scheme) throw new UsageError(scheme);
13072
15832
  announceEnvNames(actions, r);
13073
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
15833
+ const outDir = resolve18(strFlag(flags, "out") ?? "take");
13074
15834
  const storageState = takeStorageState(flags);
13075
15835
  const sessionName = takeSessionName(flags, storageState);
13076
15836
  const browserArgs = takeBrowserArgs(multi);
13077
15837
  const headers = takeHeaders(multi);
15838
+ const confirm = flags.hosted === true ? [] : takeConfirm(multi, actions, {
15839
+ dryRun: flags["dry-run"] === true,
15840
+ r
15841
+ });
13078
15842
  if (flags.hosted === true) {
13079
15843
  if (flags["dry-run"] === true)
13080
15844
  throw new UsageError(
@@ -13084,6 +15848,15 @@ async function cmdRecord(argv) {
13084
15848
  throw new UsageError(
13085
15849
  'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
13086
15850
  );
15851
+ const refusals = hostedRefusals(actions);
15852
+ if (refusals.length)
15853
+ throw new UsageError(
15854
+ `a hosted take cannot run this script: ${refusals.join("; ")}`
15855
+ );
15856
+ if (multi.confirm?.length)
15857
+ throw new UsageError(
15858
+ "--confirm presses a guarded step on this machine; a hosted take runs no guarded step, so drop --hosted to record it here"
15859
+ );
13087
15860
  const origin = platformOrigin({
13088
15861
  origin: strFlag(flags, "origin"),
13089
15862
  api: strFlag(flags, "api")
@@ -13183,6 +15956,8 @@ async function cmdRecord(argv) {
13183
15956
  headers,
13184
15957
  context: context2,
13185
15958
  dryRun: true,
15959
+ verbose: flags.verbose === true,
15960
+ onEvent: (e) => r.event(e),
13186
15961
  // A rehearsal is always strict, and it touches no directory.
13187
15962
  onArrival: wallGate(flags, r, { strict: true })
13188
15963
  })
@@ -13191,9 +15966,28 @@ async function cmdRecord(argv) {
13191
15966
  const lines = steps.map((s) => {
13192
15967
  const rect = s.rect ? ` rect ${s.rect.x},${s.rect.y} ${s.rect.w}\xD7${s.rect.h}` : "";
13193
15968
  const what = s.selector ? ` ${s.selector}` : "";
13194
- return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? " NOT FOUND" : rect}${s.navigated ? " \u2192 navigated" : ""}`;
15969
+ const out = rec.outputs.find((o) => o.step === s.step);
15970
+ const notes = [
15971
+ ...rec.disabled.some((d) => d.step === s.step) ? ["DISABLED, pressed anyway"] : [],
15972
+ ...s.guard && !s.skipped ? [`guarded (${s.guard}), hovered, not pressed`] : [],
15973
+ ...s.followsGuard !== void 0 ? [
15974
+ `not waited: it follows #${s.followsGuard} (guarded, not pressed)`
15975
+ ] : [],
15976
+ ...s.offCameraMs !== void 0 ? [
15977
+ `${s.timedOut ? "timed out after" : "held after"} ${(s.offCameraMs / 1e3).toFixed(1)} s off camera`
15978
+ ] : [],
15979
+ ...out ? [`\u2192 ${out.name} (${out.bytes} bytes, not kept in a rehearsal)`] : []
15980
+ ].map((n) => ` ${n}`);
15981
+ return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? ` ${s.reason ?? "NOT FOUND"}` : rect}${s.navigated ? " \u2192 navigated" : ""}${notes.join("")}`;
13195
15982
  });
13196
- const failed = rec.skipped.length > 0 || rec.navTimeout || maskMisses(rec).length > 0;
15983
+ for (const [i, step] of actions.steps.entries())
15984
+ if (!steps.some((s) => s.step === i))
15985
+ lines.push(
15986
+ ` \u2013 #${i}${step.id ? ` (${step.id})` : ""} ${step.do} not run`
15987
+ );
15988
+ const resolved = steps.filter((s) => !s.skipped).length;
15989
+ const failed = strictFails(rec) || rec.stopped !== null;
15990
+ const unconfirmed = guardVerdict(actions, confirm);
13197
15991
  const carried = [
13198
15992
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
13199
15993
  ...browserArgs.map((a) => `--browser-arg=${a}`),
@@ -13201,9 +15995,11 @@ async function cmdRecord(argv) {
13201
15995
  // printed.
13202
15996
  ...Object.keys(headers).map((k) => `--header ${k}=\u2026`),
13203
15997
  ...sessionName ? [`--session ${sessionName}`] : [],
13204
- ...flags["allow-wall"] === true ? ["--allow-wall"] : []
15998
+ ...flags["allow-wall"] === true ? ["--allow-wall"] : [],
15999
+ ...confirm.map((c) => `--confirm ${c}`)
13205
16000
  ].join(" ");
13206
16001
  const nextOut = strFlag(flags, "out") ?? "take";
16002
+ const guardNote = !unconfirmed.ok ? unconfirmed.unconfirmed.length ? ` (it refuses until you add ${unconfirmed.unconfirmed.map((id) => `--confirm ${id}`).join(" ")}: ${unconfirmed.unconfirmed.length === 1 ? "that step spends" : "those steps spend"} something real)` : ` (${unconfirmed.unknown.map((c) => `--confirm ${c}`).join(", ")} names no guarded step)` : "";
13207
16003
  r.done(
13208
16004
  {
13209
16005
  dryRun: true,
@@ -13211,28 +16007,33 @@ async function cmdRecord(argv) {
13211
16007
  seconds: +((Date.now() - started) / 1e3).toFixed(1),
13212
16008
  steps,
13213
16009
  skipped: rec.skipped,
16010
+ disabled: rec.disabled,
16011
+ skips: rec.skips,
16012
+ outputs: rec.outputs,
16013
+ pageErrors: rec.pageErrors,
16014
+ ...rec.stopped ? { stopped: rec.stopped } : {},
13214
16015
  navTimeout: rec.navTimeout,
13215
16016
  exposures: rec.exposures,
13216
16017
  masks: rec.masks
13217
16018
  },
13218
- `${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${steps.length - rec.skipped.length}/${steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
16019
+ `${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${resolved}/${actions.steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
13219
16020
  ${lines.join("\n")}
13220
- ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict`}${exposureNote(rec)}`
16021
+ ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${rec.stopped ? `STOPPED: ${rec.stopped.reason}. ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict${guardNote}`}${exposureNote(rec)}`
13221
16022
  );
13222
- return failed ? EXIT_USAGE : EXIT_OK;
16023
+ return rec.stopped ? EXIT_ERROR : failed ? EXIT_USAGE : EXIT_OK;
13223
16024
  } finally {
13224
16025
  await browser2.close();
13225
16026
  }
13226
16027
  }
13227
16028
  const paths = takePaths(outDir);
13228
16029
  const prepare = async () => {
13229
- if (existsSync23(join25(outDir, "meta.json"))) {
16030
+ if (existsSync27(join29(outDir, "meta.json"))) {
13230
16031
  const { prevDoc, kept } = await prepareReRecord(outDir);
13231
16032
  r.log(
13232
16033
  `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" : "")
13233
16034
  );
13234
16035
  }
13235
- await mkdir8(outDir, { recursive: true });
16036
+ await mkdir13(outDir, { recursive: true });
13236
16037
  await ensureTakeDir(outDir);
13237
16038
  };
13238
16039
  const browser = await launchBrowser(browserArgs);
@@ -13247,6 +16048,9 @@ ${lines.join("\n")}
13247
16048
  storageState,
13248
16049
  headers,
13249
16050
  context: context2,
16051
+ confirm,
16052
+ verbose: flags.verbose === true,
16053
+ onEvent: (e) => r.event(e),
13250
16054
  onArrival: wallGate(flags, r, {
13251
16055
  strict: flags.strict === true,
13252
16056
  prepare
@@ -13265,9 +16069,29 @@ ${lines.join("\n")}
13265
16069
  const plan = await planTake(outDir, { backdrop });
13266
16070
  const clicks = rec.events.filter((e) => e.type === "down").length;
13267
16071
  const strict = flags.strict === true;
13268
- const strictFail = strict && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0);
16072
+ const strictFail = strict && strictFails(rec);
13269
16073
  const skippedNote = rec.skipped.length ? `
13270
- SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map((s) => `#${s.step} ${s.do} ${s.selector}`).join(", ")}` : "";
16074
+ SKIPPED ${rec.skipped.length} step(s): ${rec.skipped.map(skippedLine).join(", ")}` : "";
16075
+ const stepNote = stepReportNote(rec);
16076
+ if (rec.stopped) {
16077
+ r.done(
16078
+ {
16079
+ take: outDir,
16080
+ ok: false,
16081
+ stopped: rec.stopped,
16082
+ durationMs: rec.meta.durationMs,
16083
+ skipped: rec.skipped,
16084
+ disabled: rec.disabled,
16085
+ confirmed: rec.confirmed,
16086
+ skips: rec.skips,
16087
+ outputs: rec.outputs,
16088
+ pageErrors: rec.pageErrors
16089
+ },
16090
+ `FAILED: ${rec.stopped.reason}.
16091
+ The footage up to it is in ${outDir}.${skippedNote}${stepNote}${navTimeoutNote(rec)}`
16092
+ );
16093
+ return EXIT_ERROR;
16094
+ }
13271
16095
  r.done(
13272
16096
  {
13273
16097
  take: outDir,
@@ -13287,11 +16111,16 @@ ${lines.join("\n")}
13287
16111
  ...rec.wall ? { wall: rec.wall } : {},
13288
16112
  exposures: rec.exposures,
13289
16113
  ...rec.masks.length ? { masks: rec.masks } : {},
16114
+ disabled: rec.disabled,
16115
+ confirmed: rec.confirmed,
16116
+ skips: rec.skips,
16117
+ outputs: rec.outputs,
16118
+ pageErrors: rec.pageErrors,
13290
16119
  ...strictFail ? { strictFailed: true } : {}
13291
16120
  },
13292
- strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
13293
- ${(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}
13294
- Next: edit ${join25(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
16121
+ strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${exposureNote(rec)}` : `Take ready: ${outDir}
16122
+ ${(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}${stepNote}${navTimeoutNote(rec)}
16123
+ Next: edit ${join29(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
13295
16124
  );
13296
16125
  return strictFail ? EXIT_USAGE : EXIT_OK;
13297
16126
  } finally {
@@ -13301,7 +16130,7 @@ ${lines.join("\n")}
13301
16130
  async function cmdCreate2(argv) {
13302
16131
  const { positionals, flags, multi } = parseArgs(
13303
16132
  argv,
13304
- BOOLEAN_FLAGS12,
16133
+ BOOLEAN_FLAGS15,
13305
16134
  MULTI_FLAGS2
13306
16135
  );
13307
16136
  const actionsPath = strFlag(flags, "actions");
@@ -13315,12 +16144,12 @@ async function cmdCreate2(argv) {
13315
16144
  const url = strFlag(flags, "url") ?? actions.url;
13316
16145
  if (!url)
13317
16146
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13318
- const scheme = urlSchemeProblem(url);
16147
+ const scheme = urlSchemeProblem2(url);
13319
16148
  if (scheme) throw new UsageError(scheme);
13320
16149
  announceEnvNames(actions, r);
13321
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
16150
+ const outDir = resolve18(strFlag(flags, "out") ?? "take");
13322
16151
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
13323
- const out = positionals[0] ?? join25(outDir, `out.${format}`);
16152
+ const out = positionals[0] ?? join29(outDir, `out.${format}`);
13324
16153
  const parallel = numFlag(flags, "parallel", 1);
13325
16154
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13326
16155
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13331,15 +16160,16 @@ async function cmdCreate2(argv) {
13331
16160
  const sessionName = takeSessionName(flags, storageState);
13332
16161
  const browserArgs = takeBrowserArgs(multi);
13333
16162
  const headers = takeHeaders(multi);
16163
+ const confirm = takeConfirm(multi, actions, { dryRun: false, r });
13334
16164
  const paths = takePaths(outDir);
13335
16165
  const prepare = async () => {
13336
- if (existsSync23(join25(outDir, "meta.json"))) {
16166
+ if (existsSync27(join29(outDir, "meta.json"))) {
13337
16167
  const { prevDoc, kept } = await prepareReRecord(outDir);
13338
16168
  r.log(
13339
16169
  `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" : "")
13340
16170
  );
13341
16171
  }
13342
- await mkdir8(outDir, { recursive: true });
16172
+ await mkdir13(outDir, { recursive: true });
13343
16173
  await ensureTakeDir(outDir);
13344
16174
  };
13345
16175
  const browser = await launchBrowser(browserArgs);
@@ -13354,6 +16184,9 @@ async function cmdCreate2(argv) {
13354
16184
  storageState,
13355
16185
  headers,
13356
16186
  context: context2,
16187
+ confirm,
16188
+ verbose: flags.verbose === true,
16189
+ onEvent: (e) => r.event(e),
13357
16190
  onArrival: wallGate(flags, r, {
13358
16191
  strict: flags.strict === true,
13359
16192
  prepare
@@ -13370,11 +16203,25 @@ async function cmdCreate2(argv) {
13370
16203
  await releaseFrames(paths, flags, r.log);
13371
16204
  r.event({ event: "phase", phase: "plan" });
13372
16205
  await planTake(outDir, { backdrop });
13373
- if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped || maskMisses(rec).length > 0)) {
16206
+ if (rec.stopped) {
16207
+ r.done(
16208
+ {
16209
+ take: outDir,
16210
+ ok: false,
16211
+ stopped: rec.stopped,
16212
+ skipped: rec.skipped,
16213
+ rendered: false
16214
+ },
16215
+ `FAILED: ${rec.stopped.reason}. NOT rendering; the footage up to it is in ${outDir}.`
16216
+ );
16217
+ return EXIT_ERROR;
16218
+ }
16219
+ if (flags.strict === true && strictFails(rec)) {
13374
16220
  r.done(
13375
16221
  {
13376
16222
  take: outDir,
13377
16223
  skipped: rec.skipped,
16224
+ disabled: rec.disabled,
13378
16225
  navTimeout: rec.navTimeout,
13379
16226
  capped: rec.capped,
13380
16227
  strictFailed: true,
@@ -13417,20 +16264,26 @@ async function cmdCreate2(argv) {
13417
16264
  dead: rec.dead,
13418
16265
  exposures: rec.exposures,
13419
16266
  ...rec.masks.length ? { masks: rec.masks } : {},
16267
+ disabled: rec.disabled,
16268
+ confirmed: rec.confirmed,
16269
+ skips: rec.skips,
16270
+ outputs: rec.outputs,
16271
+ pageErrors: rec.pageErrors,
13420
16272
  ...flags.draft === true ? { draft: true } : {}
13421
16273
  },
13422
16274
  `Created ${result.out} (${(result.bytes / 1024).toFixed(0)} KB, ${result.width}x${result.height}@${result.fps}, ${result.duration.toFixed(1)}s, ${result.zoomSpans} zoom spans, ${result.clicks} click effects${result.audio ? ", audio" : ""}${flags.draft === true ? ", DRAFT" : ""})
13423
- take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${exposureNote(rec)}`
16275
+ take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${stepReportNote(rec)}${navTimeoutNote(rec)}${exposureNote(rec)}`
13424
16276
  );
13425
16277
  return EXIT_OK;
13426
16278
  } finally {
13427
16279
  await browser.close();
13428
16280
  }
13429
16281
  }
13430
- async function cmdPlan(argv) {
16282
+ async function cmdPlan(argvIn) {
16283
+ const argv = bareCaptionsFlag(argvIn);
13431
16284
  const { positionals, flags, multi } = parseArgs(
13432
16285
  argv,
13433
- BOOLEAN_FLAGS12,
16286
+ BOOLEAN_FLAGS15,
13434
16287
  /* @__PURE__ */ new Set(["with"])
13435
16288
  );
13436
16289
  const dir = positionals[0];
@@ -13440,26 +16293,28 @@ async function cmdPlan(argv) {
13440
16293
  );
13441
16294
  const r = createReporter(flags.json === true);
13442
16295
  if (flags.fresh === true) {
13443
- await rm2(join25(dir, "doc.json"), { force: true });
16296
+ await rm5(join29(dir, "doc.json"), { force: true });
13444
16297
  r.log("note: --fresh discarded the existing doc.json");
13445
16298
  }
13446
16299
  let reuse;
13447
16300
  if (flags.reuse === true) {
13448
- const from = resolve14(strFlag(flags, "from") ?? join25(dir, PREV_DOC_NAME));
13449
- if (!existsSync23(from)) {
16301
+ const from = resolve18(strFlag(flags, "from") ?? join29(dir, PREV_DOC_NAME));
16302
+ if (!existsSync27(from)) {
13450
16303
  throw new UsageError(
13451
16304
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
13452
16305
  );
13453
16306
  }
13454
16307
  reuse = {
13455
16308
  from,
13456
- doc: JSON.parse(await readFile13(from, "utf8"))
16309
+ doc: JSON.parse(await readFile15(from, "utf8"))
13457
16310
  };
13458
16311
  }
13459
16312
  const style = await resolveStyleRef(flags);
13460
- const hasDoc = existsSync23(join25(dir, "doc.json"));
16313
+ const hasDoc = existsSync27(join29(dir, "doc.json"));
13461
16314
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
13462
16315
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
16316
+ const stepCaptions = strFlag(flags, "captions") === "steps";
16317
+ if (stepCaptions) release.launchRoles.captions = "none";
13463
16318
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
13464
16319
  const stillRaw = strFlag(flags, "still") ?? release.launchRoles.still;
13465
16320
  const still = stillRaw === void 0 ? null : Number(stillRaw);
@@ -13487,6 +16342,16 @@ async function cmdPlan(argv) {
13487
16342
  }
13488
16343
  } : {}
13489
16344
  });
16345
+ let captions = null;
16346
+ if (stepCaptions) {
16347
+ const take = await loadTake(dir);
16348
+ captions = landStepCaptions(s.doc, /* @__PURE__ */ new Map([["", take.actions]]));
16349
+ await writeJson(take.paths.doc, s.doc, true);
16350
+ }
16351
+ const captionLines = captions ? `
16352
+ ${captions.landed.length} step caption${captions.landed.length === 1 ? "" : "s"} anchored to their steps` + (captions.skipped.length ? `
16353
+ not landed: ${captions.skipped.join("; ")}` : "") + (captions.uncaptioned.length ? `
16354
+ no step in ${captions.uncaptioned.length === 1 && captions.uncaptioned[0] === "" ? "this take" : captions.uncaptioned.map((m) => m ? `media ${m}` : "the primary").join(", ")} carries a caption` : "") : "";
13490
16355
  const layoutLines = s.layout ? `
13491
16356
  layout from ${s.styleFrom}: ${[
13492
16357
  s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
@@ -13503,7 +16368,7 @@ async function cmdPlan(argv) {
13503
16368
  ${s.reuse.flagged.join("\n ")}` : "") : "";
13504
16369
  r.done(
13505
16370
  {
13506
- take: resolve14(dir),
16371
+ take: resolve18(dir),
13507
16372
  fresh: s.fresh,
13508
16373
  cursorKept: s.cursorKept,
13509
16374
  zoomAuto: s.zoomAuto,
@@ -13513,9 +16378,10 @@ async function cmdPlan(argv) {
13513
16378
  ...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
13514
16379
  ...s.layout ? { layout: s.layout } : {},
13515
16380
  ...s.motion ? { motion: s.motion } : {},
13516
- ...s.reuse ? { reuse: s.reuse } : {}
16381
+ ...s.reuse ? { reuse: s.reuse } : {},
16382
+ ...captions ? { captions } : {}
13517
16383
  },
13518
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join25(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
16384
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join29(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}${captionLines}`
13519
16385
  );
13520
16386
  return EXIT_OK;
13521
16387
  }
@@ -13628,7 +16494,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
13628
16494
  async function cmdRender(argv) {
13629
16495
  const { positionals, flags, multi } = parseArgs(
13630
16496
  argv,
13631
- BOOLEAN_FLAGS12,
16497
+ BOOLEAN_FLAGS15,
13632
16498
  MULTI_FLAGS2
13633
16499
  );
13634
16500
  const dir = positionals[0];
@@ -13638,7 +16504,7 @@ async function cmdRender(argv) {
13638
16504
  );
13639
16505
  const r = createReporter(flags.json === true);
13640
16506
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
13641
- const out = positionals[1] ?? join25(dir, `out.${format}`);
16507
+ const out = positionals[1] ?? join29(dir, `out.${format}`);
13642
16508
  const parallel = numFlag(flags, "parallel", 1);
13643
16509
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13644
16510
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13696,7 +16562,7 @@ async function cmdRender(argv) {
13696
16562
  async function cmdFrames(argv) {
13697
16563
  const { positionals, flags, multi } = parseArgs(
13698
16564
  argv,
13699
- BOOLEAN_FLAGS12,
16565
+ BOOLEAN_FLAGS15,
13700
16566
  MULTI_FLAGS2
13701
16567
  );
13702
16568
  const dir = positionals[0];
@@ -13714,12 +16580,13 @@ async function cmdFrames(argv) {
13714
16580
  if (!m) throw new UsageError("--size expects WxH (e.g. --size 1280x800)");
13715
16581
  size = { width: Number(m[1]), height: Number(m[2]) };
13716
16582
  }
13717
- const duration = docOutputDuration3(take.doc);
16583
+ const duration = docOutputDuration4(take.doc);
13718
16584
  const frameRaw = strFlag(flags, "frame");
13719
16585
  const timesRaw = strFlag(flags, "times");
13720
16586
  const atZooms = flags["at-zooms"] === true;
13721
16587
  const atMoments = flags["at-moments"] === true;
13722
16588
  const atStill = flags["at-still"] === true;
16589
+ const strict = flags.strict === true;
13723
16590
  let times;
13724
16591
  try {
13725
16592
  times = frameRaw !== void 0 ? parseTimes2(frameRaw, duration) : timesRaw !== void 0 ? parseTimes2(timesRaw, duration) : atZooms || atMoments ? [] : parseTimes2("0,25%,50%,75%,100%", duration);
@@ -13749,6 +16616,13 @@ async function cmdFrames(argv) {
13749
16616
  }
13750
16617
  });
13751
16618
  const index = await writeIndexJson(result);
16619
+ for (const z of result.skippedZooms ?? [])
16620
+ r.warn(
16621
+ `zoom ${z.id}${z.media ? ` (media ${z.media})` : ""} has no apex to still: ${z.reason}`
16622
+ );
16623
+ const unrendered = result.frames.filter((f) => f.rendered === false);
16624
+ for (const f of unrendered) r.warn(unrenderedZoomLine(f));
16625
+ const zoomTag = (f) => `${f.zoomId ? ` ${f.zoomId}` : ""}${f.media ? ` on ${f.media}` : ""}${f.rendered === false ? ", DID NOT RENDER" : ""}`;
13752
16626
  r.done(
13753
16627
  {
13754
16628
  outDir: result.outDir,
@@ -13756,12 +16630,14 @@ async function cmdFrames(argv) {
13756
16630
  width: result.width,
13757
16631
  height: result.height,
13758
16632
  duration: result.duration,
13759
- frames: result.frames
16633
+ frames: result.frames,
16634
+ ...result.skippedZooms ? { skippedZooms: result.skippedZooms } : {},
16635
+ ...atZooms ? { unrenderedZooms: unrendered.length } : {}
13760
16636
  },
13761
16637
  `Wrote ${result.frames.length} still(s) to ${result.outDir} (${result.width}x${result.height})
13762
- ${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ? " [zoom apex]" : f.kind === "moment" ? ` [${f.momentId}]` : ""} \u2192 ${f.file}`).join("\n ")}`
16638
+ ${result.frames.map((f) => `${f.time.toFixed(2)}s${f.kind === "zoom" ? ` [zoom apex${zoomTag(f)}]` : f.kind === "moment" ? ` [${f.momentId}]` : ""} \u2192 ${f.file}`).join("\n ")}`
13763
16639
  );
13764
- return EXIT_OK;
16640
+ return unrendered.length && strict ? EXIT_USAGE : EXIT_OK;
13765
16641
  } finally {
13766
16642
  await browser.close();
13767
16643
  }
@@ -13769,7 +16645,7 @@ async function cmdFrames(argv) {
13769
16645
  async function cmdDeliver(argv) {
13770
16646
  const { positionals, flags, multi } = parseArgs(
13771
16647
  argv,
13772
- BOOLEAN_FLAGS12,
16648
+ BOOLEAN_FLAGS15,
13773
16649
  MULTI_FLAGS2
13774
16650
  );
13775
16651
  const dir = positionals[0];
@@ -13869,15 +16745,15 @@ async function resolveStyleRef(flags) {
13869
16745
  }
13870
16746
  async function resolveDocRef(ref, flags, what) {
13871
16747
  const styleRef = ref;
13872
- const file = existsSync23(styleRef) ? resolve14(
16748
+ const file = existsSync27(styleRef) ? resolve18(
13873
16749
  styleRef,
13874
- existsSync23(join25(styleRef, "doc.json")) ? "doc.json" : ""
16750
+ existsSync27(join29(styleRef, "doc.json")) ? "doc.json" : ""
13875
16751
  ) : null;
13876
- if (file && existsSync23(file)) {
16752
+ if (file && existsSync27(file)) {
13877
16753
  return {
13878
16754
  from: file,
13879
16755
  doc: migrateHostedDoc4(
13880
- JSON.parse(await readFile13(file, "utf8"))
16756
+ JSON.parse(await readFile15(file, "utf8"))
13881
16757
  )
13882
16758
  };
13883
16759
  }
@@ -13919,7 +16795,7 @@ async function officialTemplate(origin, ref) {
13919
16795
  }
13920
16796
  }
13921
16797
  async function cmdDigest(argv) {
13922
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16798
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
13923
16799
  const dir = positionals[0];
13924
16800
  if (!dir)
13925
16801
  throw new UsageError(
@@ -13929,7 +16805,7 @@ async function cmdDigest(argv) {
13929
16805
  const take = await loadTake(dir);
13930
16806
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
13931
16807
  const transcriptPath = strFlag(flags, "transcript");
13932
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile13(transcriptPath, "utf8"))) : null;
16808
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
13933
16809
  const style = await resolveStyleRef(flags);
13934
16810
  const noFrames = flags["no-frames"] === true;
13935
16811
  let browser = null;
@@ -13960,7 +16836,7 @@ async function cmdDigest(argv) {
13960
16836
  kinds,
13961
16837
  frames: d.images.full,
13962
16838
  crops: d.images.crop,
13963
- sheet: d.images.sheet ? join25(result.outDir, d.images.sheet) : null,
16839
+ sheet: d.images.sheet ? join29(result.outDir, d.images.sheet) : null,
13964
16840
  scenes: kinds.scene ?? 0,
13965
16841
  sourceDuration: d.take.sourceDuration,
13966
16842
  outputDuration: d.take.outputDuration,
@@ -13983,19 +16859,19 @@ async function cmdDigest(argv) {
13983
16859
  }
13984
16860
  }
13985
16861
  async function cmdOpen(argv) {
13986
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16862
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
13987
16863
  const dir = positionals[0];
13988
16864
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
13989
16865
  const r = createReporter(flags.json === true);
13990
16866
  const take = await loadTake(dir);
13991
- if (!existsSync23(take.paths.recording)) {
16867
+ if (!existsSync27(take.paths.recording)) {
13992
16868
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
13993
16869
  }
13994
16870
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
13995
16871
  /\/+$/,
13996
16872
  ""
13997
16873
  );
13998
- const server = await startTakeServer(resolve14(dir), {});
16874
+ const server = await startTakeServer(resolve18(dir), {});
13999
16875
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
14000
16876
  r.event({ event: "open", server: server.base, url });
14001
16877
  r.log(`take served at ${server.base}`);
@@ -14018,7 +16894,7 @@ async function cmdOpen(argv) {
14018
16894
  return EXIT_OK;
14019
16895
  }
14020
16896
  async function cmdCallout(argv) {
14021
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16897
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14022
16898
  const dir = positionals[0];
14023
16899
  const shape = positionals[1];
14024
16900
  if (!dir || !shape || !isCalloutShape(shape))
@@ -14042,7 +16918,7 @@ async function cmdCallout(argv) {
14042
16918
  `no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
14043
16919
  );
14044
16920
  const codeFile = strFlag(flags, "code-file");
14045
- const code = codeFile ? await readFile13(codeFile, "utf8") : strFlag(flags, "code");
16921
+ const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
14046
16922
  const words2 = {
14047
16923
  kicker: strFlag(flags, "kicker"),
14048
16924
  title: strFlag(flags, "title"),
@@ -14117,11 +16993,11 @@ async function cmdCallout(argv) {
14117
16993
  return EXIT_OK;
14118
16994
  }
14119
16995
  async function cmdValidate(argv) {
14120
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
16996
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14121
16997
  const target = positionals[0];
14122
16998
  if (!target) throw new UsageError("vos validate <actions.json|take>");
14123
16999
  const r = createReporter(flags.json === true);
14124
- const kitTarget = target.endsWith("kit.json") ? target : existsSync23(join25(target, "kit.json")) && !isTakeDir(target) ? join25(target, "kit.json") : null;
17000
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync27(join29(target, "kit.json")) && !isTakeDir(target) ? join29(target, "kit.json") : null;
14125
17001
  if (kitTarget) {
14126
17002
  const picture = flags.picture === true;
14127
17003
  const verdict = await validateKit(kitTarget, { picture });
@@ -14153,9 +17029,9 @@ async function cmdValidate(argv) {
14153
17029
  r.done({ valid: true, target }, `${target}: valid actions file`);
14154
17030
  return EXIT_OK;
14155
17031
  }
14156
- if (!isTakeDir(target) && existsSync23(join25(target, "config.json"))) {
17032
+ if (!isTakeDir(target) && existsSync27(join29(target, "config.json"))) {
14157
17033
  const parsed = JSON.parse(
14158
- await readFile13(join25(target, "config.json"), "utf8")
17034
+ await readFile15(join29(target, "config.json"), "utf8")
14159
17035
  );
14160
17036
  const pre = preflightConfig(parsed);
14161
17037
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -14196,7 +17072,8 @@ async function cmdValidate(argv) {
14196
17072
  const take = await loadTake(target);
14197
17073
  const problems = [];
14198
17074
  const warnings = [];
14199
- if (!existsSync23(take.paths.recording)) {
17075
+ const notes = [];
17076
+ if (!existsSync27(take.paths.recording)) {
14200
17077
  problems.push("missing recording (re-run record)");
14201
17078
  } else {
14202
17079
  const mismatch = containerMismatch(
@@ -14222,6 +17099,7 @@ async function cmdValidate(argv) {
14222
17099
  const lint = lintDoc(take.doc);
14223
17100
  problems.push(...lint.problems.map((p) => `doc.json: ${p}`));
14224
17101
  warnings.push(...lint.warnings.map((w) => `doc.json: ${w}`));
17102
+ notes.push(...(lint.notes ?? []).map((n) => `doc.json: ${n}`));
14225
17103
  const probe3 = mediaProbeLints(
14226
17104
  await probeMediaUrls(collectDocMediaUrls(take.doc))
14227
17105
  );
@@ -14260,26 +17138,37 @@ async function cmdValidate(argv) {
14260
17138
  }
14261
17139
  if (problems.length) {
14262
17140
  r.done(
14263
- { valid: false, target, problems, warnings },
17141
+ {
17142
+ valid: false,
17143
+ target,
17144
+ problems,
17145
+ warnings,
17146
+ ...notes.length ? { notes } : {}
17147
+ },
14264
17148
  `${target}:
14265
17149
  ${problems.join("\n ")}${warnings.length ? `
14266
17150
  warnings:
14267
- ${warnings.join("\n ")}` : ""}`
17151
+ ${warnings.join("\n ")}` : ""}${notes.length ? `
17152
+ notes:
17153
+ ${notes.join("\n ")}` : ""}`
14268
17154
  );
14269
17155
  return EXIT_ERROR;
14270
17156
  }
17157
+ const noteLines = notes.length ? `
17158
+ notes:
17159
+ ${notes.join("\n ")}` : "";
14271
17160
  r.done(
14272
- { valid: true, target, warnings },
17161
+ { valid: true, target, warnings, ...notes.length ? { notes } : {} },
14273
17162
  `${target}: take is complete and renderable${warnings.length ? `
14274
17163
  warnings:
14275
- ${warnings.join("\n ")}` : ""}`
17164
+ ${warnings.join("\n ")}` : ""}${noteLines}`
14276
17165
  );
14277
17166
  return EXIT_OK;
14278
17167
  }
14279
17168
  async function cmdPush3(argv) {
14280
17169
  const { positionals, flags, multi } = parseArgs(
14281
17170
  argv,
14282
- BOOLEAN_FLAGS12,
17171
+ BOOLEAN_FLAGS15,
14283
17172
  MULTI_FLAGS2
14284
17173
  );
14285
17174
  const dir = positionals[0];
@@ -14289,7 +17178,7 @@ async function cmdPush3(argv) {
14289
17178
  );
14290
17179
  const refusal = takePushRefusal({
14291
17180
  vos: strFlag(flags, "vos"),
14292
- trackedVosId: readSyncState(resolve14(dir))?.vosId ?? null,
17181
+ trackedVosId: readSyncState(resolve18(dir))?.vosId ?? null,
14293
17182
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
14294
17183
  (name) => hasFlag(flags, name)
14295
17184
  )
@@ -14305,7 +17194,7 @@ async function cmdPush3(argv) {
14305
17194
  `--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
14306
17195
  );
14307
17196
  const claim = await pushTakeClaimable(
14308
- resolve14(dir),
17197
+ resolve18(dir),
14309
17198
  {
14310
17199
  api: strFlag(flags, "api"),
14311
17200
  origin: strFlag(flags, "origin"),
@@ -14327,7 +17216,7 @@ reference and the only credential.`
14327
17216
  }
14328
17217
  const overrides = multi.override;
14329
17218
  const result = await pushTake(
14330
- resolve14(dir),
17219
+ resolve18(dir),
14331
17220
  {
14332
17221
  key: strFlag(flags, "key"),
14333
17222
  api: strFlag(flags, "api"),
@@ -14377,11 +17266,11 @@ ${landedLines(landed)}` : "")
14377
17266
  return EXIT_OK;
14378
17267
  }
14379
17268
  async function cmdPull2(argv) {
14380
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12, MULTI_FLAGS2);
17269
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15, MULTI_FLAGS2);
14381
17270
  const dir = positionals[0] ?? ".";
14382
17271
  const r = createReporter(flags.json === true);
14383
17272
  const result = await pullTake(
14384
- resolve14(dir),
17273
+ resolve18(dir),
14385
17274
  {
14386
17275
  key: strFlag(flags, "key"),
14387
17276
  api: strFlag(flags, "api"),
@@ -14513,6 +17402,14 @@ async function run(argv) {
14513
17402
  return await cmdActions(rest);
14514
17403
  case "ingest":
14515
17404
  return await cmdIngest(rest);
17405
+ case "check-video":
17406
+ return await cmdCheckVideo(rest);
17407
+ case "join":
17408
+ return await cmdJoin(rest);
17409
+ case "score":
17410
+ return await cmdScore(rest);
17411
+ case "look":
17412
+ return await cmdLook(rest);
14516
17413
  default:
14517
17414
  throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
14518
17415
  }
@@ -14522,7 +17419,7 @@ async function run(argv) {
14522
17419
  `);
14523
17420
  return EXIT_USAGE;
14524
17421
  }
14525
- if (e instanceof SetupError || e instanceof SetupEnvError) {
17422
+ if (e instanceof SetupError || e instanceof SetupEnvError || e instanceof RecordRefusedError) {
14526
17423
  process.stderr.write(`${e.message}
14527
17424
  `);
14528
17425
  return EXIT_USAGE;
@@ -14545,7 +17442,7 @@ async function run(argv) {
14545
17442
  }
14546
17443
  }
14547
17444
  async function cmdSession(argv) {
14548
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
17445
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
14549
17446
  const [sub, arg] = positionals;
14550
17447
  const r = createReporter(flags.json === true);
14551
17448
  const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
@@ -14581,7 +17478,7 @@ async function cmdSession(argv) {
14581
17478
  if (!arg) throw new UsageError(usage);
14582
17479
  const bad = sessionNameVerdict(arg);
14583
17480
  if (bad) throw new UsageError(bad);
14584
- if (!existsSync23(sessionDir(arg)))
17481
+ if (!existsSync27(sessionDir(arg)))
14585
17482
  throw new UsageError(`no session "${arg}" (vos session list)`);
14586
17483
  const summary3 = await summarizeSession(arg);
14587
17484
  const url = strFlag(flags, "url");
@@ -14676,9 +17573,10 @@ export {
14676
17573
  convertAgentBrowser,
14677
17574
  MULTI_FLAGS2 as MULTI_FLAGS,
14678
17575
  HELP,
17576
+ strictFails,
14679
17577
  takeBrowserArgs,
14680
17578
  takeBackdrop,
14681
17579
  verbHelp,
14682
17580
  run
14683
17581
  };
14684
- //# sourceMappingURL=chunk-IJZ3C435.js.map
17582
+ //# sourceMappingURL=chunk-INRUYQ6J.js.map