@vosjs/cli 0.31.1 → 0.33.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.
@@ -6,9 +6,9 @@ import {
6
6
  } from "./chunk-NHKHTIDR.js";
7
7
 
8
8
  // src/plugin/run.ts
9
- import { mkdir as mkdir9, readFile as readFile14, rm as rm4 } from "fs/promises";
10
- import { existsSync as existsSync14 } from "fs";
11
- import { join as join17, resolve as resolve9 } from "path";
9
+ import { mkdir as mkdir9, readFile as readFile15, rm as rm4 } from "fs/promises";
10
+ import { existsSync as existsSync15 } from "fs";
11
+ import { join as join18, resolve as resolve9 } from "path";
12
12
  import {
13
13
  END_CARD_FROM as END_CARD_FROM2,
14
14
  docOutputDuration as docOutputDuration3,
@@ -210,9 +210,16 @@ import {
210
210
  ZOOM_LEVEL_MIN,
211
211
  ZOOM_SPAN_MIN,
212
212
  docCardLayout,
213
+ docZoomTrack,
214
+ findStep,
213
215
  outputEnd,
216
+ pinBox,
217
+ pinCandidates,
218
+ pinRectOnScreen,
219
+ pinReferent,
214
220
  ratedSegments,
215
221
  recommendedExportResolution,
222
+ resolvePins,
216
223
  spanOutputExtent,
217
224
  zoomCoversRect,
218
225
  cameraModel
@@ -944,6 +951,7 @@ function lintDoc(docIn) {
944
951
  if (!isNum(o.duration) || o.duration <= 0) {
945
952
  problems.push(`${name}.duration must be > 0 (seconds)`);
946
953
  }
954
+ checkPin(o, name, docIn, recording, problems, warnings);
947
955
  if (o.kind === "text") {
948
956
  if (typeof o.text !== "string")
949
957
  problems.push(`${name}.text must be a string`);
@@ -1455,6 +1463,106 @@ function clicksWithRects(source) {
1455
1463
  }
1456
1464
  return out;
1457
1465
  }
1466
+ var PIN_SIDES = ["auto", "right", "left", "below", "above"];
1467
+ var PIN_MARKS = ["none", "ring", "underline"];
1468
+ function checkPin(o, name, docIn, recording, problems, warnings) {
1469
+ if (o.pin === void 0) return;
1470
+ if (!isObj(o.pin)) {
1471
+ problems.push(
1472
+ `${name}.pin must be { step | press | rect, side?, gap?, mark?, leader?, color? }`
1473
+ );
1474
+ return;
1475
+ }
1476
+ const p = o.pin;
1477
+ const named = ["step", "press", "rect"].filter((k) => p[k] !== void 0);
1478
+ if (named.length !== 1) {
1479
+ problems.push(
1480
+ `${name}.pin names its referent by exactly one of step (a recorder step id or index), press (a source second) or rect (video fractions)${named.length ? ` \u2014 got ${named.join(" and ")}` : ""}`
1481
+ );
1482
+ return;
1483
+ }
1484
+ if (p.step !== void 0 && typeof p.step !== "string" && !(isNum(p.step) && Number.isInteger(p.step) && p.step >= 0)) {
1485
+ problems.push(
1486
+ `${name}.pin.step must be a step id (string) or index (integer \u2265 0)`
1487
+ );
1488
+ }
1489
+ if (p.press !== void 0 && !isNum(p.press))
1490
+ problems.push(`${name}.pin.press must be a source second (number)`);
1491
+ if (p.rect !== void 0) {
1492
+ const r = isObj(p.rect) ? p.rect : null;
1493
+ if (!r || !isNum(r.x) || !isNum(r.y) || !isNum(r.w) || !isNum(r.h)) {
1494
+ problems.push(`${name}.pin.rect must be { x, y, w, h }`);
1495
+ } else if (Math.abs(r.x) > 2 || Math.abs(r.y) > 2 || r.w > 2 || r.h > 2) {
1496
+ problems.push(
1497
+ `${name}.pin.rect looks like PIXELS (${String(r.x)}, ${String(r.y)}, ${String(r.w)}\xD7${String(r.h)}) \u2014 it is FRACTIONS of the video frame [0..1] (the zoom cx/cy convention)`
1498
+ );
1499
+ } else if (r.w <= 0 || r.h <= 0) {
1500
+ problems.push(`${name}.pin.rect must have w > 0 and h > 0`);
1501
+ }
1502
+ }
1503
+ if (p.side !== void 0 && !PIN_SIDES.includes(p.side))
1504
+ problems.push(`${name}.pin.side must be one of ${PIN_SIDES.join(" | ")}`);
1505
+ if (p.gap !== void 0 && (!isNum(p.gap) || p.gap < 0))
1506
+ problems.push(`${name}.pin.gap must be design px \u2265 0`);
1507
+ if (p.mark !== void 0 && !PIN_MARKS.includes(p.mark))
1508
+ problems.push(`${name}.pin.mark must be one of ${PIN_MARKS.join(" | ")}`);
1509
+ if (p.leader !== void 0 && typeof p.leader !== "boolean")
1510
+ problems.push(`${name}.pin.leader must be true or false`);
1511
+ if (p.color !== void 0 && typeof p.color !== "string")
1512
+ problems.push(`${name}.pin.color must be a CSS colour string`);
1513
+ if (problems.some((m) => m.startsWith(`${name}.pin`))) return;
1514
+ if (!recording) {
1515
+ problems.push(
1516
+ `${name}.pin names something on a recording's page; a program has no referents \u2014 place the layer with transform`
1517
+ );
1518
+ return;
1519
+ }
1520
+ const doc = docIn;
1521
+ const steps = doc.source.meta.steps ?? [];
1522
+ if (p.step !== void 0) {
1523
+ const step = findStep(steps, p.step);
1524
+ if (!step) {
1525
+ const known = steps.filter((s) => s.selector).map((s) => s.id !== void 0 ? s.id : String(s.step));
1526
+ problems.push(
1527
+ `${name}.pin.step "${String(p.step)}" is not a step of this take${known.length ? ` \u2014 steps with an element: ${known.join(", ")}` : " \u2014 the take has no step timeline (a human recording); pin by press or rect"}`
1528
+ );
1529
+ return;
1530
+ }
1531
+ if (step.skipped) {
1532
+ problems.push(
1533
+ `${name}.pin.step "${String(p.step)}" was skipped at record time (its selector never became visible), so it has no element to point at`
1534
+ );
1535
+ return;
1536
+ }
1537
+ }
1538
+ let ref;
1539
+ try {
1540
+ ref = pinReferent(doc, p);
1541
+ } catch {
1542
+ ref = null;
1543
+ }
1544
+ if (!ref) {
1545
+ problems.push(
1546
+ p.step !== void 0 ? `${name}.pin.step "${String(p.step)}" has no element rect and no press inside its window (a wait, scroll or move step touches nothing) \u2014 pin a hover, click, type or drag step, or a press` : p.press !== void 0 ? `${name}.pin.press ${String(p.press)}s has no press with an element rect within half a second \u2014 the presses are the cursor track's down events` : `${name}.pin.rect could not be read`
1547
+ );
1548
+ return;
1549
+ }
1550
+ if (ref.at !== null && isNum(o.start) && isNum(o.duration)) {
1551
+ const rated = ratedSegments(doc);
1552
+ for (const s of steps) {
1553
+ if (s.tStart <= ref.at) continue;
1554
+ if (s.do !== "scroll" && !s.navigated) continue;
1555
+ const out = spanOutputExtent(rated, s.tStart, s.tEnd);
1556
+ if (!out) continue;
1557
+ if (out.start < o.start + o.duration && out.end > o.start) {
1558
+ warnings.push(
1559
+ `${name} is pinned to something measured at ${ref.at.toFixed(1)}s, but ${s.do === "scroll" ? "a scroll" : "a navigation"} at ${s.tStart.toFixed(1)}s (output ${out.start.toFixed(1)}s) falls inside the layer's window \u2014 the referent may have moved; end the layer before it`
1560
+ );
1561
+ break;
1562
+ }
1563
+ }
1564
+ }
1565
+ }
1458
1566
  function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1459
1567
  try {
1460
1568
  const source = isObj(doc.source) ? doc.source : {};
@@ -1496,24 +1604,65 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1496
1604
  }
1497
1605
  }
1498
1606
  const rated = ratedSegments(docIn);
1499
- const toFrame = (nx, ny) => ({
1500
- x: (layout.dx + nx * layout.dw) / layout.W,
1501
- y: (layout.dy + ny * layout.dh) / layout.H
1502
- });
1607
+ const camera = cameraModel(docIn.frame);
1608
+ const zoomTrack = docZoomTrack(docIn);
1609
+ const pins = resolvePins(docIn, layout, camera, zoomTrack);
1503
1610
  overlays.forEach((o, i) => {
1504
- if (!isObj(o) || o.kind !== void 0 && o.kind !== "text") return;
1611
+ if (!isObj(o) || !isNum(o.start) || !isNum(o.duration)) return;
1505
1612
  const tf = isObj(o.transform) ? o.transform : null;
1506
- if (!tf || !isNum(tf.x) || !isNum(tf.y) || !isNum(o.start) || !isNum(o.duration))
1613
+ if (!tf || !isNum(tf.x) || !isNum(tf.y)) return;
1614
+ const pinned = pins.get(String(o.id));
1615
+ if (!pinned && o.pin === void 0 && (o.kind === "html" || o.kind === "image" || o.kind === "video")) {
1616
+ const cands = pinCandidates(docIn, {
1617
+ start: o.start,
1618
+ duration: o.duration
1619
+ });
1620
+ if (cands.length) {
1621
+ warnings.push(
1622
+ `overlays[${i}] is not pinned, and ${cands.length === 1 ? "a step with an element sits" : "steps with an element sit"} inside its window: ${cands.map((c) => `${String(c.step)} (${c.do}${c.selector ? ` ${c.selector}` : ""})`).join(", ")} \u2014 a callout about one of them says so: pin: { step: "${String(cands[0].step)}" }`
1623
+ );
1624
+ }
1625
+ }
1626
+ if (pinned) {
1627
+ if (pinned.clamped > 4) {
1628
+ warnings.push(
1629
+ `overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names`
1630
+ );
1631
+ }
1507
1632
  return;
1633
+ }
1634
+ let box;
1635
+ try {
1636
+ box = pinBox(o, layout);
1637
+ } catch {
1638
+ return;
1639
+ }
1508
1640
  for (const c of clicks) {
1509
1641
  const out = spanOutputExtent(rated, c.t, c.t + 1e-3);
1510
1642
  if (!out || out.start < o.start || out.start > o.start + o.duration)
1511
1643
  continue;
1512
- const a = toFrame(c.rect.x / w, c.rect.y / h);
1513
- const b = toFrame((c.rect.x + c.rect.w) / w, (c.rect.y + c.rect.h) / h);
1514
- if (tf.x >= a.x && tf.x <= b.x && tf.y >= a.y && tf.y <= b.y) {
1644
+ const target = pinRectOnScreen(
1645
+ {
1646
+ x: c.rect.x / w,
1647
+ y: c.rect.y / h,
1648
+ w: c.rect.w / w,
1649
+ h: c.rect.h / h
1650
+ },
1651
+ out.start,
1652
+ layout,
1653
+ camera,
1654
+ zoomTrack
1655
+ );
1656
+ const L = {
1657
+ x: tf.x * layout.W - box.w / 2,
1658
+ y: tf.y * layout.H - box.h / 2,
1659
+ w: box.w,
1660
+ h: box.h
1661
+ };
1662
+ const overlaps2 = L.x < target.x + target.w && L.x + L.w > target.x && L.y < target.y + target.h && L.y + L.h > target.y;
1663
+ if (overlaps2) {
1515
1664
  warnings.push(
1516
- `overlays[${i}] sits over the thing being clicked at ${c.t.toFixed(1)}s (output ${out.start.toFixed(1)}s) \u2014 move it off the target`
1665
+ `overlays[${i}] sits over the thing being clicked at ${c.t.toFixed(1)}s (output ${out.start.toFixed(1)}s) \u2014 pin it to that step (pin: { step }) so it sits beside the target and follows it, or move it off`
1517
1666
  );
1518
1667
  break;
1519
1668
  }
@@ -5157,10 +5306,208 @@ function winRate(verdicts) {
5157
5306
  return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
5158
5307
  }
5159
5308
 
5309
+ // src/plugin/callout.ts
5310
+ import { existsSync as existsSync9 } from "fs";
5311
+ import { readFile as readFile9 } from "fs/promises";
5312
+ import { join as join10 } from "path";
5313
+ import { lerpArray, sample } from "@vosjs/timeline";
5314
+ import {
5315
+ CALLOUT_DEFAULT_BODY_PX,
5316
+ CALLOUT_SHAPES,
5317
+ calloutClip,
5318
+ calloutGroundOf,
5319
+ cameraModel as cameraModel2,
5320
+ deltaE,
5321
+ docCardLayout as docCardLayout2,
5322
+ docZoomTrack as docZoomTrack2,
5323
+ hexToRgb,
5324
+ parseCssColour,
5325
+ pinBox as pinBox2,
5326
+ ratedSegments as ratedSegments5,
5327
+ resolvePins as resolvePins2,
5328
+ spanOutputExtent as spanOutputExtent5
5329
+ } from "@vosjs/studio-core";
5330
+ var CALLOUT_DEFAULT_SECONDS = 3;
5331
+ var CALLOUT_LEAD_SECONDS = 0.2;
5332
+ var CALLOUT_DELTA_E_PROBLEM = 8;
5333
+ var CALLOUT_DELTA_E_WARNING = 16;
5334
+ function registerFrom(roles, flags) {
5335
+ const ground = flags.ground ?? roles?.bgA;
5336
+ const accent = flags.accent ?? roles?.accent;
5337
+ if (!ground || !accent) return null;
5338
+ if (!hexToRgb(ground) || !hexToRgb(accent)) return null;
5339
+ const face = flags.face ?? roles?.fontBody;
5340
+ return {
5341
+ ground,
5342
+ accent,
5343
+ ...face && !/^ui-sans-serif$|^system-ui$/.test(face) ? { face } : {},
5344
+ ...flags.mono ? { mono: flags.mono } : {},
5345
+ body: flags.body ?? CALLOUT_DEFAULT_BODY_PX
5346
+ };
5347
+ }
5348
+ function appScaleAt(doc, t) {
5349
+ const layout = docCardLayout2(doc);
5350
+ const meta = doc.source.meta;
5351
+ const base = layout.dw / Math.max(1, meta.width);
5352
+ const track = docZoomTrack2(doc);
5353
+ if (!track || !track.keyframes.length) return base;
5354
+ const z = sample(track, t, lerpArray);
5355
+ return base * Math.max(1, z[0]);
5356
+ }
5357
+ function windowForStep(doc, step, seconds = CALLOUT_DEFAULT_SECONDS) {
5358
+ const steps = doc.source.meta.steps ?? [];
5359
+ const s = steps.find((x) => x.id !== void 0 && x.id === step) ?? steps.find((x) => String(x.step) === String(step));
5360
+ if (!s || s.skipped) return null;
5361
+ const rated = ratedSegments5(doc);
5362
+ const ext = spanOutputExtent5(rated, s.tStart, s.tEnd);
5363
+ if (!ext) return null;
5364
+ const at2 = s.do === "click" || s.do === "drag" ? ext.start : ext.end;
5365
+ const start = +(at2 + CALLOUT_LEAD_SECONDS).toFixed(3);
5366
+ let end = start + seconds;
5367
+ for (const n of steps) {
5368
+ if (n.tStart <= s.tStart) continue;
5369
+ if (n.do !== "scroll" && !n.navigated) continue;
5370
+ const ne = spanOutputExtent5(rated, n.tStart, n.tEnd);
5371
+ if (ne && ne.start > start && ne.start < end) end = ne.start - 0.05;
5372
+ }
5373
+ return { start, duration: +Math.max(0.5, end - start).toFixed(3) };
5374
+ }
5375
+ function composeCallout(doc, register, ask) {
5376
+ let window2 = null;
5377
+ if (ask.step !== void 0) {
5378
+ window2 = windowForStep(doc, ask.step, ask.seconds);
5379
+ if (!window2)
5380
+ throw new Error(
5381
+ `step "${String(ask.step)}" is not in the take's step timeline, was skipped, or falls outside the cut`
5382
+ );
5383
+ } else if (ask.at !== void 0) {
5384
+ window2 = {
5385
+ start: ask.at,
5386
+ duration: ask.seconds ?? CALLOUT_DEFAULT_SECONDS
5387
+ };
5388
+ } else {
5389
+ throw new Error(
5390
+ "name the step the callout is about (--step <id>), or --at <output seconds>"
5391
+ );
5392
+ }
5393
+ const pin = ask.step !== void 0 ? { step: ask.step } : ask.press !== void 0 ? { press: ask.press } : void 0;
5394
+ if (pin) {
5395
+ if (ask.side && ask.side !== "auto") pin.side = ask.side;
5396
+ if (ask.mark && ask.mark !== "none") pin.mark = ask.mark;
5397
+ if (ask.leader) pin.leader = true;
5398
+ if (ask.color) pin.color = ask.color;
5399
+ }
5400
+ const taken = new Set((doc.overlays ?? []).map((o) => o.id));
5401
+ let id = ask.id ?? `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}`;
5402
+ let n = 2;
5403
+ while (taken.has(id) && !ask.id)
5404
+ id = `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}-${n++}`;
5405
+ const clip = calloutClip(ask.shape, register, ask.words, {
5406
+ id,
5407
+ start: window2.start,
5408
+ duration: window2.duration,
5409
+ scale: appScaleAt(doc, window2.start + 0.35),
5410
+ pin,
5411
+ // The fallback place, in the margin: the lower third.
5412
+ at: { x: 0.5, y: 0.82 }
5413
+ });
5414
+ return { clip, window: window2 };
5415
+ }
5416
+ function isCalloutShape(s) {
5417
+ return CALLOUT_SHAPES.includes(s);
5418
+ }
5419
+ async function groundFindings(doc, frameAt) {
5420
+ const out = [];
5421
+ const overlays = doc.overlays ?? [];
5422
+ const layout = docCardLayout2(doc);
5423
+ const camera = cameraModel2(doc.frame);
5424
+ const track = docZoomTrack2(doc);
5425
+ const pins = resolvePins2(doc, layout, camera, track);
5426
+ for (let i = 0; i < overlays.length; i++) {
5427
+ const o = overlays[i];
5428
+ if (o.kind !== "html") continue;
5429
+ const card = calloutGroundOf(o.css ?? "");
5430
+ if (!card) {
5431
+ out.push({
5432
+ index: i,
5433
+ id: o.id,
5434
+ level: "note",
5435
+ deltaE: null,
5436
+ card: null,
5437
+ footage: null,
5438
+ message: `overlays[${i}] declares no readable background on its root rule \u2014 the ground check cannot read it`
5439
+ });
5440
+ continue;
5441
+ }
5442
+ const t = o.start + Math.min(0.35, o.duration / 2);
5443
+ const img = await frameAt(t);
5444
+ if (!img) {
5445
+ out.push({
5446
+ index: i,
5447
+ id: o.id,
5448
+ level: "note",
5449
+ deltaE: null,
5450
+ card,
5451
+ footage: null,
5452
+ message: `overlays[${i}]: the frame at ${t.toFixed(2)}s could not be read`
5453
+ });
5454
+ continue;
5455
+ }
5456
+ const box = pinBox2(o, layout);
5457
+ const pinned = pins.get(o.id);
5458
+ let cx = o.transform.x * layout.W;
5459
+ let cy = o.transform.y * layout.H;
5460
+ if (pinned) {
5461
+ const v = sample(pinned.track, t - o.start, lerpArray);
5462
+ cx = v[0] * layout.W;
5463
+ cy = v[1] * layout.H;
5464
+ }
5465
+ const sx = img.w / layout.W;
5466
+ const sy = img.h / layout.H;
5467
+ const rect = {
5468
+ x: (cx - box.w / 2) * sx,
5469
+ y: (cy - box.h / 2) * sy,
5470
+ w: box.w * sx,
5471
+ h: box.h * sy
5472
+ };
5473
+ const footage = medianColour(img, rect);
5474
+ const cardRgb = parseCssColour(card);
5475
+ if (!cardRgb) continue;
5476
+ const d = deltaE(cardRgb, footage);
5477
+ const fh = `#${footage.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
5478
+ const level = d < CALLOUT_DELTA_E_PROBLEM ? "problem" : d < CALLOUT_DELTA_E_WARNING ? "warning" : "note";
5479
+ out.push({
5480
+ index: i,
5481
+ id: o.id,
5482
+ level,
5483
+ deltaE: +d.toFixed(1),
5484
+ card,
5485
+ footage: fh,
5486
+ message: level === "note" ? `overlays[${i}] ground ${card} over footage ${fh}: \u0394E ${d.toFixed(1)}, reads as lifted` : `overlays[${i}] ground ${card} is within \u0394E ${d.toFixed(1)} of the footage it covers (${fh}) at ${t.toFixed(2)}s \u2014 it reads as one more panel of the app; invert its value (vos callout does) or move it to the margin`
5487
+ });
5488
+ }
5489
+ return out;
5490
+ }
5491
+ async function footageFrames(browser, dir, doc, times, framesTake2) {
5492
+ const out = /* @__PURE__ */ new Map();
5493
+ if (!times.length) return out;
5494
+ const bare = { ...doc, overlays: [] };
5495
+ const outDir = join10(dir, ".ground-check");
5496
+ const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
5497
+ for (const f of res.frames) {
5498
+ if (!existsSync9(f.file)) continue;
5499
+ out.set(
5500
+ +f.time.toFixed(3),
5501
+ decodePng(new Uint8Array(await readFile9(f.file)))
5502
+ );
5503
+ }
5504
+ return out;
5505
+ }
5506
+
5160
5507
  // src/plugin/kitPicture.ts
5161
5508
  import { execFile } from "child_process";
5162
5509
  import { promisify } from "util";
5163
- import { readFile as readFile9 } from "fs/promises";
5510
+ import { readFile as readFile10 } from "fs/promises";
5164
5511
  var execFileP = promisify(execFile);
5165
5512
  var SUBJECT_BAND = { min: 0.6, max: 0.92 };
5166
5513
  var BLANK_INK = 0.12;
@@ -5188,7 +5535,7 @@ function apcaContrast(text, bg) {
5188
5535
  return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
5189
5536
  }
5190
5537
  var pct = (v) => `${Math.round(v * 100)}%`;
5191
- var hexToRgb = (h) => {
5538
+ var hexToRgb2 = (h) => {
5192
5539
  const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
5193
5540
  if (!m) return null;
5194
5541
  const n = parseInt(m[1], 16);
@@ -5337,7 +5684,7 @@ function textFindings(a, img) {
5337
5684
  });
5338
5685
  }
5339
5686
  }
5340
- const rgb = t.color ? hexToRgb(t.color) : null;
5687
+ const rgb = t.color ? hexToRgb2(t.color) : null;
5341
5688
  if (rgb) {
5342
5689
  const ground = medianColour(img, box);
5343
5690
  const lc = apcaContrast(rgb, ground);
@@ -5436,7 +5783,7 @@ async function pictureChecks(assets) {
5436
5783
  let ffmpeg = null;
5437
5784
  for (const a of assets) {
5438
5785
  if (/\.png$/i.test(a.file)) {
5439
- const img = decodePng(new Uint8Array(await readFile9(a.file)));
5786
+ const img = decodePng(new Uint8Array(await readFile10(a.file)));
5440
5787
  if (!img) {
5441
5788
  findings.push({
5442
5789
  code: "unreadable",
@@ -5529,9 +5876,9 @@ function formatFinding(f) {
5529
5876
  }
5530
5877
 
5531
5878
  // src/plugin/validateKit.ts
5532
- import { existsSync as existsSync9 } from "fs";
5533
- import { readFile as readFile10, stat as stat2 } from "fs/promises";
5534
- import { dirname as dirname2, isAbsolute as isAbsolute4, join as join10 } from "path";
5879
+ import { existsSync as existsSync10 } from "fs";
5880
+ import { readFile as readFile11, stat as stat2 } from "fs/promises";
5881
+ import { dirname as dirname2, isAbsolute as isAbsolute4, join as join11 } from "path";
5535
5882
  import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5536
5883
  var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5537
5884
  function pngDimensions(bytes) {
@@ -5550,7 +5897,7 @@ async function probeVideo(path) {
5550
5897
  const MB = await import("mediabunny");
5551
5898
  const input = new MB.Input({
5552
5899
  formats: MB.ALL_FORMATS,
5553
- source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
5900
+ source: new MB.BufferSource(new Uint8Array(await readFile11(path)))
5554
5901
  });
5555
5902
  try {
5556
5903
  const track = await input.getPrimaryVideoTrack();
@@ -5571,7 +5918,7 @@ async function validateKit(kitPath, opts = {}) {
5571
5918
  const measured = [];
5572
5919
  let kit;
5573
5920
  try {
5574
- kit = JSON.parse(await readFile10(kitPath, "utf8"));
5921
+ kit = JSON.parse(await readFile11(kitPath, "utf8"));
5575
5922
  } catch (e) {
5576
5923
  return {
5577
5924
  valid: false,
@@ -5590,8 +5937,8 @@ async function validateKit(kitPath, opts = {}) {
5590
5937
  }
5591
5938
  const base = dirname2(kitPath);
5592
5939
  const resolvePath = (p) => {
5593
- if (isAbsolute4(p) && existsSync9(p)) return p;
5594
- const beside = join10(base, p.split("/").pop() ?? p);
5940
+ if (isAbsolute4(p) && existsSync10(p)) return p;
5941
+ const beside = join11(base, p.split("/").pop() ?? p);
5595
5942
  return beside;
5596
5943
  };
5597
5944
  const perDestination = /* @__PURE__ */ new Map();
@@ -5617,7 +5964,7 @@ async function validateKit(kitPath, opts = {}) {
5617
5964
  let h = null;
5618
5965
  let seconds = null;
5619
5966
  if (/\.(png|jpe?g|webp)$/i.test(file)) {
5620
- const head = Buffer.from(await readFile10(file));
5967
+ const head = Buffer.from(await readFile11(file));
5621
5968
  const kind = sniffImage(head);
5622
5969
  if (file.toLowerCase().endsWith(".png") && kind !== "png")
5623
5970
  problems.push(
@@ -5727,7 +6074,7 @@ async function validateKit(kitPath, opts = {}) {
5727
6074
 
5728
6075
  // src/plugin/recorder.ts
5729
6076
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5730
- import { join as join11 } from "path";
6077
+ import { join as join12 } from "path";
5731
6078
 
5732
6079
  // src/plugin/recordingCap.ts
5733
6080
  var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
@@ -5815,7 +6162,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5815
6162
  cdp.on("Page.screencastFrame", (ev) => {
5816
6163
  const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
5817
6164
  const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
5818
- writeFileSync2(join11(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6165
+ writeFileSync2(join12(paths.framesDir, file), Buffer.from(ev.data, "base64"));
5819
6166
  frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
5820
6167
  cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
5821
6168
  });
@@ -5894,6 +6241,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5894
6241
  const stepStart = now();
5895
6242
  const skippedBefore = skipped.length;
5896
6243
  const urlBefore = page.url();
6244
+ let stepRect = null;
5897
6245
  switch (step.do) {
5898
6246
  case "wait":
5899
6247
  await sleep(clampWait(step.ms, now(), maxSeconds));
@@ -5903,6 +6251,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5903
6251
  break;
5904
6252
  case "hover": {
5905
6253
  const rect = await boxOf(step.selector);
6254
+ stepRect = rect;
5906
6255
  if (rect) {
5907
6256
  await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
5908
6257
  log(`hover ${step.selector}`);
@@ -5913,6 +6262,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5913
6262
  }
5914
6263
  case "click": {
5915
6264
  const rect = await boxOf(step.selector);
6265
+ stepRect = rect;
5916
6266
  if (rect) {
5917
6267
  await clickAt(rect);
5918
6268
  log(`click ${step.selector}`);
@@ -5923,6 +6273,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5923
6273
  }
5924
6274
  case "type": {
5925
6275
  const rect = await boxOf(step.selector);
6276
+ stepRect = rect;
5926
6277
  if (rect) {
5927
6278
  if (step.focus !== false) await clickAt(rect);
5928
6279
  const delay = step.delayMs ?? 40;
@@ -5964,6 +6315,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
5964
6315
  let rect;
5965
6316
  if (step.selector) {
5966
6317
  const box = await boxOf(step.selector);
6318
+ stepRect = box;
5967
6319
  if (box) {
5968
6320
  rect = box;
5969
6321
  start = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
@@ -6026,7 +6378,15 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6026
6378
  tStart: +(stepStart / 1e3).toFixed(3),
6027
6379
  tEnd: +(now() / 1e3).toFixed(3),
6028
6380
  ...skipped.length > skippedBefore ? { skipped: true } : {},
6029
- ...page.url() !== urlBefore ? { navigated: true } : {}
6381
+ ...page.url() !== urlBefore ? { navigated: true } : {},
6382
+ ...stepRect ? {
6383
+ rect: {
6384
+ x: +stepRect.x.toFixed(1),
6385
+ y: +stepRect.y.toFixed(1),
6386
+ w: +stepRect.w.toFixed(1),
6387
+ h: +stepRect.h.toFixed(1)
6388
+ }
6389
+ } : {}
6030
6390
  });
6031
6391
  }
6032
6392
  if (!capped) await sleep(600);
@@ -6040,7 +6400,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6040
6400
  await sleep(200);
6041
6401
  const pageTitle = await page.title().catch(() => "");
6042
6402
  await context.close();
6043
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join11(paths.framesDir, frames[0].file))) : null;
6403
+ const firstFrame = frames[0] ? jpegDims(readFileSync3(join12(paths.framesDir, frames[0].file))) : null;
6044
6404
  const meta = {
6045
6405
  dpr: 1,
6046
6406
  zoom: 1,
@@ -6148,9 +6508,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6148
6508
  }
6149
6509
 
6150
6510
  // src/plugin/plan.ts
6151
- import { existsSync as existsSync10 } from "fs";
6152
- import { readFile as readFile11 } from "fs/promises";
6153
- import { join as join12 } from "path";
6511
+ import { existsSync as existsSync11 } from "fs";
6512
+ import { readFile as readFile12 } from "fs/promises";
6513
+ import { join as join13 } from "path";
6154
6514
  import {
6155
6515
  DEFAULT_FRAME_STYLE,
6156
6516
  STYLE_FIELDS,
@@ -6372,10 +6732,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
6372
6732
  // src/plugin/plan.ts
6373
6733
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6374
6734
  async function readDigestActivity(dir) {
6375
- const file = join12(dir, "digest", "digest.json");
6376
- if (!existsSync10(file)) return null;
6735
+ const file = join13(dir, "digest", "digest.json");
6736
+ if (!existsSync11(file)) return null;
6377
6737
  try {
6378
- const d = JSON.parse(await readFile11(file, "utf8"));
6738
+ const d = JSON.parse(await readFile12(file, "utf8"));
6379
6739
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
6380
6740
  } catch {
6381
6741
  return null;
@@ -6585,17 +6945,17 @@ async function planTake(dir, opts = {}) {
6585
6945
 
6586
6946
  // src/plugin/sync.ts
6587
6947
  import { createHash } from "crypto";
6588
- import { existsSync as existsSync12 } from "fs";
6589
- import { readFile as readFile12 } from "fs/promises";
6948
+ import { existsSync as existsSync13 } from "fs";
6949
+ import { readFile as readFile13 } from "fs/promises";
6590
6950
  import { createInterface } from "readline/promises";
6591
- import { basename, join as join15 } from "path";
6951
+ import { basename, join as join16 } from "path";
6592
6952
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
6593
6953
 
6594
6954
  // src/plugin/media.ts
6595
6955
  import { isKeyedOverlay } from "@vosjs/studio-core";
6596
- import { createWriteStream, existsSync as existsSync11 } from "fs";
6956
+ import { createWriteStream, existsSync as existsSync12 } from "fs";
6597
6957
  import { writeFile as writeFile9 } from "fs/promises";
6598
- import { join as join13 } from "path";
6958
+ import { join as join14 } from "path";
6599
6959
  import { Readable } from "stream";
6600
6960
  import { pipeline } from "stream/promises";
6601
6961
  var MIC_NAME = "mic.webm";
@@ -6719,8 +7079,8 @@ async function pullMedia(ctx, dir, doc, log) {
6719
7079
  const url = doc.source[key];
6720
7080
  const assetId = assetIdOf(url);
6721
7081
  if (!assetId) continue;
6722
- const target = join13(dir, file);
6723
- if (existsSync11(target)) {
7082
+ const target = join14(dir, file);
7083
+ if (existsSync12(target)) {
6724
7084
  result.kept.push(file);
6725
7085
  } else {
6726
7086
  const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
@@ -6745,8 +7105,8 @@ async function pullMedia(ctx, dir, doc, log) {
6745
7105
  for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
6746
7106
  const assetId = assetIdOf(ref.key);
6747
7107
  const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
6748
- const existing = existsSync11(join13(dir, "media")) ? (await import("fs/promises").then(
6749
- (fs) => fs.readdir(join13(dir, "media"))
7108
+ const existing = existsSync12(join14(dir, "media")) ? (await import("fs/promises").then(
7109
+ (fs) => fs.readdir(join14(dir, "media"))
6750
7110
  )).find((f) => f.startsWith(`${assetId}.`)) : void 0;
6751
7111
  let file;
6752
7112
  if (existing) {
@@ -6763,9 +7123,9 @@ async function pullMedia(ctx, dir, doc, log) {
6763
7123
  }
6764
7124
  const ext = extensionFor(res.headers.get("content-type") ?? "");
6765
7125
  file = `media/${assetId}${ext}`;
6766
- const target = join13(dir, file);
7126
+ const target = join14(dir, file);
6767
7127
  await import("fs/promises").then(
6768
- (fs) => fs.mkdir(join13(dir, "media"), { recursive: true })
7128
+ (fs) => fs.mkdir(join14(dir, "media"), { recursive: true })
6769
7129
  );
6770
7130
  await pipeline(
6771
7131
  Readable.fromWeb(res.body),
@@ -6779,17 +7139,17 @@ async function pullMedia(ctx, dir, doc, log) {
6779
7139
  }
6780
7140
  ref.set(file);
6781
7141
  }
6782
- const metaPath = join13(dir, "meta.json");
6783
- if (!existsSync11(metaPath)) await writeJson(metaPath, doc.source.meta, true);
6784
- const cursorPath = join13(dir, "cursor.json");
6785
- if (!existsSync11(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
6786
- await writeFile9(join13(dir, "doc.json"), JSON.stringify(doc, null, 2));
7142
+ const metaPath = join14(dir, "meta.json");
7143
+ if (!existsSync12(metaPath)) await writeJson(metaPath, doc.source.meta, true);
7144
+ const cursorPath = join14(dir, "cursor.json");
7145
+ if (!existsSync12(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
7146
+ await writeFile9(join14(dir, "doc.json"), JSON.stringify(doc, null, 2));
6787
7147
  return result;
6788
7148
  }
6789
7149
 
6790
7150
  // src/plugin/folder.ts
6791
7151
  import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
6792
- import { join as join14 } from "path";
7152
+ import { join as join15 } from "path";
6793
7153
  import { recipeHints } from "@vosjs/shared/frontmatter";
6794
7154
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
6795
7155
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
@@ -6965,18 +7325,18 @@ async function cmdPull(argv) {
6965
7325
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
6966
7326
  const payload = res.body;
6967
7327
  const out = strFlag(flags, "out") ?? folder.slug;
6968
- await mkdir7(join14(out, "recipes"), { recursive: true });
7328
+ await mkdir7(join15(out, "recipes"), { recursive: true });
6969
7329
  const lines = [];
6970
7330
  const recipes = [];
6971
7331
  for (const rec of payload.recipes) {
6972
- const path = join14(out, "recipes", safeName(rec.filename));
7332
+ const path = join15(out, "recipes", safeName(rec.filename));
6973
7333
  await writeFile10(path, rec.body ?? "");
6974
7334
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
6975
7335
  }
6976
7336
  for (const rec of payload.inheritedRecipes) {
6977
7337
  const from = safeName(rec.folderSlug ?? "inherited");
6978
- await mkdir7(join14(out, "recipes", "_inherited", from), { recursive: true });
6979
- const path = join14(
7338
+ await mkdir7(join15(out, "recipes", "_inherited", from), { recursive: true });
7339
+ const path = join15(
6980
7340
  out,
6981
7341
  "recipes",
6982
7342
  "_inherited",
@@ -6991,7 +7351,7 @@ async function cmdPull(argv) {
6991
7351
  }
6992
7352
  const members = [];
6993
7353
  for (const v of payload.voses) {
6994
- const dir = join14(out, "members", safeName(v.slug || v.title || v.id));
7354
+ const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
6995
7355
  await mkdir7(dir, { recursive: true });
6996
7356
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
6997
7357
  if (cfg.status !== 200) {
@@ -6999,7 +7359,7 @@ async function cmdPull(argv) {
6999
7359
  continue;
7000
7360
  }
7001
7361
  await writeFile10(
7002
- join14(dir, "config.json"),
7362
+ join15(dir, "config.json"),
7003
7363
  JSON.stringify(cfg.body.config, null, 2)
7004
7364
  );
7005
7365
  let take = false;
@@ -7011,7 +7371,7 @@ async function cmdPull(argv) {
7011
7371
  );
7012
7372
  if (doc.status === 200) {
7013
7373
  await writeFile10(
7014
- join14(dir, "doc.json"),
7374
+ join15(dir, "doc.json"),
7015
7375
  JSON.stringify(doc.body, null, 2)
7016
7376
  );
7017
7377
  take = true;
@@ -7128,7 +7488,7 @@ async function pushTake(dir, flags, r) {
7128
7488
  throw new Error(`doc.json fails lint:
7129
7489
  ${lint.problems.join("\n ")}`);
7130
7490
  }
7131
- if (!existsSync12(take.paths.recording)) {
7491
+ if (!existsSync13(take.paths.recording)) {
7132
7492
  throw new Error("no recording.webm in this take");
7133
7493
  }
7134
7494
  const ctx = apiContext(flags);
@@ -7160,7 +7520,7 @@ async function pushTake(dir, flags, r) {
7160
7520
  throw new Error("push cancelled");
7161
7521
  }
7162
7522
  }
7163
- const bytes = await readFile12(take.paths.recording);
7523
+ const bytes = await readFile13(take.paths.recording);
7164
7524
  const hash = createHash("sha256").update(bytes).digest("hex");
7165
7525
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
7166
7526
  const upload = await api(ctx, "/assets/recording", {
@@ -7199,12 +7559,12 @@ async function pushTake(dir, flags, r) {
7199
7559
  delete docForPush.source.micKey;
7200
7560
  }
7201
7561
  for (const ref of docMediaRefs(docForPush)) {
7202
- const file = join15(dir, takeRelativeFile(ref.key));
7203
- if (!existsSync12(file)) {
7562
+ const file = join16(dir, takeRelativeFile(ref.key));
7563
+ if (!existsSync13(file)) {
7204
7564
  r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
7205
7565
  continue;
7206
7566
  }
7207
- const media = await readFile12(file);
7567
+ const media = await readFile13(file);
7208
7568
  const mediaHash = createHash("sha256").update(media).digest("hex");
7209
7569
  const type = mediaContentType(file);
7210
7570
  const put = await api(ctx, "/assets/recording", {
@@ -7340,9 +7700,9 @@ async function pullTake(dir, flags, r) {
7340
7700
  };
7341
7701
  }
7342
7702
  let media2;
7343
- if (flags.media && existsSync12(join15(dir, "doc.json"))) {
7703
+ if (flags.media && existsSync13(join16(dir, "doc.json"))) {
7344
7704
  const local = JSON.parse(
7345
- await readFile12(join15(dir, "doc.json"), "utf8")
7705
+ await readFile13(join16(dir, "doc.json"), "utf8")
7346
7706
  );
7347
7707
  media2 = await pullMedia(ctx, dir, local, r.log);
7348
7708
  }
@@ -7402,10 +7762,10 @@ async function pullTake(dir, flags, r) {
7402
7762
  }
7403
7763
  const hostedDoc = migrateHostedDoc3(docRes.json);
7404
7764
  const doc = hostedDoc;
7405
- if (existsSync12(join15(dir, RECORDING_NAME))) {
7765
+ if (existsSync13(join16(dir, RECORDING_NAME))) {
7406
7766
  doc.source.videoKey = RECORDING_NAME;
7407
7767
  }
7408
- await writeJson(join15(dir, "doc.json"), doc, true);
7768
+ await writeJson(join16(dir, "doc.json"), doc, true);
7409
7769
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
7410
7770
  const versions = await api(ctx, `/vos/${vosId}/versions`);
7411
7771
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -7424,10 +7784,10 @@ async function pullTake(dir, flags, r) {
7424
7784
  }
7425
7785
 
7426
7786
  // src/plugin/program.ts
7427
- import { existsSync as existsSync13 } from "fs";
7428
- import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
7787
+ import { existsSync as existsSync14 } from "fs";
7788
+ import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
7429
7789
  import { createInterface as createInterface2 } from "readline/promises";
7430
- import { basename as basename2, dirname as dirname3, join as join16 } from "path";
7790
+ import { basename as basename2, dirname as dirname3, join as join17 } from "path";
7431
7791
  import {
7432
7792
  CURRENT_CONFIG_VERSION,
7433
7793
  migrateConfig,
@@ -7597,8 +7957,8 @@ function preflightConfig(parsed) {
7597
7957
  }
7598
7958
  function resolveConfigPath(target) {
7599
7959
  if (target.endsWith(".json")) return target;
7600
- const inDir = join16(target, "config.json");
7601
- if (existsSync13(inDir)) return inDir;
7960
+ const inDir = join17(target, "config.json");
7961
+ if (existsSync14(inDir)) return inDir;
7602
7962
  throw new UsageError(
7603
7963
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
7604
7964
  );
@@ -7626,7 +7986,7 @@ async function cmdFetch(argv) {
7626
7986
  const out = strFlag(flags, "out") ?? slug;
7627
7987
  await mkdir8(out, { recursive: true });
7628
7988
  await writeFile11(
7629
- join16(out, "config.json"),
7989
+ join17(out, "config.json"),
7630
7990
  JSON.stringify(cfg.body.config, null, 2)
7631
7991
  );
7632
7992
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -7651,12 +8011,12 @@ async function cmdFetch(argv) {
7651
8011
  const hosted = migrateHostedDoc4(doc.body);
7652
8012
  const { config: own } = await writeProgramDoc(out, hosted);
7653
8013
  if (own) {
7654
- await writeFile11(join16(out, "config.json"), JSON.stringify(own, null, 2));
8014
+ await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
7655
8015
  }
7656
8016
  } else if (doc.status === 200) {
7657
8017
  take = true;
7658
8018
  const hosted = migrateHostedDoc4(doc.body);
7659
- await writeFile11(join16(out, "doc.json"), JSON.stringify(hosted, null, 2));
8019
+ await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
7660
8020
  if (flags.media === true) {
7661
8021
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
7662
8022
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -7769,7 +8129,7 @@ async function cmdPushProgram(argv) {
7769
8129
  api: strFlag(flags, "api")
7770
8130
  });
7771
8131
  const dir = dirname3(source);
7772
- const parsed = JSON.parse(await readFile13(source, "utf8"));
8132
+ const parsed = JSON.parse(await readFile14(source, "utf8"));
7773
8133
  const pre = preflightConfig(parsed);
7774
8134
  if (!pre.ok || !pre.config) {
7775
8135
  for (const issue of pre.issues) r.log(`error ${issue}`);
@@ -8010,10 +8370,10 @@ async function cmdPullProgram(argv) {
8010
8370
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
8011
8371
  if (cfg.status !== 200)
8012
8372
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
8013
- const configPath = join16(dir, "config.json");
8373
+ const configPath = join17(dir, "config.json");
8014
8374
  let backedUp = false;
8015
- if (existsSync13(configPath)) {
8016
- await writeFile11(join16(dir, "config.backup.json"), await readFile13(configPath));
8375
+ if (existsSync14(configPath)) {
8376
+ await writeFile11(join17(dir, "config.backup.json"), await readFile14(configPath));
8017
8377
  backedUp = true;
8018
8378
  }
8019
8379
  let headConfig = cfg.body.config;
@@ -8046,7 +8406,7 @@ async function cmdPullProgram(argv) {
8046
8406
  protected: protectedIds,
8047
8407
  changes,
8048
8408
  out: configPath,
8049
- backup: backedUp ? join16(dir, "config.backup.json") : null
8409
+ backup: backedUp ? join17(dir, "config.backup.json") : null
8050
8410
  },
8051
8411
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
8052
8412
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -8108,9 +8468,9 @@ function storedProgramConfig(config, programDoc) {
8108
8468
  return lowerProgramDoc(doc).config;
8109
8469
  }
8110
8470
  async function readProgramDoc(dir, config) {
8111
- const docPath = join16(dir, "doc.json");
8112
- if (!existsSync13(docPath)) return null;
8113
- const parsed = JSON.parse(await readFile13(docPath, "utf8"));
8471
+ const docPath = join17(dir, "doc.json");
8472
+ if (!existsSync14(docPath)) return null;
8473
+ const parsed = JSON.parse(await readFile14(docPath, "utf8"));
8114
8474
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
8115
8475
  return null;
8116
8476
  const raw = parsed;
@@ -8121,7 +8481,7 @@ async function writeProgramDoc(dir, hosted) {
8121
8481
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
8122
8482
  const { config, ...rest } = program;
8123
8483
  const onDisk = { ...hosted, program: rest };
8124
- await writeFile11(join16(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8484
+ await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8125
8485
  return {
8126
8486
  config: config && typeof config === "object" ? config : null
8127
8487
  };
@@ -9177,8 +9537,8 @@ async function cmdActions(argv) {
9177
9537
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
9178
9538
  viewport = { width: Number(m[1]), height: Number(m[2]) };
9179
9539
  }
9180
- const { readFile: readFile15 } = await import("fs/promises");
9181
- const text = await readFile15(resolve8(input), "utf8");
9540
+ const { readFile: readFile16 } = await import("fs/promises");
9541
+ const text = await readFile16(resolve8(input), "utf8");
9182
9542
  const { records, problems } = parseAgentBrowserLog(text);
9183
9543
  for (const p of problems) r.log(` ${p}`);
9184
9544
  const result = convertAgentBrowser(records, {
@@ -9232,7 +9592,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
9232
9592
  "yes",
9233
9593
  "composed",
9234
9594
  "check",
9235
- "motion"
9595
+ "motion",
9596
+ "leader"
9236
9597
  ]);
9237
9598
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
9238
9599
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -9247,6 +9608,7 @@ Take pipeline
9247
9608
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
9248
9609
  vos brand <url> [--out BRAND.md] [--json]
9249
9610
  vos open <take> [--studio <url>] [--print]
9611
+ vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]
9250
9612
  vos validate <actions.json|take|kit.json> [--picture] [--json]
9251
9613
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
9252
9614
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
@@ -9480,7 +9842,7 @@ lint-gated, so a bad override fails like a bad doc.json):
9480
9842
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
9481
9843
  `;
9482
9844
  async function loadActions(file) {
9483
- const raw = JSON.parse(await readFile14(file, "utf8"));
9845
+ const raw = JSON.parse(await readFile15(file, "utf8"));
9484
9846
  const errors = validateActions(raw);
9485
9847
  if (errors.length)
9486
9848
  throw new UsageError(`invalid actions file:
@@ -9539,7 +9901,7 @@ async function cmdRecord(argv) {
9539
9901
  const outDir = resolve9(strFlag(flags, "out") ?? "take");
9540
9902
  const backdrop = await takeBackdrop(flags, r);
9541
9903
  const maxDurationSeconds = await maxDuration(flags, r);
9542
- if (existsSync14(join17(outDir, "meta.json"))) {
9904
+ if (existsSync15(join18(outDir, "meta.json"))) {
9543
9905
  const { prevDoc, kept } = await prepareReRecord(outDir);
9544
9906
  r.log(
9545
9907
  `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" : "")
@@ -9586,7 +9948,7 @@ async function cmdRecord(argv) {
9586
9948
  },
9587
9949
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
9588
9950
  ${(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${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
9589
- Next: edit ${join17(outDir, "doc.json")} (optional), then: vos render ${outDir}`
9951
+ Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}`
9590
9952
  );
9591
9953
  return strictFail ? EXIT_USAGE : EXIT_OK;
9592
9954
  } finally {
@@ -9615,14 +9977,14 @@ async function cmdCreate2(argv) {
9615
9977
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9616
9978
  throw new UsageError("--format must be webm or mp4");
9617
9979
  const format = fmtRaw;
9618
- const out = positionals[0] ?? join17(outDir, `out.${format}`);
9980
+ const out = positionals[0] ?? join18(outDir, `out.${format}`);
9619
9981
  const parallel = numFlag(flags, "parallel", 1);
9620
9982
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9621
9983
  throw new UsageError("--parallel expects an integer between 1 and 16");
9622
9984
  }
9623
9985
  const backdrop = await takeBackdrop(flags, r);
9624
9986
  const maxDurationSeconds = await maxDuration(flags, r);
9625
- if (existsSync14(join17(outDir, "meta.json"))) {
9987
+ if (existsSync15(join18(outDir, "meta.json"))) {
9626
9988
  const { prevDoc, kept } = await prepareReRecord(outDir);
9627
9989
  r.log(
9628
9990
  `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" : "")
@@ -9713,24 +10075,24 @@ async function cmdPlan(argv) {
9713
10075
  );
9714
10076
  const r = createReporter(flags.json === true);
9715
10077
  if (flags.fresh === true) {
9716
- await rm4(join17(dir, "doc.json"), { force: true });
10078
+ await rm4(join18(dir, "doc.json"), { force: true });
9717
10079
  r.log("note: --fresh discarded the existing doc.json");
9718
10080
  }
9719
10081
  let reuse;
9720
10082
  if (flags.reuse === true) {
9721
- const from = resolve9(strFlag(flags, "from") ?? join17(dir, PREV_DOC_NAME));
9722
- if (!existsSync14(from)) {
10083
+ const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
10084
+ if (!existsSync15(from)) {
9723
10085
  throw new UsageError(
9724
10086
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
9725
10087
  );
9726
10088
  }
9727
10089
  reuse = {
9728
10090
  from,
9729
- doc: JSON.parse(await readFile14(from, "utf8"))
10091
+ doc: JSON.parse(await readFile15(from, "utf8"))
9730
10092
  };
9731
10093
  }
9732
10094
  const style = await resolveStyleRef(flags);
9733
- const hasDoc = existsSync14(join17(dir, "doc.json"));
10095
+ const hasDoc = existsSync15(join18(dir, "doc.json"));
9734
10096
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
9735
10097
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
9736
10098
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -9788,7 +10150,7 @@ async function cmdPlan(argv) {
9788
10150
  ...s.motion ? { motion: s.motion } : {},
9789
10151
  ...s.reuse ? { reuse: s.reuse } : {}
9790
10152
  },
9791
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join17(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
10153
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join18(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
9792
10154
  );
9793
10155
  return EXIT_OK;
9794
10156
  }
@@ -9914,7 +10276,7 @@ async function cmdRender(argv) {
9914
10276
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9915
10277
  throw new UsageError("--format must be webm or mp4");
9916
10278
  const format = fmtRaw;
9917
- const out = positionals[1] ?? join17(dir, `out.${format}`);
10279
+ const out = positionals[1] ?? join18(dir, `out.${format}`);
9918
10280
  const parallel = numFlag(flags, "parallel", 1);
9919
10281
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9920
10282
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -10161,15 +10523,15 @@ async function resolveStyleRef(flags) {
10161
10523
  }
10162
10524
  async function resolveDocRef(ref, flags, what) {
10163
10525
  const styleRef = ref;
10164
- const file = existsSync14(styleRef) ? resolve9(
10526
+ const file = existsSync15(styleRef) ? resolve9(
10165
10527
  styleRef,
10166
- existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
10528
+ existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
10167
10529
  ) : null;
10168
- if (file && existsSync14(file)) {
10530
+ if (file && existsSync15(file)) {
10169
10531
  return {
10170
10532
  from: file,
10171
10533
  doc: migrateHostedDoc5(
10172
- JSON.parse(await readFile14(file, "utf8"))
10534
+ JSON.parse(await readFile15(file, "utf8"))
10173
10535
  )
10174
10536
  };
10175
10537
  }
@@ -10221,7 +10583,7 @@ async function cmdDigest(argv) {
10221
10583
  const take = await loadTake(dir);
10222
10584
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
10223
10585
  const transcriptPath = strFlag(flags, "transcript");
10224
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile14(transcriptPath, "utf8"))) : null;
10586
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
10225
10587
  const style = await resolveStyleRef(flags);
10226
10588
  const noFrames = flags["no-frames"] === true;
10227
10589
  let browser = null;
@@ -10252,7 +10614,7 @@ async function cmdDigest(argv) {
10252
10614
  kinds,
10253
10615
  frames: d.images.full,
10254
10616
  crops: d.images.crop,
10255
- sheet: d.images.sheet ? join17(result.outDir, d.images.sheet) : null,
10617
+ sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
10256
10618
  scenes: kinds.scene ?? 0,
10257
10619
  sourceDuration: d.take.sourceDuration,
10258
10620
  outputDuration: d.take.outputDuration,
@@ -10276,7 +10638,7 @@ async function cmdOpen(argv) {
10276
10638
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
10277
10639
  const r = createReporter(flags.json === true);
10278
10640
  const take = await loadTake(dir);
10279
- if (!existsSync14(take.paths.recording)) {
10641
+ if (!existsSync15(take.paths.recording)) {
10280
10642
  throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
10281
10643
  }
10282
10644
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
@@ -10317,12 +10679,12 @@ async function cmdJudge(argv) {
10317
10679
  "vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
10318
10680
  );
10319
10681
  const r = createReporter(flags.json === true);
10320
- const kitFile = target.endsWith("kit.json") ? target : join17(target, "kit.json");
10321
- if (!existsSync14(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
10322
- if (!existsSync14(against))
10682
+ const kitFile = target.endsWith("kit.json") ? target : join18(target, "kit.json");
10683
+ if (!existsSync15(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
10684
+ if (!existsSync15(against))
10323
10685
  throw new UsageError(`${against}: no reference manifest`);
10324
10686
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
10325
- const verdicts = JSON.parse(await readFile14(result.verdictFile, "utf8")).verdicts;
10687
+ const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
10326
10688
  const rate = winRate(verdicts);
10327
10689
  const lines = result.sheets.map(
10328
10690
  (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
@@ -10337,12 +10699,109 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
10337
10699
  );
10338
10700
  return EXIT_OK;
10339
10701
  }
10702
+ async function cmdCallout(argv) {
10703
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10704
+ const dir = positionals[0];
10705
+ const shape = positionals[1];
10706
+ if (!dir || !shape || !isCalloutShape(shape))
10707
+ throw new UsageError(
10708
+ 'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex --accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]'
10709
+ );
10710
+ const r = createReporter(flags.json === true);
10711
+ const take = await loadTake(dir);
10712
+ if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
10713
+ const brand = await readBrandBesideTake(dir, strFlag(flags, "brand"));
10714
+ const bodyPx = strFlag(flags, "body-px");
10715
+ const register = registerFrom(brand?.roles ?? null, {
10716
+ ground: strFlag(flags, "ground"),
10717
+ accent: strFlag(flags, "accent"),
10718
+ face: strFlag(flags, "font"),
10719
+ mono: strFlag(flags, "mono"),
10720
+ body: bodyPx !== void 0 ? Number(bodyPx) : void 0
10721
+ });
10722
+ if (!register)
10723
+ throw new UsageError(
10724
+ `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`
10725
+ );
10726
+ const codeFile = strFlag(flags, "code-file");
10727
+ const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
10728
+ const words = {
10729
+ kicker: strFlag(flags, "kicker"),
10730
+ title: strFlag(flags, "title"),
10731
+ body: strFlag(flags, "body"),
10732
+ code: code?.replace(/\\n/g, "\n")
10733
+ };
10734
+ if (shape === "code" && !words.code)
10735
+ throw new UsageError(
10736
+ 'a code callout needs --code "\u2026" or --code-file <file>'
10737
+ );
10738
+ if (shape === "note" && !words.title)
10739
+ throw new UsageError('a note needs --title "\u2026"');
10740
+ if (shape === "tag" && !words.kicker && !words.title)
10741
+ throw new UsageError('a tag needs --kicker "\u2026"');
10742
+ const stepRaw = strFlag(flags, "step");
10743
+ const atRaw = strFlag(flags, "at");
10744
+ const secondsRaw = strFlag(flags, "seconds");
10745
+ const side = strFlag(flags, "side");
10746
+ const mark = strFlag(flags, "mark");
10747
+ let composed;
10748
+ try {
10749
+ composed = composeCallout(take.doc, register, {
10750
+ shape,
10751
+ words,
10752
+ id: strFlag(flags, "id"),
10753
+ step: stepRaw !== void 0 ? /^\d+$/.test(stepRaw) ? Number(stepRaw) : stepRaw : void 0,
10754
+ at: atRaw !== void 0 ? Number(atRaw) : void 0,
10755
+ seconds: secondsRaw !== void 0 ? Number(secondsRaw) : void 0,
10756
+ side,
10757
+ mark,
10758
+ leader: flags.leader === true,
10759
+ color: strFlag(flags, "color")
10760
+ });
10761
+ } catch (e) {
10762
+ throw new UsageError(e instanceof Error ? e.message : String(e));
10763
+ }
10764
+ const { clip, window: window2 } = composed;
10765
+ if (flags.print === true) {
10766
+ r.done(
10767
+ { clip, window: window2, register, brand: brand?.file ?? null },
10768
+ JSON.stringify(clip, null, 2)
10769
+ );
10770
+ return EXIT_OK;
10771
+ }
10772
+ const doc = take.doc;
10773
+ const overlays = (doc.overlays ?? []).filter((o) => o.id !== clip.id);
10774
+ const next = { ...doc, overlays: [...overlays, clip] };
10775
+ const lint = lintDoc(next);
10776
+ if (lint.problems.length) {
10777
+ r.done(
10778
+ { valid: false, problems: lint.problems, warnings: lint.warnings, clip },
10779
+ `the callout would not validate:
10780
+ ${lint.problems.join("\n ")}`
10781
+ );
10782
+ return EXIT_ERROR;
10783
+ }
10784
+ await writeJson(take.paths.doc, next, true);
10785
+ r.done(
10786
+ {
10787
+ clip,
10788
+ window: window2,
10789
+ register,
10790
+ brand: brand?.file ?? null,
10791
+ warnings: lint.warnings
10792
+ },
10793
+ `${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, written to ${take.paths.doc}${lint.warnings.length ? `
10794
+ warnings:
10795
+ ${lint.warnings.join("\n ")}` : ""}`
10796
+ );
10797
+ return EXIT_OK;
10798
+ }
10340
10799
  async function cmdValidate(argv) {
10341
10800
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10342
10801
  const target = positionals[0];
10343
10802
  if (!target) throw new UsageError("vos validate <actions.json|take>");
10344
10803
  const r = createReporter(flags.json === true);
10345
- const kitTarget = target.endsWith("kit.json") ? target : existsSync14(join17(target, "kit.json")) && !isTakeDir(target) ? join17(target, "kit.json") : null;
10804
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync15(join18(target, "kit.json")) && !isTakeDir(target) ? join18(target, "kit.json") : null;
10346
10805
  if (kitTarget) {
10347
10806
  const picture = flags.picture === true;
10348
10807
  const verdict = await validateKit(kitTarget, { picture });
@@ -10374,9 +10833,9 @@ async function cmdValidate(argv) {
10374
10833
  r.done({ valid: true, target }, `${target}: valid actions file`);
10375
10834
  return EXIT_OK;
10376
10835
  }
10377
- if (!isTakeDir(target) && existsSync14(join17(target, "config.json"))) {
10836
+ if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
10378
10837
  const parsed = JSON.parse(
10379
- await readFile14(join17(target, "config.json"), "utf8")
10838
+ await readFile15(join18(target, "config.json"), "utf8")
10380
10839
  );
10381
10840
  const pre = preflightConfig(parsed);
10382
10841
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -10417,7 +10876,7 @@ async function cmdValidate(argv) {
10417
10876
  const take = await loadTake(target);
10418
10877
  const problems = [];
10419
10878
  const warnings = [];
10420
- if (!existsSync14(take.paths.recording))
10879
+ if (!existsSync15(take.paths.recording))
10421
10880
  problems.push("missing recording.webm (re-run record)");
10422
10881
  if (!take.doc) problems.push("missing doc.json (run plan)");
10423
10882
  if (take.doc) {
@@ -10429,6 +10888,36 @@ async function cmdValidate(argv) {
10429
10888
  );
10430
10889
  problems.push(...probe.problems);
10431
10890
  warnings.push(...probe.warnings);
10891
+ if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
10892
+ const doc = take.doc;
10893
+ const times = [
10894
+ ...new Set(
10895
+ (doc.overlays ?? []).filter((o) => o.kind === "html").map((o) => +(o.start + Math.min(0.35, o.duration / 2)).toFixed(3))
10896
+ )
10897
+ ];
10898
+ const browser = await launchBrowser();
10899
+ try {
10900
+ const frames = await footageFrames(
10901
+ browser,
10902
+ target,
10903
+ doc,
10904
+ times,
10905
+ framesTake
10906
+ );
10907
+ const findings = await groundFindings(
10908
+ doc,
10909
+ async (t) => frames.get(+t.toFixed(3)) ?? null
10910
+ );
10911
+ for (const f of findings) {
10912
+ if (f.level === "problem") problems.push(`doc.json: ${f.message}`);
10913
+ else if (f.level === "warning")
10914
+ warnings.push(`doc.json: ${f.message}`);
10915
+ else r.event({ event: "ground", ...f });
10916
+ }
10917
+ } finally {
10918
+ await browser.close();
10919
+ }
10920
+ }
10432
10921
  }
10433
10922
  if (problems.length) {
10434
10923
  r.done(
@@ -10545,6 +11034,8 @@ async function run(argv) {
10545
11034
  return await cmdOpen(rest);
10546
11035
  case "validate":
10547
11036
  return await cmdValidate(rest);
11037
+ case "callout":
11038
+ return await cmdCallout(rest);
10548
11039
  case "judge":
10549
11040
  return await cmdJudge(rest);
10550
11041
  case "fetch":
@@ -10628,4 +11119,4 @@ export {
10628
11119
  convertAgentBrowser,
10629
11120
  run
10630
11121
  };
10631
- //# sourceMappingURL=chunk-APVJ4MDJ.js.map
11122
+ //# sourceMappingURL=chunk-6O55OD6D.js.map