@vosjs/cli 0.54.1 → 0.55.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.
@@ -99,8 +99,8 @@ function landedLines(l) {
99
99
 
100
100
  // src/plugin/run.ts
101
101
  import { mkdir as mkdir11, readFile as readFile17, rm as rm5 } from "fs/promises";
102
- import { existsSync as existsSync23 } from "fs";
103
- import { join as join27, resolve as resolve15 } from "path";
102
+ import { existsSync as existsSync24 } from "fs";
103
+ import { join as join28, resolve as resolve15 } from "path";
104
104
  import {
105
105
  END_CARD_FROM as END_CARD_FROM2,
106
106
  docOutputDuration as docOutputDuration3,
@@ -9235,28 +9235,350 @@ async function cmdCompare(argv) {
9235
9235
  }
9236
9236
 
9237
9237
  // src/plugin/port.ts
9238
- import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
9239
- import { dirname as dirname8, join as join23, resolve as resolve11 } from "path";
9238
+ import { existsSync as existsSync20, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9239
+ import { dirname as dirname8, join as join24, resolve as resolve11 } from "path";
9240
9240
  import { pathToFileURL as pathToFileURL3 } from "url";
9241
9241
 
9242
9242
  // src/plugin/portInventory.ts
9243
9243
  import { spawnSync as spawnSync2 } from "child_process";
9244
9244
  import {
9245
- existsSync as existsSync17,
9245
+ existsSync as existsSync18,
9246
9246
  mkdirSync as mkdirSync3,
9247
- readFileSync as readFileSync5,
9248
- readdirSync as readdirSync4,
9249
- statSync as statSync5
9247
+ readFileSync as readFileSync6,
9248
+ readdirSync as readdirSync5,
9249
+ statSync as statSync6
9250
9250
  } from "fs";
9251
- import { basename as basename7, dirname as dirname6, join as join21, relative as relative2, resolve as resolve9 } from "path";
9251
+ import { dirname as dirname6, join as join22, relative as relative2, resolve as resolve9 } from "path";
9252
9252
  import { pathToFileURL as pathToFileURL2 } from "url";
9253
9253
  import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
9254
+
9255
+ // src/plugin/portRemotion.ts
9256
+ import { existsSync as existsSync17, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
9257
+ import { createServer as createServer2 } from "http";
9258
+ import { createRequire } from "module";
9259
+ import { basename as basename7, extname as extname3, join as join21 } from "path";
9260
+ function themeColors(sources) {
9261
+ const out = {};
9262
+ const re = /\bconst\s+([A-Za-z_$][\w$]*)\s*(?::\s*string\s*)?=\s*['"](#[0-9a-fA-F]{3,8})['"]/g;
9263
+ for (const src of sources)
9264
+ for (const m of src.matchAll(re)) {
9265
+ const key = m[1].toLowerCase().replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
9266
+ if (!(key in out)) out[key] = m[2].toLowerCase();
9267
+ }
9268
+ return out;
9269
+ }
9270
+ function remotionScenes(seqs, fps) {
9271
+ const seen = /* @__PURE__ */ new Map();
9272
+ const live = seqs.filter((s) => s.durationInFrames > 0);
9273
+ const overlaps2 = (a, b) => a.from < b.from + b.durationInFrames && b.from < a.from + a.durationInFrames;
9274
+ return live.filter(
9275
+ (s) => !live.some(
9276
+ (o) => o !== s && o.durationInFrames > s.durationInFrames && overlaps2(o, s)
9277
+ )
9278
+ ).sort((a, b) => a.from - b.from).map((s, i) => {
9279
+ const base = s.name ?? `scene${i + 1}`;
9280
+ const n = (seen.get(base) ?? 0) + 1;
9281
+ seen.set(base, n);
9282
+ return {
9283
+ name: n > 1 ? `${base}${n}` : base,
9284
+ start: Math.round(s.from / fps * 1e3) / 1e3,
9285
+ duration: Math.round(s.durationInFrames / fps * 1e3) / 1e3
9286
+ };
9287
+ });
9288
+ }
9289
+ function mergeSceneTexts(perScene) {
9290
+ const sig = (t) => `${t.text}|${t.font.family}|${t.font.size}|${t.box ? `${Math.round(t.box.x / 8)},${Math.round(t.box.y / 8)}` : "-"}`;
9291
+ const counts = /* @__PURE__ */ new Map();
9292
+ for (const s of perScene)
9293
+ for (const k of new Set(s.texts.map(sig)))
9294
+ counts.set(k, (counts.get(k) ?? 0) + 1);
9295
+ const global = perScene.length >= 3 ? Math.ceil(perScene.length / 2) : Infinity;
9296
+ const out = [];
9297
+ const placed = /* @__PURE__ */ new Set();
9298
+ const textPlaced = /* @__PURE__ */ new Set();
9299
+ for (const s of perScene)
9300
+ for (const t of s.texts) {
9301
+ const k = sig(t);
9302
+ const shared = (counts.get(k) ?? 0) >= global;
9303
+ const id = shared ? k : `${s.scene}|${k}`;
9304
+ if (placed.has(id)) continue;
9305
+ const shape = `${t.text.replace(/\d/g, "0")}|${t.font.family}|${t.box ? Math.round(t.box.y / 8) : "-"}`;
9306
+ if (/\d/.test(t.text) && textPlaced.has(shape)) continue;
9307
+ placed.add(id);
9308
+ textPlaced.add(shape);
9309
+ out.push({
9310
+ ...t,
9311
+ id: `${t.id}-${out.length}`,
9312
+ scene: shared ? null : s.scene
9313
+ });
9314
+ }
9315
+ return out;
9316
+ }
9317
+ function sourceTexts(dir) {
9318
+ const out = [];
9319
+ const walk = (d, depth) => {
9320
+ if (depth > 4 || out.length > 200) return;
9321
+ for (const name of readdirSync4(d)) {
9322
+ if (name === "node_modules" || name.startsWith(".")) continue;
9323
+ const p = join21(d, name);
9324
+ if (statSync5(p).isDirectory()) walk(p, depth + 1);
9325
+ else if (/\.(tsx?|jsx?|mjs)$/.test(name))
9326
+ out.push(readFileSync5(p, "utf8"));
9327
+ }
9328
+ };
9329
+ const src = join21(dir, "src");
9330
+ walk(existsSync17(src) ? src : dir, 0);
9331
+ return out;
9332
+ }
9333
+ function entryPoint(dir) {
9334
+ for (const p of [
9335
+ "src/index.ts",
9336
+ "src/index.tsx",
9337
+ "src/index.js",
9338
+ "src/index.jsx",
9339
+ "remotion/index.ts",
9340
+ "remotion/index.tsx"
9341
+ ])
9342
+ if (existsSync17(join21(dir, p))) return join21(dir, p);
9343
+ return null;
9344
+ }
9345
+ var TYPES = {
9346
+ ".html": "text/html",
9347
+ ".js": "text/javascript",
9348
+ ".css": "text/css",
9349
+ ".json": "application/json",
9350
+ ".woff2": "font/woff2",
9351
+ ".woff": "font/woff",
9352
+ ".ttf": "font/ttf",
9353
+ ".png": "image/png",
9354
+ ".jpg": "image/jpeg",
9355
+ ".svg": "image/svg+xml",
9356
+ ".wav": "audio/wav",
9357
+ ".mp3": "audio/mpeg",
9358
+ ".ogg": "audio/ogg",
9359
+ ".mp4": "video/mp4",
9360
+ ".webm": "video/webm"
9361
+ };
9362
+ var INIT = `(() => {
9363
+ window.remotion_puppeteerTimeout = 30000
9364
+ window.remotion_isMainTab = true
9365
+ window.remotion_mediaCacheSizeInBytes = 100000000
9366
+ window.remotion_initialMemoryAvailable = 1000000000
9367
+ window.remotion_sampleRate = 48000
9368
+ window.process = window.process || {}
9369
+ window.process.env = window.process.env || {}
9370
+ window.process.env.NODE_ENV = 'production'
9371
+ window.remotion_envVariables = '{}'
9372
+ window.remotion_inputProps = '{}'
9373
+ window.remotion_initialFrame = 0
9374
+ window.remotion_attempt = 1
9375
+ window.remotion_audioEnabled = false
9376
+ window.remotion_videoEnabled = false
9377
+ window.remotion_logLevel = 'error'
9378
+ })()`;
9379
+ var READ_TREE = `(() => {
9380
+ let fiber = null
9381
+ for (const el of document.querySelectorAll('body, body *')) {
9382
+ const k = Object.keys(el).find((k) => k.startsWith('__reactFiber$') || k.startsWith('__reactContainer$'))
9383
+ if (k) { fiber = el[k]; break }
9384
+ }
9385
+ while (fiber && fiber.return) fiber = fiber.return
9386
+ const nameOf = (t) => t && (t.displayName || t.name || (t.render && (t.render.displayName || t.render.name))) || null
9387
+ const internal = /^(Sequence|AbsoluteFill|Freeze|Loop|Series|Fragment)$|Provider|Context|Forwarding|^Inner/
9388
+ const seqs = []
9389
+ const audio = []
9390
+ const walk = (f, offset) => {
9391
+ for (let x = f; x; x = x.sibling) {
9392
+ const name = nameOf(x.elementType)
9393
+ const p = x.memoizedProps || {}
9394
+ let next = offset
9395
+ if (name === 'Sequence' && typeof p.from === 'number') {
9396
+ next = offset + p.from
9397
+ if (typeof p.durationInFrames === 'number' && isFinite(p.durationInFrames)) {
9398
+ const kids = Array.isArray(p.children) ? p.children : [p.children]
9399
+ const held = kids.map((k) => k && nameOf(k.type)).find((n) => n && !internal.test(n))
9400
+ seqs.push({ from: next, durationInFrames: p.durationInFrames, name: p.name || held || null })
9401
+ }
9402
+ }
9403
+ if (name && /Audio$/.test(name) && typeof p.src === 'string')
9404
+ audio.push({ src: p.src, from: next, volume: typeof p.volume === 'number' ? p.volume : null, startFrom: typeof p.startFrom === 'number' ? p.startFrom : (typeof p.trimBefore === 'number' ? p.trimBefore : null) })
9405
+ if (x.child) walk(x.child, next)
9406
+ }
9407
+ }
9408
+ walk(fiber, 0)
9409
+ // Nested sequences repeat their parent's window at the same offset: keep the outermost.
9410
+ const seen = new Set()
9411
+ return { seqs: seqs.filter((s) => { const k = s.from + ':' + s.durationInFrames; if (seen.has(k)) return false; seen.add(k); return true }), audio }
9412
+ })()`;
9413
+ async function readRemotion(dir, log) {
9414
+ const entry = entryPoint(dir);
9415
+ let bundler;
9416
+ try {
9417
+ bundler = createRequire(join21(dir, "package.json"))("@remotion/bundler");
9418
+ } catch {
9419
+ log?.(
9420
+ "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"
9421
+ );
9422
+ return null;
9423
+ }
9424
+ if (!entry) {
9425
+ log?.(
9426
+ "a Remotion project with no src/index entry (registerRoot): reading only the render"
9427
+ );
9428
+ return null;
9429
+ }
9430
+ const out = await bundler.bundle({ entryPoint: entry, onProgress: () => {
9431
+ } });
9432
+ const server = createServer2((rq, rs) => {
9433
+ const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
9434
+ let f = join21(out, path);
9435
+ if (!f.startsWith(out) || !existsSync17(f) || statSync5(f).isDirectory())
9436
+ f = join21(out, "index.html");
9437
+ rs.writeHead(200, {
9438
+ "content-type": TYPES[extname3(f)] ?? "application/octet-stream"
9439
+ });
9440
+ rs.end(readFileSync5(f));
9441
+ }).listen(0, "127.0.0.1");
9442
+ await new Promise((r) => server.once("listening", r));
9443
+ const port = server.address().port;
9444
+ const browser = await launchBrowser();
9445
+ try {
9446
+ const ctx = await browser.newContext({
9447
+ viewport: { width: 1920, height: 1080 }
9448
+ });
9449
+ const p = await ctx.newPage();
9450
+ await p.addInitScript(INIT);
9451
+ await p.goto(`http://127.0.0.1:${port}/index.html`);
9452
+ await p.waitForFunction(
9453
+ 'typeof window.getStaticCompositions === "function"',
9454
+ null,
9455
+ { timeout: 3e4 }
9456
+ );
9457
+ await p.evaluate("window.remotion_setBundleMode({ type: 'evaluation' })");
9458
+ await p.waitForFunction("window.remotion_renderReady === true", null, {
9459
+ timeout: 3e4
9460
+ });
9461
+ const comps = await p.evaluate("window.getStaticCompositions()");
9462
+ if (!comps.length) {
9463
+ log?.(
9464
+ "the Remotion project registers no composition: reading only the render"
9465
+ );
9466
+ return null;
9467
+ }
9468
+ const c = comps[0];
9469
+ if (comps.length > 1)
9470
+ log?.(`${comps.length} compositions; reading the first, ${c.id}`);
9471
+ await p.setViewportSize({ width: c.width, height: c.height });
9472
+ await p.evaluate(
9473
+ `window.remotion_setBundleMode(${JSON.stringify({
9474
+ type: "composition",
9475
+ compositionName: c.id,
9476
+ serializedResolvedPropsWithSchema: JSON.stringify(c.defaultProps ?? {}),
9477
+ compositionDurationInFrames: c.durationInFrames,
9478
+ compositionFps: c.fps,
9479
+ compositionHeight: c.height,
9480
+ compositionWidth: c.width,
9481
+ compositionDefaultCodec: null,
9482
+ compositionDefaultOutName: null,
9483
+ compositionDefaultVideoImageFormat: null,
9484
+ compositionDefaultPixelFormat: null,
9485
+ compositionDefaultProResProfile: null,
9486
+ compositionDefaultSampleRate: null
9487
+ })})`
9488
+ );
9489
+ await p.waitForFunction("window.remotion_renderReady === true", null, {
9490
+ timeout: 3e4
9491
+ });
9492
+ const seek = async (frame) => {
9493
+ await p.evaluate(
9494
+ `window.remotion_setFrame(${frame}, ${JSON.stringify(c.id)}, 1)`
9495
+ );
9496
+ await p.waitForFunction("window.remotion_renderReady === true", null, {
9497
+ timeout: 3e4
9498
+ });
9499
+ await p.evaluate("document.fonts && document.fonts.ready");
9500
+ };
9501
+ await seek(0);
9502
+ const tree = await p.evaluate(READ_TREE);
9503
+ let scenes = remotionScenes(tree.seqs, c.fps);
9504
+ const total = c.durationInFrames / c.fps;
9505
+ if (!scenes.length) scenes = [{ name: c.id, start: 0, duration: total }];
9506
+ const perScene = [];
9507
+ let first = null;
9508
+ const colors = /* @__PURE__ */ new Set();
9509
+ const gaps = /* @__PURE__ */ new Set();
9510
+ for (const s of scenes) {
9511
+ const frame = Math.min(
9512
+ c.durationInFrames - 1,
9513
+ Math.round((s.start + s.duration * 0.9) * c.fps)
9514
+ );
9515
+ await seek(frame);
9516
+ const read = await p.evaluate(READ_PAGE);
9517
+ const unboxed = read.texts.filter((t) => !t.box);
9518
+ if (unboxed.length) {
9519
+ await seek(Math.round((s.start + s.duration * 0.5) * c.fps));
9520
+ const mid = await p.evaluate(READ_PAGE);
9521
+ for (const t of unboxed) {
9522
+ const m = mid.texts.find(
9523
+ (x) => x.box && x.text === t.text && x.font.family === t.font.family
9524
+ );
9525
+ if (m) t.box = m.box;
9526
+ }
9527
+ }
9528
+ s.ground = await p.evaluate(READ_GROUND);
9529
+ first ??= read;
9530
+ for (const x of read.colors ?? []) colors.add(x);
9531
+ for (const g of read.gaps ?? []) gaps.add(g);
9532
+ perScene.push({ scene: s.name, texts: read.texts ?? [] });
9533
+ }
9534
+ const publicDir = join21(dir, "public");
9535
+ const media = tree.audio.map((a) => {
9536
+ const name = basename7(a.src.split("?")[0]);
9537
+ const local = existsSync17(join21(publicDir, name)) ? `public/${name}` : a.src;
9538
+ return {
9539
+ kind: "audio",
9540
+ src: local,
9541
+ start: Math.round(a.from / c.fps * 1e3) / 1e3,
9542
+ duration: null,
9543
+ volume: a.volume,
9544
+ mediaStart: a.startFrom != null ? Math.round(a.startFrom / c.fps * 1e3) / 1e3 : null
9545
+ };
9546
+ });
9547
+ return {
9548
+ width: c.width,
9549
+ height: c.height,
9550
+ fps: c.fps,
9551
+ duration: Math.round(total * 1e3) / 1e3,
9552
+ texts: mergeSceneTexts(perScene),
9553
+ palette: themeColors(sourceTexts(dir)),
9554
+ colors: [...colors],
9555
+ gaps: [...gaps],
9556
+ media,
9557
+ canvases: first?.canvases ?? [],
9558
+ scenes,
9559
+ sceneEls: [],
9560
+ variables: c.defaultProps ?? {},
9561
+ scriptText: "",
9562
+ composition: c.id
9563
+ };
9564
+ } catch (e) {
9565
+ log?.(
9566
+ `the Remotion bundle did not mount (${e instanceof Error ? e.message.split("\n")[0] : String(e)}): reading only the render`
9567
+ );
9568
+ return null;
9569
+ } finally {
9570
+ await browser.close();
9571
+ server.close();
9572
+ }
9573
+ }
9574
+
9575
+ // src/plugin/portInventory.ts
9254
9576
  function detectEngine(dir) {
9255
- if (existsSync17(join21(dir, "hyperframes.json"))) return "hyperframes";
9256
- const pkg = join21(dir, "package.json");
9257
- if (existsSync17(pkg)) {
9577
+ if (existsSync18(join22(dir, "hyperframes.json"))) return "hyperframes";
9578
+ const pkg = join22(dir, "package.json");
9579
+ if (existsSync18(pkg)) {
9258
9580
  try {
9259
- const p = JSON.parse(readFileSync5(pkg, "utf8"));
9581
+ const p = JSON.parse(readFileSync6(pkg, "utf8"));
9260
9582
  const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
9261
9583
  if ("remotion" in deps) return "remotion";
9262
9584
  if ("hyperframes" in deps) return "hyperframes";
@@ -9268,14 +9590,14 @@ function detectEngine(dir) {
9268
9590
  return "page";
9269
9591
  }
9270
9592
  function sourcePage(source) {
9271
- if (!existsSync17(source)) return null;
9272
- if (statSync5(source).isFile()) return /\.html?$/i.test(source) ? source : null;
9273
- const index = join21(source, "index.html");
9274
- return existsSync17(index) ? index : null;
9593
+ if (!existsSync18(source)) return null;
9594
+ if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
9595
+ const index = join22(source, "index.html");
9596
+ return existsSync18(index) ? index : null;
9275
9597
  }
9276
9598
  function findRender(dir) {
9277
- const videos = (d) => existsSync17(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join21(d, f)) : [];
9278
- for (const d of [join21(dir, "renders"), join21(dir, "out"), dir]) {
9599
+ const videos = (d) => existsSync18(d) ? readdirSync5(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join22(d, f)) : [];
9600
+ for (const d of [join22(dir, "renders"), join22(dir, "out"), dir]) {
9279
9601
  const found = videos(d);
9280
9602
  if (found.length) return found[0];
9281
9603
  }
@@ -9385,31 +9707,68 @@ var READ_PAGE = `(() => {
9385
9707
  const sx = rootRect.width ? width / rootRect.width : 1
9386
9708
  const sy = rootRect.height ? height / rootRect.height : 1
9387
9709
 
9710
+ // The faces that actually render: a family loaded only italic, or only at
9711
+ // one weight, draws that face whatever the CSS asked for.
9712
+ const loaded = {}
9713
+ if (document.fonts) for (const f of document.fonts) {
9714
+ if (f.status !== 'loaded') continue
9715
+ const fam = f.family.replace(/^["']|["']$/g, '')
9716
+ const e = loaded[fam] || (loaded[fam] = { styles: new Set(), weights: new Set() })
9717
+ e.styles.add(f.style)
9718
+ const w = Number(f.weight)
9719
+ if (Number.isFinite(w)) e.weights.add(w)
9720
+ }
9721
+ const renderedFace = (fam, style, weight) => {
9722
+ const e = loaded[fam]
9723
+ if (!e) return { style, weight }
9724
+ const st = e.styles.has(style) ? style : style === 'normal' && e.styles.has('italic') ? 'italic' : style
9725
+ const ws = [...e.weights]
9726
+ const wt = !ws.length || ws.includes(weight) ? weight : ws.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a))
9727
+ return { style: st, weight: wt }
9728
+ }
9388
9729
  const texts = []
9389
9730
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
9390
9731
  const seen = new Set()
9391
9732
  let n = 0
9392
9733
  while (walker.nextNode()) {
9393
9734
  const node = walker.currentNode
9394
- const text = (node.textContent || '').replace(/\\s+/g, ' ').trim()
9395
- const el = node.parentElement
9396
- if (!text || !el || ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TITLE'].includes(el.tagName)) continue
9397
- if (seen.has(el)) continue
9398
- seen.add(el)
9399
- const cs = getComputedStyle(el)
9400
- const r = el.getBoundingClientRect()
9735
+ let el = node.parentElement
9736
+ if (!el || !(node.textContent || '').trim() || el.closest('script, style, noscript, title')) continue
9737
+ // A word animated letter by letter is one span per glyph: read the word,
9738
+ // once, as a split word, never as its letters.
9739
+ let split = null
9740
+ const glyphs = (a) => a && a.children.length >= 3 && [...a.childNodes].every((c) => c.nodeType === 1 ? (c.textContent || '').trim().length <= 2 : !(c.textContent || '').trim())
9741
+ for (let a = el.parentElement, up = 0; a && up < 3; a = a.parentElement, up++) {
9742
+ if (glyphs(a)) { el = a; split = 'chars'; break }
9743
+ }
9744
+ // A line with a styled word inside it ("TYPE <span>that</span> MOVES")
9745
+ // is read run by run, each with its own box, or the runs after the
9746
+ // first are lost.
9747
+ const mixed = !split && [...el.children].some((c) => (c.textContent || '').trim())
9748
+ const unit = mixed ? node : el
9749
+ if (seen.has(unit)) continue
9750
+ seen.add(unit)
9751
+ const text = ((mixed ? node.textContent : el.textContent) || '').replace(/\\s+/g, ' ').trim()
9752
+ if (!text) continue
9753
+ // A split word wears its letters' face, never its container's.
9754
+ const cs = getComputedStyle(split ? node.parentElement : el)
9755
+ let r
9756
+ if (mixed) { const range = document.createRange(); range.selectNodeContents(node); r = range.getBoundingClientRect() }
9757
+ else r = el.getBoundingClientRect()
9401
9758
  const box = r.width && r.height ? {
9402
9759
  x: Math.round((r.left - rootRect.left) * sx), y: Math.round((r.top - rootRect.top) * sy),
9403
9760
  width: Math.round(r.width * sx), height: Math.round(r.height * sy) } : null
9404
9761
  const ls = parseFloat(cs.letterSpacing)
9405
- el.setAttribute('data-vos-inv', String(texts.length))
9762
+ if (!mixed) el.setAttribute('data-vos-inv', String(texts.length))
9406
9763
  const sceneEl = el.closest('.scene, [data-scene], section[id]')
9764
+ const fam = cs.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g, '')
9765
+ const face = renderedFace(fam, cs.fontStyle, Number(cs.fontWeight) || 400)
9407
9766
  texts.push({
9408
- id: el.id || (el.className && typeof el.className === 'string' ? el.className.split(' ')[0] : '') || el.tagName.toLowerCase() + '-' + (n++),
9409
- text, tag: el.tagName.toLowerCase(), box, scene: sceneEl && sceneEl.id ? sceneEl.id : null,
9410
- font: { family: cs.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g, ''),
9411
- size: Math.round(parseFloat(cs.fontSize) * sy), weight: Number(cs.fontWeight) || 400,
9412
- style: cs.fontStyle, color: cs.color, letterSpacing: Number.isFinite(ls) ? Math.round(ls * sy * 100) / 100 : 0,
9767
+ id: (mixed ? '' : el.id || (el.className && typeof el.className === 'string' ? el.className.split(' ')[0] : '')) || el.tagName.toLowerCase() + '-' + (n++),
9768
+ text, tag: el.tagName.toLowerCase(), box, scene: sceneEl && sceneEl.id ? sceneEl.id : null, split,
9769
+ font: { family: fam,
9770
+ size: Math.round(parseFloat(cs.fontSize) * sy), weight: face.weight,
9771
+ style: face.style, color: cs.color, letterSpacing: Number.isFinite(ls) ? Math.round(ls * sy * 100) / 100 : 0,
9413
9772
  transform: cs.textTransform },
9414
9773
  })
9415
9774
  }
@@ -9463,6 +9822,24 @@ var READ_PAGE = `(() => {
9463
9822
  texts, palette, colors: [...colors], gaps: [...gaps], media, canvases, scenes, sceneEls, variables, scriptText,
9464
9823
  }
9465
9824
  })()`;
9825
+ var READ_GROUND = `(() => {
9826
+ const W = innerWidth, H = innerHeight
9827
+ const counts = {}
9828
+ for (const [px, py] of [[0.5, 0.5], [0.06, 0.06], [0.94, 0.94], [0.06, 0.94], [0.94, 0.06]]) {
9829
+ for (const el of document.elementsFromPoint(px * W, py * H)) {
9830
+ const cs = getComputedStyle(el)
9831
+ const bg = cs.backgroundColor
9832
+ if (!bg || bg === 'transparent' || /rgba\\([^)]*,\\s*0\\)$/.test(bg)) continue
9833
+ const r = el.getBoundingClientRect()
9834
+ if (r.width < W * 0.9 || r.height < H * 0.9 || Number(cs.opacity) < 0.99) continue
9835
+ counts[bg] = (counts[bg] || 0) + 1
9836
+ break
9837
+ }
9838
+ }
9839
+ let best = null
9840
+ for (const k in counts) if (!best || counts[k] > counts[best]) best = k
9841
+ return best
9842
+ })()`;
9466
9843
  var SEEK_AND_READ = `((args) => {
9467
9844
  const tls = window.__timelines ? Object.values(window.__timelines) : []
9468
9845
  if (!tls.length) return null
@@ -9482,7 +9859,7 @@ var SEEK_AND_READ = `((args) => {
9482
9859
  })`;
9483
9860
  async function readInventory(source, opts) {
9484
9861
  const abs = resolve9(source);
9485
- const dir = existsSync17(abs) && statSync5(abs).isDirectory() ? abs : dirname6(abs);
9862
+ const dir = existsSync18(abs) && statSync6(abs).isDirectory() ? abs : dirname6(abs);
9486
9863
  const engine = detectEngine(dir);
9487
9864
  const page = sourcePage(abs);
9488
9865
  const render = opts.render === void 0 ? findRender(dir) : opts.render;
@@ -9527,12 +9904,15 @@ async function readInventory(source, opts) {
9527
9904
  if (scripted.length > (read.scenes?.length ?? 0)) read.scenes = scripted;
9528
9905
  for (const s of read.scenes) {
9529
9906
  const indices = read.texts.map((t2, i) => t2.scene === s.name ? i : -1).filter((i) => i >= 0);
9530
- if (!indices.length) continue;
9531
9907
  const t = s.start + (s.duration ?? 1) * 0.75;
9532
9908
  const boxes = await p.evaluate(
9533
9909
  `${SEEK_AND_READ}(${JSON.stringify({ t, indices, width: read.width, height: read.height })})`
9534
9910
  );
9535
- if (!boxes) break;
9911
+ if (!boxes)
9912
+ break;
9913
+ s.ground = await p.evaluate(
9914
+ READ_GROUND
9915
+ );
9536
9916
  for (const [i, box] of Object.entries(boxes)) {
9537
9917
  read.texts[Number(i)].box = box;
9538
9918
  settled++;
@@ -9543,9 +9923,15 @@ async function readInventory(source, opts) {
9543
9923
  await browser.close();
9544
9924
  }
9545
9925
  } else if (engine === "remotion") {
9546
- opts.log?.(
9547
- "a Remotion project: its words and colours live in its source (src/), which the agent reads; the inventory records the render and its stills"
9548
- );
9926
+ const r = await readRemotion(dir, opts.log);
9927
+ if (r) {
9928
+ read = r;
9929
+ fps = r.fps ?? fps;
9930
+ duration = r.duration ?? duration;
9931
+ opts.log?.(
9932
+ `read the Remotion composition ${String(r.composition)} in its own bundle: ${r.scenes.length} sequences as scenes, the words where they settle in each`
9933
+ );
9934
+ }
9549
9935
  }
9550
9936
  if (settled)
9551
9937
  opts.log?.(
@@ -9576,7 +9962,7 @@ async function readInventory(source, opts) {
9576
9962
  if (render && duration) {
9577
9963
  const has = spawnSync2("ffmpeg", ["-version"]).status === 0;
9578
9964
  if (has) {
9579
- const refDir = join21(opts.outDir, "ref");
9965
+ const refDir = join22(opts.outDir, "ref");
9580
9966
  mkdirSync3(refDir, { recursive: true });
9581
9967
  const times = read.scenes.length >= 3 ? read.scenes.map(
9582
9968
  (s) => s.start + (s.duration ?? 1) / 2
@@ -9585,7 +9971,7 @@ async function readInventory(source, opts) {
9585
9971
  (_, i) => i + 0.5
9586
9972
  );
9587
9973
  for (const t of times.filter((t2) => t2 < duration)) {
9588
- const file = join21(refDir, `src-${t.toFixed(2)}.png`);
9974
+ const file = join22(refDir, `src-${t.toFixed(2)}.png`);
9589
9975
  const ok = spawnSync2("ffmpeg", [
9590
9976
  "-v",
9591
9977
  "error",
@@ -9614,7 +10000,8 @@ async function readInventory(source, opts) {
9614
10000
  }
9615
10001
  const scriptStrings = scriptStringsOf(read.scriptText);
9616
10002
  return {
9617
- source: relative2(process.cwd(), abs) || basename7(abs),
10003
+ source: relative2(process.cwd(), abs) || ".",
10004
+ root: abs.endsWith(".html") ? dirname6(abs) : abs,
9618
10005
  engine,
9619
10006
  width: read.width,
9620
10007
  height: read.height,
@@ -9637,8 +10024,8 @@ async function readInventory(source, opts) {
9637
10024
 
9638
10025
  // src/plugin/portScaffold.ts
9639
10026
  import { spawnSync as spawnSync3 } from "child_process";
9640
- import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
9641
- import { basename as basename8, dirname as dirname7, join as join22, resolve as resolve10 } from "path";
10027
+ import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
10028
+ import { basename as basename8, dirname as dirname7, join as join23, resolve as resolve10 } from "path";
9642
10029
  import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
9643
10030
  var DESIGN_HEIGHT = 1080;
9644
10031
  function toHex(css) {
@@ -9727,6 +10114,7 @@ function planScaffold(inv, opts) {
9727
10114
  id: key,
9728
10115
  type: "text",
9729
10116
  content: { $data: key },
10117
+ ...t.split ? { split: { type: t.split } } : {},
9730
10118
  position: t.box ? { x: fmtPct(t.box.x / inv.width), y: fmtPct(t.box.y / inv.height) } : "center",
9731
10119
  font: {
9732
10120
  family,
@@ -9801,6 +10189,7 @@ function planScaffold(inv, opts) {
9801
10189
  data.scenes = inv.scenes.map((s) => ({
9802
10190
  name: s.name,
9803
10191
  at: s.start,
10192
+ ...s.ground ? { ground: colorOf(s.ground) } : {},
9804
10193
  until: s.duration == null ? null : Math.round((s.start + s.duration) * 1e3) / 1e3
9805
10194
  }));
9806
10195
  data.sceneElements = sceneElements;
@@ -9848,7 +10237,7 @@ function planScaffold(inv, opts) {
9848
10237
  inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
9849
10238
  const from = resolve10(opts.sourceDir, m.src);
9850
10239
  const name = basename8(from);
9851
- if (!existsSync18(from)) {
10240
+ if (!existsSync19(from)) {
9852
10241
  report.push(
9853
10242
  `- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
9854
10243
  );
@@ -9891,6 +10280,7 @@ function planScaffold(inv, opts) {
9891
10280
  elements: elements.length,
9892
10281
  params: params.length,
9893
10282
  painter,
10283
+ scores: audio.map((a) => String(a.key)),
9894
10284
  extra: report
9895
10285
  })
9896
10286
  };
@@ -9975,7 +10365,9 @@ export default {
9975
10365
  tl.addLabel(s.name, s.at)
9976
10366
  // Each scene's words show in its window; the motion replaces this.
9977
10367
  for (const id of ctx.data.sceneElements[s.name] || []) {
9978
- const p = ctx.elements.get(id).props
10368
+ // A split word's units carry their own opacity: hide the units.
10369
+ const el = ctx.elements.get(id)
10370
+ const p = el.segments && el.segments.length ? el.segments : el.props
9979
10371
  if (s.at > 0) {
9980
10372
  tl.set(p, { opacity: 0 }, 0)
9981
10373
  tl.set(p, { opacity: 1 }, s.at)
@@ -9989,22 +10381,29 @@ ${sceneTodos}
9989
10381
  },
9990
10382
 
9991
10383
  onFrame(ctx, content) {
9992
- ctx.scene.background.set(ctx.data.bg)${painterFrame}
10384
+ // Each scene's ground, by its palette key; the frame's own colour between them.
10385
+ let ground = ctx.data.bg
10386
+ for (const s of ctx.data.scenes)
10387
+ if (s.ground && ctx.time >= s.at && (s.until == null || ctx.time < s.until))
10388
+ ground = ctx.data[s.ground] ?? ground
10389
+ ctx.scene.background.set(ground)${painterFrame}
9993
10390
  },
9994
10391
  }
9995
10392
  `;
9996
10393
  }
9997
10394
  function reportText(inv, r) {
9998
10395
  const lines = [
9999
- `# Port report: ${inv.source}`,
10396
+ `# Port report: ${inv.source === "." && inv.root ? basename8(inv.root) : inv.source}`,
10000
10397
  "",
10001
10398
  `${inv.engine} piece, ${inv.width}\xD7${inv.height}${inv.fps ? ` at ${inv.fps} fps` : ""}, ${inv.duration ?? "?"} s. A program has no size of its own: render it with \`--width ${inv.width} --height ${inv.height}\`.`,
10002
10399
  "",
10003
10400
  "## Placed",
10004
10401
  "",
10005
- `- ${r.elements} words as bound text elements, ${Object.keys(inv.palette).length} palette colours from the source's custom properties, ${r.params} knobs.`,
10402
+ `- ${r.elements} words as bound text elements, ${Object.keys(inv.palette).length} palette colours the source names (its custom properties or colour constants), ${r.params} knobs.`,
10006
10403
  `- ${inv.scenes.length} scenes as labels${inv.scenes.length ? "" : " (none declared: name them as you translate)"}.`,
10007
- ...inv.media.some((m) => m.kind === "audio") ? ["- the score as a doc.json track (vos push uploads it)."] : [],
10404
+ ...r.scores.length ? [
10405
+ `- the score as a doc.json track (${r.scores.join(", ")}, copied beside the program; vos push uploads it).`
10406
+ ] : [],
10008
10407
  "",
10009
10408
  "## Faces",
10010
10409
  "",
@@ -10039,20 +10438,20 @@ function reportText(inv, r) {
10039
10438
  "",
10040
10439
  "## Reference",
10041
10440
  "",
10042
- inv.render ? `The source's render: ${inv.render}. Stills in port/ref/. Check every scene: \`npx vos compare . --against ${inv.render}\`.` : "No render of the source was found: render it with its own tool and pass it to `vos compare --against`.",
10441
+ inv.render ? `The source's render: ${inv.render}. Stills in port/ref/. Check every scene, from where the inventory ran: \`npx vos compare port/program --against ${inv.render}\`.` : "No render of the source was found: render it with its own tool and pass it to `vos compare --against`.",
10043
10442
  ""
10044
10443
  ];
10045
10444
  return lines.join("\n");
10046
10445
  }
10047
10446
  function writeScaffold(plan, outDir) {
10048
10447
  mkdirSync4(outDir, { recursive: true });
10049
- writeFileSync4(join22(outDir, "program.mjs"), plan.program);
10448
+ writeFileSync4(join23(outDir, "program.mjs"), plan.program);
10050
10449
  writeFileSync4(
10051
- join22(outDir, "doc.json"),
10450
+ join23(outDir, "doc.json"),
10052
10451
  JSON.stringify(plan.doc, null, 2) + "\n"
10053
10452
  );
10054
- writeFileSync4(join22(dirname7(outDir), "REPORT.md"), plan.report);
10055
- for (const [from, name] of plan.copies) copyFileSync(from, join22(outDir, name));
10453
+ writeFileSync4(join23(dirname7(outDir), "REPORT.md"), plan.report);
10454
+ for (const [from, name] of plan.copies) copyFileSync(from, join23(outDir, name));
10056
10455
  }
10057
10456
  function probeAudioDuration(file) {
10058
10457
  const res = spawnSync3(
@@ -10086,7 +10485,7 @@ async function inventory(argv) {
10086
10485
  outDir,
10087
10486
  log: (l) => r.log(`note: ${l}`)
10088
10487
  });
10089
- const file = join23(outDir, "inventory.json");
10488
+ const file = join24(outDir, "inventory.json");
10090
10489
  writeFileSync5(file, JSON.stringify(inv, null, 2) + "\n");
10091
10490
  const missing = inv.fonts.filter((f) => !f.catalog).map((f) => f.family);
10092
10491
  r.log(
@@ -10118,42 +10517,42 @@ function summary(inv) {
10118
10517
  async function scaffold(argv) {
10119
10518
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS7);
10120
10519
  const r = createReporter(flags.json === true);
10121
- const from = resolve11(strFlag(flags, "from") ?? join23("port", "inventory.json"));
10122
- if (!existsSync19(from))
10520
+ const from = resolve11(strFlag(flags, "from") ?? join24("port", "inventory.json"));
10521
+ if (!existsSync20(from))
10123
10522
  throw new UsageError(
10124
10523
  `no inventory at ${from}: run npx vos port inventory <source> first`
10125
10524
  );
10126
- const inv = JSON.parse(readFileSync6(from, "utf8"));
10525
+ const inv = JSON.parse(readFileSync7(from, "utf8"));
10127
10526
  const outDir = resolve11(
10128
- strFlag(flags, "out") ?? join23(dirname8(from), "program")
10527
+ strFlag(flags, "out") ?? join24(dirname8(from), "program")
10129
10528
  );
10130
10529
  const plan = planScaffold(inv, {
10131
- sourceDir: dirname8(
10132
- resolve11(
10133
- inv.source.endsWith(".html") ? inv.source : join23(inv.source, "index.html")
10134
- )
10135
- ),
10530
+ sourceDir: scaffoldSourceDir(inv),
10136
10531
  probeDuration: probeAudioDuration
10137
10532
  });
10138
10533
  writeScaffold(plan, outDir);
10139
- const programFile = join23(outDir, "program.mjs");
10534
+ const programFile = join24(outDir, "program.mjs");
10140
10535
  const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
10141
10536
  const built = buildProgram(mod.default, "program.mjs", plan.program);
10142
10537
  for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
10143
10538
  if (built.problems.length) return EXIT_ERROR;
10144
10539
  writeFileSync5(
10145
- join23(outDir, "config.json"),
10540
+ join24(outDir, "config.json"),
10146
10541
  JSON.stringify(built.config, null, 2) + "\n"
10147
10542
  );
10148
10543
  r.done(
10149
- { program: outDir, report: join23(dirname8(outDir), "REPORT.md") },
10150
- `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join23(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
10544
+ { program: outDir, report: join24(dirname8(outDir), "REPORT.md") },
10545
+ `Wrote ${outDir}/program.mjs, config.json and doc.json, and ${join24(dirname8(outDir), "REPORT.md")}. Translate the motion where it says TODO, then: npx vos build program.mjs && npx vos compare . --against <their render>`
10151
10546
  );
10152
10547
  return EXIT_OK;
10153
10548
  }
10549
+ function scaffoldSourceDir(inv) {
10550
+ if (inv.root) return inv.root;
10551
+ return inv.source.endsWith(".html") ? dirname8(resolve11(inv.source)) : resolve11(inv.source);
10552
+ }
10154
10553
 
10155
10554
  // src/plugin/recipe.ts
10156
- import { readFileSync as readFileSync7 } from "fs";
10555
+ import { readFileSync as readFileSync8 } from "fs";
10157
10556
  import { basename as basename9 } from "path";
10158
10557
  var BOOLEAN_FLAGS8 = /* @__PURE__ */ new Set(["json", "help"]);
10159
10558
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -10173,7 +10572,7 @@ async function cmdPush2(argv) {
10173
10572
  throw new UsageError(USAGE);
10174
10573
  }
10175
10574
  const filename = recipeFilename(file);
10176
- const body = readFileSync7(file);
10575
+ const body = readFileSync8(file);
10177
10576
  const r = createReporter(flags.json === true);
10178
10577
  const origin = platformOrigin({
10179
10578
  origin: strFlag(flags, "origin"),
@@ -11244,9 +11643,9 @@ async function cmdActionsScript(input, flags) {
11244
11643
  }
11245
11644
 
11246
11645
  // src/plugin/ingest.ts
11247
- import { existsSync as existsSync20 } from "fs";
11646
+ import { existsSync as existsSync21 } from "fs";
11248
11647
  import { copyFile, mkdir as mkdir10, readFile as readFile16, stat as stat4 } from "fs/promises";
11249
- import { basename as basename10, join as join24, resolve as resolve14 } from "path";
11648
+ import { basename as basename10, join as join25, resolve as resolve14 } from "path";
11250
11649
  import { inflateRawSync } from "zlib";
11251
11650
  import {
11252
11651
  cursorFromPlaywrightTrace,
@@ -11364,7 +11763,7 @@ async function cmdIngest(argv) {
11364
11763
  "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]"
11365
11764
  );
11366
11765
  const videoPath = resolve14(source);
11367
- if (!existsSync20(videoPath)) throw new UsageError(`${source}: no such file`);
11766
+ if (!existsSync21(videoPath)) throw new UsageError(`${source}: no such file`);
11368
11767
  const r = createReporter(flags.json === true);
11369
11768
  const outDir = resolve14(strFlag(flags, "out") ?? "take");
11370
11769
  const offsetMs = numFlag(flags, "offset", 0);
@@ -11391,7 +11790,7 @@ async function cmdIngest(argv) {
11391
11790
  trace = await readTraceFile(resolve14(cursorPath), offsetMs);
11392
11791
  for (const n of trace.notes) r.log(`note: ${n}`);
11393
11792
  }
11394
- if (existsSync20(join24(outDir, "meta.json"))) {
11793
+ if (existsSync21(join25(outDir, "meta.json"))) {
11395
11794
  const { prevDoc, kept } = await prepareReRecord(outDir);
11396
11795
  r.log(
11397
11796
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
@@ -11399,7 +11798,7 @@ async function cmdIngest(argv) {
11399
11798
  }
11400
11799
  await mkdir10(outDir, { recursive: true });
11401
11800
  const paths = await ensureTakeDir(outDir);
11402
- const recording = join24(outDir, `recording.${probed.ext}`);
11801
+ const recording = join25(outDir, `recording.${probed.ext}`);
11403
11802
  r.log("copying the recording\u2026");
11404
11803
  const remuxed = await copyRecording(videoPath, recording, probed.ext);
11405
11804
  const viewport = viewportFlag ?? trace?.viewport ?? {
@@ -11467,19 +11866,19 @@ async function cmdIngest(argv) {
11467
11866
  import { spawnSync as spawnSync4 } from "child_process";
11468
11867
  import {
11469
11868
  cpSync,
11470
- existsSync as existsSync22,
11869
+ existsSync as existsSync23,
11471
11870
  mkdirSync as mkdirSync6,
11472
- readFileSync as readFileSync9,
11871
+ readFileSync as readFileSync10,
11473
11872
  rmSync as rmSync3,
11474
11873
  writeFileSync as writeFileSync6
11475
11874
  } from "fs";
11476
11875
  import { homedir as homedir2 } from "os";
11477
- import { join as join26 } from "path";
11876
+ import { join as join27 } from "path";
11478
11877
  import { chromium as chromium2 } from "playwright";
11479
11878
 
11480
11879
  // src/plugin/machine.ts
11481
- import { existsSync as existsSync21, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
11482
- import { dirname as dirname9, join as join25 } from "path";
11880
+ import { existsSync as existsSync22, readFileSync as readFileSync9, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
11881
+ import { dirname as dirname9, join as join26 } from "path";
11483
11882
  var AGENT_NAMES = [
11484
11883
  "claude",
11485
11884
  "cursor",
@@ -11499,17 +11898,17 @@ var HOME_DIRS = {
11499
11898
  copilot: [".copilot"]
11500
11899
  };
11501
11900
  function detectAgents(root, opts = {}) {
11502
- const exists = opts.exists ?? existsSync21;
11901
+ const exists = opts.exists ?? existsSync22;
11503
11902
  const out = [];
11504
11903
  for (const agent of AGENT_NAMES) {
11505
11904
  for (const rel of PROJECT_DIRS[agent]) {
11506
- const dir = join25(root, rel);
11905
+ const dir = join26(root, rel);
11507
11906
  if (exists(dir)) out.push({ agent, dir, scope: "project" });
11508
11907
  }
11509
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join25(root, ".github", "copilot-instructions.md"))) {
11908
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join26(root, ".github", "copilot-instructions.md"))) {
11510
11909
  out.push({
11511
11910
  agent,
11512
- dir: join25(root, ".github", "copilot"),
11911
+ dir: join26(root, ".github", "copilot"),
11513
11912
  scope: "project"
11514
11913
  });
11515
11914
  }
@@ -11517,7 +11916,7 @@ function detectAgents(root, opts = {}) {
11517
11916
  if (opts.global && opts.home) {
11518
11917
  for (const agent of AGENT_NAMES) {
11519
11918
  for (const rel of HOME_DIRS[agent]) {
11520
- const dir = join25(opts.home, rel);
11919
+ const dir = join26(opts.home, rel);
11521
11920
  if (exists(dir)) out.push({ agent, dir, scope: "global" });
11522
11921
  }
11523
11922
  }
@@ -11535,26 +11934,26 @@ function agentsFromFlag(value, root) {
11535
11934
  return {
11536
11935
  homes: wanted.map((agent) => ({
11537
11936
  agent,
11538
- dir: join25(root, PROJECT_DIRS[agent][0]),
11937
+ dir: join26(root, PROJECT_DIRS[agent][0]),
11539
11938
  scope: "project"
11540
11939
  }))
11541
11940
  };
11542
11941
  }
11543
11942
  function skillsDirFor(home) {
11544
- return join25(home.dir, "skills");
11943
+ return join26(home.dir, "skills");
11545
11944
  }
11546
11945
  function fallbackSkillsHome(root) {
11547
- return { agent: "codex", dir: join25(root, ".agents"), scope: "project" };
11946
+ return { agent: "codex", dir: join26(root, ".agents"), scope: "project" };
11548
11947
  }
11549
- function installedSkills(dir, exists = existsSync21, list = (p) => {
11948
+ function installedSkills(dir, exists = existsSync22, list = (p) => {
11550
11949
  try {
11551
- return readdirSync5(p);
11950
+ return readdirSync6(p);
11552
11951
  } catch {
11553
11952
  return [];
11554
11953
  }
11555
11954
  }) {
11556
11955
  if (!exists(dir)) return [];
11557
- return list(dir).filter((name) => exists(join25(dir, name, "SKILL.md"))).sort();
11956
+ return list(dir).filter((name) => exists(join26(dir, name, "SKILL.md"))).sort();
11558
11957
  }
11559
11958
  var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
11560
11959
  var RULES_END = "<!-- vos:end -->";
@@ -11586,10 +11985,10 @@ ${RULES_BLOCK}
11586
11985
  }
11587
11986
  return { text, changed: text !== current };
11588
11987
  }
11589
- function rulesFileFor(root, exists = existsSync21) {
11590
- const agents = join25(root, "AGENTS.md");
11988
+ function rulesFileFor(root, exists = existsSync22) {
11989
+ const agents = join26(root, "AGENTS.md");
11591
11990
  if (exists(agents)) return agents;
11592
- const claude = join25(root, "CLAUDE.md");
11991
+ const claude = join26(root, "CLAUDE.md");
11593
11992
  if (exists(claude)) return claude;
11594
11993
  return agents;
11595
11994
  }
@@ -11598,14 +11997,14 @@ function chromeCandidates(platform, env) {
11598
11997
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
11599
11998
  if (platform === "win32")
11600
11999
  return [
11601
- join25(
12000
+ join26(
11602
12001
  env["PROGRAMFILES"] ?? "C:\\Program Files",
11603
12002
  "Google",
11604
12003
  "Chrome",
11605
12004
  "Application",
11606
12005
  "chrome.exe"
11607
12006
  ),
11608
- join25(
12007
+ join26(
11609
12008
  env["LOCALAPPDATA"] ?? "",
11610
12009
  "Google",
11611
12010
  "Chrome",
@@ -11692,13 +12091,13 @@ function doctorReport(facts) {
11692
12091
  function bundledSkillsDir(from) {
11693
12092
  let dir = dirname9(from);
11694
12093
  for (let i = 0; i < 6; i++) {
11695
- const pkg = join25(dir, "package.json");
11696
- if (existsSync21(pkg)) {
12094
+ const pkg = join26(dir, "package.json");
12095
+ if (existsSync22(pkg)) {
11697
12096
  try {
11698
- const name = JSON.parse(readFileSync8(pkg, "utf8")).name;
12097
+ const name = JSON.parse(readFileSync9(pkg, "utf8")).name;
11699
12098
  if (name === "@vosjs/cli") {
11700
- const skills = join25(dir, "skills");
11701
- return existsSync21(skills) && statSync6(skills).isDirectory() ? skills : null;
12099
+ const skills = join26(dir, "skills");
12100
+ return existsSync22(skills) && statSync7(skills).isDirectory() ? skills : null;
11702
12101
  }
11703
12102
  } catch {
11704
12103
  }
@@ -11729,7 +12128,7 @@ function npx() {
11729
12128
  function bundledChromium() {
11730
12129
  try {
11731
12130
  const p = chromium2.executablePath();
11732
- return p && existsSync22(p) ? p : null;
12131
+ return p && existsSync23(p) ? p : null;
11733
12132
  } catch {
11734
12133
  return null;
11735
12134
  }
@@ -11738,7 +12137,7 @@ function probe2() {
11738
12137
  return probeBrowser({
11739
12138
  platform: process.platform,
11740
12139
  env: process.env,
11741
- exists: existsSync22,
12140
+ exists: existsSync23,
11742
12141
  bundled: bundledChromium
11743
12142
  });
11744
12143
  }
@@ -11842,7 +12241,7 @@ async function cmdSetup(argv) {
11842
12241
  const dest = skillsDirFor(home);
11843
12242
  mkdirSync6(dest, { recursive: true });
11844
12243
  for (const name of installedSkills(bundled)) {
11845
- cpSync(join26(bundled, name), join26(dest, name), { recursive: true });
12244
+ cpSync(join27(bundled, name), join27(dest, name), { recursive: true });
11846
12245
  }
11847
12246
  }
11848
12247
  skillsVia = "bundled";
@@ -11877,7 +12276,7 @@ async function cmdSetup(argv) {
11877
12276
  let rules = null;
11878
12277
  if (!noRules) {
11879
12278
  const file = rulesFileFor(root);
11880
- const existing = existsSync22(file) ? readFileSync9(file, "utf8") : null;
12279
+ const existing = existsSync23(file) ? readFileSync10(file, "utf8") : null;
11881
12280
  const { text, changed } = applyRulesBlock(existing);
11882
12281
  if (changed) writeFileSync6(file, text);
11883
12282
  rules = { file, changed };
@@ -11991,7 +12390,7 @@ async function cmdWhoami(argv) {
11991
12390
  function cmdLogout(argv) {
11992
12391
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS10);
11993
12392
  const r = reporterFor(flags);
11994
- const had = existsSync22(CREDENTIALS_PATH);
12393
+ const had = existsSync23(CREDENTIALS_PATH);
11995
12394
  rmSync3(CREDENTIALS_PATH, { force: true });
11996
12395
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
11997
12396
  r.done(
@@ -12054,7 +12453,7 @@ Take pipeline
12054
12453
  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]
12055
12454
  a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so. Without a trace the file opens as FINISHED footage (a render from HyperFrames or Remotion, a film): no card, no browser bar, no drawn cursor, its own frame rate; --as take gives a screen demo the card
12056
12455
  vos port inventory <page.html|project dir> [--render <their render.mp4>] [--out port] [--json]
12057
- a finished piece (HyperFrames, Remotion's render, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
12456
+ a finished piece (a HyperFrames composition, a Remotion project bundled with its own bundler, a hand-rolled page) read as a browser renders it: words, palette, faces (catalog or missing), media, scenes, gaps \u2192 port/inventory.json, with stills from its render
12058
12457
  vos port scaffold [--from port/inventory.json] [--out port/program] [--json]
12059
12458
  a data-first program from it: every word a bound element, every colour a key, knobs, a label and a TODO per scene, the score as a doc.json track, port/REPORT.md
12060
12459
  vos build <program.mjs> [--out config.json] [--json]
@@ -12395,7 +12794,7 @@ function takeStorageState(flags) {
12395
12794
  const raw = strFlag(flags, "storage-state");
12396
12795
  if (!raw) return void 0;
12397
12796
  const path = resolve15(raw);
12398
- if (!existsSync23(path))
12797
+ if (!existsSync24(path))
12399
12798
  throw new UsageError(`--storage-state: no such file: ${path}`);
12400
12799
  return path;
12401
12800
  }
@@ -12408,7 +12807,7 @@ function takeSessionName(flags, storageState) {
12408
12807
  throw new UsageError(
12409
12808
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
12410
12809
  );
12411
- if (!existsSync23(sessionDir(name)))
12810
+ if (!existsSync24(sessionDir(name)))
12412
12811
  throw new UsageError(
12413
12812
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
12414
12813
  );
@@ -12631,7 +13030,7 @@ ${lines.join("\n")}
12631
13030
  }
12632
13031
  const paths = takePaths(outDir);
12633
13032
  const prepare = async () => {
12634
- if (existsSync23(join27(outDir, "meta.json"))) {
13033
+ if (existsSync24(join28(outDir, "meta.json"))) {
12635
13034
  const { prevDoc, kept } = await prepareReRecord(outDir);
12636
13035
  r.log(
12637
13036
  `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" : "")
@@ -12696,7 +13095,7 @@ ${lines.join("\n")}
12696
13095
  },
12697
13096
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
12698
13097
  ${(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}
12699
- Next: edit ${join27(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
13098
+ Next: edit ${join28(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
12700
13099
  );
12701
13100
  return strictFail ? EXIT_USAGE : EXIT_OK;
12702
13101
  } finally {
@@ -12722,7 +13121,7 @@ async function cmdCreate2(argv) {
12722
13121
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
12723
13122
  const outDir = resolve15(strFlag(flags, "out") ?? "take");
12724
13123
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
12725
- const out = positionals[0] ?? join27(outDir, `out.${format}`);
13124
+ const out = positionals[0] ?? join28(outDir, `out.${format}`);
12726
13125
  const parallel = numFlag(flags, "parallel", 1);
12727
13126
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
12728
13127
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -12735,7 +13134,7 @@ async function cmdCreate2(argv) {
12735
13134
  const headers = takeHeaders(multi);
12736
13135
  const paths = takePaths(outDir);
12737
13136
  const prepare = async () => {
12738
- if (existsSync23(join27(outDir, "meta.json"))) {
13137
+ if (existsSync24(join28(outDir, "meta.json"))) {
12739
13138
  const { prevDoc, kept } = await prepareReRecord(outDir);
12740
13139
  r.log(
12741
13140
  `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" : "")
@@ -12842,13 +13241,13 @@ async function cmdPlan(argv) {
12842
13241
  );
12843
13242
  const r = createReporter(flags.json === true);
12844
13243
  if (flags.fresh === true) {
12845
- await rm5(join27(dir, "doc.json"), { force: true });
13244
+ await rm5(join28(dir, "doc.json"), { force: true });
12846
13245
  r.log("note: --fresh discarded the existing doc.json");
12847
13246
  }
12848
13247
  let reuse;
12849
13248
  if (flags.reuse === true) {
12850
- const from = resolve15(strFlag(flags, "from") ?? join27(dir, PREV_DOC_NAME));
12851
- if (!existsSync23(from)) {
13249
+ const from = resolve15(strFlag(flags, "from") ?? join28(dir, PREV_DOC_NAME));
13250
+ if (!existsSync24(from)) {
12852
13251
  throw new UsageError(
12853
13252
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
12854
13253
  );
@@ -12859,7 +13258,7 @@ async function cmdPlan(argv) {
12859
13258
  };
12860
13259
  }
12861
13260
  const style = await resolveStyleRef(flags);
12862
- const hasDoc = existsSync23(join27(dir, "doc.json"));
13261
+ const hasDoc = existsSync24(join28(dir, "doc.json"));
12863
13262
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
12864
13263
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
12865
13264
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -12917,7 +13316,7 @@ async function cmdPlan(argv) {
12917
13316
  ...s.motion ? { motion: s.motion } : {},
12918
13317
  ...s.reuse ? { reuse: s.reuse } : {}
12919
13318
  },
12920
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join27(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
13319
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join28(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
12921
13320
  );
12922
13321
  return EXIT_OK;
12923
13322
  }
@@ -13040,7 +13439,7 @@ async function cmdRender(argv) {
13040
13439
  );
13041
13440
  const r = createReporter(flags.json === true);
13042
13441
  const format = videoFormat(strFlag(flags, "format"), positionals[1]);
13043
- const out = positionals[1] ?? join27(dir, `out.${format}`);
13442
+ const out = positionals[1] ?? join28(dir, `out.${format}`);
13044
13443
  const parallel = numFlag(flags, "parallel", 1);
13045
13444
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
13046
13445
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -13287,11 +13686,11 @@ async function resolveStyleRef(flags) {
13287
13686
  }
13288
13687
  async function resolveDocRef(ref, flags, what) {
13289
13688
  const styleRef = ref;
13290
- const file = existsSync23(styleRef) ? resolve15(
13689
+ const file = existsSync24(styleRef) ? resolve15(
13291
13690
  styleRef,
13292
- existsSync23(join27(styleRef, "doc.json")) ? "doc.json" : ""
13691
+ existsSync24(join28(styleRef, "doc.json")) ? "doc.json" : ""
13293
13692
  ) : null;
13294
- if (file && existsSync23(file)) {
13693
+ if (file && existsSync24(file)) {
13295
13694
  return {
13296
13695
  from: file,
13297
13696
  doc: migrateHostedDoc5(
@@ -13378,7 +13777,7 @@ async function cmdDigest(argv) {
13378
13777
  kinds,
13379
13778
  frames: d.images.full,
13380
13779
  crops: d.images.crop,
13381
- sheet: d.images.sheet ? join27(result.outDir, d.images.sheet) : null,
13780
+ sheet: d.images.sheet ? join28(result.outDir, d.images.sheet) : null,
13382
13781
  scenes: kinds.scene ?? 0,
13383
13782
  sourceDuration: d.take.sourceDuration,
13384
13783
  outputDuration: d.take.outputDuration,
@@ -13406,7 +13805,7 @@ async function cmdOpen(argv) {
13406
13805
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
13407
13806
  const r = createReporter(flags.json === true);
13408
13807
  const take = await loadTake(dir);
13409
- if (!existsSync23(take.paths.recording)) {
13808
+ if (!existsSync24(take.paths.recording)) {
13410
13809
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
13411
13810
  }
13412
13811
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
@@ -13447,9 +13846,9 @@ async function cmdJudge(argv) {
13447
13846
  "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)"
13448
13847
  );
13449
13848
  const r = createReporter(flags.json === true);
13450
- const kitFile = target.endsWith("kit.json") ? target : join27(target, "kit.json");
13451
- if (!existsSync23(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
13452
- if (!existsSync23(against))
13849
+ const kitFile = target.endsWith("kit.json") ? target : join28(target, "kit.json");
13850
+ if (!existsSync24(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
13851
+ if (!existsSync24(against))
13453
13852
  throw new UsageError(`${against}: no reference manifest`);
13454
13853
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
13455
13854
  const verdicts = JSON.parse(await readFile17(result.verdictFile, "utf8")).verdicts;
@@ -13571,7 +13970,7 @@ async function cmdValidate(argv) {
13571
13970
  const target = positionals[0];
13572
13971
  if (!target) throw new UsageError("vos validate <actions.json|take>");
13573
13972
  const r = createReporter(flags.json === true);
13574
- const kitTarget = target.endsWith("kit.json") ? target : existsSync23(join27(target, "kit.json")) && !isTakeDir(target) ? join27(target, "kit.json") : null;
13973
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync24(join28(target, "kit.json")) && !isTakeDir(target) ? join28(target, "kit.json") : null;
13575
13974
  if (kitTarget) {
13576
13975
  const picture = flags.picture === true;
13577
13976
  const verdict = await validateKit(kitTarget, { picture });
@@ -13603,9 +14002,9 @@ async function cmdValidate(argv) {
13603
14002
  r.done({ valid: true, target }, `${target}: valid actions file`);
13604
14003
  return EXIT_OK;
13605
14004
  }
13606
- if (!isTakeDir(target) && existsSync23(join27(target, "config.json"))) {
14005
+ if (!isTakeDir(target) && existsSync24(join28(target, "config.json"))) {
13607
14006
  const parsed = JSON.parse(
13608
- await readFile17(join27(target, "config.json"), "utf8")
14007
+ await readFile17(join28(target, "config.json"), "utf8")
13609
14008
  );
13610
14009
  const pre = preflightConfig(parsed);
13611
14010
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -13646,7 +14045,7 @@ async function cmdValidate(argv) {
13646
14045
  const take = await loadTake(target);
13647
14046
  const problems = [];
13648
14047
  const warnings = [];
13649
- if (!existsSync23(take.paths.recording)) {
14048
+ if (!existsSync24(take.paths.recording)) {
13650
14049
  problems.push("missing recording (re-run record)");
13651
14050
  } else {
13652
14051
  const mismatch = containerMismatch(
@@ -13994,7 +14393,7 @@ async function cmdSession(argv) {
13994
14393
  if (!arg) throw new UsageError(usage);
13995
14394
  const bad = sessionNameVerdict(arg);
13996
14395
  if (bad) throw new UsageError(bad);
13997
- if (!existsSync23(sessionDir(arg)))
14396
+ if (!existsSync24(sessionDir(arg)))
13998
14397
  throw new UsageError(`no session "${arg}" (vos session list)`);
13999
14398
  const summary3 = await summarizeSession(arg);
14000
14399
  const url = strFlag(flags, "url");
@@ -14098,4 +14497,4 @@ export {
14098
14497
  verbHelp,
14099
14498
  run
14100
14499
  };
14101
- //# sourceMappingURL=chunk-M7FLXLBN.js.map
14500
+ //# sourceMappingURL=chunk-FQA7LFXG.js.map