@vosjs/cli 0.67.6 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +10 -13
  2. package/dist/{assetManifest-DU5ZEIDE.js → assetManifest-3QYCLT2H.js} +3 -3
  3. package/dist/{chunk-AUFY3M22.js → chunk-2KEQ3LKH.js} +2 -2
  4. package/dist/chunk-2KEQ3LKH.js.map +1 -0
  5. package/dist/{chunk-42ZU5XUW.js → chunk-5DUXN3RX.js} +1077 -2165
  6. package/dist/chunk-5DUXN3RX.js.map +1 -0
  7. package/dist/{chunk-MDRYSHOF.js → chunk-5GZ3GPAK.js} +2 -2
  8. package/dist/{chunk-C5VKACGQ.js → chunk-FL3E27WE.js} +13 -2
  9. package/dist/{chunk-C5VKACGQ.js.map → chunk-FL3E27WE.js.map} +1 -1
  10. package/dist/{chunk-OFTRK3Y3.js → chunk-Y5E672GT.js} +23 -4
  11. package/dist/chunk-Y5E672GT.js.map +1 -0
  12. package/dist/cli.js +7 -7
  13. package/dist/index.d.ts +17 -1
  14. package/dist/index.js +5 -5
  15. package/dist/manifest-QLQKVSML.js +8 -0
  16. package/dist/{run-4VSB2XHR.js → run-NDBUFOIG.js} +5 -5
  17. package/package.json +7 -7
  18. package/schema/channel-specs.json +5 -233
  19. package/schema/doc.schema.json +3 -3
  20. package/skills/VERSION +1 -1
  21. package/skills/launch-kit/SKILL.md +101 -223
  22. package/skills/launch-kit/references/channel-specs.md +14 -44
  23. package/skills/product-video/SKILL.md +9 -14
  24. package/skills/product-video/references/destinations.md +12 -23
  25. package/skills/vos-footage/SKILL.md +1 -1
  26. package/skills/vos-migrate/SKILL.md +3 -3
  27. package/dist/chunk-42ZU5XUW.js.map +0 -1
  28. package/dist/chunk-AUFY3M22.js.map +0 -1
  29. package/dist/chunk-OFTRK3Y3.js.map +0 -1
  30. package/dist/manifest-ZBK6IERM.js +0 -8
  31. /package/dist/{assetManifest-DU5ZEIDE.js.map → assetManifest-3QYCLT2H.js.map} +0 -0
  32. /package/dist/{chunk-MDRYSHOF.js.map → chunk-5GZ3GPAK.js.map} +0 -0
  33. /package/dist/{manifest-ZBK6IERM.js.map → manifest-QLQKVSML.js.map} +0 -0
  34. /package/dist/{run-4VSB2XHR.js.map → run-NDBUFOIG.js.map} +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  reencodeStill,
10
10
  renderStill,
11
11
  videoFormat
12
- } from "./chunk-MDRYSHOF.js";
12
+ } from "./chunk-5GZ3GPAK.js";
13
13
  import {
14
14
  PREV_DOC_NAME,
15
15
  RECORDING_NAME,
@@ -17,6 +17,7 @@ import {
17
17
  docMediaRefs,
18
18
  dropScreencastFrames,
19
19
  ensureTakeDir,
20
+ fileSha256,
20
21
  hostedLiteralWarnings,
21
22
  isTakeRelativeKey,
22
23
  loadTake,
@@ -26,14 +27,16 @@ import {
26
27
  pullMedia,
27
28
  readJson,
28
29
  readMediaHead,
30
+ recordHostedMedia,
29
31
  sourceSidecarRefs,
30
32
  takeMediaFile,
31
33
  takePaths,
34
+ takeRelativeFile,
32
35
  uploadAsset,
33
36
  uploadDocRefs,
34
37
  uploadManifest,
35
38
  writeJson
36
- } from "./chunk-OFTRK3Y3.js";
39
+ } from "./chunk-Y5E672GT.js";
37
40
  import {
38
41
  CREDENTIALS_PATH,
39
42
  MEDIA_HEAD_BYTES,
@@ -66,7 +69,7 @@ import {
66
69
  strFlag,
67
70
  writeCredential,
68
71
  writeSyncState
69
- } from "./chunk-C5VKACGQ.js";
72
+ } from "./chunk-FL3E27WE.js";
70
73
  import {
71
74
  deadLine,
72
75
  deadWarns
@@ -128,13 +131,13 @@ function landedLines(l) {
128
131
  }
129
132
 
130
133
  // src/plugin/run.ts
131
- import { mkdir as mkdir9, readFile as readFile15, rm as rm3 } from "fs/promises";
132
- import { existsSync as existsSync24 } from "fs";
133
- import { join as join26, resolve as resolve15 } from "path";
134
+ import { mkdir as mkdir8, readFile as readFile12, rm as rm2 } from "fs/promises";
135
+ import { existsSync as existsSync22 } from "fs";
136
+ import { join as join24, resolve as resolve13 } from "path";
134
137
  import {
135
138
  END_CARD_FROM as END_CARD_FROM2,
136
139
  docOutputDuration as docOutputDuration3,
137
- migrateHostedDoc as migrateHostedDoc5
140
+ migrateHostedDoc as migrateHostedDoc4
138
141
  } from "@vosjs/studio-core";
139
142
 
140
143
  // src/plugin/output.ts
@@ -2117,10 +2120,10 @@ function startTakeServer(rootDir, pages) {
2117
2120
  res.writeHead(404).end();
2118
2121
  });
2119
2122
  return new Promise(
2120
- (resolve16) => server.listen(0, () => {
2123
+ (resolve14) => server.listen(0, () => {
2121
2124
  const addr = server.address();
2122
2125
  const port = typeof addr === "object" && addr ? addr.port : 0;
2123
- resolve16({ base: `http://localhost:${port}`, close: () => server.close() });
2126
+ resolve14({ base: `http://localhost:${port}`, close: () => server.close() });
2124
2127
  })
2125
2128
  );
2126
2129
  }
@@ -2170,8 +2173,8 @@ function renderPageHtml() {
2170
2173
  ${tweenRuntimeCode}
2171
2174
  // Vos Element System (bundled IIFE) \u2014 the same injection the engine's
2172
2175
  // render template performs. Take configs never touch ctx.elements, but a
2173
- // POSTER program (the deliver --poster leg) is element text + an image
2174
- // over the ground, and without this the elements silently never draw.
2176
+ // program may draw element text and images over its ground, and without
2177
+ // this the elements silently never draw.
2175
2178
  ${elementsBundleCode}
2176
2179
  window.__vos__ = window.__vos__ || {}
2177
2180
  window.__vos__.elements = __vosElementsFactory.createVosElements(THREE)
@@ -2357,14 +2360,14 @@ async function renderInPage(opts) {
2357
2360
  w.__error = String(e instanceof Error && e.stack || e);
2358
2361
  }
2359
2362
  }
2360
- async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress) {
2361
- const outName = `render-chunk-${chunk2.index}.tmp`;
2363
+ async function renderChunk(context2, serverBase, chunk, opts, onChunkProgress) {
2364
+ const outName = `render-chunk-${chunk.index}.tmp`;
2362
2365
  const page = await context2.newPage();
2363
2366
  try {
2364
2367
  page.on("console", (m) => {
2365
2368
  if (m.type() === "error" || process.env.VOS_RENDER_DEBUG)
2366
2369
  process.stderr.write(
2367
- ` [render page ${chunk2.index} ${m.type()}] ${m.text()}
2370
+ ` [render page ${chunk.index} ${m.type()}] ${m.text()}
2368
2371
  `
2369
2372
  );
2370
2373
  });
@@ -2372,7 +2375,7 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2372
2375
  page.on(
2373
2376
  "pageerror",
2374
2377
  (e) => process.stderr.write(
2375
- ` [render pageerror ${chunk2.index}] ${String(e)}
2378
+ ` [render pageerror ${chunk.index}] ${String(e)}
2376
2379
  `
2377
2380
  )
2378
2381
  );
@@ -2391,8 +2394,8 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2391
2394
  W: opts.width,
2392
2395
  H: opts.height,
2393
2396
  fps: opts.fps,
2394
- startFrame: chunk2.startFrame + frameOffset,
2395
- endFrame: chunk2.endFrame + frameOffset,
2397
+ startFrame: chunk.startFrame + frameOffset,
2398
+ endFrame: chunk.endFrame + frameOffset,
2396
2399
  format: opts.format,
2397
2400
  bitrate: opts.bitrate ?? 1e7,
2398
2401
  mediabunnyUrl: MEDIABUNNY_URL,
@@ -2403,10 +2406,10 @@ async function renderChunk(context2, serverBase, chunk2, opts, onChunkProgress)
2403
2406
  audioDuration: audio?.duration
2404
2407
  }).catch(() => {
2405
2408
  });
2406
- const timeoutMs = 12e4 + chunk2.frameCount * 500;
2409
+ const timeoutMs = 12e4 + chunk.frameCount * 500;
2407
2410
  await waitForPageDone(
2408
2411
  page,
2409
- `render chunk ${chunk2.index}`,
2412
+ `render chunk ${chunk.index}`,
2410
2413
  onChunkProgress,
2411
2414
  timeoutMs
2412
2415
  );
@@ -2438,8 +2441,8 @@ async function renderAnimation(browser, opts) {
2438
2441
  };
2439
2442
  const outNames = await Promise.all(
2440
2443
  chunks.map(
2441
- (chunk2) => renderChunk(context2, server.base, chunk2, effOpts, (fraction) => {
2442
- progress[chunk2.index] = fraction;
2444
+ (chunk) => renderChunk(context2, server.base, chunk, effOpts, (fraction) => {
2445
+ progress[chunk.index] = fraction;
2443
2446
  reportProgress();
2444
2447
  })
2445
2448
  )
@@ -2454,9 +2457,9 @@ async function renderAnimation(browser, opts) {
2454
2457
  return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
2455
2458
  }
2456
2459
  const { bytes } = await concatEncodedVideo(
2457
- chunks.map((chunk2, i) => ({
2460
+ chunks.map((chunk, i) => ({
2458
2461
  data: new Uint8Array(files[i]),
2459
- duration: chunk2.duration
2462
+ duration: chunk.duration
2460
2463
  })),
2461
2464
  { format: opts.format, frameRate: opts.fps }
2462
2465
  );
@@ -3398,11 +3401,10 @@ async function writeIndexJson(result) {
3398
3401
  }
3399
3402
 
3400
3403
  // src/plugin/deliver.ts
3401
- import { mkdir as mkdir3, mkdtemp, rename as rename3, rm as rm2, stat, writeFile as writeFile5 } from "fs/promises";
3402
- import { tmpdir } from "os";
3403
- import { join as join5, relative, resolve as resolve4 } from "path";
3404
- import { existsSync as existsSync5 } from "fs";
3405
- import { readFile as readFile5 } from "fs/promises";
3404
+ import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
3405
+ import { join as join4, relative, resolve as resolve3 } from "path";
3406
+ import { existsSync as existsSync4 } from "fs";
3407
+ import { readFile as readFile4 } from "fs/promises";
3406
3408
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
3407
3409
  import {
3408
3410
  CHANNEL_SPECS_VERIFIED,
@@ -3520,614 +3522,119 @@ async function renderTake(browser, dir, outFile, opts) {
3520
3522
  };
3521
3523
  }
3522
3524
 
3525
+ // src/plugin/motionPlan.ts
3526
+ import {
3527
+ END_CARD_FROM,
3528
+ END_CARD_SECONDS,
3529
+ applyTemplate,
3530
+ docOutputDuration,
3531
+ dropTemplate,
3532
+ endCardClips,
3533
+ migrateMotion,
3534
+ ratedSegments as ratedSegments4,
3535
+ sameMedia,
3536
+ spanOutputExtent as spanOutputExtent4
3537
+ } from "@vosjs/studio-core";
3538
+
3523
3539
  // src/plugin/moments.ts
3524
- import { ratedSegments as ratedSegments3, spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3540
+ import { spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
3541
+ var STEP_SETTLE_SECONDS = 0.4;
3542
+ function stepOutputTime(rated, step, settle) {
3543
+ const src = step.tEnd + settle;
3544
+ const ext = spanOutputExtent3(rated, src, src + 1e-3);
3545
+ if (!ext) return null;
3546
+ return ext.start;
3547
+ }
3525
3548
 
3526
- // src/plugin/picture.ts
3527
- import { inflateSync } from "zlib";
3528
- var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
3529
- function decodePng(bytes) {
3530
- if (bytes.length < 33) return null;
3531
- for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
3532
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3533
- let pos = 8;
3534
- let w = 0;
3535
- let h = 0;
3536
- let depth = 0;
3537
- let colorType = 0;
3538
- let interlace = 0;
3539
- const idat = [];
3540
- while (pos + 8 <= bytes.length) {
3541
- const len = view.getUint32(pos);
3542
- const type = String.fromCharCode(
3543
- bytes[pos + 4],
3544
- bytes[pos + 5],
3545
- bytes[pos + 6],
3546
- bytes[pos + 7]
3549
+ // src/plugin/motionPlan.ts
3550
+ var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
3551
+ "x-feed-cut",
3552
+ "youtube-main-demo",
3553
+ "shorts-linkedin-vertical-cut"
3554
+ ]);
3555
+ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
3556
+ var BED_ID = "bed";
3557
+ var CLICK_ID_PREFIX = "click-";
3558
+ var CAPTION_ID_PREFIX = "caption-";
3559
+ var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3560
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
3561
+ var TRANSITION_CLIP_MIN = 1;
3562
+ function proposeTransitions(input, kind) {
3563
+ const doc = structuredClone(input);
3564
+ const skipped = [];
3565
+ let count = 0;
3566
+ const steps = doc.source.meta.steps ?? [];
3567
+ for (const step of steps) {
3568
+ if (!step.navigated || step.skipped) continue;
3569
+ const name = step.id ?? String(step.step);
3570
+ const wait = step.do === "wait";
3571
+ const outEnd = round3(wait ? step.tStart : step.tEnd);
3572
+ const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3573
+ const segs = doc.segments;
3574
+ if (segs.some(
3575
+ (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
3576
+ ))
3577
+ continue;
3578
+ const i = segs.findIndex(
3579
+ (s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
3547
3580
  );
3548
- const start = pos + 8;
3549
- if (type === "IHDR") {
3550
- w = view.getUint32(start);
3551
- h = view.getUint32(start + 4);
3552
- depth = bytes[start + 8];
3553
- colorType = bytes[start + 9];
3554
- interlace = bytes[start + 12];
3555
- } else if (type === "IDAT") {
3556
- idat.push(bytes.subarray(start, start + len));
3557
- } else if (type === "IEND") break;
3558
- pos = start + len + 4;
3559
- }
3560
- if (!w || !h || depth !== 8 || interlace !== 0) return null;
3561
- const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
3562
- if (!channels) return null;
3563
- const total = idat.reduce((n, c) => n + c.length, 0);
3564
- const joined = new Uint8Array(total);
3565
- let off2 = 0;
3566
- for (const c of idat) {
3567
- joined.set(c, off2);
3568
- off2 += c.length;
3569
- }
3570
- let raw;
3571
- try {
3572
- raw = new Uint8Array(inflateSync(joined));
3573
- } catch {
3574
- return null;
3575
- }
3576
- const stride = w * channels;
3577
- if (raw.length < (stride + 1) * h) return null;
3578
- const out = new Uint8Array(w * h * 4);
3579
- const prev = new Uint8Array(stride);
3580
- const cur = new Uint8Array(stride);
3581
- for (let y = 0; y < h; y++) {
3582
- const filter = raw[y * (stride + 1)];
3583
- const rowStart = y * (stride + 1) + 1;
3584
- for (let i = 0; i < stride; i++) {
3585
- const x = raw[rowStart + i];
3586
- const a = i >= channels ? cur[i - channels] : 0;
3587
- const b = prev[i];
3588
- const c = i >= channels ? prev[i - channels] : 0;
3589
- let v;
3590
- switch (filter) {
3591
- case 0:
3592
- v = x;
3593
- break;
3594
- case 1:
3595
- v = x + a;
3596
- break;
3597
- case 2:
3598
- v = x + b;
3599
- break;
3600
- case 3:
3601
- v = x + (a + b >> 1);
3602
- break;
3603
- case 4: {
3604
- const p = a + b - c;
3605
- const pa = Math.abs(p - a);
3606
- const pb = Math.abs(p - b);
3607
- const pc = Math.abs(p - c);
3608
- v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
3609
- break;
3610
- }
3611
- default:
3612
- return null;
3613
- }
3614
- cur[i] = v & 255;
3615
- }
3616
- for (let px = 0; px < w; px++) {
3617
- const o = (y * w + px) * 4;
3618
- const s = px * channels;
3619
- if (channels >= 3) {
3620
- out[o] = cur[s];
3621
- out[o + 1] = cur[s + 1];
3622
- out[o + 2] = cur[s + 2];
3623
- out[o + 3] = channels === 4 ? cur[s + 3] : 255;
3624
- } else {
3625
- out[o] = out[o + 1] = out[o + 2] = cur[s];
3626
- out[o + 3] = channels === 2 ? cur[s + 1] : 255;
3627
- }
3581
+ if (i < 0) {
3582
+ skipped.push(
3583
+ `no transition at step ${name}: the page change sits too close to a clip's edge`
3584
+ );
3585
+ continue;
3628
3586
  }
3629
- prev.set(cur);
3587
+ const seg = segs[i];
3588
+ const before = { ...seg, out: outEnd, anim: { exit: kind } };
3589
+ const after = { ...seg, in: inStart, anim: { enter: kind } };
3590
+ if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
3591
+ if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
3592
+ doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
3593
+ count++;
3630
3594
  }
3631
- return { w, h, data: out };
3595
+ return { doc, count, skipped };
3632
3596
  }
3633
- var at = (img, x, y) => {
3634
- const o = (y * img.w + x) * 4;
3635
- return [img.data[o], img.data[o + 1], img.data[o + 2]];
3636
- };
3637
- var median = (a) => {
3638
- const s = [...a].sort((x, y) => x - y);
3639
- return s[s.length >> 1] ?? 0;
3640
- };
3641
- function groundColour(img) {
3642
- const ring = Math.max(1, Math.round(Math.min(img.w, img.h) * 0.01));
3643
- const r = [];
3644
- const g = [];
3645
- const b = [];
3646
- const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 400));
3647
- for (let y = 0; y < img.h; y += step) {
3648
- for (let x = 0; x < img.w; x += step) {
3649
- if (x < ring || y < ring || x >= img.w - ring || y >= img.h - ring) {
3650
- const p = at(img, x, y);
3651
- r.push(p[0]);
3652
- g.push(p[1]);
3653
- b.push(p[2]);
3654
- }
3655
- }
3597
+ function pickTrack(catalog, ask) {
3598
+ if (!catalog || !ask || off(ask)) return null;
3599
+ const want = ask.trim().toLowerCase();
3600
+ return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
3601
+ }
3602
+ function clickTimes(doc, range) {
3603
+ const rated = ratedSegments4(doc);
3604
+ const out = [];
3605
+ for (const e of doc.source.cursor) {
3606
+ if (e.type !== "down") continue;
3607
+ const src = e.t / 1e3;
3608
+ const ext = spanOutputExtent4(rated, src, src + 1e-3);
3609
+ if (!ext) continue;
3610
+ const t = ext.start;
3611
+ if (t < range[0] || t > range[1]) continue;
3612
+ if (out.length && t - out[out.length - 1] < 0.12) continue;
3613
+ out.push(+(t - range[0]).toFixed(3));
3656
3614
  }
3657
- return [median(r), median(g), median(b)];
3615
+ return out;
3658
3616
  }
3659
- var hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
3660
- var delta = (p, q) => Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
3661
- function cardBounds(img, ground, threshold = 60) {
3662
- let x0 = img.w;
3663
- let y0 = img.h;
3664
- let x1 = -1;
3665
- let y1 = -1;
3666
- const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 700));
3667
- for (let y = 0; y < img.h; y += step) {
3668
- for (let x = 0; x < img.w; x += step) {
3669
- if (delta(at(img, x, y), ground) > threshold) {
3670
- if (x < x0) x0 = x;
3671
- if (y < y0) y0 = y;
3672
- if (x > x1) x1 = x;
3673
- if (y > y1) y1 = y;
3674
- }
3675
- }
3617
+ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
3618
+ const rate = s.rate && s.rate > 0 ? s.rate : 1;
3619
+ return acc + (s.out - s.in) / rate;
3620
+ }, 0);
3621
+ function templateWords(words2) {
3622
+ const headline = (words2.headline ?? "").trim();
3623
+ const kicker = (words2.kicker ?? "").trim();
3624
+ const brand = (words2.brand ?? "").trim();
3625
+ const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
3626
+ const out = {};
3627
+ if (headline) {
3628
+ out["stage-title"] = headline;
3629
+ out["endcard-title"] = headline;
3676
3630
  }
3677
- if (x1 < 0) return null;
3678
- return { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 };
3679
- }
3680
- function haloReading(img, ground, card) {
3681
- const band = Math.max(2, Math.round(Math.min(img.w, img.h) * 0.012));
3682
- const meanRect = (x0, y0, x1, y1) => {
3683
- let sum = 0;
3684
- let n = 0;
3685
- const sx = Math.max(1, Math.round((x1 - x0) / 60));
3686
- const sy = Math.max(1, Math.round((y1 - y0) / 60));
3687
- for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
3688
- for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
3689
- sum += delta(at(img, x, y), ground);
3690
- n++;
3691
- }
3692
- }
3693
- return n ? sum / n : 0;
3694
- };
3695
- const rise = (bands) => {
3696
- const [outer, mid] = bands;
3697
- return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
3698
- };
3699
- const readings = [];
3700
- const yA = card.y + card.h * 0.2;
3701
- const yB = card.y + card.h * 0.8;
3702
- if (card.x >= 1 && card.w > band * 4) {
3703
- const x0 = card.x;
3704
- readings.push(
3705
- rise([
3706
- meanRect(x0, yA, x0 + band, yB),
3707
- meanRect(x0 + band, yA, x0 + band * 2, yB),
3708
- meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
3709
- ])
3710
- );
3711
- }
3712
- if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
3713
- const x1 = card.x + card.w;
3714
- readings.push(
3715
- rise([
3716
- meanRect(x1 - band, yA, x1, yB),
3717
- meanRect(x1 - band * 2, yA, x1 - band, yB),
3718
- meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
3719
- ])
3720
- );
3721
- }
3722
- if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
3723
- const y1 = card.y + card.h;
3724
- const xA = card.x + card.w * 0.2;
3725
- const xB = card.x + card.w * 0.8;
3726
- readings.push(
3727
- rise([
3728
- meanRect(xA, y1 - band, xB, y1),
3729
- meanRect(xA, y1 - band * 2, xB, y1 - band),
3730
- meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
3731
- ])
3732
- );
3733
- }
3734
- return readings.length ? Math.max(...readings) : null;
3735
- }
3736
- function edgeContrast(img, ground, card) {
3737
- const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
3738
- const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
3739
- const col = (x) => {
3740
- if (x < 0 || x >= img.w) return 0;
3741
- const ds = [];
3742
- const step = Math.max(1, Math.round((yB - yA) / 80));
3743
- for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
3744
- return median(ds);
3745
- };
3746
- const depth = Math.min(24, Math.floor(card.w / 8));
3747
- let best = 0;
3748
- for (let d = 0; d < depth; d++) {
3749
- best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
3750
- }
3751
- return best;
3752
- }
3753
- function inkCoverage(img, rect, threshold = 48) {
3754
- const x0 = Math.max(0, Math.floor(rect.x));
3755
- const y0 = Math.max(0, Math.floor(rect.y));
3756
- const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
3757
- const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
3758
- if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
3759
- const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
3760
- const r = [];
3761
- const g = [];
3762
- const b = [];
3763
- const pts = [];
3764
- for (let y = y0; y < y1; y += step) {
3765
- for (let x = x0; x < x1; x += step) {
3766
- const p = at(img, x, y);
3767
- pts.push(p);
3768
- r.push(p[0]);
3769
- g.push(p[1]);
3770
- b.push(p[2]);
3771
- }
3772
- }
3773
- const base = [median(r), median(g), median(b)];
3774
- let ink = 0;
3775
- for (const p of pts) if (delta(p, base) > threshold) ink++;
3776
- return pts.length ? ink / pts.length : 0;
3777
- }
3778
- function differenceHash(img, rect) {
3779
- const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
3780
- const cols = 9;
3781
- const rows = 8;
3782
- const cell = [];
3783
- for (let cy = 0; cy < rows; cy++) {
3784
- for (let cx = 0; cx < cols; cx++) {
3785
- const x0 = Math.floor(r.x + cx * r.w / cols);
3786
- const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
3787
- const y0 = Math.floor(r.y + cy * r.h / rows);
3788
- const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
3789
- let sum = 0;
3790
- let n = 0;
3791
- const sx = Math.max(1, Math.floor((x1 - x0) / 8));
3792
- const sy = Math.max(1, Math.floor((y1 - y0) / 8));
3793
- for (let y = y0; y < y1 && y < img.h; y += sy) {
3794
- for (let x = x0; x < x1 && x < img.w; x += sx) {
3795
- const p = at(img, Math.max(0, x), Math.max(0, y));
3796
- sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
3797
- n++;
3798
- }
3799
- }
3800
- cell.push(n ? sum / n : 0);
3801
- }
3802
- }
3803
- let bits = "";
3804
- for (let cy = 0; cy < rows; cy++) {
3805
- let byte = 0;
3806
- for (let cx = 0; cx < cols - 1; cx++) {
3807
- const l = cell[cy * cols + cx];
3808
- const rr = cell[cy * cols + cx + 1];
3809
- byte = byte << 1 | (l > rr ? 1 : 0);
3810
- }
3811
- bits += byte.toString(16).padStart(2, "0");
3812
- }
3813
- return bits;
3814
- }
3815
- function hammingDistance(a, b) {
3816
- let d = 0;
3817
- for (let i = 0; i < Math.min(a.length, b.length); i++) {
3818
- let x = parseInt(a[i], 16) ^ parseInt(b[i], 16);
3819
- while (x) {
3820
- d += x & 1;
3821
- x >>= 1;
3822
- }
3823
- }
3824
- return d + Math.abs(a.length - b.length) * 4;
3825
- }
3826
- function luminance(c) {
3827
- const lin = (v) => {
3828
- const s = v / 255;
3829
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
3830
- };
3831
- return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
3832
- }
3833
- function lightness(c) {
3834
- const y = luminance(c);
3835
- return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
3836
- }
3837
- function medianColour(img, rect) {
3838
- const r = [];
3839
- const g = [];
3840
- const b = [];
3841
- const x0 = Math.max(0, Math.floor(rect.x));
3842
- const y0 = Math.max(0, Math.floor(rect.y));
3843
- const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
3844
- const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
3845
- const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
3846
- for (let y = y0; y < y1; y += step) {
3847
- for (let x = x0; x < x1; x += step) {
3848
- const p = at(img, x, y);
3849
- r.push(p[0]);
3850
- g.push(p[1]);
3851
- b.push(p[2]);
3852
- }
3853
- }
3854
- return [median(r), median(g), median(b)];
3855
- }
3856
- function edgeEnergy(img, width) {
3857
- const cols = Math.max(2, Math.min(width, img.w));
3858
- const rows = Math.max(2, Math.round(cols * img.h / img.w));
3859
- const lum = [];
3860
- for (let cy = 0; cy < rows; cy++) {
3861
- for (let cx = 0; cx < cols; cx++) {
3862
- const x = Math.min(img.w - 1, Math.floor((cx + 0.5) * img.w / cols));
3863
- const y = Math.min(img.h - 1, Math.floor((cy + 0.5) * img.h / rows));
3864
- const p = at(img, x, y);
3865
- lum.push(0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]);
3866
- }
3867
- }
3868
- let sum = 0;
3869
- let n = 0;
3870
- for (let cy = 0; cy < rows; cy++) {
3871
- for (let cx = 1; cx < cols; cx++) {
3872
- sum += Math.abs(lum[cy * cols + cx] - lum[cy * cols + cx - 1]);
3873
- n++;
3874
- }
3875
- }
3876
- return n ? sum / n : 0;
3877
- }
3878
- function measureStill(img) {
3879
- const g = groundColour(img);
3880
- const strong = cardBounds(img, g, 60);
3881
- const faint = cardBounds(img, g, 12) ?? strong;
3882
- const card = faint;
3883
- const bleed = [];
3884
- let pad = null;
3885
- let widthPct = null;
3886
- let shadow = null;
3887
- let edge = 0;
3888
- let separation = null;
3889
- if (card) {
3890
- pad = {
3891
- left: card.x / img.w,
3892
- right: (img.w - card.x - card.w) / img.w,
3893
- top: card.y / img.h,
3894
- bottom: (img.h - card.y - card.h) / img.h
3895
- };
3896
- for (const side of ["left", "right", "top", "bottom"])
3897
- if (pad[side] < 5e-3) bleed.push(side);
3898
- widthPct = card.w / img.w;
3899
- shadow = haloReading(img, g, card);
3900
- edge = edgeContrast(img, g, card);
3901
- const body = strong ?? card;
3902
- const inner = {
3903
- x: body.x + body.w * 0.1,
3904
- y: body.y + body.h * 0.1,
3905
- w: body.w * 0.8,
3906
- h: body.h * 0.8
3907
- };
3908
- separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
3909
- }
3910
- const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
3911
- return {
3912
- w: img.w,
3913
- h: img.h,
3914
- ground: hex(g),
3915
- card,
3916
- widthPct,
3917
- pad,
3918
- bleed,
3919
- shadow,
3920
- edge,
3921
- ink: inkCoverage(img, inkRect),
3922
- separation,
3923
- hash: differenceHash(img, inkRect)
3924
- };
3925
- }
3926
-
3927
- // src/plugin/moments.ts
3928
- var STEP_SETTLE_SECONDS = 0.4;
3929
- var NOT_A_GESTURE = /* @__PURE__ */ new Set(["wait", "goto", "sleep"]);
3930
- function momentCandidates(doc, duration) {
3931
- const rated = ratedSegments3(doc);
3932
- const out = [];
3933
- const push = (c) => {
3934
- if (c.time < 0 || c.time > duration) return;
3935
- if (out.some((o) => Math.abs(o.time - c.time) < 0.25)) return;
3936
- out.push(c);
3937
- };
3938
- const steps = doc.source.meta.steps ?? [];
3939
- for (let i = 0; i < steps.length; i++) {
3940
- const s = steps[i];
3941
- if (s.skipped || NOT_A_GESTURE.has(s.do)) continue;
3942
- const t = stepOutputTime(rated, s, settleAfter(steps, i));
3943
- if (t === null) continue;
3944
- push({ time: t, source: "step", step: s.id ?? s.step });
3945
- }
3946
- let dropped = 0;
3947
- const apexes = [];
3948
- for (const z of doc.zoom) {
3949
- const ext = spanOutputExtent3(rated, z.in, z.out);
3950
- if (ext) apexes.push((ext.start + ext.end) / 2);
3951
- else dropped++;
3952
- }
3953
- for (const t of apexes.sort((a, b) => a - b))
3954
- push({ time: t, source: "zoom" });
3955
- if (!out.length) {
3956
- for (const p of [0.1, 0.3, 0.5, 0.7, 0.9])
3957
- push({ time: p * duration, source: "spread" });
3958
- }
3959
- return { candidates: out, dropped };
3960
- }
3961
- function settleAfter(steps, i) {
3962
- const next = steps[i + 1];
3963
- if (next && next.do === "wait" && !next.skipped)
3964
- return Math.max(0, next.tEnd - steps[i].tEnd);
3965
- return STEP_SETTLE_SECONDS;
3966
- }
3967
- function stepOutputTime(rated, step, settle) {
3968
- const src = step.tEnd + settle;
3969
- const ext = spanOutputExtent3(rated, src, src + 1e-3);
3970
- if (!ext) return null;
3971
- return ext.start;
3972
- }
3973
- function resolveStepTime(doc, entry) {
3974
- const m = /^step:([^+-]+)([+-]\d+(?:\.\d+)?)?$/.exec(entry.trim());
3975
- if (!m) return null;
3976
- const key = m[1];
3977
- const offset = m[2] ? Number(m[2]) : STEP_SETTLE_SECONDS;
3978
- const steps = doc.source.meta.steps ?? [];
3979
- const step = steps.find((s) => s.id === key) ?? (/^\d+$/.test(key) ? steps.find((s) => s.step === Number(key)) : void 0);
3980
- if (!step) {
3981
- const known = steps.map((s) => s.id ?? String(s.step)).filter((s) => s.length);
3982
- throw new Error(
3983
- `--times ${entry}: no step "${key}" in the take's step timeline${known.length ? ` (steps: ${known.join(", ")})` : " (the take carries none: record it with the current CLI)"}`
3984
- );
3985
- }
3986
- const t = stepOutputTime(ratedSegments3(doc), step, offset);
3987
- if (t === null)
3988
- throw new Error(
3989
- `--times ${entry}: step "${key}" (${step.tEnd.toFixed(2)}s in the recording) falls outside the cut`
3990
- );
3991
- return t;
3992
- }
3993
- var DUPLICATE_BITS = 6;
3994
- var POPULATED_INK = 0.12;
3995
- function pickMoments(measured, opts = {}) {
3996
- const minInk = opts.minInk ?? 0.06;
3997
- const relative4 = opts.relativeInk ?? 0.4;
3998
- const bits = opts.duplicateBits ?? DUPLICATE_BITS;
3999
- const inks = measured.map((m) => m.ink).sort((a, b) => a - b);
4000
- const median2 = inks[inks.length >> 1] ?? 0;
4001
- const floor = Math.max(minInk, Math.min(POPULATED_INK, median2 * relative4));
4002
- const kept = [];
4003
- const dropped = [];
4004
- for (const m of measured) {
4005
- if (m.ink < floor) {
4006
- dropped.push(
4007
- `blank at ${m.time.toFixed(2)}s: ${Math.round(m.ink * 100)}% ink (the floor is ${Math.round(floor * 100)}%; the take's median is ${Math.round(median2 * 100)}%)`
4008
- );
4009
- continue;
4010
- }
4011
- const twin = kept.find((k) => hammingDistance(k.hash, m.hash) <= bits);
4012
- if (twin) {
4013
- dropped.push(
4014
- `${m.time.toFixed(2)}s is the same frame as ${twin.time.toFixed(2)}s`
4015
- );
4016
- continue;
4017
- }
4018
- kept.push(m);
4019
- }
4020
- if (!kept.length && measured.length) {
4021
- const best = [...measured].sort((a, b) => b.ink - a.ink)[0];
4022
- kept.push(best);
4023
- dropped.push(
4024
- `every candidate is under the blank floor; kept ${best.time.toFixed(2)}s (${Math.round(best.ink * 100)}% ink) as the least empty`
4025
- );
4026
- }
4027
- return { times: kept.map((k) => k.time), dropped };
4028
- }
4029
-
4030
- // src/plugin/motionPlan.ts
4031
- import {
4032
- END_CARD_FROM,
4033
- END_CARD_SECONDS,
4034
- applyTemplate,
4035
- docOutputDuration,
4036
- dropTemplate,
4037
- endCardClips,
4038
- migrateMotion,
4039
- ratedSegments as ratedSegments4,
4040
- sameMedia,
4041
- spanOutputExtent as spanOutputExtent4
4042
- } from "@vosjs/studio-core";
4043
- var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
4044
- "x-feed-cut",
4045
- "youtube-main-demo",
4046
- "shorts-linkedin-vertical-cut"
4047
- ]);
4048
- var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
4049
- var BED_ID = "bed";
4050
- var CLICK_ID_PREFIX = "click-";
4051
- var CAPTION_ID_PREFIX = "caption-";
4052
- var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
4053
- var round3 = (v) => Math.round(v * 1e3) / 1e3;
4054
- var TRANSITION_CLIP_MIN = 1;
4055
- function proposeTransitions(input, kind) {
4056
- const doc = structuredClone(input);
4057
- const skipped = [];
4058
- let count = 0;
4059
- const steps = doc.source.meta.steps ?? [];
4060
- for (const step of steps) {
4061
- if (!step.navigated || step.skipped) continue;
4062
- const name = step.id ?? String(step.step);
4063
- const wait = step.do === "wait";
4064
- const outEnd = round3(wait ? step.tStart : step.tEnd);
4065
- const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
4066
- const segs = doc.segments;
4067
- if (segs.some(
4068
- (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
4069
- ))
4070
- continue;
4071
- const i = segs.findIndex(
4072
- (s) => !s.media && outEnd >= s.in + TRANSITION_CLIP_MIN && inStart <= s.out - TRANSITION_CLIP_MIN
4073
- );
4074
- if (i < 0) {
4075
- skipped.push(
4076
- `no transition at step ${name}: the page change sits too close to a clip's edge`
4077
- );
4078
- continue;
4079
- }
4080
- const seg = segs[i];
4081
- const before = { ...seg, out: outEnd, anim: { exit: kind } };
4082
- const after = { ...seg, in: inStart, anim: { enter: kind } };
4083
- if (seg.anim?.enter !== void 0) before.anim.enter = seg.anim.enter;
4084
- if (seg.anim?.exit !== void 0) after.anim.exit = seg.anim.exit;
4085
- doc.segments = [...segs.slice(0, i), before, after, ...segs.slice(i + 1)];
4086
- count++;
4087
- }
4088
- return { doc, count, skipped };
4089
- }
4090
- function pickTrack(catalog, ask) {
4091
- if (!catalog || !ask || off(ask)) return null;
4092
- const want = ask.trim().toLowerCase();
4093
- return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
4094
- }
4095
- function clickTimes(doc, range) {
4096
- const rated = ratedSegments4(doc);
4097
- const out = [];
4098
- for (const e of doc.source.cursor) {
4099
- if (e.type !== "down") continue;
4100
- const src = e.t / 1e3;
4101
- const ext = spanOutputExtent4(rated, src, src + 1e-3);
4102
- if (!ext) continue;
4103
- const t = ext.start;
4104
- if (t < range[0] || t > range[1]) continue;
4105
- if (out.length && t - out[out.length - 1] < 0.12) continue;
4106
- out.push(+(t - range[0]).toFixed(3));
4107
- }
4108
- return out;
4109
- }
4110
- var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
4111
- const rate = s.rate && s.rate > 0 ? s.rate : 1;
4112
- return acc + (s.out - s.in) / rate;
4113
- }, 0);
4114
- function templateWords(words2) {
4115
- const headline = (words2.headline ?? "").trim();
4116
- const kicker = (words2.kicker ?? "").trim();
4117
- const brand = (words2.brand ?? "").trim();
4118
- const sub = [brand, (words2.release ?? "").trim()].filter(Boolean).join(" ");
4119
- const out = {};
4120
- if (headline) {
4121
- out["stage-title"] = headline;
4122
- out["endcard-title"] = headline;
4123
- }
4124
- if (kicker) out["stage-kicker"] = kicker;
4125
- if (brand) {
4126
- out["stage-brand"] = brand;
4127
- out["endcard-mark"] = brand;
4128
- }
4129
- if (sub && sub !== headline) out["endcard-sub"] = sub;
4130
- return out;
3631
+ if (kicker) out["stage-kicker"] = kicker;
3632
+ if (brand) {
3633
+ out["stage-brand"] = brand;
3634
+ out["endcard-mark"] = brand;
3635
+ }
3636
+ if (sub && sub !== headline) out["endcard-sub"] = sub;
3637
+ return out;
4131
3638
  }
4132
3639
  function resolveAnchor(doc, at2) {
4133
3640
  if (typeof at2 !== "object") return at2;
@@ -4328,7 +3835,6 @@ function destinationMechanics(d, doc) {
4328
3835
  const set = [];
4329
3836
  const unset = [];
4330
3837
  const notes = [];
4331
- if (d.kind !== "video") return { set, unset, notes };
4332
3838
  const loop = LOOP_DESTINATIONS.has(d.id);
4333
3839
  const sound = SOUND_DESTINATIONS.has(d.id);
4334
3840
  const portrait = d.px.w / d.px.h < 0.9;
@@ -4380,161 +3886,12 @@ function isLightHexGround(hex2) {
4380
3886
  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
4381
3887
  }
4382
3888
 
4383
- // src/plugin/posterDoc.ts
4384
- import { existsSync as existsSync4 } from "fs";
4385
- import { readFile as readFile4 } from "fs/promises";
4386
- import { isAbsolute, join as join4, resolve as resolve3 } from "path";
4387
- import {
4388
- computeCardLayout,
4389
- docStillTime as docStillTime2,
4390
- migrateHostedDoc,
4391
- overlayPlainText,
4392
- overlayRect,
4393
- resolveOverlayStyle as resolveOverlayStyle2
4394
- } from "@vosjs/studio-core";
4395
- var POSTER_CLASSES = [
4396
- "landscape",
4397
- "square",
4398
- "portrait",
4399
- "tile"
4400
- ];
4401
- var TILE_MAX_PX = 700;
4402
- function posterClassFor(px) {
4403
- if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
4404
- const r = px.w / Math.max(1, px.h);
4405
- if (r < 0.87) return "portrait";
4406
- if (r <= 1.15) return "square";
4407
- return "landscape";
4408
- }
4409
- var ROLE_ALL = "poster";
4410
- var roleFor = (cls) => `poster-${cls}`;
4411
- async function readPosterAt(cls, path, takeDir, from) {
4412
- let file = path;
4413
- let docDir = path;
4414
- if (existsSync4(join4(path, "doc.json"))) {
4415
- file = join4(path, "doc.json");
4416
- } else if (!/\.json$/i.test(path) || !existsSync4(path)) {
4417
- return null;
4418
- } else {
4419
- docDir = resolve3(path, "..");
4420
- }
4421
- let raw;
4422
- try {
4423
- raw = JSON.parse(await readFile4(file, "utf8"));
4424
- } catch {
4425
- return null;
4426
- }
4427
- if (!raw || typeof raw !== "object") return null;
4428
- const source = raw.source;
4429
- if (!source || typeof source.videoKey !== "string") return null;
4430
- const doc = migrateHostedDoc(
4431
- raw
4432
- );
4433
- const ownFootage = existsSync4(join4(docDir, "meta.json")) && existsSync4(join4(docDir, source.videoKey));
4434
- const sync = readSyncState(docDir);
4435
- return {
4436
- cls,
4437
- file,
4438
- takeDir: ownFootage ? docDir : takeDir,
4439
- ownFootage,
4440
- doc,
4441
- ...sync?.vosId ? { vosId: sync.vosId } : {},
4442
- from
4443
- };
4444
- }
4445
- async function findPosterDocs(takeDir, launchRoles) {
4446
- const out = {};
4447
- const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
4448
- for (const cls of POSTER_CLASSES) {
4449
- const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
4450
- const candidates = [];
4451
- if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
4452
- const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
4453
- candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
4454
- }
4455
- candidates.push({
4456
- path: join4(takeDir, "poster", cls),
4457
- from: `poster/${cls}`
4458
- });
4459
- candidates.push({ path: join4(takeDir, "poster"), from: "poster" });
4460
- for (const c of candidates) {
4461
- const ref = await readPosterAt(cls, c.path, takeDir, c.from);
4462
- if (ref) {
4463
- out[cls] = ref;
4464
- break;
4465
- }
4466
- }
4467
- }
4468
- return out;
4469
- }
4470
- function posterStillTime(doc, duration) {
4471
- return Math.max(0, Math.min(duration, docStillTime2(doc)));
4472
- }
4473
- var DESIGN_H = 1080;
4474
- function designFrame(px) {
4475
- return {
4476
- W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
4477
- H: DESIGN_H
4478
- };
4479
- }
4480
- function posterShotRect(doc, px) {
4481
- const { W, H } = designFrame(px);
4482
- const meta = doc.source.meta;
4483
- const layout = computeCardLayout(
4484
- doc.frame,
4485
- {
4486
- width: meta.captureWidth ?? meta.width,
4487
- height: meta.captureHeight ?? meta.height
4488
- },
4489
- W,
4490
- H
4491
- );
4492
- const r3 = (v) => Math.round(v * 1e3) / 1e3;
4493
- return {
4494
- x: r3(layout.cardX / W),
4495
- y: r3(layout.cardY / H),
4496
- w: r3(layout.cardW / W),
4497
- h: r3(layout.cardH / H)
4498
- };
4499
- }
4500
- function estimateWidth(text, font, letterSpacingPx = 0) {
4501
- const m = /(\d+(?:\.\d+)?)px/.exec(font);
4502
- const px = m ? Number(m[1]) : 32;
4503
- return text.length * (px * 0.55 + letterSpacingPx);
4504
- }
4505
- function posterTextBoxes(doc, px, time) {
4506
- const { W, H } = designFrame(px);
4507
- const out = [];
4508
- for (const clip of doc.overlays ?? []) {
4509
- if (clip.kind !== "text") continue;
4510
- if (time < clip.start || time > clip.start + clip.duration) continue;
4511
- const rect = overlayRect(clip, estimateWidth, W, H);
4512
- const style = resolveOverlayStyle2(clip);
4513
- const r3 = (v) => Math.round(v * 1e3) / 1e3;
4514
- out.push({
4515
- x: r3((rect.cx - rect.w / 2) / W),
4516
- y: r3((rect.cy - rect.h / 2) / H),
4517
- w: r3(rect.w / W),
4518
- h: r3(rect.h / H),
4519
- role: clip.preset === "title" ? "headline" : "body",
4520
- label: overlayPlainText(clip).split("\n")[0].slice(0, 40),
4521
- .../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
4522
- });
4523
- }
4524
- return out;
4525
- }
4526
-
4527
3889
  // src/plugin/deliver.ts
4528
3890
  var CHANNEL_ALIASES = {
4529
- ph: "producthunt",
4530
3891
  yt: "youtube",
4531
3892
  gh: "github",
4532
- li: "linkedin",
4533
3893
  shorts: "shorts-linkedin"
4534
3894
  };
4535
- var NOT_FROM_FOOTAGE = {
4536
- "cws-icon": "an icon is brand art, not footage \u2014 supply it from the kit"
4537
- };
4538
3895
  function resolveChannels(raw) {
4539
3896
  const known = [...new Set(DESTINATIONS.map((d) => d.channel))];
4540
3897
  const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
@@ -4545,26 +3902,26 @@ function resolveChannels(raw) {
4545
3902
  const id = CHANNEL_ALIASES[w] ?? w;
4546
3903
  if (!known.includes(id))
4547
3904
  throw new Error(
4548
- `unknown channel "${w}" \u2014 channels: ${known.join(", ")} (or all)`
3905
+ `unknown channel "${w}" \u2014 deliver renders video cuts for: ${known.join(", ")} (or all)`
4549
3906
  );
4550
3907
  if (!out.includes(id)) out.push(id);
4551
3908
  }
4552
3909
  return out;
4553
3910
  }
4554
3911
  async function readBrandBesideTake(dir, explicit) {
4555
- const candidates = explicit ? [explicit] : [join5(dir, "BRAND.md"), join5(dir, "..", "BRAND.md")];
3912
+ const candidates = explicit ? [explicit] : [join4(dir, "BRAND.md"), join4(dir, "..", "BRAND.md")];
4556
3913
  for (const file of candidates) {
4557
- if (!existsSync5(file)) continue;
4558
- const roles = parseFrontmatter(await readFile5(file, "utf8"));
3914
+ if (!existsSync4(file)) continue;
3915
+ const roles = parseFrontmatter(await readFile4(file, "utf8"));
4559
3916
  return { file, roles };
4560
3917
  }
4561
3918
  return null;
4562
3919
  }
4563
3920
  async function readLaunchBesideTake(dir, explicit) {
4564
- const candidates = explicit ? [explicit] : [join5(dir, "LAUNCH.md"), join5(dir, "..", "LAUNCH.md")];
3921
+ const candidates = explicit ? [explicit] : [join4(dir, "LAUNCH.md"), join4(dir, "..", "LAUNCH.md")];
4565
3922
  for (const file of candidates) {
4566
- if (!existsSync5(file)) continue;
4567
- return { file, roles: parseFrontmatter(await readFile5(file, "utf8")) };
3923
+ if (!existsSync4(file)) continue;
3924
+ return { file, roles: parseFrontmatter(await readFile4(file, "utf8")) };
4568
3925
  }
4569
3926
  return null;
4570
3927
  }
@@ -4589,7 +3946,7 @@ async function resolveLook(dir, opts) {
4589
3946
  roles
4590
3947
  };
4591
3948
  }
4592
- function lookOverrides(look, placement, size, video, opts) {
3949
+ function lookOverrides(look, placement, size, video, opts = {}) {
4593
3950
  const inset = cardInset(look, size, video, placement);
4594
3951
  const set = [
4595
3952
  "frame.fit=contain",
@@ -4606,70 +3963,8 @@ function lookOverrides(look, placement, size, video, opts) {
4606
3963
  set.push(`frame.borderColor=${look.borderColor ?? "#000000"}`);
4607
3964
  }
4608
3965
  if (!opts.keepMedia) set.push("frame.backgroundMedia=null");
4609
- if (opts.still) {
4610
- set.push(
4611
- "zoom=[]",
4612
- "tilt=[]",
4613
- "cursor.visible=false",
4614
- "cursor.clickFx.style=none"
4615
- );
4616
- }
4617
3966
  return set;
4618
3967
  }
4619
- var PROBE_WIDTH = 640;
4620
- async function pickStillTimes(browser, dir, doc, duration, opts) {
4621
- const { candidates, dropped } = momentCandidates(doc, duration);
4622
- const notes = [];
4623
- if (dropped > 0)
4624
- notes.push(
4625
- `${dropped} of ${doc.zoom.length} zoom apex(es) fall outside the cut`
4626
- );
4627
- if (!candidates.length) return { times: [], moments: [], notes };
4628
- const meta = doc.source.meta;
4629
- const vw = meta.captureWidth ?? meta.width;
4630
- const vh = meta.captureHeight ?? meta.height;
4631
- const probeDir = await mkdtemp(join5(tmpdir(), "vos-moments-"));
4632
- try {
4633
- const probe3 = await framesTake(browser, dir, {
4634
- times: candidates.map((c) => c.time),
4635
- width: PROBE_WIDTH,
4636
- height: Math.max(2, Math.round(PROBE_WIDTH * vh / vw / 2) * 2),
4637
- outDir: probeDir,
4638
- overrides: {
4639
- ...opts.overrides,
4640
- set: [...SCREENSHOT_DEFAULTS, ...opts.overrides?.set ?? []]
4641
- }
4642
- });
4643
- const byTime = /* @__PURE__ */ new Map();
4644
- for (const frame of probe3.frames) {
4645
- const img = decodePng(new Uint8Array(await readFile5(frame.file)));
4646
- if (!img) continue;
4647
- const whole = { x: 0, y: 0, w: img.w, h: img.h };
4648
- byTime.set(+frame.time.toFixed(3), {
4649
- ink: inkCoverage(img, whole),
4650
- hash: differenceHash(img, whole)
4651
- });
4652
- }
4653
- const measured = [];
4654
- for (const c of candidates) {
4655
- const m = byTime.get(+c.time.toFixed(3));
4656
- if (m) measured.push({ time: c.time, ...m });
4657
- }
4658
- const pick = pickMoments(measured);
4659
- const kept = candidates.filter((c) => pick.times.includes(c.time));
4660
- const bySource = (s) => candidates.filter((c) => c.source === s).length;
4661
- notes.push(
4662
- `${candidates.length} candidate(s): ${bySource("step")} from steps, ${bySource("zoom")} from zoom apexes, ${bySource("spread")} from the spread; ${kept.length} kept`
4663
- );
4664
- return {
4665
- times: pick.times,
4666
- moments: kept,
4667
- notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
4668
- };
4669
- } finally {
4670
- await rm2(probeDir, { recursive: true, force: true });
4671
- }
4672
- }
4673
3968
  var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
4674
3969
  var specWords = (d) => {
4675
3970
  const parts = [`${d.px.w}x${d.px.h}`];
@@ -4679,62 +3974,17 @@ var specWords = (d) => {
4679
3974
  parts.push(`\u2264${(d.maxBytes / 1024 / 1024).toFixed(0)}MB`);
4680
3975
  return parts.join(" ");
4681
3976
  };
4682
- var FULL_BLEED = [
4683
- "frame.padding=0",
4684
- "frame.radius=0",
4685
- "frame.shadow=0",
4686
- "frame.border=0",
4687
- "frame.browserBar.kind=none",
4688
- "cursor.visible=false",
4689
- "cursor.clickFx.style=none"
4690
- ];
4691
- var SCREENSHOT_DEFAULTS = [...FULL_BLEED, "zoom=[]", "tilt=[]"];
4692
- function stillOverridesFor(d, opts, video) {
4693
- const set = [];
4694
- if (d.genre === "screenshot" && !opts.composed) {
4695
- if (d.fit === "cover") set.push("frame.fit=cover");
4696
- set.push(...SCREENSHOT_DEFAULTS);
4697
- } else if (d.genre === "card" && !opts.composed && opts.look && video && d.px) {
4698
- set.push(
4699
- ...lookOverrides(opts.look, "card", d.px, video, {
4700
- still: true,
4701
- keepMedia: opts.overrides?.background !== void 0
4702
- })
4703
- );
4704
- } else {
4705
- if (d.fit === "cover") set.push("frame.fit=cover");
4706
- if (d.genre === "card" && !opts.composed) set.push(...FULL_BLEED);
4707
- }
4708
- set.push(...opts.overrides?.set ?? []);
4709
- if (!set.length) return opts.overrides;
4710
- return { ...opts.overrides, set };
4711
- }
4712
3977
  async function deliverTake(browser, dir, opts) {
4713
3978
  const take = await loadTake(dir);
4714
3979
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
4715
3980
  const doc = take.doc;
4716
3981
  const duration = docOutputDuration2(doc);
4717
3982
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4718
- const outDir = resolve4(opts.outDir ?? join5(dir, "kit"));
3983
+ const outDir = resolve3(opts.outDir ?? join4(dir, "kit"));
4719
3984
  await mkdir3(outDir, { recursive: true });
4720
3985
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4721
3986
  const assets = [];
4722
3987
  const skipped = [];
4723
- let stillTimes;
4724
- let moments;
4725
- if (opts.times?.length) {
4726
- stillTimes = opts.times;
4727
- moments = opts.times.map((time) => ({ time, source: "times" }));
4728
- } else {
4729
- opts.onPhase?.("moments (the step timeline, the zoom apexes, the spread)");
4730
- const picked = await pickStillTimes(browser, dir, doc, duration, opts);
4731
- stillTimes = picked.times;
4732
- moments = picked.moments;
4733
- for (const n of picked.notes) {
4734
- if (n.startsWith("moment: ")) skipped.push(n);
4735
- else opts.onPhase?.(`note: ${n}`);
4736
- }
4737
- }
4738
3988
  const meta0 = doc.source.meta;
4739
3989
  const video = {
4740
3990
  w: meta0.captureWidth ?? meta0.width,
@@ -4745,7 +3995,6 @@ async function deliverTake(browser, dir, opts) {
4745
3995
  if (opts.look) {
4746
3996
  set.push(
4747
3997
  ...lookOverrides(opts.look, "hero", d.px, video, {
4748
- still: false,
4749
3998
  keepMedia: opts.overrides?.background !== void 0
4750
3999
  })
4751
4000
  );
@@ -4757,181 +4006,59 @@ async function deliverTake(browser, dir, opts) {
4757
4006
  if (!set.length && !mech.unset.length) return opts.overrides;
4758
4007
  return { ...opts.overrides, set, unset: mech.unset };
4759
4008
  };
4760
- const posters = await findPosterDocs(dir, opts.launchRoles);
4761
- const posterCardIds = /* @__PURE__ */ new Set();
4762
- const missing = /* @__PURE__ */ new Set();
4763
- for (const d of destinations) {
4764
- if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
4765
- continue;
4766
- const cls = posterClassFor(d.px);
4767
- const ref = posters[cls];
4768
- if (!ref) {
4769
- missing.add(cls);
4770
- continue;
4771
- }
4772
- posterCardIds.add(d.id);
4773
- const label = `${d.channel} ${d.asset}`;
4774
- const posterDuration = docOutputDuration2(ref.doc);
4775
- const time = posterStillTime(ref.doc, posterDuration);
4776
- opts.onPhase?.(
4777
- `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
4778
- );
4779
- const shotDir = await mkdtemp(join5(tmpdir(), "vos-poster-"));
4780
- try {
4781
- const captured = await framesTake(browser, ref.takeDir, {
4782
- times: [time],
4783
- width: d.px.w,
4784
- height: d.px.h,
4785
- outDir: shotDir,
4786
- doc: ref.doc,
4787
- overrides: opts.overrides
4788
- });
4789
- const to = join5(outDir, `${d.id}.png`);
4790
- await rename3(captured.frames[0].file, to);
4791
- const bytes = (await stat(to)).size;
4792
- if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4793
- skipped.push(
4794
- `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
4795
- );
4796
- continue;
4797
- }
4798
- const shot = posterShotRect(ref.doc, d.px);
4799
- assets.push({
4800
- channel: d.channel,
4801
- asset: d.asset,
4802
- destination: d.id,
4803
- path: relative(outDir, to),
4804
- w: d.px.w,
4805
- h: d.px.h,
4806
- bytes,
4807
- seconds: null,
4808
- frameTime: time,
4809
- source: "poster",
4810
- poster: {
4811
- class: cls,
4812
- file: relative(dir, ref.file),
4813
- ...ref.vosId ? { vosId: ref.vosId } : {}
4814
- },
4815
- text: posterTextBoxes(ref.doc, d.px, time),
4816
- shot,
4817
- ...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
4818
- });
4819
- } finally {
4820
- await rm2(shotDir, { recursive: true, force: true });
4821
- }
4822
- }
4823
- if (missing.size) {
4824
- const classes = [...missing].sort().join(", ");
4825
- skipped.push(
4826
- `note: no poster document for the ${classes} class${missing.size > 1 ? "es" : ""} beside the take (poster/<class>/doc.json, or LAUNCH.md poster:), so those cards are the take's own frame`
4827
- );
4828
- }
4829
4009
  for (const d of destinations) {
4830
4010
  const label = `${d.channel} ${d.asset}`;
4831
- const excuse = NOT_FROM_FOOTAGE[d.id];
4832
- if (excuse) {
4833
- skipped.push(`${label}: ${excuse}`);
4011
+ if (d.minSeconds !== void 0 && videoSeconds < d.minSeconds) {
4012
+ skipped.push(
4013
+ `${label}: spec wants ${d.minSeconds}-${d.maxSeconds ?? "\u221E"}s, the ${opts.range ? "range" : "take"} is ${videoSeconds.toFixed(0)}s`
4014
+ );
4834
4015
  continue;
4835
4016
  }
4836
- if (posterCardIds.has(d.id)) continue;
4837
- if (d.kind === "video") {
4838
- if (d.minSeconds !== void 0 && videoSeconds < d.minSeconds) {
4839
- skipped.push(
4840
- `${label}: spec wants ${d.minSeconds}-${d.maxSeconds ?? "\u221E"}s, the ${opts.range ? "range" : "take"} is ${videoSeconds.toFixed(0)}s`
4841
- );
4842
- continue;
4843
- }
4844
- let range = opts.range;
4845
- let seconds = videoSeconds;
4846
- if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
4847
- if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
4848
- range = [0, d.maxSeconds];
4849
- seconds = d.maxSeconds;
4850
- opts.onPhase?.(
4851
- `note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
4852
- );
4853
- } else {
4854
- skipped.push(
4855
- `${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
4856
- );
4857
- continue;
4858
- }
4859
- }
4860
- opts.onPhase?.(`${label} (${specWords(d)})`);
4861
- const outFile = join5(outDir, `${d.id}.${d.format}`);
4862
- const bitrate = d.maxBytes !== void 0 ? Math.min(
4863
- 1e7,
4864
- Math.floor(d.maxBytes * 8 / seconds * 0.85)
4865
- ) : void 0;
4866
- const result = await renderTake(browser, dir, outFile, {
4867
- width: d.px.w,
4868
- height: d.px.h,
4869
- format: "mp4",
4870
- parallel: opts.parallel,
4871
- range,
4872
- bitrate,
4873
- overrides: videoOverrides(d),
4874
- onProgress: opts.onProgress
4875
- });
4876
- if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
4017
+ let range = opts.range;
4018
+ let seconds = videoSeconds;
4019
+ if (d.maxSeconds !== void 0 && videoSeconds > d.maxSeconds) {
4020
+ if (LOOP_DESTINATIONS.has(d.id) && !opts.range) {
4021
+ range = [0, d.maxSeconds];
4022
+ seconds = d.maxSeconds;
4023
+ opts.onPhase?.(
4024
+ `note: ${label} takes the first ${d.maxSeconds}s of the ${videoSeconds.toFixed(0)}s take (a loop's cap)`
4025
+ );
4026
+ } else {
4877
4027
  skipped.push(
4878
- `${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
4028
+ `${label}: spec caps at ${d.maxSeconds}s, the take is ${videoSeconds.toFixed(0)}s \u2014 cut it (--range, or trim segments in doc.json)`
4879
4029
  );
4880
4030
  continue;
4881
4031
  }
4882
- assets.push({
4883
- channel: d.channel,
4884
- asset: d.asset,
4885
- destination: d.id,
4886
- path: relative(outDir, outFile),
4887
- w: result.width,
4888
- h: result.height,
4889
- bytes: result.bytes,
4890
- seconds: result.duration,
4891
- frameTime: null
4892
- });
4893
- continue;
4894
4032
  }
4895
- const wanted = d.kind === "still-set" ? stillTimes.slice(0, d.count?.max ?? stillTimes.length) : stillTimes.slice(0, 1);
4896
- opts.onPhase?.(`${label} (${specWords(d)}, ${wanted.length} still(s))`);
4897
- const overrides = stillOverridesFor(d, opts, video);
4898
- const captured = await framesTake(browser, dir, {
4899
- times: wanted,
4033
+ opts.onPhase?.(`${label} (${specWords(d)})`);
4034
+ const outFile = join4(outDir, `${d.id}.${d.format}`);
4035
+ const bitrate = d.maxBytes !== void 0 ? Math.min(1e7, Math.floor(d.maxBytes * 8 / seconds * 0.85)) : void 0;
4036
+ const result = await renderTake(browser, dir, outFile, {
4900
4037
  width: d.px.w,
4901
4038
  height: d.px.h,
4902
- outDir,
4903
- overrides
4039
+ format: "mp4",
4040
+ parallel: opts.parallel,
4041
+ range,
4042
+ bitrate,
4043
+ overrides: videoOverrides(d),
4044
+ onProgress: opts.onProgress
4904
4045
  });
4905
- for (let i = 0; i < captured.frames.length; i++) {
4906
- const frame = captured.frames[i];
4907
- const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
4908
- const to = join5(outDir, name);
4909
- await rename3(frame.file, to);
4910
- const bytes = (await stat(to)).size;
4911
- if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4912
- skipped.push(
4913
- `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
4914
- );
4915
- continue;
4916
- }
4917
- assets.push({
4918
- channel: d.channel,
4919
- asset: d.asset,
4920
- destination: d.id,
4921
- path: relative(outDir, to),
4922
- w: captured.width,
4923
- h: captured.height,
4924
- bytes,
4925
- seconds: null,
4926
- frameTime: frame.time,
4927
- ...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
4928
- });
4929
- }
4930
- if (d.count && captured.frames.length < d.count.min) {
4046
+ if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
4931
4047
  skipped.push(
4932
- `${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
4048
+ `${label}: ${overCeiling(result.bytes, d.maxBytes)} (kept at ${outFile})`
4933
4049
  );
4050
+ continue;
4934
4051
  }
4052
+ assets.push({
4053
+ channel: d.channel,
4054
+ asset: d.asset,
4055
+ destination: d.id,
4056
+ path: relative(outDir, outFile),
4057
+ w: result.width,
4058
+ h: result.height,
4059
+ bytes: result.bytes,
4060
+ seconds: result.duration
4061
+ });
4935
4062
  }
4936
4063
  const kit = {
4937
4064
  release: opts.release ?? null,
@@ -4939,19 +4066,18 @@ async function deliverTake(browser, dir, opts) {
4939
4066
  take: dir,
4940
4067
  produced: (/* @__PURE__ */ new Date()).toISOString(),
4941
4068
  specsVerified: CHANNEL_SPECS_VERIFIED,
4942
- moments,
4943
4069
  skipped,
4944
4070
  assets
4945
4071
  };
4946
- const kitFile = join5(outDir, "kit.json");
4072
+ const kitFile = join4(outDir, "kit.json");
4947
4073
  await writeFile5(kitFile, JSON.stringify(kit, null, 2));
4948
4074
  return { kit, kitFile, outDir };
4949
4075
  }
4950
4076
 
4951
4077
  // src/plugin/markAsset.ts
4952
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
4953
- import { existsSync as existsSync6 } from "fs";
4954
- import { isAbsolute as isAbsolute2, join as join6, resolve as resolve5 } from "path";
4078
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
4079
+ import { existsSync as existsSync5 } from "fs";
4080
+ import { isAbsolute, join as join5, resolve as resolve4 } from "path";
4955
4081
  function markExtension(url) {
4956
4082
  const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4957
4083
  if (!m) return null;
@@ -5032,389 +4158,464 @@ async function fetchBrandMarks(takeDir, roles) {
5032
4158
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
5033
4159
  bytes = new Uint8Array(await res.arrayBuffer());
5034
4160
  } else {
5035
- const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
5036
- if (!existsSync6(file)) throw new Error("no such file");
5037
- bytes = new Uint8Array(await readFile6(file));
4161
+ const file = isAbsolute(src) ? src : resolve4(takeDir, src);
4162
+ if (!existsSync5(file)) throw new Error("no such file");
4163
+ bytes = new Uint8Array(await readFile5(file));
4164
+ }
4165
+ } catch (e) {
4166
+ out.notes.push(
4167
+ `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
4168
+ );
4169
+ return null;
4170
+ }
4171
+ const aspect = imageAspect(bytes, ext);
4172
+ if (!aspect) {
4173
+ out.notes.push(
4174
+ `${role}: ${src} has no readable size; the wordmark stands in`
4175
+ );
4176
+ return null;
4177
+ }
4178
+ await mkdir4(join5(takeDir, "brand"), { recursive: true });
4179
+ await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
4180
+ return { key: `/brand/${name}.${ext}`, aspect };
4181
+ };
4182
+ out.light = await one("logoUrl", "mark");
4183
+ out.dark = await one("logoOnDarkUrl", "mark-on-dark");
4184
+ return out;
4185
+ }
4186
+
4187
+ // src/plugin/officialTemplates.ts
4188
+ var OFFICIAL_END_CARD_TITLE = "End card";
4189
+ function templateKey(s) {
4190
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
4191
+ }
4192
+ function findOfficialByRef(rows, ref) {
4193
+ const key = templateKey(ref);
4194
+ if (!key) return null;
4195
+ for (const row of rows) {
4196
+ if (templateKey(row.title) === key) return row;
4197
+ if (row.slug && templateKey(row.slug) === key) return row;
4198
+ }
4199
+ return null;
4200
+ }
4201
+
4202
+ // src/plugin/music.ts
4203
+ async function fetchMusicCatalog(origin) {
4204
+ const base = origin.replace(/\/+$/, "");
4205
+ const res = await fetch(`${base}/api/music`);
4206
+ if (!res.ok) throw new Error(`GET ${base}/api/music answered ${res.status}`);
4207
+ const body = await res.json();
4208
+ return {
4209
+ tracks: Array.isArray(body.tracks) ? body.tracks : [],
4210
+ sfx: Array.isArray(body.sfx) ? body.sfx : []
4211
+ };
4212
+ }
4213
+
4214
+ // src/plugin/fontName.ts
4215
+ import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
4216
+ function catalogFamily(family) {
4217
+ const name = family.trim();
4218
+ if (findFontFamily2(name)) return name;
4219
+ const base = name.replace(/\s+Variable$/i, "");
4220
+ if (base !== name) {
4221
+ const hit = findFontFamily2(base);
4222
+ if (hit) return hit.family;
4223
+ }
4224
+ return family;
4225
+ }
4226
+
4227
+ // src/plugin/callout.ts
4228
+ import { existsSync as existsSync6 } from "fs";
4229
+ import { readFile as readFile6 } from "fs/promises";
4230
+ import { join as join6 } from "path";
4231
+ import { lerpArray, sample } from "@vosjs/timeline";
4232
+ import {
4233
+ CALLOUT_DEFAULT_BODY_PX,
4234
+ CALLOUT_SHAPES,
4235
+ calloutClip,
4236
+ calloutLook,
4237
+ calloutGroundOf,
4238
+ cameraModel as cameraModel2,
4239
+ deltaE,
4240
+ docCardLayout as docCardLayout2,
4241
+ docZoomTrack as docZoomTrack2,
4242
+ hexToRgb,
4243
+ parseCssColour,
4244
+ pinBox as pinBox2,
4245
+ ratedSegments as ratedSegments5,
4246
+ resolvePins as resolvePins2,
4247
+ spanOutputExtent as spanOutputExtent5
4248
+ } from "@vosjs/studio-core";
4249
+
4250
+ // src/plugin/picture.ts
4251
+ import { inflateSync } from "zlib";
4252
+ var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
4253
+ function decodePng(bytes) {
4254
+ if (bytes.length < 33) return null;
4255
+ for (let i = 0; i < 8; i++) if (bytes[i] !== PNG_SIG[i]) return null;
4256
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4257
+ let pos = 8;
4258
+ let w = 0;
4259
+ let h = 0;
4260
+ let depth = 0;
4261
+ let colorType = 0;
4262
+ let interlace = 0;
4263
+ const idat = [];
4264
+ while (pos + 8 <= bytes.length) {
4265
+ const len = view.getUint32(pos);
4266
+ const type = String.fromCharCode(
4267
+ bytes[pos + 4],
4268
+ bytes[pos + 5],
4269
+ bytes[pos + 6],
4270
+ bytes[pos + 7]
4271
+ );
4272
+ const start = pos + 8;
4273
+ if (type === "IHDR") {
4274
+ w = view.getUint32(start);
4275
+ h = view.getUint32(start + 4);
4276
+ depth = bytes[start + 8];
4277
+ colorType = bytes[start + 9];
4278
+ interlace = bytes[start + 12];
4279
+ } else if (type === "IDAT") {
4280
+ idat.push(bytes.subarray(start, start + len));
4281
+ } else if (type === "IEND") break;
4282
+ pos = start + len + 4;
4283
+ }
4284
+ if (!w || !h || depth !== 8 || interlace !== 0) return null;
4285
+ const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : colorType === 0 ? 1 : 0;
4286
+ if (!channels) return null;
4287
+ const total = idat.reduce((n, c) => n + c.length, 0);
4288
+ const joined = new Uint8Array(total);
4289
+ let off2 = 0;
4290
+ for (const c of idat) {
4291
+ joined.set(c, off2);
4292
+ off2 += c.length;
4293
+ }
4294
+ let raw;
4295
+ try {
4296
+ raw = new Uint8Array(inflateSync(joined));
4297
+ } catch {
4298
+ return null;
4299
+ }
4300
+ const stride = w * channels;
4301
+ if (raw.length < (stride + 1) * h) return null;
4302
+ const out = new Uint8Array(w * h * 4);
4303
+ const prev = new Uint8Array(stride);
4304
+ const cur = new Uint8Array(stride);
4305
+ for (let y = 0; y < h; y++) {
4306
+ const filter = raw[y * (stride + 1)];
4307
+ const rowStart = y * (stride + 1) + 1;
4308
+ for (let i = 0; i < stride; i++) {
4309
+ const x = raw[rowStart + i];
4310
+ const a = i >= channels ? cur[i - channels] : 0;
4311
+ const b = prev[i];
4312
+ const c = i >= channels ? prev[i - channels] : 0;
4313
+ let v;
4314
+ switch (filter) {
4315
+ case 0:
4316
+ v = x;
4317
+ break;
4318
+ case 1:
4319
+ v = x + a;
4320
+ break;
4321
+ case 2:
4322
+ v = x + b;
4323
+ break;
4324
+ case 3:
4325
+ v = x + (a + b >> 1);
4326
+ break;
4327
+ case 4: {
4328
+ const p = a + b - c;
4329
+ const pa = Math.abs(p - a);
4330
+ const pb = Math.abs(p - b);
4331
+ const pc = Math.abs(p - c);
4332
+ v = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
4333
+ break;
4334
+ }
4335
+ default:
4336
+ return null;
4337
+ }
4338
+ cur[i] = v & 255;
4339
+ }
4340
+ for (let px = 0; px < w; px++) {
4341
+ const o = (y * w + px) * 4;
4342
+ const s = px * channels;
4343
+ if (channels >= 3) {
4344
+ out[o] = cur[s];
4345
+ out[o + 1] = cur[s + 1];
4346
+ out[o + 2] = cur[s + 2];
4347
+ out[o + 3] = channels === 4 ? cur[s + 3] : 255;
4348
+ } else {
4349
+ out[o] = out[o + 1] = out[o + 2] = cur[s];
4350
+ out[o + 3] = channels === 2 ? cur[s + 1] : 255;
4351
+ }
4352
+ }
4353
+ prev.set(cur);
4354
+ }
4355
+ return { w, h, data: out };
4356
+ }
4357
+ var at = (img, x, y) => {
4358
+ const o = (y * img.w + x) * 4;
4359
+ return [img.data[o], img.data[o + 1], img.data[o + 2]];
4360
+ };
4361
+ var median = (a) => {
4362
+ const s = [...a].sort((x, y) => x - y);
4363
+ return s[s.length >> 1] ?? 0;
4364
+ };
4365
+ function groundColour(img) {
4366
+ const ring = Math.max(1, Math.round(Math.min(img.w, img.h) * 0.01));
4367
+ const r = [];
4368
+ const g = [];
4369
+ const b = [];
4370
+ const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 400));
4371
+ for (let y = 0; y < img.h; y += step) {
4372
+ for (let x = 0; x < img.w; x += step) {
4373
+ if (x < ring || y < ring || x >= img.w - ring || y >= img.h - ring) {
4374
+ const p = at(img, x, y);
4375
+ r.push(p[0]);
4376
+ g.push(p[1]);
4377
+ b.push(p[2]);
4378
+ }
4379
+ }
4380
+ }
4381
+ return [median(r), median(g), median(b)];
4382
+ }
4383
+ var hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
4384
+ var delta = (p, q) => Math.abs(p[0] - q[0]) + Math.abs(p[1] - q[1]) + Math.abs(p[2] - q[2]);
4385
+ function cardBounds(img, ground, threshold = 60) {
4386
+ let x0 = img.w;
4387
+ let y0 = img.h;
4388
+ let x1 = -1;
4389
+ let y1 = -1;
4390
+ const step = Math.max(1, Math.round(Math.max(img.w, img.h) / 700));
4391
+ for (let y = 0; y < img.h; y += step) {
4392
+ for (let x = 0; x < img.w; x += step) {
4393
+ if (delta(at(img, x, y), ground) > threshold) {
4394
+ if (x < x0) x0 = x;
4395
+ if (y < y0) y0 = y;
4396
+ if (x > x1) x1 = x;
4397
+ if (y > y1) y1 = y;
4398
+ }
4399
+ }
4400
+ }
4401
+ if (x1 < 0) return null;
4402
+ return { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 };
4403
+ }
4404
+ function haloReading(img, ground, card) {
4405
+ const band = Math.max(2, Math.round(Math.min(img.w, img.h) * 0.012));
4406
+ const meanRect = (x0, y0, x1, y1) => {
4407
+ let sum = 0;
4408
+ let n = 0;
4409
+ const sx = Math.max(1, Math.round((x1 - x0) / 60));
4410
+ const sy = Math.max(1, Math.round((y1 - y0) / 60));
4411
+ for (let y = Math.max(0, y0); y < Math.min(img.h, y1); y += sy) {
4412
+ for (let x = Math.max(0, x0); x < Math.min(img.w, x1); x += sx) {
4413
+ sum += delta(at(img, x, y), ground);
4414
+ n++;
5038
4415
  }
5039
- } catch (e) {
5040
- out.notes.push(
5041
- `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
5042
- );
5043
- return null;
5044
- }
5045
- const aspect = imageAspect(bytes, ext);
5046
- if (!aspect) {
5047
- out.notes.push(
5048
- `${role}: ${src} has no readable size; the wordmark stands in`
5049
- );
5050
- return null;
5051
4416
  }
5052
- await mkdir4(join6(takeDir, "brand"), { recursive: true });
5053
- await writeFile6(join6(takeDir, "brand", `${name}.${ext}`), bytes);
5054
- return { key: `/brand/${name}.${ext}`, aspect };
4417
+ return n ? sum / n : 0;
5055
4418
  };
5056
- out.light = await one("logoUrl", "mark");
5057
- out.dark = await one("logoOnDarkUrl", "mark-on-dark");
5058
- return out;
5059
- }
5060
-
5061
- // src/plugin/officialTemplates.ts
5062
- var OFFICIAL_END_CARD_TITLE = "End card";
5063
- function templateKey(s) {
5064
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "");
5065
- }
5066
- function findOfficialByRef(rows, ref) {
5067
- const key = templateKey(ref);
5068
- if (!key) return null;
5069
- for (const row of rows) {
5070
- if (templateKey(row.title) === key) return row;
5071
- if (row.slug && templateKey(row.slug) === key) return row;
4419
+ const rise = (bands) => {
4420
+ const [outer, mid] = bands;
4421
+ return outer >= 3 && mid - outer >= 1.5 ? mid : 0;
4422
+ };
4423
+ const readings = [];
4424
+ const yA = card.y + card.h * 0.2;
4425
+ const yB = card.y + card.h * 0.8;
4426
+ if (card.x >= 1 && card.w > band * 4) {
4427
+ const x0 = card.x;
4428
+ readings.push(
4429
+ rise([
4430
+ meanRect(x0, yA, x0 + band, yB),
4431
+ meanRect(x0 + band, yA, x0 + band * 2, yB),
4432
+ meanRect(x0 + band * 2, yA, x0 + band * 3, yB)
4433
+ ])
4434
+ );
5072
4435
  }
5073
- return null;
4436
+ if (card.x + card.w <= img.w - 1 && card.w > band * 4) {
4437
+ const x1 = card.x + card.w;
4438
+ readings.push(
4439
+ rise([
4440
+ meanRect(x1 - band, yA, x1, yB),
4441
+ meanRect(x1 - band * 2, yA, x1 - band, yB),
4442
+ meanRect(x1 - band * 3, yA, x1 - band * 2, yB)
4443
+ ])
4444
+ );
4445
+ }
4446
+ if (card.y + card.h <= img.h - 1 && card.h > band * 4) {
4447
+ const y1 = card.y + card.h;
4448
+ const xA = card.x + card.w * 0.2;
4449
+ const xB = card.x + card.w * 0.8;
4450
+ readings.push(
4451
+ rise([
4452
+ meanRect(xA, y1 - band, xB, y1),
4453
+ meanRect(xA, y1 - band * 2, xB, y1 - band),
4454
+ meanRect(xA, y1 - band * 3, xB, y1 - band * 2)
4455
+ ])
4456
+ );
4457
+ }
4458
+ return readings.length ? Math.max(...readings) : null;
5074
4459
  }
5075
-
5076
- // src/plugin/music.ts
5077
- async function fetchMusicCatalog(origin) {
5078
- const base = origin.replace(/\/+$/, "");
5079
- const res = await fetch(`${base}/api/music`);
5080
- if (!res.ok) throw new Error(`GET ${base}/api/music answered ${res.status}`);
5081
- const body = await res.json();
5082
- return {
5083
- tracks: Array.isArray(body.tracks) ? body.tracks : [],
5084
- sfx: Array.isArray(body.sfx) ? body.sfx : []
4460
+ function edgeContrast(img, ground, card) {
4461
+ const yA = Math.max(0, Math.round(card.y + card.h * 0.2));
4462
+ const yB = Math.min(img.h, Math.round(card.y + card.h * 0.8));
4463
+ const col = (x) => {
4464
+ if (x < 0 || x >= img.w) return 0;
4465
+ const ds = [];
4466
+ const step = Math.max(1, Math.round((yB - yA) / 80));
4467
+ for (let y = yA; y < yB; y += step) ds.push(delta(at(img, x, y), ground));
4468
+ return median(ds);
5085
4469
  };
4470
+ const depth = Math.min(24, Math.floor(card.w / 8));
4471
+ let best = 0;
4472
+ for (let d = 0; d < depth; d++) {
4473
+ best = Math.max(best, col(card.x + d), col(card.x + card.w - 1 - d));
4474
+ }
4475
+ return best;
5086
4476
  }
5087
-
5088
- // src/plugin/judge.ts
5089
- import { existsSync as existsSync7 } from "fs";
5090
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
5091
- import { dirname, isAbsolute as isAbsolute3, join as join7, resolve as resolve6 } from "path";
5092
- import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
5093
-
5094
- // src/plugin/shotBake.ts
5095
- import { deflateSync } from "zlib";
5096
- var crcTable = (() => {
5097
- const t = new Uint32Array(256);
5098
- for (let n = 0; n < 256; n++) {
5099
- let c = n;
5100
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
5101
- t[n] = c >>> 0;
5102
- }
5103
- return t;
5104
- })();
5105
- function crc32(buf) {
5106
- let c = 4294967295;
5107
- for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
5108
- return (c ^ 4294967295) >>> 0;
5109
- }
5110
- function chunk(type, body) {
5111
- const out = new Uint8Array(12 + body.length);
5112
- const dv = new DataView(out.buffer);
5113
- dv.setUint32(0, body.length);
5114
- for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
5115
- out.set(body, 8);
5116
- dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
5117
- return out;
5118
- }
5119
- function encodePng(img) {
5120
- const stride = img.w * 4;
5121
- const raw = new Uint8Array((stride + 1) * img.h);
5122
- for (let y = 0; y < img.h; y++) {
5123
- raw[y * (stride + 1)] = 2;
5124
- for (let i = 0; i < stride; i++) {
5125
- const cur = img.data[y * stride + i];
5126
- const up = y ? img.data[(y - 1) * stride + i] : 0;
5127
- raw[y * (stride + 1) + 1 + i] = cur - up & 255;
5128
- }
5129
- }
5130
- const ihdr = new Uint8Array(13);
5131
- const dv = new DataView(ihdr.buffer);
5132
- dv.setUint32(0, img.w);
5133
- dv.setUint32(4, img.h);
5134
- ihdr[8] = 8;
5135
- ihdr[9] = 6;
5136
- const parts = [
5137
- new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
5138
- chunk("IHDR", ihdr),
5139
- chunk("IDAT", new Uint8Array(deflateSync(raw))),
5140
- chunk("IEND", new Uint8Array(0))
5141
- ];
5142
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
5143
- let o = 0;
5144
- for (const p of parts) {
5145
- out.set(p, o);
5146
- o += p.length;
4477
+ function inkCoverage(img, rect, threshold = 48) {
4478
+ const x0 = Math.max(0, Math.floor(rect.x));
4479
+ const y0 = Math.max(0, Math.floor(rect.y));
4480
+ const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
4481
+ const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
4482
+ if (x1 - x0 < 2 || y1 - y0 < 2) return 0;
4483
+ const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 300));
4484
+ const r = [];
4485
+ const g = [];
4486
+ const b = [];
4487
+ const pts = [];
4488
+ for (let y = y0; y < y1; y += step) {
4489
+ for (let x = x0; x < x1; x += step) {
4490
+ const p = at(img, x, y);
4491
+ pts.push(p);
4492
+ r.push(p[0]);
4493
+ g.push(p[1]);
4494
+ b.push(p[2]);
4495
+ }
5147
4496
  }
5148
- return out;
4497
+ const base = [median(r), median(g), median(b)];
4498
+ let ink = 0;
4499
+ for (const p of pts) if (delta(p, base) > threshold) ink++;
4500
+ return pts.length ? ink / pts.length : 0;
5149
4501
  }
5150
-
5151
- // src/plugin/judge.ts
5152
- var RUBRIC = [
5153
- "A plate, and room on it: the subject sits at 74 to 89% of the width with headroom; nobody bleeds a window on all four sides.",
5154
- "The window is an object: rounded corners and a shadow with weight (a soft wide one on a light plate, a tight glow on a dark one).",
5155
- "It enters: the first frame is not a static, centred, flat window (a tilt-in, a pull-out, a rise).",
5156
- "The camera zooms to the affordance, holds through the response, and pulls out to show the consequence.",
5157
- "Beats are cut at stillness; no wipes.",
5158
- "Content is staged: real data, a populated state, never an empty canvas or a wallpaper.",
5159
- "The cursor is a character: visible, smoothed, a press on click, never drifting through dead space.",
5160
- "The last frame is a poster: a resolved state you could ship as the still.",
5161
- "The message has its own column or its own frame; the headline is two to six words over up to three lines at about 40% of the height; the wordmark is anchored.",
5162
- "The poster test: would you post this frame as a still, alone?",
5163
- "The thumbnail test: does it read at half size?",
5164
- "Name three ways this asset acknowledges THIS product (its data, its state, its brand), not a generic window."
5165
- ];
5166
- function rolesFor(asset) {
5167
- if ((asset.source === "poster" || asset.source === "stage") && asset.template)
5168
- return [asset.template.replace(/-stage$/, "")];
5169
- const spec = DESTINATIONS2.find((d) => d.id === asset.destination);
5170
- if (spec?.kind === "video") {
5171
- const portrait = spec.px.w < spec.px.h;
5172
- return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
5173
- }
5174
- if (spec?.genre === "card")
5175
- return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5176
- if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
5177
- return [];
5178
- }
5179
- function resample(img, w, h) {
5180
- const out = new Uint8Array(w * h * 4);
5181
- const sx = img.w / w;
5182
- const sy = img.h / h;
5183
- for (let y = 0; y < h; y++) {
5184
- const y0 = Math.floor(y * sy);
5185
- const y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
5186
- for (let x = 0; x < w; x++) {
5187
- const x0 = Math.floor(x * sx);
5188
- const x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
5189
- let r = 0;
5190
- let g = 0;
5191
- let b = 0;
5192
- let a = 0;
4502
+ function differenceHash(img, rect) {
4503
+ const r = rect ?? { x: 0, y: 0, w: img.w, h: img.h };
4504
+ const cols = 9;
4505
+ const rows = 8;
4506
+ const cell = [];
4507
+ for (let cy = 0; cy < rows; cy++) {
4508
+ for (let cx = 0; cx < cols; cx++) {
4509
+ const x0 = Math.floor(r.x + cx * r.w / cols);
4510
+ const x1 = Math.max(x0 + 1, Math.floor(r.x + (cx + 1) * r.w / cols));
4511
+ const y0 = Math.floor(r.y + cy * r.h / rows);
4512
+ const y1 = Math.max(y0 + 1, Math.floor(r.y + (cy + 1) * r.h / rows));
4513
+ let sum = 0;
5193
4514
  let n = 0;
5194
- for (let yy = y0; yy < y1 && yy < img.h; yy++) {
5195
- for (let xx = x0; xx < x1 && xx < img.w; xx++) {
5196
- const o2 = (yy * img.w + xx) * 4;
5197
- r += img.data[o2];
5198
- g += img.data[o2 + 1];
5199
- b += img.data[o2 + 2];
5200
- a += img.data[o2 + 3];
4515
+ const sx = Math.max(1, Math.floor((x1 - x0) / 8));
4516
+ const sy = Math.max(1, Math.floor((y1 - y0) / 8));
4517
+ for (let y = y0; y < y1 && y < img.h; y += sy) {
4518
+ for (let x = x0; x < x1 && x < img.w; x += sx) {
4519
+ const p = at(img, Math.max(0, x), Math.max(0, y));
4520
+ sum += 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
5201
4521
  n++;
5202
4522
  }
5203
4523
  }
5204
- const o = (y * w + x) * 4;
5205
- out[o] = n ? r / n : 0;
5206
- out[o + 1] = n ? g / n : 0;
5207
- out[o + 2] = n ? b / n : 0;
5208
- out[o + 3] = n ? a / n : 0;
4524
+ cell.push(n ? sum / n : 0);
5209
4525
  }
5210
4526
  }
5211
- return { w, h, data: out };
5212
- }
5213
- function composeSheet(left, right, height = 540, gutter = 32) {
5214
- const lw = Math.max(1, Math.round(left.w * height / left.h));
5215
- const rw = Math.max(1, Math.round(right.w * height / right.h));
5216
- const L = resample(left, lw, height);
5217
- const R = resample(right, rw, height);
5218
- const margin = 24;
5219
- const band = 8;
5220
- const w = margin * 2 + lw + gutter + rw;
5221
- const h = margin * 2 + height + band + 6;
5222
- const out = new Uint8Array(w * h * 4);
5223
- for (let i = 0; i < w * h; i++) {
5224
- out[i * 4] = 240;
5225
- out[i * 4 + 1] = 242;
5226
- out[i * 4 + 2] = 244;
5227
- out[i * 4 + 3] = 255;
5228
- }
5229
- const blit = (src, x0, y0) => {
5230
- for (let y = 0; y < src.h; y++) {
5231
- for (let x = 0; x < src.w; x++) {
5232
- const si = (y * src.w + x) * 4;
5233
- const a = src.data[si + 3] / 255;
5234
- const o = ((y0 + y) * w + x0 + x) * 4;
5235
- for (let c = 0; c < 3; c++)
5236
- out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
5237
- }
4527
+ let bits = "";
4528
+ for (let cy = 0; cy < rows; cy++) {
4529
+ let byte = 0;
4530
+ for (let cx = 0; cx < cols - 1; cx++) {
4531
+ const l = cell[cy * cols + cx];
4532
+ const rr = cell[cy * cols + cx + 1];
4533
+ byte = byte << 1 | (l > rr ? 1 : 0);
5238
4534
  }
5239
- };
5240
- blit(L, margin, margin);
5241
- blit(R, margin + lw + gutter, margin);
5242
- const fill = (x0, x1, y0, y1, v) => {
5243
- for (let y = y0; y < y1; y++)
5244
- for (let x = x0; x < x1; x++) {
5245
- const o = (y * w + x) * 4;
5246
- out[o] = v;
5247
- out[o + 1] = v;
5248
- out[o + 2] = v;
5249
- }
5250
- };
5251
- fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
5252
- fill(
5253
- margin + lw + gutter,
5254
- margin + lw + gutter + rw,
5255
- margin + height + 6,
5256
- margin + height + 6 + band,
5257
- 200
5258
- );
5259
- return { w, h, data: out };
4535
+ bits += byte.toString(16).padStart(2, "0");
4536
+ }
4537
+ return bits;
5260
4538
  }
5261
- async function judgeKit(kitPath, manifestPath, outDir) {
5262
- const kit = JSON.parse(await readFile7(kitPath, "utf8"));
5263
- const manifest = JSON.parse(
5264
- await readFile7(manifestPath, "utf8")
5265
- );
5266
- const manifestDir = dirname(resolve6(manifestPath));
5267
- const kitDir = dirname(resolve6(kitPath));
5268
- const out = resolve6(outDir ?? join7(kitDir, "judge"));
5269
- await mkdir5(out, { recursive: true });
5270
- const sheets = [];
5271
- const skipped = [];
5272
- const refCache = /* @__PURE__ */ new Map();
5273
- const loadRef = async (file) => {
5274
- if (refCache.has(file)) return refCache.get(file) ?? null;
5275
- const p = join7(manifestDir, file);
5276
- const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
5277
- refCache.set(file, img);
5278
- return img;
4539
+ function luminance(c) {
4540
+ const lin = (v) => {
4541
+ const s = v / 255;
4542
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
5279
4543
  };
5280
- for (const a of kit.assets) {
5281
- if (!/\.png$/i.test(a.path)) continue;
5282
- const file = isAbsolute3(a.path) && existsSync7(a.path) ? a.path : join7(kitDir, a.path.split("/").pop() ?? a.path);
5283
- const img = existsSync7(file) ? decodePng(new Uint8Array(await readFile7(file))) : null;
5284
- if (!img) {
5285
- skipped.push(`${a.destination ?? a.path}: unreadable`);
5286
- continue;
5287
- }
5288
- const roles = rolesFor({
5289
- destination: a.destination,
5290
- source: a.source,
5291
- template: a.template,
5292
- path: a.path
5293
- });
5294
- const ref = manifest.assets.find(
5295
- (r) => roles.includes(r.role) && r.genre !== "context"
5296
- );
5297
- if (!ref) {
5298
- skipped.push(
5299
- `${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
5300
- );
5301
- continue;
5302
- }
5303
- const refImg = await loadRef(ref.file);
5304
- if (!refImg) {
5305
- skipped.push(
5306
- `${a.destination ?? a.path}: reference ${ref.file} unreadable`
5307
- );
5308
- continue;
4544
+ return 0.2126 * lin(c[0]) + 0.7152 * lin(c[1]) + 0.0722 * lin(c[2]);
4545
+ }
4546
+ function lightness(c) {
4547
+ const y = luminance(c);
4548
+ return y > 8856e-6 ? 116 * Math.cbrt(y) - 16 : 903.3 * y;
4549
+ }
4550
+ function medianColour(img, rect) {
4551
+ const r = [];
4552
+ const g = [];
4553
+ const b = [];
4554
+ const x0 = Math.max(0, Math.floor(rect.x));
4555
+ const y0 = Math.max(0, Math.floor(rect.y));
4556
+ const x1 = Math.min(img.w, Math.ceil(rect.x + rect.w));
4557
+ const y1 = Math.min(img.h, Math.ceil(rect.y + rect.h));
4558
+ const step = Math.max(1, Math.round(Math.max(x1 - x0, y1 - y0) / 200));
4559
+ for (let y = y0; y < y1; y += step) {
4560
+ for (let x = x0; x < x1; x += step) {
4561
+ const p = at(img, x, y);
4562
+ r.push(p[0]);
4563
+ g.push(p[1]);
4564
+ b.push(p[2]);
5309
4565
  }
5310
- const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
5311
- const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
5312
- const nameA = `${id}--A.png`;
5313
- const nameB = `${id}--B.png`;
5314
- await writeFile7(join7(out, nameA), encodePng(composeSheet(img, refImg)));
5315
- await writeFile7(join7(out, nameB), encodePng(composeSheet(refImg, img)));
5316
- const rubric = [
5317
- `# ${id} against ${ref.id} (${ref.file})`,
5318
- "",
5319
- `Sheet A: the kit asset LEFT (dark band), the reference RIGHT (light band). Sheet B: the reverse.`,
5320
- `Judge each sheet on its own, both orders, and write the verdict where the two agree; a disagreement is a tie.`,
5321
- "",
5322
- `## The reference`,
5323
- ref.layout,
5324
- ...ref.facts ? ["", "```json", JSON.stringify(ref.facts, null, 2), "```"] : [],
5325
- "",
5326
- `Rule: ${ref.rule}`,
5327
- "",
5328
- "## The rubric",
5329
- ...RUBRIC.map((r, i) => `${i + 1}. ${r}`),
5330
- "",
5331
- "## Verdict",
5332
- "In judge.json: `win` true when the kit asset is at least as good as the reference on the rules that apply to its genre, false when it is worse, null for a tie; `reasons` names the rules by number.",
5333
- ""
5334
- ].join("\n");
5335
- await writeFile7(join7(out, `${id}.md`), rubric);
5336
- sheets.push({
5337
- asset: id,
5338
- reference: ref.id,
5339
- sheetA: nameA,
5340
- sheetB: nameB,
5341
- rubric: `${id}.md`
5342
- });
5343
4566
  }
5344
- const verdictFile = join7(out, "judge.json");
5345
- if (!existsSync7(verdictFile)) {
5346
- await writeFile7(
5347
- verdictFile,
5348
- JSON.stringify(
5349
- {
5350
- kit: kitPath,
5351
- manifest: manifestPath,
5352
- verdicts: sheets.map((s) => ({
5353
- asset: s.asset,
5354
- reference: s.reference,
5355
- A: null,
5356
- B: null,
5357
- win: null,
5358
- reasons: []
5359
- }))
5360
- },
5361
- null,
5362
- 2
5363
- )
5364
- );
4567
+ return [median(r), median(g), median(b)];
4568
+ }
4569
+ function measureStill(img) {
4570
+ const g = groundColour(img);
4571
+ const strong = cardBounds(img, g, 60);
4572
+ const faint = cardBounds(img, g, 12) ?? strong;
4573
+ const card = faint;
4574
+ const bleed = [];
4575
+ let pad = null;
4576
+ let widthPct = null;
4577
+ let shadow = null;
4578
+ let edge = 0;
4579
+ let separation = null;
4580
+ if (card) {
4581
+ pad = {
4582
+ left: card.x / img.w,
4583
+ right: (img.w - card.x - card.w) / img.w,
4584
+ top: card.y / img.h,
4585
+ bottom: (img.h - card.y - card.h) / img.h
4586
+ };
4587
+ for (const side of ["left", "right", "top", "bottom"])
4588
+ if (pad[side] < 5e-3) bleed.push(side);
4589
+ widthPct = card.w / img.w;
4590
+ shadow = haloReading(img, g, card);
4591
+ edge = edgeContrast(img, g, card);
4592
+ const body = strong ?? card;
4593
+ const inner = {
4594
+ x: body.x + body.w * 0.1,
4595
+ y: body.y + body.h * 0.1,
4596
+ w: body.w * 0.8,
4597
+ h: body.h * 0.8
4598
+ };
4599
+ separation = Math.abs(lightness(medianColour(img, inner)) - lightness(g));
5365
4600
  }
5366
- return { sheets, skipped, outDir: out, verdictFile };
5367
- }
5368
- function winRate(verdicts) {
5369
- const judgedList = verdicts.filter(
5370
- (v) => v.win !== null || (v.reasons?.length ?? 0) > 0
5371
- );
5372
- const wins = judgedList.filter((v) => v.win === true).length;
5373
- const ties = judgedList.filter((v) => v.win === null).length;
5374
- const judged = judgedList.length;
4601
+ const inkRect = strong ?? card ?? { x: 0, y: 0, w: img.w, h: img.h };
5375
4602
  return {
5376
- wins,
5377
- ties,
5378
- judged,
5379
- rate: judged ? (wins + ties / 2) / judged : null
4603
+ w: img.w,
4604
+ h: img.h,
4605
+ ground: hex(g),
4606
+ card,
4607
+ widthPct,
4608
+ pad,
4609
+ bleed,
4610
+ shadow,
4611
+ edge,
4612
+ ink: inkCoverage(img, inkRect),
4613
+ separation,
4614
+ hash: differenceHash(img, inkRect)
5380
4615
  };
5381
4616
  }
5382
4617
 
5383
- // src/plugin/fontName.ts
5384
- import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
5385
- function catalogFamily(family) {
5386
- const name = family.trim();
5387
- if (findFontFamily2(name)) return name;
5388
- const base = name.replace(/\s+Variable$/i, "");
5389
- if (base !== name) {
5390
- const hit = findFontFamily2(base);
5391
- if (hit) return hit.family;
5392
- }
5393
- return family;
5394
- }
5395
-
5396
4618
  // src/plugin/callout.ts
5397
- import { existsSync as existsSync8 } from "fs";
5398
- import { readFile as readFile8 } from "fs/promises";
5399
- import { join as join8 } from "path";
5400
- import { lerpArray, sample } from "@vosjs/timeline";
5401
- import {
5402
- CALLOUT_DEFAULT_BODY_PX,
5403
- CALLOUT_SHAPES,
5404
- calloutClip,
5405
- calloutLook,
5406
- calloutGroundOf,
5407
- cameraModel as cameraModel2,
5408
- deltaE,
5409
- docCardLayout as docCardLayout2,
5410
- docZoomTrack as docZoomTrack2,
5411
- hexToRgb,
5412
- parseCssColour,
5413
- pinBox as pinBox2,
5414
- ratedSegments as ratedSegments5,
5415
- resolvePins as resolvePins2,
5416
- spanOutputExtent as spanOutputExtent5
5417
- } from "@vosjs/studio-core";
5418
4619
  var CALLOUT_DEFAULT_SECONDS = 3;
5419
4620
  var CALLOUT_LEAD_SECONDS = 0.2;
5420
4621
  var CALLOUT_DELTA_E_PROBLEM = 8;
@@ -5593,13 +4794,13 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5593
4794
  const out = /* @__PURE__ */ new Map();
5594
4795
  if (!times.length) return out;
5595
4796
  const bare = { ...doc, overlays: [] };
5596
- const outDir = join8(dir, ".ground-check");
4797
+ const outDir = join6(dir, ".ground-check");
5597
4798
  const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
5598
4799
  for (const f of res.frames) {
5599
- if (!existsSync8(f.file)) continue;
4800
+ if (!existsSync6(f.file)) continue;
5600
4801
  out.set(
5601
4802
  +f.time.toFixed(3),
5602
- decodePng(new Uint8Array(await readFile8(f.file)))
4803
+ decodePng(new Uint8Array(await readFile6(f.file)))
5603
4804
  );
5604
4805
  }
5605
4806
  return out;
@@ -5608,241 +4809,9 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5608
4809
  // src/plugin/kitPicture.ts
5609
4810
  import { execFile } from "child_process";
5610
4811
  import { promisify } from "util";
5611
- import { readFile as readFile9 } from "fs/promises";
5612
4812
  var execFileP = promisify(execFile);
5613
- var SUBJECT_BAND = { min: 0.6, max: 0.92 };
5614
4813
  var BLANK_INK = 0.12;
5615
- var SEPARATION_L = 8;
5616
- var SHADOW_PRESENT = 6;
5617
- var EDGE_PRESENT = 40;
5618
- var DUPLICATE_BITS2 = 6;
5619
- var HALFSIZE_KEEP = 0.45;
5620
- function apcaContrast(text, bg) {
5621
- const lum = (c) => {
5622
- const ch = (v) => Math.pow(v / 255, 2.4);
5623
- let y = 0.2126729 * ch(c[0]) + 0.7151522 * ch(c[1]) + 0.072175 * ch(c[2]);
5624
- if (y < 0.022) y += Math.pow(0.022 - y, 1.414);
5625
- return y;
5626
- };
5627
- const yt = lum(text);
5628
- const yb = lum(bg);
5629
- if (Math.abs(yb - yt) < 5e-4) return 0;
5630
- let sapc;
5631
- if (yb > yt) {
5632
- sapc = (Math.pow(yb, 0.56) - Math.pow(yt, 0.57)) * 1.14;
5633
- return sapc < 0.1 ? 0 : (sapc - 0.027) * 100;
5634
- }
5635
- sapc = (Math.pow(yb, 0.65) - Math.pow(yt, 0.62)) * 1.14;
5636
- return sapc > -0.1 ? 0 : -(sapc + 0.027) * 100;
5637
- }
5638
4814
  var pct = (v) => `${Math.round(v * 100)}%`;
5639
- var hexToRgb2 = (h) => {
5640
- const m = /^#([0-9a-f]{6})$/i.exec(h.trim());
5641
- if (!m) return null;
5642
- const n = parseInt(m[1], 16);
5643
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
5644
- };
5645
- function stillFindings(a, img, m) {
5646
- const out = [];
5647
- const genre = a.spec?.genre;
5648
- const bledAll = m.bleed.length === 4;
5649
- if (a.shot) {
5650
- const sx = Math.max(0, a.shot.x) * img.w;
5651
- const sy = Math.max(0, a.shot.y) * img.h;
5652
- const sw = Math.min(1, a.shot.x + a.shot.w) * img.w - sx;
5653
- const sh = Math.min(1, a.shot.y + a.shot.h) * img.h - sy;
5654
- const rect = { x: sx, y: sy, w: Math.max(1, sw), h: Math.max(1, sh) };
5655
- const ink = inkCoverage(img, rect);
5656
- if (ink < (a.crop ? BLANK_INK / 2 : BLANK_INK)) {
5657
- out.push({
5658
- code: "blank",
5659
- severity: "error",
5660
- asset: a.destination,
5661
- message: `${pct(ink)} ink inside the shot: the moment shows a wallpaper, an empty canvas or a flat panel`,
5662
- fixHint: "pick a moment after a gesture landed (--shot-time, or --times step:<id>) and stage the set before recording",
5663
- bbox: rect
5664
- });
5665
- }
5666
- if (!a.crop && (a.shot.w < 0.5 || a.shot.w > 1.2)) {
5667
- out.push({
5668
- code: "subject",
5669
- severity: "error",
5670
- asset: a.destination,
5671
- message: `the shot is ${pct(a.shot.w)} of the width; a card sits at 60 to 92%, a bleed a little past the edge`,
5672
- fixHint: "the template layout places the slot; fix its place for this aspect",
5673
- bbox: rect
5674
- });
5675
- }
5676
- for (const f of textFindings(a, img)) out.push(f);
5677
- return out;
5678
- }
5679
- const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5680
- if (m.ink < BLANK_INK) {
5681
- out.push({
5682
- code: "blank",
5683
- severity: "error",
5684
- asset: a.destination,
5685
- message: `${pct(m.ink)} ink inside the ${m.card ? "card" : "frame"}: a wallpaper, an empty canvas or a flat panel, not the product doing something`,
5686
- fixHint: "pick a moment after a gesture landed (--times step:<id>) and stage the set before recording: real data, a populated state",
5687
- bbox: subject
5688
- });
5689
- }
5690
- if (genre === "card") {
5691
- if (bledAll || m.widthPct === null) {
5692
- out.push({
5693
- code: "subject",
5694
- severity: "error",
5695
- asset: a.destination,
5696
- message: bledAll ? "the picture fills the frame on all four sides: a crop, not a card on a ground" : "no card found: the frame is one flat tone",
5697
- fixHint: "present the card in a look (vos deliver reads BRAND.md or --look) or render this destination from a poster template"
5698
- });
5699
- } else if (m.widthPct < SUBJECT_BAND.min || m.widthPct > SUBJECT_BAND.max) {
5700
- out.push({
5701
- code: "subject",
5702
- severity: "error",
5703
- asset: a.destination,
5704
- message: `the card is ${pct(m.widthPct)} of the width; the references sit at ${pct(SUBJECT_BAND.min)} to ${pct(SUBJECT_BAND.max)}`,
5705
- fixHint: "the look places the card at 84% (frame.inset); a poster template sets its own placement",
5706
- bbox: m.card ?? void 0
5707
- });
5708
- }
5709
- if (m.card && !bledAll) {
5710
- const sep = m.separation ?? 0;
5711
- const shadow = m.shadow ?? 0;
5712
- if (sep < SEPARATION_L && shadow < SHADOW_PRESENT && m.edge < EDGE_PRESENT) {
5713
- out.push({
5714
- code: "separation",
5715
- severity: "error",
5716
- asset: a.destination,
5717
- message: `the card and the ground are ${sep.toFixed(1)} L* apart with no shadow halo (${shadow.toFixed(1)}) and no drawn edge (${m.edge.toFixed(0)}): the card dissolves into the plate`,
5718
- fixHint: "a contact shadow (frame.shadowContact) plus a hairline (frame.border with borderColor) makes a light card sit on a light ground; the plate look sets both",
5719
- bbox: m.card
5720
- });
5721
- }
5722
- }
5723
- }
5724
- if (genre === "screenshot" && a.composed && m.card && (m.widthPct ?? 0) < 0.98) {
5725
- out.push({
5726
- code: "subject",
5727
- severity: "error",
5728
- asset: a.destination,
5729
- message: `a store screenshot must be the real page full bleed; this one shows a ${pct(m.widthPct ?? 0)} card on a ground`,
5730
- fixHint: "drop --composed: the screenshot genre renders the page with no chrome and no padding",
5731
- bbox: m.card
5732
- });
5733
- }
5734
- if (img.w <= 500 && (a.spec?.text === "none" || genre === "card")) {
5735
- const full = edgeEnergy(img, Math.min(img.w, 400));
5736
- const half = edgeEnergy(img, Math.min(img.w, 400) >> 1);
5737
- const keep = full > 0 ? half / full : 1;
5738
- if (keep < HALFSIZE_KEEP) {
5739
- out.push({
5740
- code: "halfsize",
5741
- severity: "warning",
5742
- asset: a.destination,
5743
- message: `at half size the tile keeps ${pct(keep)} of its edges: fine text and thin lines vanish where the store shows it small`,
5744
- fixHint: "a tile is the subject large and saturated with no text (the store rule); render it from the card-on-gradient template"
5745
- });
5746
- }
5747
- }
5748
- for (const f of textFindings(a, img)) out.push(f);
5749
- return out;
5750
- }
5751
- function textFindings(a, img) {
5752
- const out = [];
5753
- for (const t of a.text ?? []) {
5754
- const box = {
5755
- x: t.x * img.w,
5756
- y: t.y * img.h,
5757
- w: t.w * img.w,
5758
- h: t.h * img.h
5759
- };
5760
- const name = t.label ? `"${t.label}"` : "a text box";
5761
- if (box.x < 0 || box.y < 0 || box.x + box.w > img.w + 0.5 || box.y + box.h > img.h + 0.5) {
5762
- out.push({
5763
- code: "sliced",
5764
- severity: "error",
5765
- asset: a.destination,
5766
- message: `${name} crosses the frame edge: cut mid-word`,
5767
- fixHint: "shorten the line, or let the template recompose (headline lines at 12 to 14% of the height, inside the safe rect)",
5768
- bbox: box
5769
- });
5770
- }
5771
- const s = a.spec?.safe;
5772
- if (s) {
5773
- const sx = s.x * img.w;
5774
- const sy = s.y * img.h;
5775
- const sw = s.w * img.w;
5776
- const sh = s.h * img.h;
5777
- if (box.x < sx - 0.5 || box.y < sy - 0.5 || box.x + box.w > sx + sw + 0.5 || box.y + box.h > sy + sh + 0.5) {
5778
- out.push({
5779
- code: "safe",
5780
- severity: "warning",
5781
- asset: a.destination,
5782
- message: `${name} sits outside the destination's safe rect (${pct(s.w)}x${pct(s.h)} at ${pct(s.x)},${pct(s.y)}): the platform's chrome or crop covers it`,
5783
- fixHint: "move the text inside the safe rect; the template reads channel-specs safe",
5784
- bbox: box
5785
- });
5786
- }
5787
- }
5788
- const rgb = t.color ? hexToRgb2(t.color) : null;
5789
- if (rgb) {
5790
- const ground = medianColour(img, box);
5791
- const lc = apcaContrast(rgb, ground);
5792
- const floor = t.role === "body" ? 75 : 60;
5793
- if (lc < floor) {
5794
- out.push({
5795
- code: "contrast",
5796
- severity: "warning",
5797
- asset: a.destination,
5798
- message: `${name} reads APCA Lc ${lc.toFixed(0)} on its ground; ${t.role === "body" ? "body" : "a headline"} wants ${floor}`,
5799
- fixHint: "darken the ink or lighten the ground under the text (BRAND.md ink on bgA/bgB, never on the shot)",
5800
- bbox: box
5801
- });
5802
- }
5803
- }
5804
- }
5805
- if (a.spec?.text === "none" && (a.text?.length ?? 0) > 0) {
5806
- out.push({
5807
- code: "safe",
5808
- severity: "warning",
5809
- asset: a.destination,
5810
- message: "this destination wants no text (the picture carries it alone) and the kit put words on it",
5811
- fixHint: "render the tile from the card-on-gradient template with no headline"
5812
- });
5813
- }
5814
- return out;
5815
- }
5816
- function duplicateFindings(stills) {
5817
- const groups = [];
5818
- const seen = /* @__PURE__ */ new Set();
5819
- const comparable = (a, b) => a.genre === "screenshot" || b.genre === "screenshot" ? a.destination === b.destination : true;
5820
- for (let i = 0; i < stills.length; i++) {
5821
- if (seen.has(i)) continue;
5822
- const g = [stills[i]];
5823
- for (let j = i + 1; j < stills.length; j++) {
5824
- if (seen.has(j)) continue;
5825
- if (!comparable(stills[i], stills[j])) continue;
5826
- if (hammingDistance(stills[i].hash, stills[j].hash) <= DUPLICATE_BITS2) {
5827
- g.push(stills[j]);
5828
- seen.add(j);
5829
- }
5830
- }
5831
- if (g.length > 1) groups.push(g);
5832
- }
5833
- return groups.map((g) => {
5834
- const composed = g.every(
5835
- (s) => stills.find((x) => x.destination === s.destination)?.composed
5836
- );
5837
- return {
5838
- code: "duplicate",
5839
- severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
5840
- asset: g.map((s) => s.destination).join(", "),
5841
- message: composed ? `${g.length} channels share one cover: ${g.map((s) => s.destination).join(", ")}` : `${g.length} assets share one frame${g[0].time !== null ? ` (${g[0].time.toFixed(2)}s)` : ""}: ${g.map((s) => s.destination).join(", ")}`,
5842
- fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster document per class (poster/<class>/doc.json) or per destination (LAUNCH.md poster roles) gives each channel its own cover"
5843
- };
5844
- });
5845
- }
5846
4815
  async function videoFrame(file, at2, ffmpeg2) {
5847
4816
  try {
5848
4817
  const { stdout } = await execFileP(
@@ -5877,91 +4846,62 @@ async function onPath(bin) {
5877
4846
  return false;
5878
4847
  }
5879
4848
  }
4849
+ function frameFindings(asset, name, img, m) {
4850
+ const out = [];
4851
+ const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
4852
+ const ink = inkCoverage(img, subject);
4853
+ if (ink < BLANK_INK) {
4854
+ out.push({
4855
+ code: "firstlast",
4856
+ severity: "error",
4857
+ asset,
4858
+ message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
4859
+ fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card"
4860
+ });
4861
+ }
4862
+ if (m.bleed.length === 4) {
4863
+ out.push({
4864
+ code: "firstlast",
4865
+ severity: "warning",
4866
+ asset,
4867
+ message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
4868
+ fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
4869
+ });
4870
+ }
4871
+ return out;
4872
+ }
5880
4873
  async function pictureChecks(assets) {
5881
4874
  const findings = [];
5882
4875
  const measured = [];
5883
- const stills = [];
5884
4876
  let ffmpeg2 = null;
5885
4877
  for (const a of assets) {
5886
- if (/\.png$/i.test(a.file)) {
5887
- const img = decodePng(new Uint8Array(await readFile9(a.file)));
5888
- if (!img) {
5889
- findings.push({
5890
- code: "unreadable",
5891
- severity: "info",
5892
- asset: a.destination,
5893
- message: `${a.path} is not an 8-bit non-interlaced PNG; the picture checks cannot read it`,
5894
- fixHint: "render stills through vos deliver or vos frames, which write readable PNGs"
5895
- });
5896
- measured.push({ destination: a.destination, measure: null });
5897
- continue;
5898
- }
5899
- const m = measureStill(img);
5900
- measured.push({ destination: a.destination, measure: m });
5901
- findings.push(...stillFindings(a, img, m));
5902
- if (a.spec?.kind !== "video")
5903
- stills.push({
5904
- destination: a.destination,
5905
- hash: m.hash,
5906
- time: null,
5907
- genre: a.spec?.genre,
5908
- composed: !!a.shot
5909
- });
4878
+ if (!/\.(mp4|webm|mov)$/i.test(a.file)) continue;
4879
+ if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
4880
+ if (!ffmpeg2) {
4881
+ findings.push({
4882
+ code: "firstlast",
4883
+ severity: "info",
4884
+ asset: a.destination,
4885
+ message: "ffmpeg is not on PATH, so the first and last frames were not read",
4886
+ fixHint: "install ffmpeg to have the video checks run"
4887
+ });
4888
+ measured.push({ destination: a.destination, measure: null });
5910
4889
  continue;
5911
4890
  }
5912
- if (/\.(mp4|webm|mov)$/i.test(a.file)) {
5913
- if (ffmpeg2 === null) ffmpeg2 = await onPath("ffmpeg");
5914
- if (!ffmpeg2) {
5915
- findings.push({
5916
- code: "firstlast",
5917
- severity: "info",
5918
- asset: a.destination,
5919
- message: "ffmpeg is not on PATH, so the first and last frames were not read",
5920
- fixHint: "install ffmpeg to have the video checks run"
5921
- });
5922
- measured.push({ destination: a.destination, measure: null });
5923
- continue;
5924
- }
5925
- const seconds = a.seconds ?? 0;
5926
- const first = await videoFrame(a.file, 0, "ffmpeg");
5927
- const last = await videoFrame(
5928
- a.file,
5929
- Math.max(0, seconds - 0.1),
5930
- "ffmpeg"
5931
- );
5932
- for (const [name, img] of [
5933
- ["first", first],
5934
- ["last", last]
5935
- ]) {
5936
- if (!img) continue;
5937
- const m = measureStill(img);
5938
- if (name === "first")
5939
- measured.push({ destination: a.destination, measure: m });
5940
- const subject = m.card ?? { x: 0, y: 0, w: img.w, h: img.h };
5941
- const ink = inkCoverage(img, subject);
5942
- if (ink < BLANK_INK) {
5943
- findings.push({
5944
- code: "firstlast",
5945
- severity: "error",
5946
- asset: a.destination,
5947
- message: `the ${name} frame is ${pct(ink)} ink: a ${name === "first" ? "cold open on nothing" : "clip that ends on nothing"}`,
5948
- fixHint: name === "first" ? "open on the product (trim the head, or an entrance over a populated frame)" : "end on a resolved state or an end card (the last frame is the poster)"
5949
- });
5950
- }
5951
- if (m.bleed.length === 4 && a.spec?.genre !== "screenshot") {
5952
- findings.push({
5953
- code: "firstlast",
5954
- severity: "warning",
5955
- asset: a.destination,
5956
- message: `the ${name} frame fills the frame on all four sides: no ground, no room`,
5957
- fixHint: "present the cut in a look (vos deliver reads BRAND.md or --look)"
5958
- });
5959
- }
5960
- }
5961
- continue;
4891
+ const seconds = a.seconds ?? 0;
4892
+ const first = await videoFrame(a.file, 0, "ffmpeg");
4893
+ const last = await videoFrame(a.file, Math.max(0, seconds - 0.1), "ffmpeg");
4894
+ for (const [name, img] of [
4895
+ ["first", first],
4896
+ ["last", last]
4897
+ ]) {
4898
+ if (!img) continue;
4899
+ const m = measureStill(img);
4900
+ if (name === "first")
4901
+ measured.push({ destination: a.destination, measure: m });
4902
+ findings.push(...frameFindings(a.destination, name, img, m));
5962
4903
  }
5963
4904
  }
5964
- findings.push(...duplicateFindings(stills));
5965
4905
  const order = {
5966
4906
  error: 0,
5967
4907
  warning: 1,
@@ -5977,28 +4917,15 @@ function formatFinding(f) {
5977
4917
  }
5978
4918
 
5979
4919
  // src/plugin/validateKit.ts
5980
- import { existsSync as existsSync9 } from "fs";
5981
- import { readFile as readFile10, stat as stat2 } from "fs/promises";
5982
- import { dirname as dirname2, isAbsolute as isAbsolute4, join as join9 } from "path";
5983
- import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5984
- var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5985
- function pngDimensions(bytes) {
5986
- if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIG2)) return null;
5987
- if (bytes.toString("ascii", 12, 16) !== "IHDR") return null;
5988
- return { w: bytes.readUInt32BE(16), h: bytes.readUInt32BE(20) };
5989
- }
5990
- function sniffImage(bytes) {
5991
- if (bytes.subarray(0, 8).equals(PNG_SIG2)) return "png";
5992
- if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
5993
- return "webp";
5994
- if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216) return "jpeg";
5995
- return "unknown";
5996
- }
4920
+ import { existsSync as existsSync7 } from "fs";
4921
+ import { readFile as readFile7, stat } from "fs/promises";
4922
+ import { dirname, isAbsolute as isAbsolute2, join as join7 } from "path";
4923
+ import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
5997
4924
  async function probeVideo(path) {
5998
4925
  const MB = await import("mediabunny");
5999
4926
  const input = new MB.Input({
6000
4927
  formats: MB.ALL_FORMATS,
6001
- source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
4928
+ source: new MB.BufferSource(new Uint8Array(await readFile7(path)))
6002
4929
  });
6003
4930
  try {
6004
4931
  const track = await input.getPrimaryVideoTrack();
@@ -6010,7 +4937,7 @@ async function probeVideo(path) {
6010
4937
  }
6011
4938
  }
6012
4939
  var specById = new Map(
6013
- DESTINATIONS3.map((d) => [d.id, d])
4940
+ DESTINATIONS2.map((d) => [d.id, d])
6014
4941
  );
6015
4942
  var near = (a, b, tol) => Math.abs(a - b) <= tol;
6016
4943
  async function validateKit(kitPath, opts = {}) {
@@ -6019,7 +4946,7 @@ async function validateKit(kitPath, opts = {}) {
6019
4946
  const measured = [];
6020
4947
  let kit;
6021
4948
  try {
6022
- kit = JSON.parse(await readFile10(kitPath, "utf8"));
4949
+ kit = JSON.parse(await readFile7(kitPath, "utf8"));
6023
4950
  } catch (e) {
6024
4951
  return {
6025
4952
  valid: false,
@@ -6036,13 +4963,12 @@ async function validateKit(kitPath, opts = {}) {
6036
4963
  measured
6037
4964
  };
6038
4965
  }
6039
- const base = dirname2(kitPath);
4966
+ const base = dirname(kitPath);
6040
4967
  const resolvePath = (p) => {
6041
- if (isAbsolute4(p) && existsSync9(p)) return p;
6042
- const beside = join9(base, p.split("/").pop() ?? p);
4968
+ if (isAbsolute2(p) && existsSync7(p)) return p;
4969
+ const beside = join7(base, p.split("/").pop() ?? p);
6043
4970
  return beside;
6044
4971
  };
6045
- const perDestination = /* @__PURE__ */ new Map();
6046
4972
  const pictureAssets = [];
6047
4973
  for (const a of kit.assets) {
6048
4974
  const id = a.destination ?? `${a.channel}-${a.asset}`;
@@ -6050,12 +4976,11 @@ async function validateKit(kitPath, opts = {}) {
6050
4976
  const file = resolvePath(a.path);
6051
4977
  let bytes;
6052
4978
  try {
6053
- bytes = (await stat2(file)).size;
4979
+ bytes = (await stat(file)).size;
6054
4980
  } catch {
6055
4981
  problems.push(`${label}: ${a.path} is missing`);
6056
4982
  continue;
6057
4983
  }
6058
- perDestination.set(id, (perDestination.get(id) ?? 0) + 1);
6059
4984
  if (bytes !== a.bytes)
6060
4985
  problems.push(
6061
4986
  `${label}: manifest says ${a.bytes} bytes, the file is ${bytes}`
@@ -6064,25 +4989,7 @@ async function validateKit(kitPath, opts = {}) {
6064
4989
  let w = null;
6065
4990
  let h = null;
6066
4991
  let seconds = null;
6067
- if (/\.(png|jpe?g|webp)$/i.test(file)) {
6068
- const head = Buffer.from(await readFile10(file));
6069
- const kind = sniffImage(head);
6070
- if (file.toLowerCase().endsWith(".png") && kind !== "png")
6071
- problems.push(
6072
- `${label}: ${a.path} is named .png but its bytes are ${kind} \u2014 stores refuse a mislabelled image`
6073
- );
6074
- const dims = kind === "png" ? pngDimensions(head) : null;
6075
- if (dims) {
6076
- w = dims.w;
6077
- h = dims.h;
6078
- if (w !== a.w || h !== a.h)
6079
- problems.push(
6080
- `${label}: manifest says ${a.w}x${a.h}, the file is ${w}x${h}`
6081
- );
6082
- } else if (kind === "png") {
6083
- problems.push(`${label}: ${a.path} has no readable IHDR`);
6084
- }
6085
- } else if (/\.(mp4|webm|mov)$/i.test(file)) {
4992
+ if (/\.(mp4|webm|mov)$/i.test(file)) {
6086
4993
  try {
6087
4994
  const v = await probeVideo(file);
6088
4995
  if (!v) problems.push(`${label}: ${a.path} has no video track`);
@@ -6105,21 +5012,14 @@ async function validateKit(kitPath, opts = {}) {
6105
5012
  );
6106
5013
  }
6107
5014
  } else {
6108
- warnings.push(
6109
- `${label}: ${a.path} is not an image or video the kit verifies`
6110
- );
5015
+ warnings.push(`${label}: ${a.path} is not a video the kit verifies`);
6111
5016
  }
6112
5017
  measured.push({ destination: id, path: a.path, w, h, bytes, seconds });
6113
5018
  pictureAssets.push({
6114
5019
  destination: id,
6115
5020
  path: a.path,
6116
5021
  file,
6117
- spec,
6118
- text: a.text,
6119
- seconds,
6120
- composed: a.composed,
6121
- shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
6122
- crop: a.crop
5022
+ seconds
6123
5023
  });
6124
5024
  if (!spec) {
6125
5025
  if (a.channel !== "demo")
@@ -6146,17 +5046,6 @@ async function validateKit(kitPath, opts = {}) {
6146
5046
  `${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
6147
5047
  );
6148
5048
  }
6149
- if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
6150
- warnings.push(
6151
- `${label}: spec renders png, the file is ${file.split(".").pop()}`
6152
- );
6153
- }
6154
- for (const [id, n] of perDestination) {
6155
- const spec = specById.get(id);
6156
- if (spec?.count && (n < spec.count.min || n > spec.count.max))
6157
- problems.push(
6158
- `${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
6159
- );
6160
5049
  }
6161
5050
  if (!opts.picture) {
6162
5051
  return { valid: problems.length === 0, problems, warnings, measured };
@@ -6175,7 +5064,7 @@ async function validateKit(kitPath, opts = {}) {
6175
5064
 
6176
5065
  // src/plugin/recorder.ts
6177
5066
  import { writeFileSync } from "fs";
6178
- import { join as join10 } from "path";
5067
+ import { join as join8 } from "path";
6179
5068
  import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
6180
5069
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
6181
5070
  const dry = opts.dryRun === true;
@@ -6185,7 +5074,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6185
5074
  actions,
6186
5075
  {
6187
5076
  frame: (file, bytes) => {
6188
- writeFileSync(join10(paths.framesDir, file), bytes);
5077
+ writeFileSync(join8(paths.framesDir, file), bytes);
6189
5078
  }
6190
5079
  },
6191
5080
  log,
@@ -6237,11 +5126,11 @@ import {
6237
5126
 
6238
5127
  // src/plugin/session.ts
6239
5128
  import { spawn } from "child_process";
6240
- import { existsSync as existsSync10, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
5129
+ import { existsSync as existsSync8, mkdirSync, readdirSync, rmSync, statSync as statSync2 } from "fs";
6241
5130
  import { homedir } from "os";
6242
- import { join as join11 } from "path";
5131
+ import { join as join9 } from "path";
6243
5132
  import { chromium } from "playwright";
6244
- var SESSIONS_DIR = join11(homedir(), ".config", "vos", "sessions");
5133
+ var SESSIONS_DIR = join9(homedir(), ".config", "vos", "sessions");
6245
5134
  var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
6246
5135
  function sessionNameVerdict(name) {
6247
5136
  if (!name) return "a session needs a name: --name <app>";
@@ -6250,20 +5139,20 @@ function sessionNameVerdict(name) {
6250
5139
  return null;
6251
5140
  }
6252
5141
  function sessionDir(name) {
6253
- return join11(SESSIONS_DIR, name);
5142
+ return join9(SESSIONS_DIR, name);
6254
5143
  }
6255
5144
  function systemChromePath() {
6256
5145
  const explicit = process.env.VOS_BROWSER_PATH;
6257
5146
  if (explicit) return explicit;
6258
5147
  const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
6259
- join11(
5148
+ join9(
6260
5149
  process.env["PROGRAMFILES"] ?? "C:\\Program Files",
6261
5150
  "Google",
6262
5151
  "Chrome",
6263
5152
  "Application",
6264
5153
  "chrome.exe"
6265
5154
  ),
6266
- join11(
5155
+ join9(
6267
5156
  process.env["LOCALAPPDATA"] ?? "",
6268
5157
  "Google",
6269
5158
  "Chrome",
@@ -6275,7 +5164,7 @@ function systemChromePath() {
6275
5164
  "/usr/bin/google-chrome-stable",
6276
5165
  "/opt/google/chrome/chrome"
6277
5166
  ];
6278
- const found = candidates.find((c) => existsSync10(c));
5167
+ const found = candidates.find((c) => existsSync8(c));
6279
5168
  if (!found)
6280
5169
  throw new Error(
6281
5170
  "no Google Chrome found: a session needs the system Chrome (the bundled Chromium cannot read a profile Chrome wrote). Install Chrome, or set VOS_BROWSER_PATH"
@@ -6298,15 +5187,15 @@ async function openSession(name, url, opts = { log: () => {
6298
5187
  opts.log(
6299
5188
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
6300
5189
  );
6301
- return new Promise((resolve16, reject) => {
5190
+ return new Promise((resolve14, reject) => {
6302
5191
  const child = spawn(exe, args, { stdio: "ignore" });
6303
5192
  child.on("error", reject);
6304
- child.on("exit", (code) => resolve16({ code }));
5193
+ child.on("exit", (code) => resolve14({ code }));
6305
5194
  });
6306
5195
  }
6307
5196
  async function withSession(name, fn, opts = {}) {
6308
5197
  const dir = sessionDir(name);
6309
- if (!existsSync10(dir))
5198
+ if (!existsSync8(dir))
6310
5199
  throw new Error(`no session "${name}" (vos session list)`);
6311
5200
  const explicit = process.env.VOS_BROWSER_PATH;
6312
5201
  const ctx = await chromium.launchPersistentContext(dir, {
@@ -6355,9 +5244,9 @@ function summaryLine(s) {
6355
5244
  return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
6356
5245
  }
6357
5246
  function listSessions() {
6358
- if (!existsSync10(SESSIONS_DIR)) return [];
5247
+ if (!existsSync8(SESSIONS_DIR)) return [];
6359
5248
  return readdirSync(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
6360
- const st = statSync2(join11(SESSIONS_DIR, d.name));
5249
+ const st = statSync2(join9(SESSIONS_DIR, d.name));
6361
5250
  return {
6362
5251
  name: d.name,
6363
5252
  ageDays: Math.floor(
@@ -6368,7 +5257,7 @@ function listSessions() {
6368
5257
  }
6369
5258
  function removeSession(name) {
6370
5259
  const dir = sessionDir(name);
6371
- if (!existsSync10(dir)) return false;
5260
+ if (!existsSync8(dir)) return false;
6372
5261
  rmSync(dir, { recursive: true, force: true });
6373
5262
  return true;
6374
5263
  }
@@ -6433,9 +5322,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6433
5322
  }
6434
5323
 
6435
5324
  // src/plugin/plan.ts
6436
- import { existsSync as existsSync11 } from "fs";
6437
- import { readFile as readFile11 } from "fs/promises";
6438
- import { join as join12 } from "path";
5325
+ import { existsSync as existsSync9 } from "fs";
5326
+ import { readFile as readFile8 } from "fs/promises";
5327
+ import { join as join10 } from "path";
6439
5328
  import {
6440
5329
  DEFAULT_FRAME_STYLE,
6441
5330
  STYLE_FIELDS,
@@ -6657,10 +5546,10 @@ function retimeCut(prev, newSteps, newDurationMs) {
6657
5546
  // src/plugin/plan.ts
6658
5547
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6659
5548
  async function readDigestActivity(dir) {
6660
- const file = join12(dir, "digest", "digest.json");
6661
- if (!existsSync11(file)) return null;
5549
+ const file = join10(dir, "digest", "digest.json");
5550
+ if (!existsSync9(file)) return null;
6662
5551
  try {
6663
- const d = JSON.parse(await readFile11(file, "utf8"));
5552
+ const d = JSON.parse(await readFile8(file, "utf8"));
6664
5553
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
6665
5554
  } catch {
6666
5555
  return null;
@@ -6874,17 +5763,17 @@ async function planTake(dir, opts = {}) {
6874
5763
 
6875
5764
  // src/plugin/sync.ts
6876
5765
  import { createHash } from "crypto";
6877
- import { existsSync as existsSync12, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
6878
- import { readFile as readFile12 } from "fs/promises";
5766
+ import { existsSync as existsSync10, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
5767
+ import { readFile as readFile9 } from "fs/promises";
6879
5768
  import { createInterface } from "readline/promises";
6880
- import { basename, join as join14 } from "path";
6881
- import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
5769
+ import { basename, join as join12 } from "path";
5770
+ import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
6882
5771
 
6883
5772
  // src/plugin/folder.ts
6884
- import { mkdir as mkdir6, writeFile as writeFile8 } from "fs/promises";
6885
- import { join as join13 } from "path";
5773
+ import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
5774
+ import { join as join11 } from "path";
6886
5775
  import { recipeHints } from "@vosjs/shared/frontmatter";
6887
- import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
5776
+ import { migrateHostedDoc } from "@vosjs/studio-core";
6888
5777
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
6889
5778
  async function listFolders(origin, key) {
6890
5779
  const r = await apiJson(origin, "/api/folders", { key });
@@ -7058,25 +5947,25 @@ async function cmdPull(argv) {
7058
5947
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
7059
5948
  const payload = res.body;
7060
5949
  const out = strFlag(flags, "out") ?? folder.slug;
7061
- await mkdir6(join13(out, "recipes"), { recursive: true });
5950
+ await mkdir5(join11(out, "recipes"), { recursive: true });
7062
5951
  const lines = [];
7063
5952
  const recipes = [];
7064
5953
  for (const rec of payload.recipes) {
7065
- const path = join13(out, "recipes", safeName(rec.filename));
7066
- await writeFile8(path, rec.body ?? "");
5954
+ const path = join11(out, "recipes", safeName(rec.filename));
5955
+ await writeFile7(path, rec.body ?? "");
7067
5956
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
7068
5957
  }
7069
5958
  for (const rec of payload.inheritedRecipes) {
7070
5959
  const from = safeName(rec.folderSlug ?? "inherited");
7071
- await mkdir6(join13(out, "recipes", "_inherited", from), { recursive: true });
7072
- const path = join13(
5960
+ await mkdir5(join11(out, "recipes", "_inherited", from), { recursive: true });
5961
+ const path = join11(
7073
5962
  out,
7074
5963
  "recipes",
7075
5964
  "_inherited",
7076
5965
  from,
7077
5966
  safeName(rec.filename)
7078
5967
  );
7079
- await writeFile8(path, rec.body ?? "");
5968
+ await writeFile7(path, rec.body ?? "");
7080
5969
  recipes.push({
7081
5970
  path,
7082
5971
  line: `${recipeLine(rec.filename, rec.body ?? "")} [inherited from ${rec.folderName ?? from}]`
@@ -7084,15 +5973,15 @@ async function cmdPull(argv) {
7084
5973
  }
7085
5974
  const members = [];
7086
5975
  for (const v of payload.voses) {
7087
- const dir = join13(out, "members", safeName(v.slug || v.title || v.id));
7088
- await mkdir6(dir, { recursive: true });
5976
+ const dir = join11(out, "members", safeName(v.slug || v.title || v.id));
5977
+ await mkdir5(dir, { recursive: true });
7089
5978
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
7090
5979
  if (cfg.status !== 200) {
7091
5980
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
7092
5981
  continue;
7093
5982
  }
7094
- await writeFile8(
7095
- join13(dir, "config.json"),
5983
+ await writeFile7(
5984
+ join11(dir, "config.json"),
7096
5985
  JSON.stringify(cfg.body.config, null, 2)
7097
5986
  );
7098
5987
  let take = false;
@@ -7103,13 +5992,13 @@ async function cmdPull(argv) {
7103
5992
  { key }
7104
5993
  );
7105
5994
  if (doc.status === 200) {
7106
- await writeFile8(
7107
- join13(dir, "doc.json"),
5995
+ await writeFile7(
5996
+ join11(dir, "doc.json"),
7108
5997
  JSON.stringify(doc.body, null, 2)
7109
5998
  );
7110
5999
  take = true;
7111
6000
  if (flags.media === true) {
7112
- const hosted = migrateHostedDoc2(doc.body);
6001
+ const hosted = migrateHostedDoc(doc.body);
7113
6002
  await pullMedia({ origin, key }, dir, hosted, r.log);
7114
6003
  }
7115
6004
  }
@@ -7230,8 +6119,8 @@ function storageStateInDir(dir) {
7230
6119
  if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
7231
6120
  continue;
7232
6121
  try {
7233
- const v = JSON.parse(readFileSync2(join14(dir, f), "utf8"));
7234
- if (looksLikeStorageState(v)) return join14(dir, f);
6122
+ const v = JSON.parse(readFileSync2(join12(dir, f), "utf8"));
6123
+ if (looksLikeStorageState(v)) return join12(dir, f);
7235
6124
  } catch {
7236
6125
  }
7237
6126
  }
@@ -7252,7 +6141,7 @@ async function pushTake(dir, flags, r) {
7252
6141
  throw new Error(`doc.json fails lint:
7253
6142
  ${lint.problems.join("\n ")}`);
7254
6143
  }
7255
- if (!existsSync12(take.paths.recording)) {
6144
+ if (!existsSync10(take.paths.recording)) {
7256
6145
  throw new Error(
7257
6146
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
7258
6147
  );
@@ -7301,7 +6190,7 @@ async function pushTake(dir, flags, r) {
7301
6190
  throw new Error("push cancelled");
7302
6191
  }
7303
6192
  }
7304
- const bytes = await readFile12(take.paths.recording);
6193
+ const bytes = await readFile9(take.paths.recording);
7305
6194
  const hash = createHash("sha256").update(bytes).digest("hex");
7306
6195
  const { type: contentType } = resolveMediaType({
7307
6196
  head: bytes.subarray(0, MEDIA_HEAD_BYTES),
@@ -7473,9 +6362,9 @@ async function pullTake(dir, flags, r) {
7473
6362
  };
7474
6363
  }
7475
6364
  let media2;
7476
- if (flags.media && existsSync12(join14(dir, "doc.json"))) {
6365
+ if (flags.media && existsSync10(join12(dir, "doc.json"))) {
7477
6366
  const local = JSON.parse(
7478
- await readFile12(join14(dir, "doc.json"), "utf8")
6367
+ await readFile9(join12(dir, "doc.json"), "utf8")
7479
6368
  );
7480
6369
  media2 = await pullMedia(ctx, dir, local, r.log);
7481
6370
  }
@@ -7533,13 +6422,13 @@ async function pullTake(dir, flags, r) {
7533
6422
  docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
7534
6423
  );
7535
6424
  }
7536
- const hostedDoc = migrateHostedDoc3(docRes.json);
6425
+ const hostedDoc = migrateHostedDoc2(docRes.json);
7537
6426
  const doc = hostedDoc;
7538
6427
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
7539
- if (existsSync12(localRecording)) {
6428
+ if (existsSync10(localRecording)) {
7540
6429
  doc.source.videoKey = basename(localRecording);
7541
6430
  }
7542
- await writeJson(join14(dir, "doc.json"), doc, true);
6431
+ await writeJson(join12(dir, "doc.json"), doc, true);
7543
6432
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
7544
6433
  const versions = await api(ctx, `/vos/${vosId}/versions`);
7545
6434
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -7556,18 +6445,90 @@ async function pullTake(dir, flags, r) {
7556
6445
  media
7557
6446
  };
7558
6447
  }
6448
+ async function pushTakeClaimable(dir, flags, r) {
6449
+ const stateFile = storageStateInDir(dir);
6450
+ if (stateFile)
6451
+ throw new Error(
6452
+ `${stateFile} looks like a Playwright storage state (cookies + origins): a live session in the take directory would be sent with it. Move it out of the take and push again`
6453
+ );
6454
+ const take = await loadTake(dir);
6455
+ if (!take.doc) {
6456
+ throw new Error("no doc.json in this take \u2014 run `vos plan` first");
6457
+ }
6458
+ const lint = lintDoc(take.doc);
6459
+ if (lint.problems.length) {
6460
+ throw new Error(`doc.json fails lint:
6461
+ ${lint.problems.join("\n ")}`);
6462
+ }
6463
+ const state = readSyncState(dir);
6464
+ const hosted = state?.hostedMedia ?? {};
6465
+ const docForPush = structuredClone(take.doc);
6466
+ const refs = [
6467
+ ...isTakeRelativeKey(docForPush.source.videoKey) ? [
6468
+ {
6469
+ where: "the recording",
6470
+ key: docForPush.source.videoKey,
6471
+ set: (next) => {
6472
+ docForPush.source.videoKey = next;
6473
+ }
6474
+ }
6475
+ ] : [],
6476
+ ...sourceSidecarRefs(docForPush),
6477
+ ...docMediaRefs(docForPush)
6478
+ ];
6479
+ for (const ref of refs) {
6480
+ const file = takeRelativeFile(ref.key);
6481
+ const origin2 = hosted[file];
6482
+ if (!origin2) {
6483
+ throw new Error(
6484
+ `${ref.where} (${file}) is not a file this take fetched from vos.so, and a claimable push never uploads footage. Push the take with an account (vos login, then vos push ${dir}), or remix a public take fetched with \`vos fetch <url> --media\``
6485
+ );
6486
+ }
6487
+ const path = join12(dir, file);
6488
+ const now = existsSync10(path) ? await fileSha256(path) : null;
6489
+ if (now !== origin2.sha256) {
6490
+ throw new Error(
6491
+ `${ref.where} (${file}) changed since it was fetched, so it is no longer the hosted file. A claimable push names only the original; push with an account to upload the new one`
6492
+ );
6493
+ }
6494
+ ref.set(origin2.ref);
6495
+ }
6496
+ const lowered = lowerToComposition3(docForPush);
6497
+ const config = { ...lowered.config, data: lowered.data };
6498
+ const title = (flags.title ?? (state?.title ? `${state.title} remix` : basename(dir))).slice(0, 200);
6499
+ const body = { title, config, doc: docForPush };
6500
+ if (state?.vosId) body.remixOfId = state.vosId;
6501
+ if (flags.prompt) body.prompt = flags.prompt;
6502
+ if (flags.share) body.share = true;
6503
+ const origin = platformOrigin(flags);
6504
+ r.log(`creating a claimable push of ${title}\u2026`);
6505
+ const res = await apiJson(origin, "/api/claim", { method: "POST", body });
6506
+ if (res.status !== 201) {
6507
+ throw new Error(
6508
+ `claimable push failed (${res.status}): ${String(res.body.error ?? "")}${res.body.hint ? ` (${String(res.body.hint)})` : ""}`
6509
+ );
6510
+ }
6511
+ serverWarnings(res.body, (l) => r.log(l));
6512
+ const created = res.body.vos ?? {};
6513
+ return {
6514
+ id: created.id ?? null,
6515
+ title,
6516
+ claimUrl: String(res.body.claimUrl ?? ""),
6517
+ expiresAt: String(res.body.expiresAt ?? "")
6518
+ };
6519
+ }
7559
6520
 
7560
6521
  // src/plugin/program.ts
7561
- import { existsSync as existsSync14 } from "fs";
7562
- import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile9 } from "fs/promises";
6522
+ import { existsSync as existsSync12 } from "fs";
6523
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
7563
6524
  import { createInterface as createInterface2 } from "readline/promises";
7564
- import { basename as basename3, dirname as dirname3, join as join15 } from "path";
6525
+ import { basename as basename3, dirname as dirname2, join as join13 } from "path";
7565
6526
  import {
7566
6527
  CURRENT_CONFIG_VERSION,
7567
6528
  migrateConfig,
7568
6529
  vosConfigJsonSchema
7569
6530
  } from "@vosjs/core";
7570
- import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc4 } from "@vosjs/studio-core";
6531
+ import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7571
6532
 
7572
6533
  // src/plugin/login.ts
7573
6534
  import { hostname } from "os";
@@ -7695,12 +6656,12 @@ async function handoffLogin(url, r, opts = {}) {
7695
6656
 
7696
6657
  // src/plugin/programFiles.ts
7697
6658
  import { basename as basename2, extname as extname2, relative as relative2 } from "path";
7698
- import { existsSync as existsSync13, realpathSync } from "fs";
6659
+ import { existsSync as existsSync11, realpathSync } from "fs";
7699
6660
  function isLocalName(value) {
7700
6661
  if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
7701
6662
  if (value.startsWith("$")) return false;
7702
6663
  if (value.startsWith("<svg") || value.startsWith("<?xml")) return false;
7703
- if (value.startsWith("/")) return existsSync13(value);
6664
+ if (value.startsWith("/")) return existsSync11(value);
7704
6665
  return true;
7705
6666
  }
7706
6667
  var FILE_EXT = /\.(png|jpe?g|webp|gif|avif|svg|mp4|webm|mov|mp3|m4a|ogg|wav|flac|aac|glb|gltf|woff2?|ttf|otf|hdr|exr)$/i;
@@ -7875,8 +6836,8 @@ function preflightConfig(parsed) {
7875
6836
  }
7876
6837
  function resolveConfigPath(target) {
7877
6838
  if (target.endsWith(".json")) return target;
7878
- const inDir = join15(target, "config.json");
7879
- if (existsSync14(inDir)) return inDir;
6839
+ const inDir = join13(target, "config.json");
6840
+ if (existsSync12(inDir)) return inDir;
7880
6841
  throw new UsageError(
7881
6842
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
7882
6843
  );
@@ -7902,9 +6863,9 @@ async function cmdFetch(argv) {
7902
6863
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
7903
6864
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
7904
6865
  const out = strFlag(flags, "out") ?? slug;
7905
- await mkdir7(out, { recursive: true });
7906
- await writeFile9(
7907
- join15(out, "config.json"),
6866
+ await mkdir6(out, { recursive: true });
6867
+ await writeFile8(
6868
+ join13(out, "config.json"),
7908
6869
  JSON.stringify(cfg.body.config, null, 2)
7909
6870
  );
7910
6871
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -7926,32 +6887,33 @@ async function cmdFetch(argv) {
7926
6887
  }
7927
6888
  );
7928
6889
  if (doc.status === 200 && !("source" in doc.body)) {
7929
- const hosted = migrateHostedDoc4(doc.body);
6890
+ const hosted = migrateHostedDoc3(doc.body);
7930
6891
  const { config: own } = await writeProgramDoc(out, hosted);
7931
6892
  if (own) {
7932
- await writeFile9(join15(out, "config.json"), JSON.stringify(own, null, 2));
6893
+ await writeFile8(join13(out, "config.json"), JSON.stringify(own, null, 2));
7933
6894
  }
7934
6895
  } else if (doc.status === 200) {
7935
6896
  take = true;
7936
- const hosted = migrateHostedDoc4(doc.body);
7937
- await writeFile9(join15(out, "doc.json"), JSON.stringify(hosted, null, 2));
6897
+ const hosted = migrateHostedDoc3(doc.body);
6898
+ await writeFile8(join13(out, "doc.json"), JSON.stringify(hosted, null, 2));
7938
6899
  if (flags.media === true) {
7939
6900
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
6901
+ await recordHostedMedia(out, vosId, media);
7940
6902
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
7941
6903
  }
7942
6904
  }
7943
6905
  }
7944
6906
  let assetsLine = "";
7945
6907
  if (!take) {
7946
- const configPath = join15(out, "config.json");
7947
- const written = JSON.parse(await readFile13(configPath, "utf8"));
6908
+ const configPath = join13(out, "config.json");
6909
+ const written = JSON.parse(await readFile10(configPath, "utf8"));
7948
6910
  const hostedCount = manifestRefs(written).filter(
7949
6911
  ({ ref }) => hostedAssetId(ref, [origin, "https://vos.so", "https://www.vos.so"])
7950
6912
  ).length;
7951
6913
  if (hostedCount && flags.media === true) {
7952
6914
  const pulled = await pullManifest({ origin, key }, out, written, r.log);
7953
6915
  if (pulled) {
7954
- await writeFile9(
6916
+ await writeFile8(
7955
6917
  configPath,
7956
6918
  JSON.stringify({ ...written, assets: pulled.assets }, null, 2)
7957
6919
  );
@@ -8070,8 +7032,8 @@ async function cmdPushProgram(argv) {
8070
7032
  origin: strFlag(flags, "origin"),
8071
7033
  api: strFlag(flags, "api")
8072
7034
  });
8073
- const dir = dirname3(source);
8074
- const parsed = JSON.parse(await readFile13(source, "utf8"));
7035
+ const dir = dirname2(source);
7036
+ const parsed = JSON.parse(await readFile10(source, "utf8"));
8075
7037
  const pre = preflightConfig(parsed);
8076
7038
  if (!pre.ok || !pre.config) {
8077
7039
  for (const issue of pre.issues) r.log(`error ${issue}`);
@@ -8142,6 +7104,9 @@ async function cmdPushProgram(argv) {
8142
7104
  if (uploadToken) body.uploadToken = uploadToken;
8143
7105
  if (prompt) body.prompt = prompt;
8144
7106
  if (flags.share === true) body.share = true;
7107
+ const remixOf = strFlag(flags, "remix-of");
7108
+ const remixOfId2 = remixOf ? parseVosId(remixOf) : state?.vosId;
7109
+ if (remixOfId2) body.remixOfId = remixOfId2;
8145
7110
  const slug = strFlag(flags, "slug");
8146
7111
  if (slug) body.slug = slug;
8147
7112
  const res = await apiJson(origin, "/api/claim", { method: "POST", body });
@@ -8351,7 +7316,7 @@ ${landedLines(landed2)}` : "")
8351
7316
  async function cmdPullProgram(argv) {
8352
7317
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
8353
7318
  const target = positionals[0] ?? ".";
8354
- const dir = target.endsWith(".json") ? dirname3(target) : target;
7319
+ const dir = target.endsWith(".json") ? dirname2(target) : target;
8355
7320
  const r = createReporter(flags.json === true);
8356
7321
  const origin = platformOrigin({
8357
7322
  origin: strFlag(flags, "origin"),
@@ -8416,10 +7381,10 @@ async function cmdPullProgram(argv) {
8416
7381
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
8417
7382
  if (cfg.status !== 200)
8418
7383
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
8419
- const configPath = join15(dir, "config.json");
7384
+ const configPath = join13(dir, "config.json");
8420
7385
  let backedUp = false;
8421
- if (existsSync14(configPath)) {
8422
- await writeFile9(join15(dir, "config.backup.json"), await readFile13(configPath));
7386
+ if (existsSync12(configPath)) {
7387
+ await writeFile8(join13(dir, "config.backup.json"), await readFile10(configPath));
8423
7388
  backedUp = true;
8424
7389
  }
8425
7390
  let headConfig = cfg.body.config;
@@ -8433,12 +7398,12 @@ async function cmdPullProgram(argv) {
8433
7398
  if (docRes.status === 200 && !("source" in docRes.body)) {
8434
7399
  const { config: own } = await writeProgramDoc(
8435
7400
  dir,
8436
- migrateHostedDoc4(docRes.body)
7401
+ migrateHostedDoc3(docRes.body)
8437
7402
  );
8438
7403
  if (own) headConfig = own;
8439
7404
  }
8440
7405
  }
8441
- await writeFile9(configPath, JSON.stringify(headConfig, null, 2));
7406
+ await writeFile8(configPath, JSON.stringify(headConfig, null, 2));
8442
7407
  writeSyncState(dir, {
8443
7408
  vosId,
8444
7409
  versionId: typeof head.id === "string" ? head.id : since
@@ -8452,7 +7417,7 @@ async function cmdPullProgram(argv) {
8452
7417
  protected: protectedIds,
8453
7418
  changes,
8454
7419
  out: configPath,
8455
- backup: backedUp ? join15(dir, "config.backup.json") : null
7420
+ backup: backedUp ? join13(dir, "config.backup.json") : null
8456
7421
  },
8457
7422
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
8458
7423
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -8560,9 +7525,9 @@ function storedProgramConfig(config, programDoc) {
8560
7525
  return lowerProgramDoc(doc).config;
8561
7526
  }
8562
7527
  async function readProgramDoc(dir, config) {
8563
- const docPath = join15(dir, "doc.json");
8564
- if (!existsSync14(docPath)) return null;
8565
- const parsed = JSON.parse(await readFile13(docPath, "utf8"));
7528
+ const docPath = join13(dir, "doc.json");
7529
+ if (!existsSync12(docPath)) return null;
7530
+ const parsed = JSON.parse(await readFile10(docPath, "utf8"));
8566
7531
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
8567
7532
  return null;
8568
7533
  const raw = parsed;
@@ -8570,23 +7535,23 @@ async function readProgramDoc(dir, config) {
8570
7535
  return { ...raw, program: { ...program, config } };
8571
7536
  }
8572
7537
  async function setProgramStill(dir, t) {
8573
- const docPath = join15(dir, "doc.json");
7538
+ const docPath = join13(dir, "doc.json");
8574
7539
  let doc = { program: {}, audio: [] };
8575
- if (existsSync14(docPath)) {
8576
- const parsed = JSON.parse(await readFile13(docPath, "utf8"));
7540
+ if (existsSync12(docPath)) {
7541
+ const parsed = JSON.parse(await readFile10(docPath, "utf8"));
8577
7542
  if (!parsed || typeof parsed !== "object" || "source" in parsed)
8578
7543
  throw new UsageError(
8579
7544
  `${docPath} is a take's document; a take's cover is its own "still" field`
8580
7545
  );
8581
7546
  doc = parsed;
8582
7547
  }
8583
- await writeFile9(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
7548
+ await writeFile8(docPath, JSON.stringify({ ...doc, still: t }, null, 2) + "\n");
8584
7549
  }
8585
7550
  async function writeProgramDoc(dir, hosted) {
8586
7551
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
8587
7552
  const { config, ...rest } = program;
8588
7553
  const onDisk = { ...hosted, program: rest };
8589
- await writeFile9(join15(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
7554
+ await writeFile8(join13(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8590
7555
  return {
8591
7556
  config: config && typeof config === "object" ? config : null
8592
7557
  };
@@ -8998,8 +7963,8 @@ async function cmdAsset(argv) {
8998
7963
  }
8999
7964
 
9000
7965
  // src/plugin/remove.ts
9001
- import { existsSync as existsSync15, rmSync as rmSync2, statSync as statSync3 } from "fs";
9002
- import { join as join16 } from "path";
7966
+ import { existsSync as existsSync13, rmSync as rmSync2, statSync as statSync3 } from "fs";
7967
+ import { join as join14 } from "path";
9003
7968
  import { createInterface as createInterface3 } from "readline/promises";
9004
7969
 
9005
7970
  // src/plugin/trash.ts
@@ -9178,7 +8143,7 @@ async function cmdDelete(argv) {
9178
8143
  api: strFlag(flags, "api")
9179
8144
  }).replace(/\/+$/, "");
9180
8145
  const key = requireCredential(strFlag(flags, "key"));
9181
- const dir = existsSync15(target) && statSync3(target).isDirectory() ? target : null;
8146
+ const dir = existsSync13(target) && statSync3(target).isDirectory() ? target : null;
9182
8147
  const vosId = dir ? readSyncState(dir)?.vosId : parseVosId(target);
9183
8148
  if (!vosId)
9184
8149
  throw new UsageError(
@@ -9221,7 +8186,7 @@ async function cmdDelete(argv) {
9221
8186
  const tracking = dir ?? trackingCwd(vosId);
9222
8187
  let unlinked = false;
9223
8188
  if (tracking) {
9224
- rmSync2(join16(tracking, SYNC_STATE_NAME), { force: true });
8189
+ rmSync2(join14(tracking, SYNC_STATE_NAME), { force: true });
9225
8190
  unlinked = true;
9226
8191
  }
9227
8192
  const trashed = res.body.trashed === true;
@@ -9246,13 +8211,13 @@ async function cmdDelete(argv) {
9246
8211
  }
9247
8212
  function trackingCwd(vosId) {
9248
8213
  const cwd = process.cwd();
9249
- if (!existsSync15(join16(cwd, SYNC_STATE_NAME))) return null;
8214
+ if (!existsSync13(join14(cwd, SYNC_STATE_NAME))) return null;
9250
8215
  return readSyncState(cwd)?.vosId === vosId ? cwd : null;
9251
8216
  }
9252
8217
 
9253
8218
  // src/plugin/buildProgram.ts
9254
8219
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
9255
- import { basename as basename5, dirname as dirname4, join as join17, resolve as resolve7 } from "path";
8220
+ import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve5 } from "path";
9256
8221
  import { pathToFileURL } from "url";
9257
8222
  import { parse } from "acorn";
9258
8223
  import { fullAncestor } from "acorn-walk";
@@ -9452,7 +8417,7 @@ async function cmdBuild(argv) {
9452
8417
  if (!source)
9453
8418
  throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
9454
8419
  const r = createReporter(flags.json === true);
9455
- const file = resolve7(source);
8420
+ const file = resolve5(source);
9456
8421
  const text = readFileSync4(file, "utf8");
9457
8422
  const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
9458
8423
  const program = mod.default ?? mod.program;
@@ -9469,8 +8434,8 @@ async function cmdBuild(argv) {
9469
8434
  );
9470
8435
  return EXIT_ERROR;
9471
8436
  }
9472
- const out = resolve7(
9473
- strFlag(flags, "out") ?? join17(dirname4(file), "config.json")
8437
+ const out = resolve5(
8438
+ strFlag(flags, "out") ?? join15(dirname3(file), "config.json")
9474
8439
  );
9475
8440
  writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
9476
8441
  const sidecar = out.replace(/\.json$/, "") + ".sources.json";
@@ -9487,8 +8452,8 @@ async function cmdBuild(argv) {
9487
8452
 
9488
8453
  // src/plugin/compare.ts
9489
8454
  import { spawnSync } from "child_process";
9490
- import { existsSync as existsSync16, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
9491
- import { dirname as dirname5, join as join18, resolve as resolve8 } from "path";
8455
+ import { existsSync as existsSync14, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
8456
+ import { dirname as dirname4, join as join16, resolve as resolve6 } from "path";
9492
8457
  var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
9493
8458
  function sampleTimes(duration, every) {
9494
8459
  const step = every > 0 ? every : 1;
@@ -9549,13 +8514,13 @@ async function cmdCompare(argv) {
9549
8514
  );
9550
8515
  requireFfmpeg();
9551
8516
  const r = createReporter(flags.json === true);
9552
- const ref = resolve8(against);
8517
+ const ref = resolve6(against);
9553
8518
  const loaded = await loadVosConfig(source);
9554
8519
  for (const w of loaded.warnings) r.log(`note: ${w}`);
9555
8520
  const config = loaded.config;
9556
8521
  const duration = configDuration(config) ?? 5;
9557
8522
  const assets = await programAssets(config, {
9558
- baseDir: /^https?:\/\//.test(source) ? null : existsSync16(source) && statSync4(source).isDirectory() ? source : dirname5(source),
8523
+ baseDir: /^https?:\/\//.test(source) ? null : existsSync14(source) && statSync4(source).isDirectory() ? source : dirname4(source),
9559
8524
  origin: platformOrigin({}),
9560
8525
  key: resolveCredential(),
9561
8526
  log: (line) => r.log(line)
@@ -9571,16 +8536,16 @@ async function cmdCompare(argv) {
9571
8536
  const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
9572
8537
  const timesRaw = strFlag(flags, "times");
9573
8538
  const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
9574
- const outDir = resolve8(strFlag(flags, "out") ?? "compare");
8539
+ const outDir = resolve6(strFlag(flags, "out") ?? "compare");
9575
8540
  mkdirSync2(outDir, { recursive: true });
9576
8541
  const browser = await launchBrowser();
9577
8542
  const scores = [];
9578
8543
  try {
9579
8544
  for (const t of times) {
9580
8545
  const tag = t.toFixed(2);
9581
- const refPng = join18(outDir, `src-${tag}.png`);
9582
- const vosPng = join18(outDir, `vos-${tag}.png`);
9583
- const sheet = join18(outDir, `sheet-${tag}.png`);
8546
+ const refPng = join16(outDir, `src-${tag}.png`);
8547
+ const vosPng = join16(outDir, `vos-${tag}.png`);
8548
+ const sheet = join16(outDir, `sheet-${tag}.png`);
9584
8549
  const grab = ffmpeg([
9585
8550
  "-v",
9586
8551
  "error",
@@ -9637,7 +8602,7 @@ async function cmdCompare(argv) {
9637
8602
  } finally {
9638
8603
  await browser.close();
9639
8604
  }
9640
- const overview = join18(outDir, "overview.png");
8605
+ const overview = join16(outDir, "overview.png");
9641
8606
  if (scores.length) {
9642
8607
  const inputs = scores.flatMap((s) => ["-i", s.sheet]);
9643
8608
  const scaled = scores.map((_, i) => `[${i}:v]scale=iw/3:-2[s${i}]`).join(";");
@@ -9663,7 +8628,7 @@ async function cmdCompare(argv) {
9663
8628
  overview
9664
8629
  };
9665
8630
  writeFileSync3(
9666
- join18(outDir, "report.json"),
8631
+ join16(outDir, "report.json"),
9667
8632
  JSON.stringify(report, null, 2) + "\n"
9668
8633
  );
9669
8634
  r.done(
@@ -9674,28 +8639,28 @@ async function cmdCompare(argv) {
9674
8639
  }
9675
8640
 
9676
8641
  // src/plugin/port.ts
9677
- import { existsSync as existsSync20, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9678
- import { dirname as dirname8, join as join22, resolve as resolve11 } from "path";
8642
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
8643
+ import { dirname as dirname7, join as join20, resolve as resolve9 } from "path";
9679
8644
  import { pathToFileURL as pathToFileURL3 } from "url";
9680
8645
 
9681
8646
  // src/plugin/portInventory.ts
9682
8647
  import { spawnSync as spawnSync2 } from "child_process";
9683
8648
  import {
9684
- existsSync as existsSync18,
8649
+ existsSync as existsSync16,
9685
8650
  mkdirSync as mkdirSync3,
9686
8651
  readFileSync as readFileSync6,
9687
8652
  readdirSync as readdirSync4,
9688
8653
  statSync as statSync6
9689
8654
  } from "fs";
9690
- import { dirname as dirname6, join as join20, relative as relative3, resolve as resolve9 } from "path";
8655
+ import { dirname as dirname5, join as join18, relative as relative3, resolve as resolve7 } from "path";
9691
8656
  import { pathToFileURL as pathToFileURL2 } from "url";
9692
8657
  import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
9693
8658
 
9694
8659
  // src/plugin/portRemotion.ts
9695
- import { existsSync as existsSync17, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
8660
+ import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
9696
8661
  import { createServer as createServer2 } from "http";
9697
8662
  import { createRequire } from "module";
9698
- import { basename as basename6, extname as extname3, join as join19 } from "path";
8663
+ import { basename as basename6, extname as extname3, join as join17 } from "path";
9699
8664
  function themeColors(sources) {
9700
8665
  const out = {};
9701
8666
  const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
@@ -9761,14 +8726,14 @@ function sourceTexts(dir) {
9761
8726
  if (depth > 4 || out.length > 200) return;
9762
8727
  for (const name of readdirSync3(d)) {
9763
8728
  if (name === "node_modules" || name.startsWith(".")) continue;
9764
- const p = join19(d, name);
8729
+ const p = join17(d, name);
9765
8730
  if (statSync5(p).isDirectory()) walk(p, depth + 1);
9766
8731
  else if (/\.(tsx?|jsx?|mjs)$/.test(name))
9767
8732
  out.push(readFileSync5(p, "utf8"));
9768
8733
  }
9769
8734
  };
9770
- const src = join19(dir, "src");
9771
- walk(existsSync17(src) ? src : dir, 0);
8735
+ const src = join17(dir, "src");
8736
+ walk(existsSync15(src) ? src : dir, 0);
9772
8737
  return out;
9773
8738
  }
9774
8739
  function entryPoint(dir) {
@@ -9780,7 +8745,7 @@ function entryPoint(dir) {
9780
8745
  "remotion/index.ts",
9781
8746
  "remotion/index.tsx"
9782
8747
  ])
9783
- if (existsSync17(join19(dir, p))) return join19(dir, p);
8748
+ if (existsSync15(join17(dir, p))) return join17(dir, p);
9784
8749
  return null;
9785
8750
  }
9786
8751
  var TYPES = {
@@ -9855,7 +8820,7 @@ async function readRemotion(dir, log) {
9855
8820
  const entry = entryPoint(dir);
9856
8821
  let bundler;
9857
8822
  try {
9858
- bundler = createRequire(join19(dir, "package.json"))("@remotion/bundler");
8823
+ bundler = createRequire(join17(dir, "package.json"))("@remotion/bundler");
9859
8824
  } catch {
9860
8825
  log?.(
9861
8826
  "a Remotion project whose dependencies are not installed: run `npm install` in it so vos can read its compositions (words, scenes, faces); reading only the render"
@@ -9872,9 +8837,9 @@ async function readRemotion(dir, log) {
9872
8837
  } });
9873
8838
  const server = createServer2((rq, rs) => {
9874
8839
  const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
9875
- let f = join19(out, path);
9876
- if (!f.startsWith(out) || !existsSync17(f) || statSync5(f).isDirectory())
9877
- f = join19(out, "index.html");
8840
+ let f = join17(out, path);
8841
+ if (!f.startsWith(out) || !existsSync15(f) || statSync5(f).isDirectory())
8842
+ f = join17(out, "index.html");
9878
8843
  rs.writeHead(200, {
9879
8844
  "content-type": TYPES[extname3(f)] ?? "application/octet-stream"
9880
8845
  });
@@ -9973,10 +8938,10 @@ async function readRemotion(dir, log) {
9973
8938
  perScene.push({ scene: s.name, texts: read.texts ?? [] });
9974
8939
  }
9975
8940
  const sources = sourceTexts(dir);
9976
- const publicDir = join19(dir, "public");
8941
+ const publicDir = join17(dir, "public");
9977
8942
  const media = tree.audio.map((a) => {
9978
8943
  const name = basename6(a.src.split("?")[0]);
9979
- const local = existsSync17(join19(publicDir, name)) ? `public/${name}` : a.src;
8944
+ const local = existsSync15(join17(publicDir, name)) ? `public/${name}` : a.src;
9980
8945
  return {
9981
8946
  kind: "audio",
9982
8947
  src: local,
@@ -10017,9 +8982,9 @@ async function readRemotion(dir, log) {
10017
8982
 
10018
8983
  // src/plugin/portInventory.ts
10019
8984
  function detectEngine(dir) {
10020
- if (existsSync18(join20(dir, "hyperframes.json"))) return "hyperframes";
10021
- const pkg = join20(dir, "package.json");
10022
- if (existsSync18(pkg)) {
8985
+ if (existsSync16(join18(dir, "hyperframes.json"))) return "hyperframes";
8986
+ const pkg = join18(dir, "package.json");
8987
+ if (existsSync16(pkg)) {
10023
8988
  try {
10024
8989
  const p = JSON.parse(readFileSync6(pkg, "utf8"));
10025
8990
  const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
@@ -10033,14 +8998,14 @@ function detectEngine(dir) {
10033
8998
  return "page";
10034
8999
  }
10035
9000
  function sourcePage(source) {
10036
- if (!existsSync18(source)) return null;
9001
+ if (!existsSync16(source)) return null;
10037
9002
  if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
10038
- const index = join20(source, "index.html");
10039
- return existsSync18(index) ? index : null;
9003
+ const index = join18(source, "index.html");
9004
+ return existsSync16(index) ? index : null;
10040
9005
  }
10041
9006
  function findRender(dir) {
10042
- const videos = (d) => existsSync18(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join20(d, f)) : [];
10043
- for (const d of [join20(dir, "renders"), join20(dir, "out"), dir]) {
9007
+ const videos = (d) => existsSync16(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join18(d, f)) : [];
9008
+ for (const d of [join18(dir, "renders"), join18(dir, "out"), dir]) {
10044
9009
  const found = videos(d);
10045
9010
  if (found.length) return found[0];
10046
9011
  }
@@ -10310,8 +9275,8 @@ var SEEK_AND_READ = `((args) => {
10310
9275
  return out
10311
9276
  })`;
10312
9277
  async function readInventory(source, opts) {
10313
- const abs = resolve9(source);
10314
- const dir = existsSync18(abs) && statSync6(abs).isDirectory() ? abs : dirname6(abs);
9278
+ const abs = resolve7(source);
9279
+ const dir = existsSync16(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
10315
9280
  const engine = detectEngine(dir);
10316
9281
  const page = sourcePage(abs);
10317
9282
  const render = opts.render === void 0 ? findRender(dir) : opts.render;
@@ -10414,7 +9379,7 @@ async function readInventory(source, opts) {
10414
9379
  if (render && duration) {
10415
9380
  const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
10416
9381
  if (has) {
10417
- const refDir = join20(opts.outDir, "ref");
9382
+ const refDir = join18(opts.outDir, "ref");
10418
9383
  mkdirSync3(refDir, { recursive: true });
10419
9384
  const times = read.scenes.length >= 3 ? read.scenes.map(
10420
9385
  (s) => s.start + (s.duration ?? 1) / 2
@@ -10423,7 +9388,7 @@ async function readInventory(source, opts) {
10423
9388
  (_, i) => i + 0.5
10424
9389
  );
10425
9390
  for (const t of times.filter((t2) => t2 < duration)) {
10426
- const file = join20(refDir, `src-${t.toFixed(2)}.png`);
9391
+ const file = join18(refDir, `src-${t.toFixed(2)}.png`);
10427
9392
  const ok = spawnSync2("ffmpeg", [
10428
9393
  "-v",
10429
9394
  "error",
@@ -10453,7 +9418,7 @@ async function readInventory(source, opts) {
10453
9418
  const scriptStrings = scriptStringsOf(read.scriptText);
10454
9419
  return {
10455
9420
  source: relative3(process.cwd(), abs) || ".",
10456
- root: abs.endsWith(".html") ? dirname6(abs) : abs,
9421
+ root: abs.endsWith(".html") ? dirname5(abs) : abs,
10457
9422
  engine,
10458
9423
  width: read.width,
10459
9424
  height: read.height,
@@ -10477,8 +9442,8 @@ async function readInventory(source, opts) {
10477
9442
 
10478
9443
  // src/plugin/portScaffold.ts
10479
9444
  import { spawnSync as spawnSync3 } from "child_process";
10480
- import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
10481
- import { basename as basename7, dirname as dirname7, join as join21, resolve as resolve10 } from "path";
9445
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
9446
+ import { basename as basename7, dirname as dirname6, join as join19, resolve as resolve8 } from "path";
10482
9447
  import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
10483
9448
 
10484
9449
  // src/plugin/painterLib.ts
@@ -10887,9 +9852,9 @@ function planScaffold(inv, opts) {
10887
9852
  const audio = [];
10888
9853
  const copies = [];
10889
9854
  inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
10890
- const from = resolve10(opts.sourceDir, m.src);
9855
+ const from = resolve8(opts.sourceDir, m.src);
10891
9856
  const name = basename7(from);
10892
- if (!existsSync19(from)) {
9857
+ if (!existsSync17(from)) {
10893
9858
  report.push(
10894
9859
  `- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
10895
9860
  );
@@ -11087,13 +10052,13 @@ function reportText(inv, r) {
11087
10052
  }
11088
10053
  function writeScaffold(plan, outDir) {
11089
10054
  mkdirSync4(outDir, { recursive: true });
11090
- writeFileSync4(join21(outDir, "program.mjs"), plan.program);
10055
+ writeFileSync4(join19(outDir, "program.mjs"), plan.program);
11091
10056
  writeFileSync4(
11092
- join21(outDir, "doc.json"),
10057
+ join19(outDir, "doc.json"),
11093
10058
  JSON.stringify(plan.doc, null, 2) + "\n"
11094
10059
  );
11095
- writeFileSync4(join21(dirname7(outDir), "REPORT.md"), plan.report);
11096
- for (const [from, name] of plan.copies) copyFileSync(from, join21(outDir, name));
10060
+ writeFileSync4(join19(dirname6(outDir), "REPORT.md"), plan.report);
10061
+ for (const [from, name] of plan.copies) copyFileSync(from, join19(outDir, name));
11097
10062
  }
11098
10063
  function probeAudioDuration(file) {
11099
10064
  const res = spawnSync3(
@@ -11119,15 +10084,15 @@ async function inventory(argv) {
11119
10084
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11120
10085
  const source = positionals[0] ?? ".";
11121
10086
  const r = createReporter(flags.json === true);
11122
- const outDir = resolve11(strFlag(flags, "out") ?? "port");
10087
+ const outDir = resolve9(strFlag(flags, "out") ?? "port");
11123
10088
  mkdirSync5(outDir, { recursive: true });
11124
10089
  const renderFlag = strFlag(flags, "render");
11125
10090
  const inv = await readInventory(source, {
11126
- render: renderFlag === void 0 ? void 0 : resolve11(renderFlag),
10091
+ render: renderFlag === void 0 ? void 0 : resolve9(renderFlag),
11127
10092
  outDir,
11128
10093
  log: (l) => r.log(`note: ${l}`)
11129
10094
  });
11130
- const file = join22(outDir, "inventory.json");
10095
+ const file = join20(outDir, "inventory.json");
11131
10096
  writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
11132
10097
  const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
11133
10098
  r.log(
@@ -11159,38 +10124,38 @@ function summary(inv) {
11159
10124
  async function scaffold(argv) {
11160
10125
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11161
10126
  const r = createReporter(flags.json === true);
11162
- const from = resolve11(strFlag(flags, "from") ?? join22("port", "inventory.json"));
11163
- if (!existsSync20(from))
10127
+ const from = resolve9(strFlag(flags, "from") ?? join20("port", "inventory.json"));
10128
+ if (!existsSync18(from))
11164
10129
  throw new UsageError(
11165
10130
  `no inventory at ${from}: run npx vos port inventory <source> first`
11166
10131
  );
11167
10132
  const inv = JSON.parse(readFileSync7(from, "utf8"));
11168
- const outDir = resolve11(
11169
- strFlag(flags, "out") ?? join22(dirname8(from), "program")
10133
+ const outDir = resolve9(
10134
+ strFlag(flags, "out") ?? join20(dirname7(from), "program")
11170
10135
  );
11171
10136
  const plan = planScaffold(inv, {
11172
10137
  sourceDir: scaffoldSourceDir(inv),
11173
10138
  probeDuration: probeAudioDuration
11174
10139
  });
11175
10140
  writeScaffold(plan, outDir);
11176
- const programFile = join22(outDir, "program.mjs");
10141
+ const programFile = join20(outDir, "program.mjs");
11177
10142
  const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
11178
10143
  const built = buildProgram(mod.default, "program.mjs", plan.program);
11179
10144
  for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
11180
10145
  if (built.problems.length) return EXIT_ERROR;
11181
10146
  writeFileSync5(
11182
- join22(outDir, "config.json"),
10147
+ join20(outDir, "config.json"),
11183
10148
  JSON.stringify(built.config, null, 2) + "\n"
11184
10149
  );
11185
10150
  r.done(
11186
- { program: outDir, report: join22(dirname8(outDir), "REPORT.md") },
11187
- `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join22(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
10151
+ { program: outDir, report: join20(dirname7(outDir), "REPORT.md") },
10152
+ `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join20(dirname7(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
11188
10153
  );
11189
10154
  return EXIT_OK;
11190
10155
  }
11191
10156
  function scaffoldSourceDir(inv) {
11192
10157
  if (inv.root) return inv.root;
11193
- return inv.source.endsWith(".html") ? dirname8(resolve11(inv.source)) : resolve11(inv.source);
10158
+ return inv.source.endsWith(".html") ? dirname7(resolve9(inv.source)) : resolve9(inv.source);
11194
10159
  }
11195
10160
 
11196
10161
  // src/plugin/recipe.ts
@@ -11281,8 +10246,8 @@ async function cmdRecipe(argv) {
11281
10246
  }
11282
10247
 
11283
10248
  // src/plugin/brand.ts
11284
- import { writeFile as writeFile10 } from "fs/promises";
11285
- import { resolve as resolve12 } from "path";
10249
+ import { writeFile as writeFile9 } from "fs/promises";
10250
+ import { resolve as resolve10 } from "path";
11286
10251
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
11287
10252
  import { lookKindForGround } from "@vosjs/studio-core";
11288
10253
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -11570,7 +10535,7 @@ function renderBrandMd(c, claim) {
11570
10535
  "",
11571
10536
  "## Use",
11572
10537
  "",
11573
- `The poster family binds these roles: \`bgA/bgB/bgC\` for the ground, \`ink\` for type, \`fontDisplay\` for the headline (if it is not in the hosted catalog, \`vos check\` warns and the nearest hosted serif or sans stands in), \`logoUrl\` for the mark (placed beside the wordmark on the cards and the end card; \`logoOnDarkUrl\` stands in on a dark ground). A site that publishes only a favicon or an app icon gets the icon here, tile and all: publish a bare mark and name it in design.md. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
10538
+ `The look, the titles and the end card bind these roles: \`bgA/bgB/bgC\` for the ground, \`ink\` for type, \`fontDisplay\` for the headline (if it is not in the hosted catalog, \`vos check\` warns and the nearest hosted serif or sans stands in), \`logoUrl\` for the mark (placed beside the wordmark on the end card; \`logoOnDarkUrl\` stands in on a dark ground). A site that publishes only a favicon or an app icon gets the icon here, tile and all: publish a bare mark and name it in design.md. A take's frame ground is a gradient of \`accent\` toward \`bgC\` unless the brand is paper, in which case \`bgB\` toward \`bgC\`.`
11574
10539
  ];
11575
10540
  if (c.avoid.length) {
11576
10541
  lines.push("", "## Avoid (the site says)", "");
@@ -11601,7 +10566,7 @@ async function cmdBrand(argv) {
11601
10566
  throw new UsageError(`not a URL: ${raw}`);
11602
10567
  }
11603
10568
  const r = createReporter(flags.json === true);
11604
- const out = resolve12(strFlag(flags, "out") ?? "BRAND.md");
10569
+ const out = resolve10(strFlag(flags, "out") ?? "BRAND.md");
11605
10570
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
11606
10571
  const [designText, llmsText] = await Promise.all([
11607
10572
  fetchText(`${origin}/design.md`),
@@ -11635,7 +10600,7 @@ async function cmdBrand(argv) {
11635
10600
  composition,
11636
10601
  llms?.claim ?? design?.description ?? null
11637
10602
  );
11638
- await writeFile10(out, md);
10603
+ await writeFile9(out, md);
11639
10604
  const k = composition.kit;
11640
10605
  r.done(
11641
10606
  {
@@ -11750,8 +10715,8 @@ async function recordHosted(input) {
11750
10715
  }
11751
10716
 
11752
10717
  // src/plugin/agentBrowser.ts
11753
- import { writeFile as writeFile11 } from "fs/promises";
11754
- import { resolve as resolve13 } from "path";
10718
+ import { writeFile as writeFile10 } from "fs/promises";
10719
+ import { resolve as resolve11 } from "path";
11755
10720
  function parseAgentBrowserLog(text) {
11756
10721
  const records = [];
11757
10722
  const problems = [];
@@ -12222,7 +11187,7 @@ async function cmdActions(argv) {
12222
11187
  );
12223
11188
  }
12224
11189
  const r = createReporter(flags.json === true);
12225
- const out = resolve13(strFlag(flags, "out") ?? "actions.json");
11190
+ const out = resolve11(strFlag(flags, "out") ?? "actions.json");
12226
11191
  const viewportFlag = strFlag(flags, "viewport");
12227
11192
  let viewport;
12228
11193
  if (viewportFlag) {
@@ -12230,8 +11195,8 @@ async function cmdActions(argv) {
12230
11195
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
12231
11196
  viewport = { width: Number(m[1]), height: Number(m[2]) };
12232
11197
  }
12233
- const { readFile: readFile16 } = await import("fs/promises");
12234
- const text = await readFile16(resolve13(input), "utf8");
11198
+ const { readFile: readFile13 } = await import("fs/promises");
11199
+ const text = await readFile13(resolve11(input), "utf8");
12235
11200
  const { records, problems } = parseAgentBrowserLog(text);
12236
11201
  for (const p of problems) r.log(` ${p}`);
12237
11202
  const result = convertAgentBrowser(records, {
@@ -12248,7 +11213,7 @@ async function cmdActions(argv) {
12248
11213
  );
12249
11214
  return EXIT_ERROR;
12250
11215
  }
12251
- await writeFile11(out, `${JSON.stringify(result.actions, null, 2)}
11216
+ await writeFile10(out, `${JSON.stringify(result.actions, null, 2)}
12252
11217
  `);
12253
11218
  r.done(
12254
11219
  { ok: true, out, url: result.actions.url ?? null, ...summary2(result) },
@@ -12268,9 +11233,9 @@ function summary2(result) {
12268
11233
  }
12269
11234
  async function cmdActionsScript(input, flags) {
12270
11235
  const r = createReporter(flags.json === true);
12271
- const { readFile: readFile16 } = await import("fs/promises");
11236
+ const { readFile: readFile13 } = await import("fs/promises");
12272
11237
  const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
12273
- const raw = JSON.parse(await readFile16(resolve13(input), "utf8"));
11238
+ const raw = JSON.parse(await readFile13(resolve11(input), "utf8"));
12274
11239
  const errors = validateActions(raw);
12275
11240
  if (errors.length) throw new UsageError(`${input}:
12276
11241
  ${errors.join("\n ")}`);
@@ -12280,9 +11245,9 @@ async function cmdActionsScript(input, flags) {
12280
11245
  }
12281
11246
 
12282
11247
  // src/plugin/ingest.ts
12283
- import { existsSync as existsSync21 } from "fs";
12284
- import { copyFile, mkdir as mkdir8, readFile as readFile14, stat as stat3 } from "fs/promises";
12285
- import { basename as basename9, join as join23, resolve as resolve14 } from "path";
11248
+ import { existsSync as existsSync19 } from "fs";
11249
+ import { copyFile, mkdir as mkdir7, readFile as readFile11, stat as stat2 } from "fs/promises";
11250
+ import { basename as basename9, join as join21, resolve as resolve12 } from "path";
12286
11251
  import { inflateRawSync } from "zlib";
12287
11252
  import {
12288
11253
  cursorFromPlaywrightTrace,
@@ -12370,7 +11335,7 @@ async function readTraceFile(path, offsetMs) {
12370
11335
  );
12371
11336
  }
12372
11337
  if (kind === "zip") {
12373
- const bytes = new Uint8Array(await readFile14(path));
11338
+ const bytes = new Uint8Array(await readFile11(path));
12374
11339
  const entry = playwrightTraceEntry(zipEntries(bytes));
12375
11340
  if (!entry) {
12376
11341
  throw new UsageError(
@@ -12383,7 +11348,7 @@ async function readTraceFile(path, offsetMs) {
12383
11348
  );
12384
11349
  return cursorFromPlaywrightTrace(text2, { offsetMs });
12385
11350
  }
12386
- const text = await readFile14(path, "utf8");
11351
+ const text = await readFile11(path, "utf8");
12387
11352
  return cursorFromTrace(basename9(path), text, { offsetMs });
12388
11353
  }
12389
11354
  function parseViewport(v) {
@@ -12399,10 +11364,10 @@ async function cmdIngest(argv) {
12399
11364
  throw new UsageError(
12400
11365
  "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]"
12401
11366
  );
12402
- const videoPath = resolve14(source);
12403
- if (!existsSync21(videoPath)) throw new UsageError(`${source}: no such file`);
11367
+ const videoPath = resolve12(source);
11368
+ if (!existsSync19(videoPath)) throw new UsageError(`${source}: no such file`);
12404
11369
  const r = createReporter(flags.json === true);
12405
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
11370
+ const outDir = resolve12(strFlag(flags, "out") ?? "take");
12406
11371
  const offsetMs = numFlag(flags, "offset", 0);
12407
11372
  const viewportFlag = parseViewport(strFlag(flags, "viewport"));
12408
11373
  const cursorPath = strFlag(flags, "cursor");
@@ -12424,18 +11389,18 @@ async function cmdIngest(argv) {
12424
11389
  }
12425
11390
  let trace = null;
12426
11391
  if (cursorPath) {
12427
- trace = await readTraceFile(resolve14(cursorPath), offsetMs);
11392
+ trace = await readTraceFile(resolve12(cursorPath), offsetMs);
12428
11393
  for (const n of trace.notes) r.log(`note: ${n}`);
12429
11394
  }
12430
- if (existsSync21(join23(outDir, "meta.json"))) {
11395
+ if (existsSync19(join21(outDir, "meta.json"))) {
12431
11396
  const { prevDoc, kept } = await prepareReRecord(outDir);
12432
11397
  r.log(
12433
11398
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
12434
11399
  );
12435
11400
  }
12436
- await mkdir8(outDir, { recursive: true });
11401
+ await mkdir7(outDir, { recursive: true });
12437
11402
  const paths = await ensureTakeDir(outDir);
12438
- const recording = join23(outDir, `recording.${probed.ext}`);
11403
+ const recording = join21(outDir, `recording.${probed.ext}`);
12439
11404
  r.log("copying the recording\u2026");
12440
11405
  const remuxed = await copyRecording(videoPath, recording, probed.ext);
12441
11406
  const viewport = viewportFlag ?? trace?.viewport ?? {
@@ -12443,7 +11408,7 @@ async function cmdIngest(argv) {
12443
11408
  height: probed.height
12444
11409
  };
12445
11410
  const events = trace?.events ?? [];
12446
- const fileStat = await stat3(videoPath);
11411
+ const fileStat = await stat2(videoPath);
12447
11412
  const meta = {
12448
11413
  dpr: 1,
12449
11414
  zoom: 1,
@@ -12503,19 +11468,19 @@ async function cmdIngest(argv) {
12503
11468
  import { spawnSync as spawnSync4 } from "child_process";
12504
11469
  import {
12505
11470
  cpSync,
12506
- existsSync as existsSync23,
11471
+ existsSync as existsSync21,
12507
11472
  mkdirSync as mkdirSync6,
12508
11473
  readFileSync as readFileSync10,
12509
11474
  rmSync as rmSync3,
12510
11475
  writeFileSync as writeFileSync6
12511
11476
  } from "fs";
12512
11477
  import { homedir as homedir2 } from "os";
12513
- import { join as join25 } from "path";
11478
+ import { join as join23 } from "path";
12514
11479
  import { chromium as chromium2 } from "playwright";
12515
11480
 
12516
11481
  // src/plugin/machine.ts
12517
- import { existsSync as existsSync22, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12518
- import { dirname as dirname9, join as join24 } from "path";
11482
+ import { existsSync as existsSync20, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
11483
+ import { dirname as dirname8, join as join22 } from "path";
12519
11484
  var AGENT_NAMES = [
12520
11485
  "claude",
12521
11486
  "cursor",
@@ -12535,17 +11500,17 @@ var HOME_DIRS = {
12535
11500
  copilot: [".copilot"]
12536
11501
  };
12537
11502
  function detectAgents(root, opts = {}) {
12538
- const exists = opts.exists ?? existsSync22;
11503
+ const exists = opts.exists ?? existsSync20;
12539
11504
  const out = [];
12540
11505
  for (const agent of AGENT_NAMES) {
12541
11506
  for (const rel of PROJECT_DIRS[agent]) {
12542
- const dir = join24(root, rel);
11507
+ const dir = join22(root, rel);
12543
11508
  if (exists(dir)) out.push({ agent, dir, scope: "project" });
12544
11509
  }
12545
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join24(root, ".github", "copilot-instructions.md"))) {
11510
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join22(root, ".github", "copilot-instructions.md"))) {
12546
11511
  out.push({
12547
11512
  agent,
12548
- dir: join24(root, ".github", "copilot"),
11513
+ dir: join22(root, ".github", "copilot"),
12549
11514
  scope: "project"
12550
11515
  });
12551
11516
  }
@@ -12553,7 +11518,7 @@ function detectAgents(root, opts = {}) {
12553
11518
  if (opts.global && opts.home) {
12554
11519
  for (const agent of AGENT_NAMES) {
12555
11520
  for (const rel of HOME_DIRS[agent]) {
12556
- const dir = join24(opts.home, rel);
11521
+ const dir = join22(opts.home, rel);
12557
11522
  if (exists(dir)) out.push({ agent, dir, scope: "global" });
12558
11523
  }
12559
11524
  }
@@ -12571,18 +11536,18 @@ function agentsFromFlag(value, root) {
12571
11536
  return {
12572
11537
  homes: wanted.map((agent) => ({
12573
11538
  agent,
12574
- dir: join24(root, PROJECT_DIRS[agent][0]),
11539
+ dir: join22(root, PROJECT_DIRS[agent][0]),
12575
11540
  scope: "project"
12576
11541
  }))
12577
11542
  };
12578
11543
  }
12579
11544
  function skillsDirFor(home) {
12580
- return join24(home.dir, "skills");
11545
+ return join22(home.dir, "skills");
12581
11546
  }
12582
11547
  function fallbackSkillsHome(root) {
12583
- return { agent: "codex", dir: join24(root, ".agents"), scope: "project" };
11548
+ return { agent: "codex", dir: join22(root, ".agents"), scope: "project" };
12584
11549
  }
12585
- function installedSkills(dir, exists = existsSync22, list = (p) => {
11550
+ function installedSkills(dir, exists = existsSync20, list = (p) => {
12586
11551
  try {
12587
11552
  return readdirSync5(p);
12588
11553
  } catch {
@@ -12590,7 +11555,7 @@ function installedSkills(dir, exists = existsSync22, list = (p) => {
12590
11555
  }
12591
11556
  }) {
12592
11557
  if (!exists(dir)) return [];
12593
- return list(dir).filter((name) => exists(join24(dir, name, "SKILL.md"))).sort();
11558
+ return list(dir).filter((name) => exists(join22(dir, name, "SKILL.md"))).sort();
12594
11559
  }
12595
11560
  var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
12596
11561
  var RULES_END = "<!-- vos:end -->";
@@ -12623,10 +11588,10 @@ ${RULES_BLOCK}
12623
11588
  }
12624
11589
  return { text, changed: text !== current };
12625
11590
  }
12626
- function rulesFileFor(root, exists = existsSync22) {
12627
- const agents = join24(root, "AGENTS.md");
11591
+ function rulesFileFor(root, exists = existsSync20) {
11592
+ const agents = join22(root, "AGENTS.md");
12628
11593
  if (exists(agents)) return agents;
12629
- const claude = join24(root, "CLAUDE.md");
11594
+ const claude = join22(root, "CLAUDE.md");
12630
11595
  if (exists(claude)) return claude;
12631
11596
  return agents;
12632
11597
  }
@@ -12635,14 +11600,14 @@ function chromeCandidates(platform, env) {
12635
11600
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
12636
11601
  if (platform === "win32")
12637
11602
  return [
12638
- join24(
11603
+ join22(
12639
11604
  env["PROGRAMFILES"] ?? "C:\\Program Files",
12640
11605
  "Google",
12641
11606
  "Chrome",
12642
11607
  "Application",
12643
11608
  "chrome.exe"
12644
11609
  ),
12645
- join24(
11610
+ join22(
12646
11611
  env["LOCALAPPDATA"] ?? "",
12647
11612
  "Google",
12648
11613
  "Chrome",
@@ -12727,20 +11692,20 @@ function doctorReport(facts) {
12727
11692
  return { ok: nodeOk && browserOk, lines, next_step, facts };
12728
11693
  }
12729
11694
  function bundledSkillsDir(from) {
12730
- let dir = dirname9(from);
11695
+ let dir = dirname8(from);
12731
11696
  for (let i = 0; i < 6; i++) {
12732
- const pkg = join24(dir, "package.json");
12733
- if (existsSync22(pkg)) {
11697
+ const pkg = join22(dir, "package.json");
11698
+ if (existsSync20(pkg)) {
12734
11699
  try {
12735
11700
  const name = JSON.parse(readFileSync9(pkg, "utf8")).name;
12736
11701
  if (name === "@vosjs/cli") {
12737
- const skills = join24(dir, "skills");
12738
- return existsSync22(skills) && statSync7(skills).isDirectory() ? skills : null;
11702
+ const skills = join22(dir, "skills");
11703
+ return existsSync20(skills) && statSync7(skills).isDirectory() ? skills : null;
12739
11704
  }
12740
11705
  } catch {
12741
11706
  }
12742
11707
  }
12743
- const up = dirname9(dir);
11708
+ const up = dirname8(dir);
12744
11709
  if (up === dir) break;
12745
11710
  dir = up;
12746
11711
  }
@@ -12766,7 +11731,7 @@ function npx() {
12766
11731
  function bundledChromium() {
12767
11732
  try {
12768
11733
  const p = chromium2.executablePath();
12769
- return p && existsSync23(p) ? p : null;
11734
+ return p && existsSync21(p) ? p : null;
12770
11735
  } catch {
12771
11736
  return null;
12772
11737
  }
@@ -12775,7 +11740,7 @@ function probe2() {
12775
11740
  return probeBrowser({
12776
11741
  platform: process.platform,
12777
11742
  env: process.env,
12778
- exists: existsSync23,
11743
+ exists: existsSync21,
12779
11744
  bundled: bundledChromium
12780
11745
  });
12781
11746
  }
@@ -12879,7 +11844,7 @@ async function cmdSetup(argv) {
12879
11844
  const dest = skillsDirFor(home);
12880
11845
  mkdirSync6(dest, { recursive: true });
12881
11846
  for (const name of installedSkills(bundled)) {
12882
- cpSync(join25(bundled, name), join25(dest, name), { recursive: true });
11847
+ cpSync(join23(bundled, name), join23(dest, name), { recursive: true });
12883
11848
  }
12884
11849
  }
12885
11850
  skillsVia = "bundled";
@@ -12914,7 +11879,7 @@ async function cmdSetup(argv) {
12914
11879
  let rules = null;
12915
11880
  if (!noRules) {
12916
11881
  const file = rulesFileFor(root);
12917
- const existing = existsSync23(file) ? readFileSync10(file, "utf8") : null;
11882
+ const existing = existsSync21(file) ? readFileSync10(file, "utf8") : null;
12918
11883
  const { text, changed } = applyRulesBlock(existing);
12919
11884
  if (changed) writeFileSync6(file, text);
12920
11885
  rules = { file, changed };
@@ -13028,7 +11993,7 @@ async function cmdWhoami(argv) {
13028
11993
  function cmdLogout(argv) {
13029
11994
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS11);
13030
11995
  const r = reporterFor(flags);
13031
- const had = existsSync23(CREDENTIALS_PATH);
11996
+ const had = existsSync21(CREDENTIALS_PATH);
13032
11997
  rmSync3(CREDENTIALS_PATH, { force: true });
13033
11998
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
13034
11999
  r.done(
@@ -13060,14 +12025,15 @@ var BOOLEAN_FLAGS12 = /* @__PURE__ */ new Set([
13060
12025
  "media",
13061
12026
  "print",
13062
12027
  "yes",
13063
- "composed",
13064
12028
  "check",
13065
12029
  "motion",
13066
12030
  "leader",
13067
12031
  "keep-frames",
13068
12032
  "dry-run",
13069
12033
  "allow-wall",
13070
- "hosted"
12034
+ "hosted",
12035
+ "claimable",
12036
+ "share"
13071
12037
  ]);
13072
12038
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
13073
12039
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -13080,13 +12046,12 @@ Take pipeline
13080
12046
  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]
13081
12047
  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]
13082
12048
  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]
13083
- vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--launch LAUNCH.md] [--look plate|gradient|dark|none] [--brand BRAND.md] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
12049
+ 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]
13084
12050
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
13085
12051
  vos brand <url> [--out BRAND.md] [--json]
13086
12052
  vos open <take> [--studio <url>] [--print]
13087
12053
  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 (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]
13088
12054
  vos validate <actions.json|take|kit.json> [--picture] [--json]
13089
- vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
13090
12055
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
13091
12056
  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]
13092
12057
  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
@@ -13140,9 +12105,12 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
13140
12105
  the platform's typed changelog. A TAKE pushes to the vos in its
13141
12106
  vos.json; --vos on a take with none ADOPTS that vos (the take
13142
12107
  becomes its next version, built on its head), and --vos that
13143
- disagrees with vos.json is refused before anything uploads. --claimable (programs only, NO
12108
+ disagrees with vos.json is refused before anything uploads. --claimable (NO
13144
12109
  credential): creates a 72h claim link instead \u2014 hand it to the
13145
- user and nowhere else; unclaimed work is deleted after 72h.
12110
+ user and nowhere else; unclaimed work is deleted after 72h. On a
12111
+ take it is for a REMIX of a public take fetched with --media: it
12112
+ uploads nothing, so every file must be one the fetch brought home,
12113
+ unchanged, and it credits the take it came from.
13146
12114
  --override consents to touching
13147
12115
  protected (human-edited) nodes, ONLY when the user asked.
13148
12116
  Without --folder a new vos lands unfiled, at the root of your
@@ -13245,47 +12213,28 @@ recording cap, read live from GET /api/limits (30 min when the origin is
13245
12213
  unreachable): the capture stops there and says so in one line.
13246
12214
  frames captures PNG stills at OUTPUT times (seconds or % of duration);
13247
12215
  --at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
13248
- the agent's eyes AND exact-size posters/screenshots (--size WxH). render
12216
+ the agent's eyes (--size WxH for an exact size). render
13249
12217
  --range a..b --draft spot-checks a doc edit in seconds (half res, low
13250
12218
  bitrate; range in OUTPUT seconds); a --range render keeps its audio.
13251
- deliver renders the take to a release's destinations in one pass \u2014 the
13252
- channel specs (schema/channel-specs.json, verified sizes for the Chrome
13253
- Web Store, Product Hunt, X, LinkedIn, OG, GitHub, YouTube) drive stills at
13254
- exact pixels and video cuts, every artifact is VERIFIED against its spec
13255
- (px, bytes, duration; misses land in skipped[] with the reason), and
13256
- kit.json beside the assets is the manifest. Still times come from the
13257
- STORY: every step's end plus a 0.4 s settle (the response, not the
13258
- travel), then the zoom apexes, then an even spread; each candidate is
13259
- read once as the real page, blank ones (a wallpaper, an empty canvas) are
13260
- dropped and two of one frame collapse to one, with every drop said in
13261
- skipped[]. --times overrides with seconds, percents or step:<id>[+offset]
13262
- (the id from actions.json); --range cuts every video destination.
13263
- The LOOK presents the card: card-genre stills with no poster document
13264
- and every video cut sit on a ground (a cream plate, the house gradient, a
13265
- dark plate with a light streak) at ~84% of the width with headroom, a
13266
- soft ambient shadow plus a tight contact shadow, and a hairline when card
13267
- and ground are both light; a wide frame runs the card off the bottom.
13268
- --look picks a house look (or none for the pre-look crops); with no flag
13269
- the BRAND.md beside the take (or --brand <file>) decides from its look
13270
- role or its own ground, and with no brand the house gradient.
13271
- Screenshot-genre stills never take a look.
13272
- A POSTER is a document, never a template: a plain take whose card sits
13273
- where the poster wants it (frame.inset), leans (the tilt track), carries
13274
- the words and the mark as clips (ids stage-title, stage-kicker,
13275
- stage-brand, stage-mark) and ends on a trailing freeze whose start is the
13276
- still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
13277
- CWS tile + marquee, GitHub social preview) render from the poster
13278
- document of their aspect CLASS (landscape, square, portrait, tile), found
13279
- beside the take as poster/<class>/doc.json (poster/doc.json serves every
13280
- class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
13281
- the document's rest; kit.json records source poster, the class, the file,
13282
- the vos it tracks, the shot rect and the text boxes read FROM the
13283
- document. A class with no document is the take's own frame, said once.
13284
- deliver renders and verifies; it composes nothing. The composition is
13285
- made with plan --style <poster>, which copies a poster's layout onto a
13286
- take (its card placement, its stage clips with the release's words
13287
- patched in from LAUNCH.md's headline and kicker roles or --headline and
13288
- --kicker, its rest lean, its freeze), or by hand in doc.json.
12219
+ deliver renders the take to a release's video destinations in one pass \u2014
12220
+ the channel specs (schema/channel-specs.json, verified sizes for a YouTube
12221
+ demo, an X feed cut, a 9:16 cut for Shorts and LinkedIn, a GitHub README
12222
+ loop) drive each cut at exact pixels, every artifact is VERIFIED against
12223
+ its spec (px, bytes, duration; misses land in skipped[] with the reason),
12224
+ and kit.json beside the assets is the manifest. --range cuts every
12225
+ destination. A channel with no video destination is refused in words.
12226
+ The LOOK presents the card: every cut sits on a ground (a cream plate,
12227
+ the house gradient, a dark plate with a light streak) at ~84% of the width
12228
+ with headroom, a soft ambient shadow plus a tight contact shadow, and a
12229
+ hairline when card and ground are both light; a wide frame runs the card
12230
+ off the bottom. --look picks a house look (or none for the doc's own
12231
+ frame); with no flag the BRAND.md beside the take (or --brand <file>)
12232
+ decides from its look role or its own ground, and with no brand the house
12233
+ gradient.
12234
+ plan --style <doc.json|vosId> copies a signed-off take's layout onto this
12235
+ one (its card placement, its stage clips with the release's words patched
12236
+ in from LAUNCH.md's headline and kicker roles or --headline and --kicker,
12237
+ its rest lean, its freeze), or edit doc.json by hand.
13289
12238
  The cut's MOTION is the document's too, in ONE vocabulary: every visual
13290
12239
  thing (the card, a text, image or video clip, a prop) carries anim.enter,
13291
12240
  anim.exit and anim.idle; the output lasts until the last clip ends, and
@@ -13309,40 +12258,21 @@ deleted on a refresh. deliver applies each destination's MECHANICS and
13309
12258
  nothing more: the README loop drops the card's motion, every clip a
13310
12259
  template placed and every sound; a channel that autoplays muted drops the
13311
12260
  bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
13312
- the camera. Screenshot-genre
13313
- destinations (CWS screenshots, the PH gallery) are the real page at that
13314
- moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
13315
- policy); --composed keeps the cut's camera and chrome instead. --set
12261
+ the camera. --set
13316
12262
  path=value overrides the doc in memory for every render here (the user's
13317
- sets apply last). Store uploads stay manual: hand the human the kit
13318
- directory, then vos validate <kit.json> re-measures every asset from its
13319
- bytes against the channel specs; --picture adds what each asset LOOKS
13320
- like, read from its pixels: blank (a wallpaper or an empty canvas where
13321
- the product should be), duplicate (two stills of one frame), subject (the
13322
- card off the 60 to 92% band, or a crop where a card was asked for),
13323
- separation (a light card on a light ground with no shadow), halfsize (a
13324
- tile that loses its edges when the store shrinks it), and, where the kit
13325
- records its text boxes, sliced, safe and contrast (APCA Lc 60/75); a
13326
- video's first and last frames are read through ffmpeg. Every finding
13327
- carries a code, a severity, a fix hint and a box; an error fails the
13328
- verdict beside the spec problems.
13329
- judge puts the kit beside its REFERENCES: for every still that has a
13330
- reference of its role in the manifest (a template family, a card genre),
13331
- two sheets at a common height (the asset left, then right, so order
13332
- cannot bias the call) and the rubric in words, plus judge.json, a slot
13333
- per pair the judge fills (win true, false or null for a tie, with the
13334
- rule numbers). No model runs inside the verb: the skill judges the
13335
- sheets pairwise, both orders, and the verb reports the win rate beside
13336
- the spec and picture counts. The reference set is the maker's own
13337
- fixture folder with a MANIFEST.json (id, file, role, layout, facts,
13338
- rule per asset).
12263
+ sets apply last). Uploads stay manual: hand the human the kit
12264
+ directory, then vos validate <kit.json> re-measures every cut from its
12265
+ bytes against the channel specs; --picture adds what each cut LOOKS like:
12266
+ its first and last frames, read through ffmpeg, must not be blank or bled
12267
+ on all four sides. Every finding carries a code, a severity and a fix
12268
+ hint; an error fails the verdict beside the spec problems.
13339
12269
  brand writes the product's BRAND.md, witnessed: it reads /design.md when
13340
12270
  the site publishes one (the convention beside /llms.txt: fonts, logo assets,
13341
12271
  an avoid list), /llms.txt for the name and the claim, then the page itself
13342
12272
  in a browser (the body ground, the surfaces, the h1 and body faces and inks,
13343
12273
  the accent buttons and links agree on, theme-color, the icons, og:image).
13344
- The frontmatter carries the roles the poster family binds (bgA/bgB/bgC,
13345
- ink, accent, fontDisplay, logoUrl); the prose says where every value came
12274
+ The frontmatter carries the brand's roles (bgA/bgB/bgC, ink, accent,
12275
+ fontDisplay, logoUrl); the prose says where every value came
13346
12276
  from and quotes the site's own avoid list. Resolve the brand BEFORE any
13347
12277
  asset is authored; file it with vos recipe push BRAND.md --folder <slug>.
13348
12278
  digest is how an agent SEES a recording before it cuts: writes digest/ with
@@ -13398,7 +12328,7 @@ lint-gated, so a bad override fails like a bad doc.json):
13398
12328
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
13399
12329
  `;
13400
12330
  async function loadActions(file) {
13401
- const raw = JSON.parse(await readFile15(file, "utf8"));
12331
+ const raw = JSON.parse(await readFile12(file, "utf8"));
13402
12332
  const errors = validateActions(raw);
13403
12333
  if (errors.length)
13404
12334
  throw new UsageError(`invalid actions file:
@@ -13460,8 +12390,8 @@ function takeBrowserArgs(multi) {
13460
12390
  function takeStorageState(flags) {
13461
12391
  const raw = strFlag(flags, "storage-state");
13462
12392
  if (!raw) return void 0;
13463
- const path = resolve15(raw);
13464
- if (!existsSync24(path))
12393
+ const path = resolve13(raw);
12394
+ if (!existsSync22(path))
13465
12395
  throw new UsageError(`--storage-state: no such file: ${path}`);
13466
12396
  return path;
13467
12397
  }
@@ -13474,7 +12404,7 @@ function takeSessionName(flags, storageState) {
13474
12404
  throw new UsageError(
13475
12405
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
13476
12406
  );
13477
- if (!existsSync24(sessionDir(name)))
12407
+ if (!existsSync22(sessionDir(name)))
13478
12408
  throw new UsageError(
13479
12409
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
13480
12410
  );
@@ -13543,7 +12473,7 @@ async function cmdRecord(argv) {
13543
12473
  const url = strFlag(flags, "url") ?? actions.url;
13544
12474
  if (!url)
13545
12475
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13546
- const outDir = resolve15(strFlag(flags, "out") ?? "take");
12476
+ const outDir = resolve13(strFlag(flags, "out") ?? "take");
13547
12477
  const storageState = takeStorageState(flags);
13548
12478
  const sessionName = takeSessionName(flags, storageState);
13549
12479
  const browserArgs = takeBrowserArgs(multi);
@@ -13697,13 +12627,13 @@ ${lines.join("\n")}
13697
12627
  }
13698
12628
  const paths = takePaths(outDir);
13699
12629
  const prepare = async () => {
13700
- if (existsSync24(join26(outDir, "meta.json"))) {
12630
+ if (existsSync22(join24(outDir, "meta.json"))) {
13701
12631
  const { prevDoc, kept } = await prepareReRecord(outDir);
13702
12632
  r.log(
13703
12633
  `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" : "")
13704
12634
  );
13705
12635
  }
13706
- await mkdir9(outDir, { recursive: true });
12636
+ await mkdir8(outDir, { recursive: true });
13707
12637
  await ensureTakeDir(outDir);
13708
12638
  };
13709
12639
  const browser = await launchBrowser(browserArgs);
@@ -13762,7 +12692,7 @@ ${lines.join("\n")}
13762
12692
  },
13763
12693
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
13764
12694
  ${(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}
13765
- Next: edit ${join26(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
12695
+ Next: edit ${join24(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
13766
12696
  );
13767
12697
  return strictFail ? EXIT_USAGE : EXIT_OK;
13768
12698
  } finally {
@@ -13786,9 +12716,9 @@ async function cmdCreate2(argv) {
13786
12716
  const url = strFlag(flags, "url") ?? actions.url;
13787
12717
  if (!url)
13788
12718
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
13789
- const outDir = resolve15(strFlag(flags, "out") ?? "take");
12719
+ const outDir = resolve13(strFlag(flags, "out") ?? "take");
13790
12720
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
13791
- const out = positionals[0] ?? join26(outDir, `out.${format}`);
12721
+ const out = positionals[0] ?? join24(outDir, `out.${format}`);
13792
12722
  const parallel = numFlag(flags, "parallel", 1);
13793
12723
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13794
12724
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13801,13 +12731,13 @@ async function cmdCreate2(argv) {
13801
12731
  const headers = takeHeaders(multi);
13802
12732
  const paths = takePaths(outDir);
13803
12733
  const prepare = async () => {
13804
- if (existsSync24(join26(outDir, "meta.json"))) {
12734
+ if (existsSync22(join24(outDir, "meta.json"))) {
13805
12735
  const { prevDoc, kept } = await prepareReRecord(outDir);
13806
12736
  r.log(
13807
12737
  `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" : "")
13808
12738
  );
13809
12739
  }
13810
- await mkdir9(outDir, { recursive: true });
12740
+ await mkdir8(outDir, { recursive: true });
13811
12741
  await ensureTakeDir(outDir);
13812
12742
  };
13813
12743
  const browser = await launchBrowser(browserArgs);
@@ -13908,24 +12838,24 @@ async function cmdPlan(argv) {
13908
12838
  );
13909
12839
  const r = createReporter(flags.json === true);
13910
12840
  if (flags.fresh === true) {
13911
- await rm3(join26(dir, "doc.json"), { force: true });
12841
+ await rm2(join24(dir, "doc.json"), { force: true });
13912
12842
  r.log("note: --fresh discarded the existing doc.json");
13913
12843
  }
13914
12844
  let reuse;
13915
12845
  if (flags.reuse === true) {
13916
- const from = resolve15(strFlag(flags, "from") ?? join26(dir, PREV_DOC_NAME));
13917
- if (!existsSync24(from)) {
12846
+ const from = resolve13(strFlag(flags, "from") ?? join24(dir, PREV_DOC_NAME));
12847
+ if (!existsSync22(from)) {
13918
12848
  throw new UsageError(
13919
12849
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
13920
12850
  );
13921
12851
  }
13922
12852
  reuse = {
13923
12853
  from,
13924
- doc: JSON.parse(await readFile15(from, "utf8"))
12854
+ doc: JSON.parse(await readFile12(from, "utf8"))
13925
12855
  };
13926
12856
  }
13927
12857
  const style = await resolveStyleRef(flags);
13928
- const hasDoc = existsSync24(join26(dir, "doc.json"));
12858
+ const hasDoc = existsSync22(join24(dir, "doc.json"));
13929
12859
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
13930
12860
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
13931
12861
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -13971,7 +12901,7 @@ async function cmdPlan(argv) {
13971
12901
  ${s.reuse.flagged.join("\n ")}` : "") : "";
13972
12902
  r.done(
13973
12903
  {
13974
- take: resolve15(dir),
12904
+ take: resolve13(dir),
13975
12905
  fresh: s.fresh,
13976
12906
  cursorKept: s.cursorKept,
13977
12907
  zoomAuto: s.zoomAuto,
@@ -13983,7 +12913,7 @@ async function cmdPlan(argv) {
13983
12913
  ...s.motion ? { motion: s.motion } : {},
13984
12914
  ...s.reuse ? { reuse: s.reuse } : {}
13985
12915
  },
13986
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join26(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
12916
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join24(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
13987
12917
  );
13988
12918
  return EXIT_OK;
13989
12919
  }
@@ -14106,7 +13036,7 @@ async function cmdRender(argv) {
14106
13036
  );
14107
13037
  const r = createReporter(flags.json === true);
14108
13038
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
14109
- const out = positionals[1] ?? join26(dir, `out.${format}`);
13039
+ const out = positionals[1] ?? join24(dir, `out.${format}`);
14110
13040
  const parallel = numFlag(flags, "parallel", 1);
14111
13041
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
14112
13042
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -14244,7 +13174,7 @@ async function cmdDeliver(argv) {
14244
13174
  const toRaw = strFlag(flags, "to");
14245
13175
  if (!dir || !toRaw)
14246
13176
  throw new UsageError(
14247
- "vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--json]\n(hosted work: vos pull <id> --media first \u2014 deliver runs on a local take)"
13177
+ "vos deliver <take> --to youtube,x,shorts,github (or all) [--release v2.1] [--out dir] [--range a..b] [--json]\n(hosted work: vos pull <id> --media first \u2014 deliver runs on a local take)"
14248
13178
  );
14249
13179
  const r = createReporter(flags.json === true);
14250
13180
  if (!isTakeDir(dir))
@@ -14271,17 +13201,6 @@ async function cmdDeliver(argv) {
14271
13201
  }
14272
13202
  const take = await loadTake(dir);
14273
13203
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
14274
- const duration = docOutputDuration3(take.doc);
14275
- const timesRaw = strFlag(flags, "times");
14276
- let times;
14277
- if (timesRaw !== void 0) {
14278
- try {
14279
- const doc = take.doc;
14280
- times = timesRaw.split(",").map((s) => s.trim()).filter(Boolean).map((s) => resolveStepTime(doc, s) ?? parseTimes2(s, duration)[0]);
14281
- } catch (e) {
14282
- throw new UsageError(e instanceof Error ? e.message : String(e));
14283
- }
14284
- }
14285
13204
  const parallel = numFlag(flags, "parallel", 1);
14286
13205
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
14287
13206
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -14296,20 +13215,15 @@ async function cmdDeliver(argv) {
14296
13215
  throw new UsageError(e instanceof Error ? e.message : String(e));
14297
13216
  }
14298
13217
  r.log(`look: ${lookPick.from}`);
14299
- const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
14300
- if (launch) r.log(`launch: ${launch.file}`);
14301
13218
  const browser = await launchBrowser();
14302
13219
  try {
14303
13220
  const result = await deliverTake(browser, dir, {
14304
13221
  look: lookPick.look,
14305
- launchRoles: launch?.roles ?? null,
14306
13222
  channels,
14307
13223
  outDir: strFlag(flags, "out"),
14308
13224
  release: strFlag(flags, "release"),
14309
- times,
14310
13225
  range,
14311
13226
  parallel,
14312
- composed: hasFlag(flags, "composed"),
14313
13227
  overrides: {
14314
13228
  set: multi.set,
14315
13229
  background: strFlag(flags, "background"),
@@ -14326,7 +13240,7 @@ async function cmdDeliver(argv) {
14326
13240
  });
14327
13241
  const { kit } = result;
14328
13242
  const assetLines = kit.assets.map(
14329
- (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.poster ? `, poster ${a.poster.class}` : ""})`
13243
+ (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB, ${a.seconds.toFixed(1)}s)`
14330
13244
  );
14331
13245
  const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
14332
13246
  r.done(
@@ -14339,7 +13253,7 @@ async function cmdDeliver(argv) {
14339
13253
  `Wrote ${kit.assets.length} asset(s) + kit.json to ${result.outDir}` + (assetLines.length ? `
14340
13254
  ${assetLines.join("\n ")}` : "") + (skippedLines.length ? `
14341
13255
  ${skippedLines.join("\n ")}` : "") + `
14342
- Store uploads stay manual: hand the human this directory and the manifest.`
13256
+ Uploads stay manual: hand the human this directory and the manifest.`
14343
13257
  );
14344
13258
  return kit.assets.length > 0 ? EXIT_OK : EXIT_ERROR;
14345
13259
  } finally {
@@ -14353,15 +13267,15 @@ async function resolveStyleRef(flags) {
14353
13267
  }
14354
13268
  async function resolveDocRef(ref, flags, what) {
14355
13269
  const styleRef = ref;
14356
- const file = existsSync24(styleRef) ? resolve15(
13270
+ const file = existsSync22(styleRef) ? resolve13(
14357
13271
  styleRef,
14358
- existsSync24(join26(styleRef, "doc.json")) ? "doc.json" : ""
13272
+ existsSync22(join24(styleRef, "doc.json")) ? "doc.json" : ""
14359
13273
  ) : null;
14360
- if (file && existsSync24(file)) {
13274
+ if (file && existsSync22(file)) {
14361
13275
  return {
14362
13276
  from: file,
14363
- doc: migrateHostedDoc5(
14364
- JSON.parse(await readFile15(file, "utf8"))
13277
+ doc: migrateHostedDoc4(
13278
+ JSON.parse(await readFile12(file, "utf8"))
14365
13279
  )
14366
13280
  };
14367
13281
  }
@@ -14389,7 +13303,7 @@ async function resolveDocRef(ref, flags, what) {
14389
13303
  throw new UsageError(`${what}: ${styleRef} carries no doc (is it a take?)`);
14390
13304
  return {
14391
13305
  from: `${origin}/vos/${styleRef}`,
14392
- doc: migrateHostedDoc5(doc.body)
13306
+ doc: migrateHostedDoc4(doc.body)
14393
13307
  };
14394
13308
  }
14395
13309
  async function officialTemplate(origin, ref) {
@@ -14413,7 +13327,7 @@ async function cmdDigest(argv) {
14413
13327
  const take = await loadTake(dir);
14414
13328
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
14415
13329
  const transcriptPath = strFlag(flags, "transcript");
14416
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
13330
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile12(transcriptPath, "utf8"))) : null;
14417
13331
  const style = await resolveStyleRef(flags);
14418
13332
  const noFrames = flags["no-frames"] === true;
14419
13333
  let browser = null;
@@ -14444,7 +13358,7 @@ async function cmdDigest(argv) {
14444
13358
  kinds,
14445
13359
  frames: d.images.full,
14446
13360
  crops: d.images.crop,
14447
- sheet: d.images.sheet ? join26(result.outDir, d.images.sheet) : null,
13361
+ sheet: d.images.sheet ? join24(result.outDir, d.images.sheet) : null,
14448
13362
  scenes: kinds.scene ?? 0,
14449
13363
  sourceDuration: d.take.sourceDuration,
14450
13364
  outputDuration: d.take.outputDuration,
@@ -14472,14 +13386,14 @@ async function cmdOpen(argv) {
14472
13386
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
14473
13387
  const r = createReporter(flags.json === true);
14474
13388
  const take = await loadTake(dir);
14475
- if (!existsSync24(take.paths.recording)) {
13389
+ if (!existsSync22(take.paths.recording)) {
14476
13390
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
14477
13391
  }
14478
13392
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
14479
13393
  /\/+$/,
14480
13394
  ""
14481
13395
  );
14482
- const server = await startTakeServer(resolve15(dir), {});
13396
+ const server = await startTakeServer(resolve13(dir), {});
14483
13397
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
14484
13398
  r.event({ event: "open", server: server.base, url });
14485
13399
  r.log(`take served at ${server.base}`);
@@ -14504,35 +13418,6 @@ async function cmdOpen(argv) {
14504
13418
  });
14505
13419
  return EXIT_OK;
14506
13420
  }
14507
- async function cmdJudge(argv) {
14508
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
14509
- const target = positionals[0];
14510
- const against = strFlag(flags, "against");
14511
- if (!target || !against)
14512
- throw new UsageError(
14513
- "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)"
14514
- );
14515
- const r = createReporter(flags.json === true);
14516
- const kitFile = target.endsWith("kit.json") ? target : join26(target, "kit.json");
14517
- if (!existsSync24(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
14518
- if (!existsSync24(against))
14519
- throw new UsageError(`${against}: no reference manifest`);
14520
- const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
14521
- const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
14522
- const rate = winRate(verdicts);
14523
- const lines = result.sheets.map(
14524
- (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
14525
- );
14526
- r.done(
14527
- { ...result, winRate: rate },
14528
- `Wrote ${result.sheets.length} sheet pair(s) to ${result.outDir}` + (lines.length ? `
14529
- ${lines.join("\n ")}` : "") + (result.skipped.length ? `
14530
- skipped: ${result.skipped.join("\n skipped: ")}` : "") + `
14531
- Judge each pair both ways (the rubric is beside it) and fill ${result.verdictFile}` + (rate.judged ? `
14532
- Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judged (${(rate.rate * 100).toFixed(0)}%; a tie counts a half); parity with the references is 50%, the marketability bar is 40%` : "")
14533
- );
14534
- return EXIT_OK;
14535
- }
14536
13421
  async function cmdCallout(argv) {
14537
13422
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS12);
14538
13423
  const dir = positionals[0];
@@ -14558,7 +13443,7 @@ async function cmdCallout(argv) {
14558
13443
  `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`
14559
13444
  );
14560
13445
  const codeFile = strFlag(flags, "code-file");
14561
- const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
13446
+ const code = codeFile ? await readFile12(codeFile, "utf8") : strFlag(flags, "code");
14562
13447
  const words2 = {
14563
13448
  kicker: strFlag(flags, "kicker"),
14564
13449
  title: strFlag(flags, "title"),
@@ -14637,7 +13522,7 @@ async function cmdValidate(argv) {
14637
13522
  const target = positionals[0];
14638
13523
  if (!target) throw new UsageError("vos validate <actions.json|take>");
14639
13524
  const r = createReporter(flags.json === true);
14640
- const kitTarget = target.endsWith("kit.json") ? target : existsSync24(join26(target, "kit.json")) && !isTakeDir(target) ? join26(target, "kit.json") : null;
13525
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync22(join24(target, "kit.json")) && !isTakeDir(target) ? join24(target, "kit.json") : null;
14641
13526
  if (kitTarget) {
14642
13527
  const picture = flags.picture === true;
14643
13528
  const verdict = await validateKit(kitTarget, { picture });
@@ -14669,9 +13554,9 @@ async function cmdValidate(argv) {
14669
13554
  r.done({ valid: true, target }, `${target}: valid actions file`);
14670
13555
  return EXIT_OK;
14671
13556
  }
14672
- if (!isTakeDir(target) && existsSync24(join26(target, "config.json"))) {
13557
+ if (!isTakeDir(target) && existsSync22(join24(target, "config.json"))) {
14673
13558
  const parsed = JSON.parse(
14674
- await readFile15(join26(target, "config.json"), "utf8")
13559
+ await readFile12(join24(target, "config.json"), "utf8")
14675
13560
  );
14676
13561
  const pre = preflightConfig(parsed);
14677
13562
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -14712,7 +13597,7 @@ async function cmdValidate(argv) {
14712
13597
  const take = await loadTake(target);
14713
13598
  const problems = [];
14714
13599
  const warnings = [];
14715
- if (!existsSync24(take.paths.recording)) {
13600
+ if (!existsSync22(take.paths.recording)) {
14716
13601
  problems.push("missing recording (re-run record)");
14717
13602
  } else {
14718
13603
  const mismatch = containerMismatch(
@@ -14801,20 +13686,49 @@ async function cmdPush3(argv) {
14801
13686
  const dir = positionals[0];
14802
13687
  if (!dir)
14803
13688
  throw new UsageError(
14804
- "vos push <take> [--vos|--title|--label|--note|--prompt|--folder|--override|--yes|--key|--api]"
13689
+ "vos push <take> [--vos|--title|--label|--note|--prompt|--folder|--override|--yes|--key|--api|--claimable [--share]]"
14805
13690
  );
14806
13691
  const refusal = takePushRefusal({
14807
13692
  vos: strFlag(flags, "vos"),
14808
- trackedVosId: readSyncState(resolve15(dir))?.vosId ?? null,
13693
+ trackedVosId: readSyncState(resolve13(dir))?.vosId ?? null,
14809
13694
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
14810
13695
  (name) => hasFlag(flags, name)
14811
13696
  )
14812
13697
  });
14813
13698
  if (refusal) throw new UsageError(refusal);
14814
13699
  const r = createReporter(flags.json === true);
13700
+ if (flags.claimable === true) {
13701
+ const iterating = ["vos", "folder", "override", "base"].filter(
13702
+ (name) => hasFlag(flags, name)
13703
+ );
13704
+ if (iterating.length)
13705
+ throw new UsageError(
13706
+ `--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
13707
+ );
13708
+ const claim = await pushTakeClaimable(
13709
+ resolve13(dir),
13710
+ {
13711
+ api: strFlag(flags, "api"),
13712
+ origin: strFlag(flags, "origin"),
13713
+ title: strFlag(flags, "title"),
13714
+ prompt: strFlag(flags, "prompt"),
13715
+ share: flags.share === true
13716
+ },
13717
+ r
13718
+ );
13719
+ r.done(
13720
+ { ...claim },
13721
+ `Claimable push created (${claim.title})
13722
+ claim: ${claim.claimUrl}
13723
+ expires: ${claim.expiresAt} \u2014 unclaimed work is deleted after 72h
13724
+ Hand the claim link to the user and NOWHERE else \u2014 it is the only
13725
+ reference and the only credential.`
13726
+ );
13727
+ return 0;
13728
+ }
14815
13729
  const overrides = multi.override;
14816
13730
  const result = await pushTake(
14817
- resolve15(dir),
13731
+ resolve13(dir),
14818
13732
  {
14819
13733
  key: strFlag(flags, "key"),
14820
13734
  api: strFlag(flags, "api"),
@@ -14868,7 +13782,7 @@ async function cmdPull2(argv) {
14868
13782
  const dir = positionals[0] ?? ".";
14869
13783
  const r = createReporter(flags.json === true);
14870
13784
  const result = await pullTake(
14871
- resolve15(dir),
13785
+ resolve13(dir),
14872
13786
  {
14873
13787
  key: strFlag(flags, "key"),
14874
13788
  api: strFlag(flags, "api"),
@@ -14943,8 +13857,6 @@ async function run(argv) {
14943
13857
  return await cmdValidate(rest);
14944
13858
  case "callout":
14945
13859
  return await cmdCallout(rest);
14946
- case "judge":
14947
- return await cmdJudge(rest);
14948
13860
  case "fetch":
14949
13861
  return await cmdFetch(rest);
14950
13862
  case "duplicate":
@@ -15070,7 +13982,7 @@ async function cmdSession(argv) {
15070
13982
  if (!arg) throw new UsageError(usage);
15071
13983
  const bad = sessionNameVerdict(arg);
15072
13984
  if (bad) throw new UsageError(bad);
15073
- if (!existsSync24(sessionDir(arg)))
13985
+ if (!existsSync22(sessionDir(arg)))
15074
13986
  throw new UsageError(`no session "${arg}" (vos session list)`);
15075
13987
  const summary3 = await summarizeSession(arg);
15076
13988
  const url = strFlag(flags, "url");
@@ -15170,4 +14082,4 @@ export {
15170
14082
  verbHelp,
15171
14083
  run
15172
14084
  };
15173
- //# sourceMappingURL=chunk-42ZU5XUW.js.map
14085
+ //# sourceMappingURL=chunk-5DUXN3RX.js.map