@vosjs/cli 0.80.0 → 0.81.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.
@@ -28,6 +28,7 @@ import {
28
28
  isTakeRelativeKey,
29
29
  loadTake,
30
30
  localManifestRefs,
31
+ notePlannedDoc,
31
32
  prepareReRecord,
32
33
  pullManifest,
33
34
  pullMedia,
@@ -45,7 +46,7 @@ import {
45
46
  uploadDocRefs,
46
47
  uploadManifest,
47
48
  writeJson
48
- } from "./chunk-PJ6MEJZV.js";
49
+ } from "./chunk-352AC3V6.js";
49
50
  import {
50
51
  CREDENTIALS_PATH,
51
52
  MEDIA_HEAD_BYTES,
@@ -145,8 +146,8 @@ function landedLines(l) {
145
146
 
146
147
  // src/plugin/run.ts
147
148
  import { mkdir as mkdir13, readFile as readFile18, rm as rm6 } from "fs/promises";
148
- import { existsSync as existsSync29 } from "fs";
149
- import { dirname as dirname11, join as join33, resolve as resolve20 } from "path";
149
+ import { existsSync as existsSync30 } from "fs";
150
+ import { dirname as dirname11, join as join33, resolve as resolve21 } from "path";
150
151
  import {
151
152
  END_CARD_FROM as END_CARD_FROM2,
152
153
  docOutputDuration as docOutputDuration5,
@@ -662,6 +663,56 @@ function lintDoc(docIn) {
662
663
  );
663
664
  }
664
665
  }
666
+ const cursorStyle = isObj(doc.cursor) ? doc.cursor : {};
667
+ if (recording && cursorStyle.off !== void 0) {
668
+ const off2 = cursorStyle.off;
669
+ if (!Array.isArray(off2)) {
670
+ problems.push(
671
+ "cursor.off must be an array of { in, out?, from? } ranges (OUTPUT seconds)"
672
+ );
673
+ } else {
674
+ let end = null;
675
+ const filmEnd = () => {
676
+ if (end === null) {
677
+ const rated = ratedSegments(doc);
678
+ end = outputEnd(
679
+ resolveOverlayAnchors(doc, rated),
680
+ rated.reduce((a, s) => a + (s.out - s.in) / (s.rate ?? 1), 0)
681
+ );
682
+ }
683
+ return end;
684
+ };
685
+ for (const [i, raw] of off2.entries()) {
686
+ const name = `cursor.off[${i}]`;
687
+ if (!isObj(raw)) {
688
+ problems.push(
689
+ `${name} must be an object { in, out?, from? } (OUTPUT seconds)`
690
+ );
691
+ continue;
692
+ }
693
+ for (const k of Object.keys(raw))
694
+ if (k !== "in" && k !== "out" && k !== "from")
695
+ problems.push(`${name}.${k}: not a range field (in, out, from)`);
696
+ lintFrom(name, raw.from, problems);
697
+ if (!isNum(raw.in) || raw.in < 0) {
698
+ problems.push(
699
+ `${name}.in=${String(raw.in)} must be a number \u2265 0 (OUTPUT seconds)`
700
+ );
701
+ continue;
702
+ }
703
+ if (raw.out !== void 0 && (!isNum(raw.out) || raw.out <= raw.in)) {
704
+ problems.push(
705
+ `${name}.out=${String(raw.out)} must be a number past in (${raw.in}s); leave it out to hide the cursor to the end of the film`
706
+ );
707
+ continue;
708
+ }
709
+ if (raw.in > filmEnd() + 1e-6)
710
+ warnings.push(
711
+ `${name} starts at ${raw.in}s, past the film's end (${filmEnd().toFixed(2)}s): it hides nothing`
712
+ );
713
+ }
714
+ }
715
+ }
665
716
  for (const [i, seg] of (Array.isArray(doc.segments) ? doc.segments : []).entries()) {
666
717
  const hold = seg.hold;
667
718
  if (hold === void 0) continue;
@@ -4103,6 +4154,36 @@ import {
4103
4154
  spanOutputExtent as spanOutputExtent3
4104
4155
  } from "@vosjs/studio-core";
4105
4156
 
4157
+ // src/plugin/captionFit.ts
4158
+ var CAPTION_MAX = 3;
4159
+ var CAPTION_MIN = 1.2;
4160
+ var CAPTION_GAP = 0.1;
4161
+ function fitCaptions(items, slotOf, opts = {}) {
4162
+ const max = opts.max ?? CAPTION_MAX;
4163
+ const min = opts.min ?? CAPTION_MIN;
4164
+ const gap = opts.gap ?? CAPTION_GAP;
4165
+ const ordered = items.map((item) => ({ item, slot: slotOf(item) })).sort((a, b) => a.slot.start - b.slot.start);
4166
+ const kept = [];
4167
+ const dropped = [];
4168
+ let nextStart = Infinity;
4169
+ for (let i = ordered.length - 1; i >= 0; i--) {
4170
+ const { item, slot } = ordered[i];
4171
+ const clipRoom = slot.end - slot.start;
4172
+ const nextRoom = nextStart - gap - slot.start;
4173
+ const duration = Math.min(max, clipRoom, nextRoom);
4174
+ if (duration < min - 1e-9) {
4175
+ dropped.unshift({
4176
+ item,
4177
+ why: nextRoom < clipRoom ? `the next caption starts ${(nextStart - slot.start).toFixed(2)}s after it, too soon to read it (more time on its step, its ms, gives it room)` : `it plays in the last ${Math.max(0, clipRoom).toFixed(2)}s of its clip, too short to read it (a wait after its step gives it room)`
4178
+ });
4179
+ continue;
4180
+ }
4181
+ kept.unshift({ item, duration: +duration.toFixed(3) });
4182
+ nextStart = slot.start;
4183
+ }
4184
+ return { kept, dropped };
4185
+ }
4186
+
4106
4187
  // src/plugin/moments.ts
4107
4188
  import {
4108
4189
  hasReplay,
@@ -4131,6 +4212,7 @@ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
4131
4212
  var BED_ID = "bed";
4132
4213
  var CLICK_ID_PREFIX = "click-";
4133
4214
  var CAPTION_ID_PREFIX = "caption-";
4215
+ var CAPTION_BEAT_MAX = 3.5;
4134
4216
  function houseCaption(id, text, start, duration) {
4135
4217
  return {
4136
4218
  id,
@@ -4215,6 +4297,19 @@ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
4215
4297
  const rate = s.rate && s.rate > 0 ? s.rate : 1;
4216
4298
  return acc + (s.out - s.in) / rate;
4217
4299
  }, 0);
4300
+ function withEndCardCursorOff(doc, start) {
4301
+ const own = (doc.cursor.off ?? []).filter((r) => r.from !== END_CARD_FROM);
4302
+ return {
4303
+ ...doc,
4304
+ cursor: {
4305
+ ...doc.cursor,
4306
+ off: [
4307
+ ...own,
4308
+ { in: +Math.max(0, start).toFixed(3), from: END_CARD_FROM }
4309
+ ]
4310
+ }
4311
+ };
4312
+ }
4218
4313
  function templateWords(words2) {
4219
4314
  const headline = (words2.headline ?? "").trim();
4220
4315
  const kicker = (words2.kicker ?? "").trim();
@@ -4283,6 +4378,7 @@ function proposeMotion(input, opts) {
4283
4378
  skipped.push(`${t.from}: ${anchorWord(t.at)} was not recorded`);
4284
4379
  continue;
4285
4380
  }
4381
+ const before = outputLength(doc);
4286
4382
  const applied = applyTemplate(t.doc, doc, {
4287
4383
  at: at2,
4288
4384
  from: t.from,
@@ -4290,6 +4386,10 @@ function proposeMotion(input, opts) {
4290
4386
  keys
4291
4387
  });
4292
4388
  doc = applied.doc;
4389
+ if (t.from === END_CARD_FROM) {
4390
+ const starts = (doc.overlays ?? []).filter((o) => o.from === END_CARD_FROM && o.start !== void 0).map((o) => o.start);
4391
+ doc = withEndCardCursorOff(doc, Math.min(before, ...starts));
4392
+ }
4293
4393
  notes.push(`${t.from} at ${anchorWord(t.at)}`);
4294
4394
  for (const n of applied.notes) skipped.push(`${t.from}: ${n}`);
4295
4395
  }
@@ -4338,6 +4438,7 @@ function proposeMotion(input, opts) {
4338
4438
  ...doc.frame.anim ?? {},
4339
4439
  exit: { kind: "recede", seconds: END_CARD_SECONDS }
4340
4440
  };
4441
+ doc = withEndCardCursorOff(doc, endStart);
4341
4442
  notes.push("end card");
4342
4443
  } else {
4343
4444
  skipped.push(
@@ -4352,22 +4453,30 @@ function proposeMotion(input, opts) {
4352
4453
  if (opts.captions.length && !off(launch.captions)) {
4353
4454
  const rated = ratedSegments4(doc);
4354
4455
  const steps = doc.source.meta.steps ?? [];
4456
+ const beats = [];
4355
4457
  for (const c of opts.captions) {
4356
4458
  const step = steps.find(
4357
4459
  (s) => c.id !== void 0 && s.id === c.id || s.step === c.step
4358
4460
  );
4359
4461
  if (!step || step.skipped) continue;
4360
4462
  const t = stepOutputTime(rated, step, 0.2);
4361
- if (t === null || t < 0 || t > footage - 1) continue;
4463
+ if (t === null || t < 0 || t >= footage) continue;
4464
+ beats.push({ caption: c, t: +t.toFixed(3) });
4465
+ }
4466
+ const fit = fitCaptions(beats, (b) => ({ start: b.t, end: footage }), {
4467
+ max: CAPTION_BEAT_MAX
4468
+ });
4469
+ for (const { item, why } of fit.dropped)
4470
+ skipped.push(`caption for step ${item.caption.step}: ${why}`);
4471
+ for (const { item, duration } of fit.kept)
4362
4472
  captionClips.push(
4363
4473
  houseCaption(
4364
- `${CAPTION_ID_PREFIX}${c.step}`,
4365
- c.caption,
4366
- +t.toFixed(3),
4367
- Math.min(3.5, Math.max(2.5, footage - t - 0.2))
4474
+ `${CAPTION_ID_PREFIX}${item.caption.step}`,
4475
+ item.caption.caption,
4476
+ item.t,
4477
+ duration
4368
4478
  )
4369
4479
  );
4370
- }
4371
4480
  if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
4372
4481
  }
4373
4482
  const overlays = [...kept, ...captionClips];
@@ -4452,6 +4561,10 @@ function destinationMechanics(d, doc) {
4452
4561
  const keptFreezes = (doc.freeze ?? []).filter((f) => !f.from);
4453
4562
  if (keptFreezes.length !== (doc.freeze ?? []).length)
4454
4563
  set.push(`freeze=${JSON.stringify(keptFreezes)}`);
4564
+ const off2 = doc.cursor?.off ?? [];
4565
+ const keptOff = off2.filter((r) => !r.from);
4566
+ if (keptOff.length !== off2.length)
4567
+ set.push(`cursor.off=${JSON.stringify(keptOff)}`);
4455
4568
  }
4456
4569
  if (portrait) {
4457
4570
  set.push("frame.fit=cover");
@@ -5879,14 +5992,14 @@ function chromeTree(rows, pid) {
5879
5992
  }
5880
5993
  function readChromeTree(pid) {
5881
5994
  if (process.platform === "win32") return Promise.resolve(null);
5882
- return new Promise((resolve21) => {
5995
+ return new Promise((resolve22) => {
5883
5996
  execFile2(
5884
5997
  "ps",
5885
5998
  ["-A", "-o", "pid=", "-o", "ppid=", "-o", "command="],
5886
5999
  { maxBuffer: 16 * 1024 * 1024, timeout: 5e3 },
5887
6000
  (err, stdout) => {
5888
- if (err) resolve21(null);
5889
- else resolve21(chromeTree(parseProcessTable(stdout), pid));
6001
+ if (err) resolve22(null);
6002
+ else resolve22(chromeTree(parseProcessTable(stdout), pid));
5890
6003
  }
5891
6004
  );
5892
6005
  });
@@ -5988,7 +6101,7 @@ function enterPressed() {
5988
6101
  } };
5989
6102
  const rl = createInterface({ input: process.stdin });
5990
6103
  const pressed = new Promise(
5991
- (resolve21) => rl.once("line", () => resolve21())
6104
+ (resolve22) => rl.once("line", () => resolve22())
5992
6105
  );
5993
6106
  return { pressed, close: () => rl.close() };
5994
6107
  }
@@ -6161,14 +6274,14 @@ async function openSessionWindow(opts) {
6161
6274
  const child = spawn(exe, args, { stdio: "ignore" });
6162
6275
  let failed = null;
6163
6276
  let code = null;
6164
- const exited = new Promise((resolve21) => {
6277
+ const exited = new Promise((resolve22) => {
6165
6278
  child.once("exit", (c) => {
6166
6279
  code = c;
6167
- resolve21();
6280
+ resolve22();
6168
6281
  });
6169
6282
  child.once("error", (e) => {
6170
6283
  failed = e;
6171
- resolve21();
6284
+ resolve22();
6172
6285
  });
6173
6286
  });
6174
6287
  try {
@@ -6507,13 +6620,58 @@ import {
6507
6620
  import {
6508
6621
  SetupEnvError,
6509
6622
  SetupError,
6623
+ TEARDOWN_BOUND_MS,
6510
6624
  parseHeaders,
6511
6625
  resolveSetupText,
6512
6626
  runSetup,
6627
+ scriptEnvNames,
6513
6628
  setupEnvNames,
6629
+ teardownEnvProblem,
6630
+ teardownLine,
6514
6631
  validateSetup
6515
6632
  } from "@vosjs/render-core/record";
6516
6633
 
6634
+ // src/plugin/sessionExec.ts
6635
+ import { existsSync as existsSync10 } from "fs";
6636
+ import { resolve as resolve7 } from "path";
6637
+ import { pathToFileURL } from "url";
6638
+ var ExecScriptError = class extends Error {
6639
+ };
6640
+ async function loadExecScript(path) {
6641
+ const file = resolve7(path);
6642
+ if (!existsSync10(file)) throw new ExecScriptError(`no script at ${file}`);
6643
+ if (!/\.(m?js|cjs)$/.test(file))
6644
+ throw new ExecScriptError(
6645
+ `${path}: a script is a .mjs or .js module (compile TypeScript first)`
6646
+ );
6647
+ const mod = await import(pathToFileURL(file).href);
6648
+ if (typeof mod.default !== "function")
6649
+ throw new ExecScriptError(
6650
+ `${path} has no default export function: export default async ({ context, page, url }) => { \u2026 }`
6651
+ );
6652
+ return mod.default;
6653
+ }
6654
+ async function execInProfile(dir, script, opts) {
6655
+ return withProfile(
6656
+ dir,
6657
+ async (context2) => {
6658
+ const page = context2.pages()[0] ?? await context2.newPage();
6659
+ if (opts.url)
6660
+ await page.goto(opts.url, { waitUntil: "load", timeout: 3e4 });
6661
+ return await script({
6662
+ context: context2,
6663
+ page,
6664
+ name: opts.name,
6665
+ ...opts.url ? { url: opts.url } : {}
6666
+ });
6667
+ },
6668
+ {
6669
+ headless: opts.headless ?? true,
6670
+ ...opts.viewport ? { viewport: opts.viewport } : {}
6671
+ }
6672
+ );
6673
+ }
6674
+
6517
6675
  // src/plugin/recordingCap.ts
6518
6676
  import {
6519
6677
  capReached,
@@ -6569,7 +6727,7 @@ async function encodeRecording(browser, takeDir, onProgress) {
6569
6727
  }
6570
6728
 
6571
6729
  // src/plugin/plan.ts
6572
- import { existsSync as existsSync10 } from "fs";
6730
+ import { existsSync as existsSync11 } from "fs";
6573
6731
  import { readFile as readFile9 } from "fs/promises";
6574
6732
  import { join as join11 } from "path";
6575
6733
  import {
@@ -6862,9 +7020,21 @@ function outputToSource(pieces, t) {
6862
7020
  }
6863
7021
  return pieces.at(-1)?.out ?? t;
6864
7022
  }
7023
+ function footageOutputEnd(doc) {
7024
+ const own = {
7025
+ ...doc,
7026
+ freeze: (doc.freeze ?? []).filter((f) => !f.from)
7027
+ };
7028
+ return ratedSegments6(own).reduce((acc, s) => {
7029
+ const rate = s.rate && s.rate > 0 ? s.rate : 1;
7030
+ return acc + (s.out - s.in) / rate;
7031
+ }, 0);
7032
+ }
6865
7033
  function retimeOverlays(prev, next, report) {
6866
7034
  const overlays = prev.overlays ?? [];
6867
7035
  if (!overlays.length) return [];
7036
+ const oldEnd = footageOutputEnd(prev);
7037
+ const endShift = footageOutputEnd(next) - oldEnd;
6868
7038
  const oldSteps = prev.source.meta.steps ?? [];
6869
7039
  const newSteps = next.source.meta.steps ?? [];
6870
7040
  const oldPieces = ratedSegments6(prev);
@@ -6916,6 +7086,10 @@ function retimeOverlays(prev, next, report) {
6916
7086
  const moved = follow(o, at2, pin.press, placed.at);
6917
7087
  return { ...moved, pin: { ...pin, press: +placed.at.toFixed(3) } };
6918
7088
  }
7089
+ if (at2 >= oldEnd - 1e-6) {
7090
+ if (Math.abs(endShift) < 1e-6) return o;
7091
+ return { ...o, start: +Math.max(0, at2 + endShift).toFixed(3) };
7092
+ }
6919
7093
  const under = stepUnder(
6920
7094
  outputToSource(oldPieces, at2),
6921
7095
  oldSteps,
@@ -6943,7 +7117,7 @@ function retimeOverlays(prev, next, report) {
6943
7117
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6944
7118
  async function readDigestActivity(dir) {
6945
7119
  const file = join11(dir, "digest", "digest.json");
6946
- if (!existsSync10(file)) return null;
7120
+ if (!existsSync11(file)) return null;
6947
7121
  try {
6948
7122
  const d = JSON.parse(await readFile9(file, "utf8"));
6949
7123
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -7064,6 +7238,20 @@ async function planTake(dir, opts = {}) {
7064
7238
  withPageSound(doc, meta.media);
7065
7239
  if (prev.still !== void 0) doc.still = prev.still;
7066
7240
  if (opts.still != null) doc.still = opts.still;
7241
+ if (prev.cursor?.off?.length) {
7242
+ const oldEnd = footageOutputEnd(prev);
7243
+ const shift = footageOutputEnd(doc) - oldEnd;
7244
+ doc.cursor = {
7245
+ ...doc.cursor,
7246
+ off: prev.cursor.off.map(
7247
+ (r) => r.in >= oldEnd - 1e-6 && Math.abs(shift) > 1e-6 ? {
7248
+ ...r,
7249
+ in: +Math.max(0, r.in + shift).toFixed(3),
7250
+ ...r.out !== void 0 ? { out: +Math.max(0, r.out + shift).toFixed(3) } : {}
7251
+ } : { ...r }
7252
+ )
7253
+ };
7254
+ }
7067
7255
  await writeJson(take.paths.doc, doc, true);
7068
7256
  return {
7069
7257
  doc,
@@ -7173,14 +7361,14 @@ async function planTake(dir, opts = {}) {
7173
7361
 
7174
7362
  // src/plugin/sync.ts
7175
7363
  import { createHash } from "crypto";
7176
- import { existsSync as existsSync12, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
7364
+ import { existsSync as existsSync13, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
7177
7365
  import { readFile as readFile11 } from "fs/promises";
7178
7366
  import { createInterface as createInterface2 } from "readline/promises";
7179
7367
  import { basename as basename2, join as join14 } from "path";
7180
7368
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
7181
7369
 
7182
7370
  // src/plugin/takeScript.ts
7183
- import { existsSync as existsSync11 } from "fs";
7371
+ import { existsSync as existsSync12 } from "fs";
7184
7372
  import { readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
7185
7373
  import { readFileSync as readFileSync2 } from "fs";
7186
7374
  import { basename, join as join12 } from "path";
@@ -7201,26 +7389,27 @@ function scriptToWrite(doc, hasLocal) {
7201
7389
  function publicScript(script) {
7202
7390
  const peers = publicPeers(script.peers);
7203
7391
  if (peers) script = { ...script, peers };
7204
- const setup = script.setup;
7205
- if (!Array.isArray(setup)) return script;
7206
- return {
7207
- ...script,
7208
- setup: setup.map((step) => {
7209
- if (!step || typeof step !== "object") return step;
7210
- const s = step;
7211
- if (s.do === "type" && typeof s.text === "string")
7212
- return { ...s, text: "[typed value not stored]" };
7213
- if (s.do === "goto" && typeof s.url === "string") {
7214
- try {
7215
- const u = new URL(s.url);
7216
- return { ...s, url: `${u.origin}${u.pathname}` };
7217
- } catch {
7218
- return s;
7219
- }
7220
- }
7392
+ for (const key of ["setup", "teardown"]) {
7393
+ const steps = script[key];
7394
+ if (Array.isArray(steps))
7395
+ script = { ...script, [key]: steps.map(publicStep) };
7396
+ }
7397
+ return script;
7398
+ }
7399
+ function publicStep(step) {
7400
+ if (!step || typeof step !== "object") return step;
7401
+ const s = step;
7402
+ if (s.do === "type" && typeof s.text === "string")
7403
+ return { ...s, text: "[typed value not stored]" };
7404
+ if (s.do === "goto" && typeof s.url === "string") {
7405
+ try {
7406
+ const u = new URL(s.url);
7407
+ return { ...s, url: `${u.origin}${u.pathname}` };
7408
+ } catch {
7221
7409
  return s;
7222
- })
7223
- };
7410
+ }
7411
+ }
7412
+ return s;
7224
7413
  }
7225
7414
  function publicPeers(peers) {
7226
7415
  if (!Array.isArray(peers)) return null;
@@ -7233,20 +7422,20 @@ function publicPeers(peers) {
7233
7422
  }
7234
7423
  function embedScript(doc, dir) {
7235
7424
  const path = join12(dir, "actions.json");
7236
- if (!existsSync11(path)) return;
7425
+ if (!existsSync12(path)) return;
7237
7426
  const script = scriptFromText(readFileSync2(path, "utf8"));
7238
7427
  if (script) doc.source.script = publicScript(script);
7239
7428
  }
7240
7429
  async function writeScriptBeside(dir, doc) {
7241
7430
  const path = join12(dir, "actions.json");
7242
- const text = scriptToWrite(doc, existsSync11(path));
7431
+ const text = scriptToWrite(doc, existsSync12(path));
7243
7432
  if (text === null) return false;
7244
7433
  await writeFile7(path, text, "utf8");
7245
7434
  return true;
7246
7435
  }
7247
7436
  async function readScript(dir) {
7248
7437
  const path = join12(dir, "actions.json");
7249
- if (!existsSync11(path)) return null;
7438
+ if (!existsSync12(path)) return null;
7250
7439
  return scriptFromText(await readFile10(path, "utf8"));
7251
7440
  }
7252
7441
 
@@ -7737,7 +7926,7 @@ async function pushTake(dir, flags, r) {
7737
7926
  throw new Error(`doc.json fails lint:
7738
7927
  ${lint.problems.join("\n ")}`);
7739
7928
  }
7740
- if (!existsSync12(take.paths.recording)) {
7929
+ if (!existsSync13(take.paths.recording)) {
7741
7930
  throw new Error(
7742
7931
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
7743
7932
  );
@@ -7972,7 +8161,7 @@ async function pullTake(dir, flags, r) {
7972
8161
  };
7973
8162
  }
7974
8163
  let media2;
7975
- if (flags.media && existsSync12(join14(dir, "doc.json"))) {
8164
+ if (flags.media && existsSync13(join14(dir, "doc.json"))) {
7976
8165
  const local = JSON.parse(
7977
8166
  await readFile11(join14(dir, "doc.json"), "utf8")
7978
8167
  );
@@ -8035,7 +8224,7 @@ async function pullTake(dir, flags, r) {
8035
8224
  const hostedDoc = migrateHostedDoc2(docRes.json);
8036
8225
  const doc = hostedDoc;
8037
8226
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8038
- if (existsSync12(localRecording)) {
8227
+ if (existsSync13(localRecording)) {
8039
8228
  doc.source.videoKey = basename2(localRecording);
8040
8229
  }
8041
8230
  await writeJson(join14(dir, "doc.json"), doc, true);
@@ -8111,7 +8300,7 @@ async function pushTakeClaimable(dir, flags, r) {
8111
8300
  );
8112
8301
  }
8113
8302
  const path = join14(dir, file);
8114
- const now = existsSync12(path) ? await fileSha256(path) : null;
8303
+ const now = existsSync13(path) ? await fileSha256(path) : null;
8115
8304
  if (now !== origin2.sha256) {
8116
8305
  throw new PushRefusedError(
8117
8306
  `${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`
@@ -8160,7 +8349,7 @@ async function pushTakeClaimable(dir, flags, r) {
8160
8349
  }
8161
8350
 
8162
8351
  // src/plugin/program.ts
8163
- import { existsSync as existsSync14 } from "fs";
8352
+ import { existsSync as existsSync15 } from "fs";
8164
8353
  import { mkdir as mkdir7, readFile as readFile12 } from "fs/promises";
8165
8354
  import { createInterface as createInterface3 } from "readline/promises";
8166
8355
  import { basename as basename4, dirname as dirname2, join as join15 } from "path";
@@ -8173,12 +8362,12 @@ import { lowerProgramDoc, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/s
8173
8362
 
8174
8363
  // src/plugin/programFiles.ts
8175
8364
  import { basename as basename3, extname as extname2, relative as relative2 } from "path";
8176
- import { existsSync as existsSync13, realpathSync as realpathSync2 } from "fs";
8365
+ import { existsSync as existsSync14, realpathSync as realpathSync2 } from "fs";
8177
8366
  function isLocalName(value) {
8178
8367
  if (/^(https?:|data:|blob:|asset:|media:)/.test(value)) return false;
8179
8368
  if (value.startsWith("$")) return false;
8180
8369
  if (value.startsWith("<svg") || value.startsWith("<?xml")) return false;
8181
- if (value.startsWith("/")) return existsSync13(value);
8370
+ if (value.startsWith("/")) return existsSync14(value);
8182
8371
  return true;
8183
8372
  }
8184
8373
  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;
@@ -8354,7 +8543,7 @@ function preflightConfig(parsed) {
8354
8543
  function resolveConfigPath(target) {
8355
8544
  if (target.endsWith(".json")) return target;
8356
8545
  const inDir = join15(target, "config.json");
8357
- if (existsSync14(inDir)) return inDir;
8546
+ if (existsSync15(inDir)) return inDir;
8358
8547
  throw new UsageError(
8359
8548
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
8360
8549
  );
@@ -8912,7 +9101,7 @@ async function cmdPullProgram(argv) {
8912
9101
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
8913
9102
  const configPath = join15(dir, "config.json");
8914
9103
  let backedUp = false;
8915
- if (existsSync14(configPath)) {
9104
+ if (existsSync15(configPath)) {
8916
9105
  await writeFileSafe(
8917
9106
  join15(dir, "config.backup.json"),
8918
9107
  await readFile12(configPath)
@@ -9058,7 +9247,7 @@ function storedProgramConfig(config, programDoc) {
9058
9247
  }
9059
9248
  async function readProgramDoc(dir, config) {
9060
9249
  const docPath = join15(dir, "doc.json");
9061
- if (!existsSync14(docPath)) return null;
9250
+ if (!existsSync15(docPath)) return null;
9062
9251
  const parsed = JSON.parse(await readFile12(docPath, "utf8"));
9063
9252
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
9064
9253
  return null;
@@ -9069,7 +9258,7 @@ async function readProgramDoc(dir, config) {
9069
9258
  async function setProgramStill(dir, t) {
9070
9259
  const docPath = join15(dir, "doc.json");
9071
9260
  let doc = { program: {}, audio: [] };
9072
- if (existsSync14(docPath)) {
9261
+ if (existsSync15(docPath)) {
9073
9262
  const parsed = JSON.parse(await readFile12(docPath, "utf8"));
9074
9263
  if (!parsed || typeof parsed !== "object" || "source" in parsed)
9075
9264
  throw new UsageError(
@@ -9624,7 +9813,7 @@ async function cmdAsset(argv) {
9624
9813
  }
9625
9814
 
9626
9815
  // src/plugin/remove.ts
9627
- import { existsSync as existsSync15, rmSync as rmSync2, statSync as statSync3 } from "fs";
9816
+ import { existsSync as existsSync16, rmSync as rmSync2, statSync as statSync3 } from "fs";
9628
9817
  import { join as join16 } from "path";
9629
9818
  import { createInterface as createInterface4 } from "readline/promises";
9630
9819
 
@@ -9804,7 +9993,7 @@ async function cmdDelete(argv) {
9804
9993
  api: strFlag(flags, "api")
9805
9994
  }).replace(/\/+$/, "");
9806
9995
  const key = requireCredential(strFlag(flags, "key"));
9807
- const dir = existsSync15(target) && statSync3(target).isDirectory() ? target : null;
9996
+ const dir = existsSync16(target) && statSync3(target).isDirectory() ? target : null;
9808
9997
  const vosId = dir ? readSyncState(dir)?.vosId : parseVosId(target);
9809
9998
  if (!vosId)
9810
9999
  throw new UsageError(
@@ -9872,14 +10061,14 @@ async function cmdDelete(argv) {
9872
10061
  }
9873
10062
  function trackingCwd(vosId) {
9874
10063
  const cwd = process.cwd();
9875
- if (!existsSync15(join16(cwd, SYNC_STATE_NAME))) return null;
10064
+ if (!existsSync16(join16(cwd, SYNC_STATE_NAME))) return null;
9876
10065
  return readSyncState(cwd)?.vosId === vosId ? cwd : null;
9877
10066
  }
9878
10067
 
9879
10068
  // src/plugin/buildProgram.ts
9880
10069
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
9881
- import { basename as basename6, dirname as dirname3, join as join17, resolve as resolve7 } from "path";
9882
- import { pathToFileURL } from "url";
10070
+ import { basename as basename6, dirname as dirname3, join as join17, resolve as resolve8 } from "path";
10071
+ import { pathToFileURL as pathToFileURL2 } from "url";
9883
10072
  import { parse } from "acorn";
9884
10073
  import { fullAncestor } from "acorn-walk";
9885
10074
  var FUNCTION_KEYS = [
@@ -10078,9 +10267,9 @@ async function cmdBuild(argv) {
10078
10267
  if (!source)
10079
10268
  throw new UsageError("vos build <program.mjs> [--out config.json] [--json]");
10080
10269
  const r = createReporter(flags.json === true);
10081
- const file = resolve7(source);
10270
+ const file = resolve8(source);
10082
10271
  const text = readFileSync5(file, "utf8");
10083
- const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
10272
+ const mod = await import(`${pathToFileURL2(file).href}?t=${Date.now()}`);
10084
10273
  const program = mod.default ?? mod.program;
10085
10274
  if (!program || typeof program !== "object")
10086
10275
  throw new UsageError(
@@ -10095,7 +10284,7 @@ async function cmdBuild(argv) {
10095
10284
  );
10096
10285
  return EXIT_ERROR;
10097
10286
  }
10098
- const out = resolve7(
10287
+ const out = resolve8(
10099
10288
  strFlag(flags, "out") ?? join17(dirname3(file), "config.json")
10100
10289
  );
10101
10290
  writeFileSync2(out, JSON.stringify(built.config, null, 2) + "\n");
@@ -10113,8 +10302,8 @@ async function cmdBuild(argv) {
10113
10302
 
10114
10303
  // src/plugin/compare.ts
10115
10304
  import { spawnSync } from "child_process";
10116
- import { existsSync as existsSync16, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
10117
- import { dirname as dirname4, join as join18, resolve as resolve8 } from "path";
10305
+ import { existsSync as existsSync17, mkdirSync as mkdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
10306
+ import { dirname as dirname4, join as join18, resolve as resolve9 } from "path";
10118
10307
  var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set(["json", "help"]);
10119
10308
  function sampleTimes(duration, every) {
10120
10309
  const step = every > 0 ? every : 1;
@@ -10175,13 +10364,13 @@ async function cmdCompare(argv) {
10175
10364
  );
10176
10365
  requireFfmpeg();
10177
10366
  const r = createReporter(flags.json === true);
10178
- const ref = resolve8(against);
10367
+ const ref = resolve9(against);
10179
10368
  const loaded = await loadVosConfig(source);
10180
10369
  for (const w of loaded.warnings) r.log(`note: ${w}`);
10181
10370
  const config = loaded.config;
10182
10371
  const duration = configDuration(config) ?? 5;
10183
10372
  const assets = await programAssets(config, {
10184
- baseDir: /^https?:\/\//.test(source) ? null : existsSync16(source) && statSync4(source).isDirectory() ? source : dirname4(source),
10373
+ baseDir: /^https?:\/\//.test(source) ? null : existsSync17(source) && statSync4(source).isDirectory() ? source : dirname4(source),
10185
10374
  origin: platformOrigin({}),
10186
10375
  key: resolveCredential(),
10187
10376
  log: (line) => r.log(line)
@@ -10197,7 +10386,7 @@ async function cmdCompare(argv) {
10197
10386
  const threshold = Number(strFlag(flags, "threshold") ?? 0.95);
10198
10387
  const timesRaw = strFlag(flags, "times");
10199
10388
  const times = timesRaw ? parseTimes(timesRaw, duration) : sampleTimes(duration, Number(strFlag(flags, "every") ?? 1));
10200
- const outDir = resolve8(strFlag(flags, "out") ?? "compare");
10389
+ const outDir = resolve9(strFlag(flags, "out") ?? "compare");
10201
10390
  mkdirSync2(outDir, { recursive: true });
10202
10391
  const browser = await launchBrowser();
10203
10392
  const scores = [];
@@ -10300,25 +10489,25 @@ async function cmdCompare(argv) {
10300
10489
  }
10301
10490
 
10302
10491
  // src/plugin/port.ts
10303
- import { existsSync as existsSync20, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
10304
- import { dirname as dirname7, join as join22, resolve as resolve11 } from "path";
10305
- import { pathToFileURL as pathToFileURL3 } from "url";
10492
+ import { existsSync as existsSync21, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
10493
+ import { dirname as dirname7, join as join22, resolve as resolve12 } from "path";
10494
+ import { pathToFileURL as pathToFileURL4 } from "url";
10306
10495
 
10307
10496
  // src/plugin/portInventory.ts
10308
10497
  import { spawnSync as spawnSync2 } from "child_process";
10309
10498
  import {
10310
- existsSync as existsSync18,
10499
+ existsSync as existsSync19,
10311
10500
  mkdirSync as mkdirSync3,
10312
10501
  readFileSync as readFileSync7,
10313
10502
  readdirSync as readdirSync4,
10314
10503
  statSync as statSync6
10315
10504
  } from "fs";
10316
- import { dirname as dirname5, join as join20, relative as relative3, resolve as resolve9 } from "path";
10317
- import { pathToFileURL as pathToFileURL2 } from "url";
10505
+ import { dirname as dirname5, join as join20, relative as relative3, resolve as resolve10 } from "path";
10506
+ import { pathToFileURL as pathToFileURL3 } from "url";
10318
10507
  import { findFontFamily as findFontFamily3 } from "@vosjs/shared";
10319
10508
 
10320
10509
  // src/plugin/portRemotion.ts
10321
- import { existsSync as existsSync17, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
10510
+ import { existsSync as existsSync18, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
10322
10511
  import { createServer as createServer2 } from "http";
10323
10512
  import { createRequire } from "module";
10324
10513
  import { basename as basename7, extname as extname3, join as join19 } from "path";
@@ -10394,7 +10583,7 @@ function sourceTexts(dir) {
10394
10583
  }
10395
10584
  };
10396
10585
  const src = join19(dir, "src");
10397
- walk(existsSync17(src) ? src : dir, 0);
10586
+ walk(existsSync18(src) ? src : dir, 0);
10398
10587
  return out;
10399
10588
  }
10400
10589
  function entryPoint(dir) {
@@ -10406,7 +10595,7 @@ function entryPoint(dir) {
10406
10595
  "remotion/index.ts",
10407
10596
  "remotion/index.tsx"
10408
10597
  ])
10409
- if (existsSync17(join19(dir, p))) return join19(dir, p);
10598
+ if (existsSync18(join19(dir, p))) return join19(dir, p);
10410
10599
  return null;
10411
10600
  }
10412
10601
  var TYPES = {
@@ -10499,7 +10688,7 @@ async function readRemotion(dir, log) {
10499
10688
  const server = createServer2((rq, rs) => {
10500
10689
  const path = decodeURIComponent(new URL(rq.url ?? "/", "http://x").pathname);
10501
10690
  let f = join19(out, path);
10502
- if (!f.startsWith(out) || !existsSync17(f) || statSync5(f).isDirectory())
10691
+ if (!f.startsWith(out) || !existsSync18(f) || statSync5(f).isDirectory())
10503
10692
  f = join19(out, "index.html");
10504
10693
  rs.writeHead(200, {
10505
10694
  "content-type": TYPES[extname3(f)] ?? "application/octet-stream"
@@ -10602,7 +10791,7 @@ async function readRemotion(dir, log) {
10602
10791
  const publicDir = join19(dir, "public");
10603
10792
  const media = tree.audio.map((a) => {
10604
10793
  const name = basename7(a.src.split("?")[0]);
10605
- const local = existsSync17(join19(publicDir, name)) ? `public/${name}` : a.src;
10794
+ const local = existsSync18(join19(publicDir, name)) ? `public/${name}` : a.src;
10606
10795
  return {
10607
10796
  kind: "audio",
10608
10797
  src: local,
@@ -10643,9 +10832,9 @@ async function readRemotion(dir, log) {
10643
10832
 
10644
10833
  // src/plugin/portInventory.ts
10645
10834
  function detectEngine(dir) {
10646
- if (existsSync18(join20(dir, "hyperframes.json"))) return "hyperframes";
10835
+ if (existsSync19(join20(dir, "hyperframes.json"))) return "hyperframes";
10647
10836
  const pkg = join20(dir, "package.json");
10648
- if (existsSync18(pkg)) {
10837
+ if (existsSync19(pkg)) {
10649
10838
  try {
10650
10839
  const p = JSON.parse(readFileSync7(pkg, "utf8"));
10651
10840
  const deps = { ...p.dependencies ?? {}, ...p.devDependencies ?? {} };
@@ -10659,13 +10848,13 @@ function detectEngine(dir) {
10659
10848
  return "page";
10660
10849
  }
10661
10850
  function sourcePage(source) {
10662
- if (!existsSync18(source)) return null;
10851
+ if (!existsSync19(source)) return null;
10663
10852
  if (statSync6(source).isFile()) return /\.html?$/i.test(source) ? source : null;
10664
10853
  const index = join20(source, "index.html");
10665
- return existsSync18(index) ? index : null;
10854
+ return existsSync19(index) ? index : null;
10666
10855
  }
10667
10856
  function findRender(dir) {
10668
- const videos = (d) => existsSync18(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join20(d, f)) : [];
10857
+ const videos = (d) => existsSync19(d) ? readdirSync4(d).filter((f) => /\.(mp4|webm|mov)$/i.test(f)).map((f) => join20(d, f)) : [];
10669
10858
  for (const d of [join20(dir, "renders"), join20(dir, "out"), dir]) {
10670
10859
  const found = videos(d);
10671
10860
  if (found.length) return found[0];
@@ -10936,8 +11125,8 @@ var SEEK_AND_READ = `((args) => {
10936
11125
  return out
10937
11126
  })`;
10938
11127
  async function readInventory(source, opts) {
10939
- const abs = resolve9(source);
10940
- const dir = existsSync18(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
11128
+ const abs = resolve10(source);
11129
+ const dir = existsSync19(abs) && statSync6(abs).isDirectory() ? abs : dirname5(abs);
10941
11130
  const engine = detectEngine(dir);
10942
11131
  const page = sourcePage(abs);
10943
11132
  const render = opts.render === void 0 ? findRender(dir) : opts.render;
@@ -10968,7 +11157,7 @@ async function readInventory(source, opts) {
10968
11157
  });
10969
11158
  const p = await ctx.newPage();
10970
11159
  await p.addInitScript("window.__timelines = window.__timelines || {}");
10971
- await p.goto(pathToFileURL2(page).href, { waitUntil: "load" });
11160
+ await p.goto(pathToFileURL3(page).href, { waitUntil: "load" });
10972
11161
  await p.evaluate("document.fonts && document.fonts.ready");
10973
11162
  await p.waitForTimeout(300);
10974
11163
  read = await p.evaluate(READ_PAGE);
@@ -11103,8 +11292,8 @@ async function readInventory(source, opts) {
11103
11292
 
11104
11293
  // src/plugin/portScaffold.ts
11105
11294
  import { spawnSync as spawnSync3 } from "child_process";
11106
- import { copyFileSync, existsSync as existsSync19, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
11107
- import { basename as basename8, dirname as dirname6, join as join21, resolve as resolve10 } from "path";
11295
+ import { copyFileSync, existsSync as existsSync20, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
11296
+ import { basename as basename8, dirname as dirname6, join as join21, resolve as resolve11 } from "path";
11108
11297
  import { findFontFamily as findFontFamily4, fontFaceUrl, nearestFontWeight } from "@vosjs/shared";
11109
11298
 
11110
11299
  // src/plugin/painterLib.ts
@@ -11513,9 +11702,9 @@ function planScaffold(inv, opts) {
11513
11702
  const audio = [];
11514
11703
  const copies = [];
11515
11704
  inv.media.filter((m) => m.kind === "audio" && m.src).forEach((m, i) => {
11516
- const from = resolve10(opts.sourceDir, m.src);
11705
+ const from = resolve11(opts.sourceDir, m.src);
11517
11706
  const name = basename8(from);
11518
- if (!existsSync19(from)) {
11707
+ if (!existsSync20(from)) {
11519
11708
  report.push(
11520
11709
  `- the audio ${m.src} is not in the source folder; add it to doc.json by hand`
11521
11710
  );
@@ -11754,11 +11943,11 @@ async function inventory(argv) {
11754
11943
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11755
11944
  const source = positionals[0] ?? ".";
11756
11945
  const r = createReporter(flags.json === true);
11757
- const outDir = resolve11(strFlag(flags, "out") ?? "port");
11946
+ const outDir = resolve12(strFlag(flags, "out") ?? "port");
11758
11947
  mkdirSync5(outDir, { recursive: true });
11759
11948
  const renderFlag = strFlag(flags, "render");
11760
11949
  const inv = await readInventory(source, {
11761
- render: renderFlag === void 0 ? void 0 : resolve11(renderFlag),
11950
+ render: renderFlag === void 0 ? void 0 : resolve12(renderFlag),
11762
11951
  outDir,
11763
11952
  log: (l) => r.log(`note: ${l}`)
11764
11953
  });
@@ -11794,13 +11983,13 @@ function summary(inv) {
11794
11983
  async function scaffold(argv) {
11795
11984
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS8);
11796
11985
  const r = createReporter(flags.json === true);
11797
- const from = resolve11(strFlag(flags, "from") ?? join22("port", "inventory.json"));
11798
- if (!existsSync20(from))
11986
+ const from = resolve12(strFlag(flags, "from") ?? join22("port", "inventory.json"));
11987
+ if (!existsSync21(from))
11799
11988
  throw new UsageError(
11800
11989
  `no inventory at ${from}: run npx vos port inventory <source> first`
11801
11990
  );
11802
11991
  const inv = JSON.parse(readFileSync8(from, "utf8"));
11803
- const outDir = resolve11(
11992
+ const outDir = resolve12(
11804
11993
  strFlag(flags, "out") ?? join22(dirname7(from), "program")
11805
11994
  );
11806
11995
  const plan = planScaffold(inv, {
@@ -11809,7 +11998,7 @@ async function scaffold(argv) {
11809
11998
  });
11810
11999
  writeScaffold(plan, outDir);
11811
12000
  const programFile = join22(outDir, "program.mjs");
11812
- const mod = await import(`${pathToFileURL3(programFile).href}?t=${Date.now()}`);
12001
+ const mod = await import(`${pathToFileURL4(programFile).href}?t=${Date.now()}`);
11813
12002
  const built = buildProgram(mod.default, "program.mjs", plan.program);
11814
12003
  for (const p of built.problems) r.log(`error ${p.fn}: ${p.message}`);
11815
12004
  if (built.problems.length) return EXIT_ERROR;
@@ -11825,7 +12014,7 @@ async function scaffold(argv) {
11825
12014
  }
11826
12015
  function scaffoldSourceDir(inv) {
11827
12016
  if (inv.root) return inv.root;
11828
- return inv.source.endsWith(".html") ? dirname7(resolve11(inv.source)) : resolve11(inv.source);
12017
+ return inv.source.endsWith(".html") ? dirname7(resolve12(inv.source)) : resolve12(inv.source);
11829
12018
  }
11830
12019
 
11831
12020
  // src/plugin/recipe.ts
@@ -11917,7 +12106,7 @@ async function cmdRecipe(argv) {
11917
12106
 
11918
12107
  // src/plugin/brand.ts
11919
12108
  import { writeFile as writeFile8 } from "fs/promises";
11920
- import { resolve as resolve12 } from "path";
12109
+ import { resolve as resolve13 } from "path";
11921
12110
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
11922
12111
  import { lookKindForGround } from "@vosjs/studio-core";
11923
12112
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -12238,7 +12427,7 @@ async function cmdBrand(argv) {
12238
12427
  const refused = fetchRefusal(url);
12239
12428
  if (refused) throw new UsageError(refused);
12240
12429
  const r = createReporter(flags.json === true);
12241
- const out = resolve12(strFlag(flags, "out") ?? "BRAND.md");
12430
+ const out = resolve13(strFlag(flags, "out") ?? "BRAND.md");
12242
12431
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
12243
12432
  const [designText, llmsText] = await Promise.all([
12244
12433
  fetchText(`${origin}/design.md`),
@@ -12290,25 +12479,31 @@ async function cmdBrand(argv) {
12290
12479
  }
12291
12480
 
12292
12481
  // src/plugin/hostedRecord.ts
12293
- import { setupEnvNames as setupEnvNames2 } from "@vosjs/render-core/record";
12482
+ import { scriptEnvNames as scriptEnvNames2, setupEnvNames as setupEnvNames2 } from "@vosjs/render-core/record";
12294
12483
  var POLL_MS = 3e3;
12295
12484
  var DEFAULT_TIMEOUT_MS = 20 * 60 * 1e3;
12296
12485
  function hostedEnv(actions, shell) {
12297
12486
  const env = {};
12298
12487
  const missing = [];
12299
- for (const name of setupEnvNames2(actions.setup)) {
12488
+ for (const name of scriptEnvNames2(actions)) {
12300
12489
  const v = shell[name];
12301
12490
  if (v === void 0 || v === "") missing.push(name);
12302
12491
  else env[name] = v;
12303
12492
  }
12304
12493
  return { env, missing };
12305
12494
  }
12495
+ function envReaders(actions) {
12496
+ const setup = setupEnvNames2(actions.setup).length > 0;
12497
+ const teardown = setupEnvNames2(actions.teardown).length > 0;
12498
+ if (setup && teardown) return "the setup and teardown read";
12499
+ return teardown ? "the teardown reads" : "the setup reads";
12500
+ }
12306
12501
  function envNamesNotice(actions) {
12307
- const names = setupEnvNames2(actions.setup);
12502
+ const names = scriptEnvNames2(actions);
12308
12503
  if (names.length === 0) return null;
12309
12504
  return {
12310
12505
  names,
12311
- line: `the setup reads ${names.length === 1 ? "this environment variable" : "these environment variables"} from this shell: ${names.join(", ")} (names only; a value is never printed or stored)`
12506
+ line: `${envReaders(actions)} ${names.length === 1 ? "this environment variable" : "these environment variables"} from this shell: ${names.join(", ")} (names only; a value is never printed or stored)`
12312
12507
  };
12313
12508
  }
12314
12509
  function announceEnvNames(actions, r) {
@@ -12405,7 +12600,7 @@ import { urlSchemeProblem as urlSchemeProblem2 } from "@vosjs/render-core/record
12405
12600
 
12406
12601
  // src/plugin/agentBrowser.ts
12407
12602
  import { writeFile as writeFile9 } from "fs/promises";
12408
- import { resolve as resolve13 } from "path";
12603
+ import { resolve as resolve14 } from "path";
12409
12604
  function parseAgentBrowserLog(text) {
12410
12605
  const records = [];
12411
12606
  const problems = [];
@@ -12876,7 +13071,7 @@ async function cmdActions(argv) {
12876
13071
  );
12877
13072
  }
12878
13073
  const r = createReporter(flags.json === true);
12879
- const out = resolve13(strFlag(flags, "out") ?? "actions.json");
13074
+ const out = resolve14(strFlag(flags, "out") ?? "actions.json");
12880
13075
  const viewportFlag = strFlag(flags, "viewport");
12881
13076
  let viewport;
12882
13077
  if (viewportFlag) {
@@ -12885,7 +13080,7 @@ async function cmdActions(argv) {
12885
13080
  viewport = { width: Number(m[1]), height: Number(m[2]) };
12886
13081
  }
12887
13082
  const { readFile: readFile19 } = await import("fs/promises");
12888
- const text = await readFile19(resolve13(input), "utf8");
13083
+ const text = await readFile19(resolve14(input), "utf8");
12889
13084
  const { records, problems } = parseAgentBrowserLog(text);
12890
13085
  for (const p of problems) r.log(` ${p}`);
12891
13086
  const result = convertAgentBrowser(records, {
@@ -12923,8 +13118,8 @@ function summary2(result) {
12923
13118
  async function cmdActionsScript(input, flags) {
12924
13119
  const r = createReporter(flags.json === true);
12925
13120
  const { readFile: readFile19 } = await import("fs/promises");
12926
- const { shotList, formatShotList } = await import("./shotList-MSNWLVDN.js");
12927
- const raw = JSON.parse(await readFile19(resolve13(input), "utf8"));
13121
+ const { shotList, formatShotList } = await import("./shotList-PL7XP6AC.js");
13122
+ const raw = JSON.parse(await readFile19(resolve14(input), "utf8"));
12928
13123
  const errors = validateActions(raw);
12929
13124
  if (errors.length) throw new UsageError(`${input}:
12930
13125
  ${errors.join("\n ")}`);
@@ -12934,9 +13129,9 @@ async function cmdActionsScript(input, flags) {
12934
13129
  }
12935
13130
 
12936
13131
  // src/plugin/ingest.ts
12937
- import { existsSync as existsSync21 } from "fs";
13132
+ import { existsSync as existsSync22 } from "fs";
12938
13133
  import { copyFile, mkdir as mkdir8, readFile as readFile13, stat as stat2 } from "fs/promises";
12939
- import { basename as basename10, join as join23, resolve as resolve14 } from "path";
13134
+ import { basename as basename10, join as join23, resolve as resolve15 } from "path";
12940
13135
 
12941
13136
  // src/plugin/traceInflate.ts
12942
13137
  import { inflateRawSync } from "zlib";
@@ -13099,10 +13294,10 @@ async function cmdIngest(argv) {
13099
13294
  throw new UsageError(
13100
13295
  "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]"
13101
13296
  );
13102
- const videoPath = resolve14(source);
13103
- if (!existsSync21(videoPath)) throw new UsageError(`${source}: no such file`);
13297
+ const videoPath = resolve15(source);
13298
+ if (!existsSync22(videoPath)) throw new UsageError(`${source}: no such file`);
13104
13299
  const r = createReporter(flags.json === true);
13105
- const outDir = resolve14(strFlag(flags, "out") ?? "take");
13300
+ const outDir = resolve15(strFlag(flags, "out") ?? "take");
13106
13301
  const offsetMs = numFlag(flags, "offset", 0);
13107
13302
  const viewportFlag = parseViewport(strFlag(flags, "viewport"));
13108
13303
  const cursorPath = strFlag(flags, "cursor");
@@ -13124,10 +13319,10 @@ async function cmdIngest(argv) {
13124
13319
  }
13125
13320
  let trace = null;
13126
13321
  if (cursorPath) {
13127
- trace = await readTraceFile(resolve14(cursorPath), offsetMs);
13322
+ trace = await readTraceFile(resolve15(cursorPath), offsetMs);
13128
13323
  for (const n of trace.notes) r.log(`note: ${n}`);
13129
13324
  }
13130
- if (existsSync21(join23(outDir, "meta.json"))) {
13325
+ if (existsSync22(join23(outDir, "meta.json"))) {
13131
13326
  const { prevDoc, kept } = await prepareReRecord(outDir);
13132
13327
  r.log(
13133
13328
  `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
@@ -13201,9 +13396,9 @@ async function cmdIngest(argv) {
13201
13396
 
13202
13397
  // src/plugin/checkVideo.ts
13203
13398
  import { createRequire as createRequire2 } from "module";
13204
- import { existsSync as existsSync22 } from "fs";
13399
+ import { existsSync as existsSync23 } from "fs";
13205
13400
  import { mkdir as mkdir9, readFile as readFile14, stat as stat3, writeFile as writeFile10 } from "fs/promises";
13206
- import { basename as basename11, dirname as dirname8, extname as extname4, join as join24, resolve as resolve15 } from "path";
13401
+ import { basename as basename11, dirname as dirname8, extname as extname4, join as join24, resolve as resolve16 } from "path";
13207
13402
  import { ALL_FORMATS as ALL_FORMATS2, FilePathSource as FilePathSource2, Input as Input2 } from "mediabunny";
13208
13403
 
13209
13404
  // src/plugin/videoChecks.ts
@@ -13487,7 +13682,7 @@ async function probeVideoFacts(file) {
13487
13682
  async function mediabunnyBundle() {
13488
13683
  const entry = createRequire2(import.meta.url).resolve("mediabunny");
13489
13684
  const bundle = join24(dirname8(entry), "mediabunny.min.mjs");
13490
- if (existsSync22(bundle)) return readFile14(bundle, "utf8");
13685
+ if (existsSync23(bundle)) return readFile14(bundle, "utf8");
13491
13686
  return readFile14(
13492
13687
  join24(entry.split("/dist/")[0], "dist/bundles/mediabunny.min.mjs"),
13493
13688
  "utf8"
@@ -13711,7 +13906,7 @@ async function checkVideo(browser, file, outDir) {
13711
13906
  );
13712
13907
  const long = Math.max(facts.width, facts.height, 1);
13713
13908
  const side = (n) => Math.max(1, Math.round(n / long * THUMB_LONG_SIDE));
13714
- const server = await startTakeServer(dirname8(resolve15(file)), {
13909
+ const server = await startTakeServer(dirname8(resolve16(file)), {
13715
13910
  "/check.html": '<!doctype html><html><head><meta charset="utf-8"><link rel="icon" href="data:,"></head><body></body></html>'
13716
13911
  });
13717
13912
  const page = await browser.newPage();
@@ -13829,10 +14024,10 @@ async function cmdCheckVideo(argv) {
13829
14024
  throw new UsageError(
13830
14025
  "vos check-video <file.mp4|webm> [--out dir] [--strict] [--json]"
13831
14026
  );
13832
- if (!existsSync22(file)) throw new UsageError(`${file} does not exist`);
14027
+ if (!existsSync23(file)) throw new UsageError(`${file} does not exist`);
13833
14028
  const r = createReporter(flags.json === true);
13834
14029
  const strict = flags.strict === true;
13835
- const outDir = resolve15(
14030
+ const outDir = resolve16(
13836
14031
  strFlag(flags, "out") ?? join24(dirname8(file), `${basename11(file, extname4(file))}-check`)
13837
14032
  );
13838
14033
  const browser = await launchBrowser();
@@ -13848,7 +14043,7 @@ async function cmdCheckVideo(argv) {
13848
14043
  }
13849
14044
 
13850
14045
  // src/plugin/join.ts
13851
- import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
14046
+ import { existsSync as existsSync26, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
13852
14047
  import { copyFile as copyFile3, mkdir as mkdir11, readFile as readFile17, rm as rm4 } from "fs/promises";
13853
14048
  import {
13854
14049
  basename as basename13,
@@ -13856,7 +14051,7 @@ import {
13856
14051
  isAbsolute as isAbsolute4,
13857
14052
  join as join27,
13858
14053
  relative as relative5,
13859
- resolve as resolve17,
14054
+ resolve as resolve18,
13860
14055
  sep as sep4
13861
14056
  } from "path";
13862
14057
  import {
@@ -13872,7 +14067,7 @@ import {
13872
14067
  import { totalDuration as totalDuration3 } from "@vosjs/timeline";
13873
14068
 
13874
14069
  // src/plugin/score.ts
13875
- import { existsSync as existsSync23 } from "fs";
14070
+ import { existsSync as existsSync24 } from "fs";
13876
14071
  import {
13877
14072
  copyFile as copyFile2,
13878
14073
  mkdir as mkdir10,
@@ -13882,7 +14077,7 @@ import {
13882
14077
  writeFile as writeFile11
13883
14078
  } from "fs/promises";
13884
14079
  import { tmpdir as tmpdir2 } from "os";
13885
- import { basename as basename12, isAbsolute as isAbsolute3, join as join25, relative as relative4, resolve as resolve16, sep as sep3 } from "path";
14080
+ import { basename as basename12, isAbsolute as isAbsolute3, join as join25, relative as relative4, resolve as resolve17, sep as sep3 } from "path";
13886
14081
  import {
13887
14082
  NORMALIZE_MAX_LUFS as NORMALIZE_MAX_LUFS2,
13888
14083
  NORMALIZE_MIN_LUFS as NORMALIZE_MIN_LUFS2,
@@ -14297,9 +14492,9 @@ function resolveAt(doc, raw) {
14297
14492
  return { at: r.start, via: `step ${v} (${r.start.toFixed(2)}s)` };
14298
14493
  }
14299
14494
  async function resolveTrack(dir, ref, flags, dryRun) {
14300
- const local = resolve16(ref);
14301
- if (existsSync23(local)) {
14302
- const rel = relative4(resolve16(dir), local);
14495
+ const local = resolve17(ref);
14496
+ if (existsSync24(local)) {
14497
+ const rel = relative4(resolve17(dir), local);
14303
14498
  const inside = !!rel && !rel.startsWith(`..${sep3}`) && rel !== ".." && !isAbsolute3(rel);
14304
14499
  const key = inside ? rel.split(sep3).join("/") : `music/${basename12(local)}`;
14305
14500
  if (!inside && !dryRun) {
@@ -14438,7 +14633,7 @@ async function cmdScore(argv) {
14438
14633
  const placeLine = at2 && plan.swellAt !== null ? `the track starts at ${plan.in.toFixed(2)}s, so the swell lands at ${plan.swellAt.toFixed(2)}s of film (asked: ${at2.via})` : `the track starts at ${plan.in.toFixed(2)}s${at2 ? ` (asked: ${at2.via})` : ", its first rise above its median level"}`;
14439
14634
  r.done(
14440
14635
  {
14441
- take: resolve16(dir),
14636
+ take: resolve17(dir),
14442
14637
  dryRun,
14443
14638
  track: { key: track.key, name: track.name, duration: clip.duration },
14444
14639
  film: r3(film),
@@ -14534,9 +14729,6 @@ function scriptDriftLine(d, file = "actions.json") {
14534
14729
  // src/plugin/stepCaptions.ts
14535
14730
  var STEP_CAPTIONS_FROM = "steps";
14536
14731
  var STEP_CAPTION_AT = "start";
14537
- var CAPTION_MAX = 3;
14538
- var CAPTION_MIN = 1.2;
14539
- var CAPTION_GAP = 0.1;
14540
14732
  function captionOf(step, actions, script, recorded = []) {
14541
14733
  const own = step.caption;
14542
14734
  if (typeof own === "string" && own.trim()) return own.trim();
@@ -14570,6 +14762,7 @@ function landStepCaptions(doc, actions, opts = {}) {
14570
14762
  return footageEnd;
14571
14763
  };
14572
14764
  const ends = /* @__PURE__ */ new Map();
14765
+ const labels = /* @__PURE__ */ new Map();
14573
14766
  const spoken = (media, step, steps) => kept.some((o) => {
14574
14767
  if (o.kind !== "text" || o.preset !== "caption") return false;
14575
14768
  if (o.anchor && mediaKey(o.anchor.media) === media)
@@ -14622,6 +14815,7 @@ function landStepCaptions(doc, actions, opts = {}) {
14622
14815
  from: STEP_CAPTIONS_FROM
14623
14816
  };
14624
14817
  ends.set(clip, end);
14818
+ labels.set(clip, label);
14625
14819
  fresh.push(clip);
14626
14820
  report.landed.push({
14627
14821
  id,
@@ -14632,18 +14826,19 @@ function landStepCaptions(doc, actions, opts = {}) {
14632
14826
  }
14633
14827
  if (steps.length && !captioned) report.uncaptioned.push(media);
14634
14828
  }
14635
- fresh.sort((a, b) => a.start - b.start);
14636
- fresh.forEach((c, i) => {
14637
- const next = fresh[i + 1];
14638
- const clipRoom = (ends.get(c) ?? footageEnd) - c.start;
14639
- const nextRoom = next ? next.start - CAPTION_GAP - c.start : Infinity;
14640
- c.duration = +Math.min(
14641
- CAPTION_MAX,
14642
- clipRoom,
14643
- Math.max(CAPTION_MIN, nextRoom)
14644
- ).toFixed(3);
14829
+ const fit = fitCaptions(fresh, (c) => ({
14830
+ start: c.start,
14831
+ end: ends.get(c) ?? footageEnd
14832
+ }));
14833
+ for (const { item, why } of fit.dropped) {
14834
+ report.skipped.push(`${labels.get(item) ?? item.id}: ${why}`);
14835
+ report.landed = report.landed.filter((l) => l.id !== item.id);
14836
+ }
14837
+ const landedClips = fit.kept.map(({ item, duration }) => {
14838
+ item.duration = duration;
14839
+ return item;
14645
14840
  });
14646
- const overlays = [...kept, ...fresh];
14841
+ const overlays = [...kept, ...landedClips];
14647
14842
  if (overlays.length) doc.overlays = overlays;
14648
14843
  else delete doc.overlays;
14649
14844
  return report;
@@ -14951,7 +15146,7 @@ function perBeatLine(r, name = (m) => m ? `media ${m}` : "the primary") {
14951
15146
 
14952
15147
  // src/plugin/rejoin.ts
14953
15148
  import { createHash as createHash4 } from "crypto";
14954
- import { createReadStream, existsSync as existsSync24 } from "fs";
15149
+ import { createReadStream, existsSync as existsSync25 } from "fs";
14955
15150
  import { readFile as readFile16 } from "fs/promises";
14956
15151
  import { join as join26 } from "path";
14957
15152
  import {
@@ -14979,11 +15174,11 @@ async function partFootage(part) {
14979
15174
  if (!isTakeRelativeKey(key))
14980
15175
  return `key:${createHash4("sha256").update(key).digest("hex")}`;
14981
15176
  const file = join26(part.dir, takeRelativeFile(key));
14982
- return existsSync24(file) ? fileHash(file) : `key:${createHash4("sha256").update(key).digest("hex")}`;
15177
+ return existsSync25(file) ? fileHash(file) : `key:${createHash4("sha256").update(key).digest("hex")}`;
14983
15178
  }
14984
15179
  async function readJoinRecord(dir) {
14985
15180
  const file = join26(dir, JOIN_RECORD_NAME);
14986
- if (!existsSync24(file)) return null;
15181
+ if (!existsSync25(file)) return null;
14987
15182
  try {
14988
15183
  const v = JSON.parse(await readFile16(file, "utf8"));
14989
15184
  return v && v.version === 1 && Array.isArray(v.inputs) ? v : null;
@@ -14996,7 +15191,7 @@ async function recordFromFootage(dir, doc) {
14996
15191
  if (!isTakeRelativeKey(key))
14997
15192
  return `key:${createHash4("sha256").update(key).digest("hex")}`;
14998
15193
  const file = join26(dir, takeRelativeFile(key));
14999
- return existsSync24(file) ? fileHash(file) : `missing:${key}`;
15194
+ return existsSync25(file) ? fileHash(file) : `missing:${key}`;
15000
15195
  };
15001
15196
  const inputs = [
15002
15197
  {
@@ -16002,10 +16197,10 @@ var within = (inner, outer) => {
16002
16197
  return rel === "" || !rel.startsWith(`..${sep4}`) && rel !== ".." && !isAbsolute4(rel);
16003
16198
  };
16004
16199
  async function readPart(arg) {
16005
- const path = resolve17(arg);
16006
- if (!existsSync25(path)) throw new UsageError(`${arg}: no such take or file`);
16200
+ const path = resolve18(arg);
16201
+ if (!existsSync26(path)) throw new UsageError(`${arg}: no such take or file`);
16007
16202
  if (statSync7(path).isDirectory()) {
16008
- if (!existsSync25(join27(path, "meta.json")))
16203
+ if (!existsSync26(join27(path, "meta.json")))
16009
16204
  throw new UsageError(
16010
16205
  `${arg}: not a take directory (no meta.json); a video file is joined by its path`
16011
16206
  );
@@ -16052,11 +16247,11 @@ async function cmdJoin(argv) {
16052
16247
  throw new UsageError(
16053
16248
  "--force replaces the joined take and --keep-edits keeps its edits; pass one"
16054
16249
  );
16055
- const out = resolve17(outArg);
16056
- const inputs = positionals.map((p) => resolve17(p));
16250
+ const out = resolve18(outArg);
16251
+ const inputs = positionals.map((p) => resolve18(p));
16057
16252
  const muted = /* @__PURE__ */ new Set();
16058
16253
  for (const m of multi.mute ?? []) {
16059
- const i = inputs.indexOf(resolve17(m));
16254
+ const i = inputs.indexOf(resolve18(m));
16060
16255
  if (i < 0)
16061
16256
  throw new UsageError(
16062
16257
  `--mute ${m}: not one of the inputs; name a file as it is given to vos join`
@@ -16070,19 +16265,19 @@ async function cmdJoin(argv) {
16070
16265
  );
16071
16266
  }
16072
16267
  let rejoin = false;
16073
- if (existsSync25(out)) {
16268
+ if (existsSync26(out)) {
16074
16269
  if (!force && !keepEdits)
16075
16270
  throw new UsageError(
16076
16271
  `--out ${outArg} exists; pass --keep-edits to join again and keep every edit made since (its trims, spans, layers, frame, sound), --force to replace it, or name a new directory`
16077
16272
  );
16078
16273
  if (keepEdits) {
16079
- if (!existsSync25(join27(out, "doc.json")))
16274
+ if (!existsSync26(join27(out, "doc.json")))
16080
16275
  throw new UsageError(
16081
16276
  `--keep-edits: ${outArg} holds no doc.json, so there are no edits to keep; name a new directory, or --force to replace it`
16082
16277
  );
16083
16278
  rejoin = true;
16084
16279
  } else {
16085
- const isTake = existsSync25(join27(out, "doc.json")) || existsSync25(join27(out, "meta.json"));
16280
+ const isTake = existsSync26(join27(out, "doc.json")) || existsSync26(join27(out, "meta.json"));
16086
16281
  if (!isTake && readdirSync5(out).length > 0)
16087
16282
  throw new UsageError(
16088
16283
  `--force replaces a take directory, and ${outArg} holds other files; name a new directory`
@@ -16128,7 +16323,7 @@ async function cmdJoin(argv) {
16128
16323
  );
16129
16324
  }
16130
16325
  rejoined = rejoinDocs(migrateMotion3(prev), record, read, {
16131
- where: (p) => relative5(process.cwd(), resolve17(out, p)) || "."
16326
+ where: (p) => relative5(process.cwd(), resolve18(out, p)) || "."
16132
16327
  });
16133
16328
  plan = rejoined.plan;
16134
16329
  } else plan = joinDocs(parts);
@@ -16169,11 +16364,11 @@ async function cmdJoin(argv) {
16169
16364
  for (const k of rejoined.dropped) {
16170
16365
  if (!/^[A-Za-z0-9_-]+$/.test(k)) continue;
16171
16366
  const dir = join27(out, k);
16172
- if (existsSync25(dir) && statSync7(dir).isDirectory())
16367
+ if (existsSync26(dir) && statSync7(dir).isDirectory())
16173
16368
  await rm4(dir, { recursive: true, force: true });
16174
16369
  }
16175
16370
  } else {
16176
- if (existsSync25(out)) await rm4(out, { recursive: true, force: true });
16371
+ if (existsSync26(out)) await rm4(out, { recursive: true, force: true });
16177
16372
  await mkdir11(out, { recursive: true });
16178
16373
  }
16179
16374
  for (const c of plan.copies) {
@@ -16257,7 +16452,7 @@ async function cmdJoin(argv) {
16257
16452
  }
16258
16453
 
16259
16454
  // src/plugin/timeline.ts
16260
- import { join as join28, resolve as resolve18 } from "path";
16455
+ import { join as join28, resolve as resolve19 } from "path";
16261
16456
  import {
16262
16457
  docFreezes as docFreezes2,
16263
16458
  docMediaList as docMediaList4,
@@ -16548,7 +16743,7 @@ async function cmdTakeInfo(argv) {
16548
16743
  if (drift) r.warn(scriptDriftLine(drift));
16549
16744
  const timeline = takeTimeline(take.doc, /* @__PURE__ */ new Map([["", take.actions]]));
16550
16745
  r.done(
16551
- { take: resolve18(dir), doc: join28(resolve18(dir), "doc.json"), ...timeline },
16746
+ { take: resolve19(dir), doc: join28(resolve19(dir), "doc.json"), ...timeline },
16552
16747
  formatTimeline(timeline, dir)
16553
16748
  );
16554
16749
  return EXIT_OK;
@@ -16652,8 +16847,8 @@ async function cmdAudio(argv) {
16652
16847
 
16653
16848
  // src/plugin/look.ts
16654
16849
  import { mkdir as mkdir12 } from "fs/promises";
16655
- import { existsSync as existsSync26 } from "fs";
16656
- import { join as join30, resolve as resolve19 } from "path";
16850
+ import { existsSync as existsSync27 } from "fs";
16851
+ import { join as join30, resolve as resolve20 } from "path";
16657
16852
  import { headersFor, urlSchemeProblem } from "@vosjs/render-core/record";
16658
16853
  function quote(value) {
16659
16854
  return `"${value.replace(/\s+/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
@@ -17195,15 +17390,15 @@ async function cmdLook(argv) {
17195
17390
  if (scheme) throw new UsageError(scheme);
17196
17391
  const r = createReporter(flags.json === true);
17197
17392
  const viewport = parseViewport2(strFlag(flags, "viewport"));
17198
- const outDir = resolve19(strFlag(flags, "out") ?? "look");
17393
+ const outDir = resolve20(strFlag(flags, "out") ?? "look");
17199
17394
  const headed = flags.headed === true;
17200
17395
  const waitRaw = strFlag(flags, "wait");
17201
17396
  const waitMs = waitRaw === void 0 ? 0 : Number(waitRaw);
17202
17397
  if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 6e4)
17203
17398
  throw new UsageError("--wait expects milliseconds, 0 to 60000");
17204
17399
  const storageRaw = strFlag(flags, "storage-state");
17205
- const storageState = storageRaw ? resolve19(storageRaw) : void 0;
17206
- if (storageState && !existsSync26(storageState))
17400
+ const storageState = storageRaw ? resolve20(storageRaw) : void 0;
17401
+ if (storageState && !existsSync27(storageState))
17207
17402
  throw new UsageError(`--storage-state: no such file: ${storageState}`);
17208
17403
  const sessionName = strFlag(flags, "session");
17209
17404
  if (sessionName) {
@@ -17213,7 +17408,7 @@ async function cmdLook(argv) {
17213
17408
  throw new UsageError(
17214
17409
  "--session and --storage-state are two doors to one page: a persistent profile cannot take a storage state. Pass one"
17215
17410
  );
17216
- if (!existsSync26(sessionDir(sessionName)))
17411
+ if (!existsSync27(sessionDir(sessionName)))
17217
17412
  throw new UsageError(
17218
17413
  `no session "${sessionName}". Make one: vos session open <url> --name ${sessionName} (a Chrome window opens; sign in and close it)`
17219
17414
  );
@@ -17276,7 +17471,7 @@ async function cmdLook(argv) {
17276
17471
  import { spawnSync as spawnSync4 } from "child_process";
17277
17472
  import {
17278
17473
  cpSync,
17279
- existsSync as existsSync28,
17474
+ existsSync as existsSync29,
17280
17475
  mkdirSync as mkdirSync6,
17281
17476
  readFileSync as readFileSync11,
17282
17477
  rmSync as rmSync3,
@@ -17287,7 +17482,7 @@ import { join as join32 } from "path";
17287
17482
  import { chromium as chromium2 } from "playwright";
17288
17483
 
17289
17484
  // src/plugin/machine.ts
17290
- import { existsSync as existsSync27, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
17485
+ import { existsSync as existsSync28, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
17291
17486
  import { dirname as dirname10, join as join31 } from "path";
17292
17487
  var AGENT_NAMES = [
17293
17488
  "claude",
@@ -17308,7 +17503,7 @@ var HOME_DIRS = {
17308
17503
  copilot: [".copilot"]
17309
17504
  };
17310
17505
  function detectAgents(root, opts = {}) {
17311
- const exists = opts.exists ?? existsSync27;
17506
+ const exists = opts.exists ?? existsSync28;
17312
17507
  const out = [];
17313
17508
  for (const agent of AGENT_NAMES) {
17314
17509
  for (const rel of PROJECT_DIRS[agent]) {
@@ -17355,7 +17550,7 @@ function skillsDirFor(home) {
17355
17550
  function fallbackSkillsHome(root) {
17356
17551
  return { agent: "codex", dir: join31(root, ".agents"), scope: "project" };
17357
17552
  }
17358
- function installedSkills(dir, exists = existsSync27, list = (p) => {
17553
+ function installedSkills(dir, exists = existsSync28, list = (p) => {
17359
17554
  try {
17360
17555
  return readdirSync6(p);
17361
17556
  } catch {
@@ -17396,7 +17591,7 @@ ${RULES_BLOCK}
17396
17591
  }
17397
17592
  return { text, changed: text !== current };
17398
17593
  }
17399
- function rulesFileFor(root, exists = existsSync27) {
17594
+ function rulesFileFor(root, exists = existsSync28) {
17400
17595
  const agents = join31(root, "AGENTS.md");
17401
17596
  if (exists(agents)) return agents;
17402
17597
  const claude = join31(root, "CLAUDE.md");
@@ -17511,12 +17706,12 @@ function bundledSkillsDir(from) {
17511
17706
  let dir = dirname10(from);
17512
17707
  for (let i = 0; i < 6; i++) {
17513
17708
  const pkg = join31(dir, "package.json");
17514
- if (existsSync27(pkg)) {
17709
+ if (existsSync28(pkg)) {
17515
17710
  try {
17516
17711
  const name = JSON.parse(readFileSync10(pkg, "utf8")).name;
17517
17712
  if (name === "@vosjs/cli") {
17518
17713
  const skills = join31(dir, "skills");
17519
- return existsSync27(skills) && statSync8(skills).isDirectory() ? skills : null;
17714
+ return existsSync28(skills) && statSync8(skills).isDirectory() ? skills : null;
17520
17715
  }
17521
17716
  } catch {
17522
17717
  }
@@ -17552,7 +17747,7 @@ function npx() {
17552
17747
  function bundledChromium() {
17553
17748
  try {
17554
17749
  const p = chromium2.executablePath();
17555
- return p && existsSync28(p) ? p : null;
17750
+ return p && existsSync29(p) ? p : null;
17556
17751
  } catch {
17557
17752
  return null;
17558
17753
  }
@@ -17561,7 +17756,7 @@ function probe2() {
17561
17756
  return probeBrowser({
17562
17757
  platform: process.platform,
17563
17758
  env: process.env,
17564
- exists: existsSync28,
17759
+ exists: existsSync29,
17565
17760
  bundled: bundledChromium
17566
17761
  });
17567
17762
  }
@@ -17714,7 +17909,7 @@ async function cmdSetup(argv) {
17714
17909
  let rules = null;
17715
17910
  if (!noRules) {
17716
17911
  const file = rulesFileFor(root);
17717
- const existing = existsSync28(file) ? readFileSync11(file, "utf8") : null;
17912
+ const existing = existsSync29(file) ? readFileSync11(file, "utf8") : null;
17718
17913
  const { text, changed } = applyRulesBlock(existing);
17719
17914
  if (changed) writeFileSync6(file, text);
17720
17915
  rules = { file, changed };
@@ -17828,7 +18023,7 @@ async function cmdWhoami(argv) {
17828
18023
  function cmdLogout(argv) {
17829
18024
  const { flags } = parseArgs(argv, BOOLEAN_FLAGS14);
17830
18025
  const r = reporterFor(flags);
17831
- const had = existsSync28(CREDENTIALS_PATH);
18026
+ const had = existsSync29(CREDENTIALS_PATH);
17832
18027
  rmSync3(CREDENTIALS_PATH, { force: true });
17833
18028
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
17834
18029
  r.done(
@@ -17882,8 +18077,9 @@ var MULTI_FLAGS2 = /* @__PURE__ */ new Set([
17882
18077
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
17883
18078
 
17884
18079
  Take pipeline
17885
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--ready dom|networkidle] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
17886
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--ready dom|networkidle] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
18080
+ vos create --actions actions.json [--url <url>] [--out take] [--reuse] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--ready dom|networkidle] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
18081
+ vos record --actions actions.json [--url <url>] [--out take] [--reuse] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--ready dom|networkidle] [--storage-state <file>|--session <name>] [--header name=value]... [--confirm <id>]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--verbose] [--json]
18082
+ --reuse, recording again into a take that has a cut: the cut (kept as doc.prev.json) goes onto the new footage as vos plan --reuse does; without it the take is planned fresh and its last line says where the cut is
17887
18083
  vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
17888
18084
  the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
17889
18085
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions|--captions none] [--clicks none] [--still <t>] [--release v2.1] [--zooms per-beat] [--json]
@@ -17923,6 +18119,8 @@ Take pipeline
17923
18119
  see a page before scripting it, signed in the way vos record would be: <out>/look.png and every control in view with ONE proposed selector (test id, aria-label, role + name, then words in its dialog or landmark), UNIQUE or AMBIGUOUS as the recorder's own engine counts it, plus what the frame exposes. --click/--type run first, in order; a click on a spend or delete word is refused. Exit 4 at a sign-in wall
17924
18120
  vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, set the account up, close the window (or Enter)
17925
18121
  vos session check <name> [--url <url>] [--strict] headless, against a stranger: does the page open as you? exit 0/4
18122
+ vos session exec <name> <script.mjs> [--url <url>] [--headed] [--viewport WxH] [--json]
18123
+ run your own Playwright code inside the session: the module's default export gets { context, page, url, name } and what it returns is printed (result under --json). The ONLY safe way to script a session: a profile opened by any other browser (Playwright's bundled one, a mock keychain) cannot read its cookies and is signed out
17926
18124
  vos session list | rm <name> what exists and how old; delete one
17927
18125
  vos actions script <actions.json> [--json]
17928
18126
  the flow as numbered beats in words, for a person recording it by hand
@@ -18336,6 +18534,13 @@ function exposureNote(rec) {
18336
18534
  ${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
18337
18535
  ${EXPOSURE_ADVICE}`;
18338
18536
  }
18537
+ function teardownNote(rec) {
18538
+ return rec.teardown ? `
18539
+ ${teardownLine(rec.teardown)}` : "";
18540
+ }
18541
+ function teardownField(rec) {
18542
+ return rec.teardown ? { teardown: rec.teardown } : {};
18543
+ }
18339
18544
  function takeConfirm(multi, actions, opts) {
18340
18545
  const given = (multi.confirm ?? []).filter(Boolean);
18341
18546
  if (opts.dryRun) return given;
@@ -18399,8 +18604,8 @@ function takeBrowserArgs(multi) {
18399
18604
  function takeStorageState(flags) {
18400
18605
  const raw = strFlag(flags, "storage-state");
18401
18606
  if (!raw) return void 0;
18402
- const path = resolve20(raw);
18403
- if (!existsSync29(path))
18607
+ const path = resolve21(raw);
18608
+ if (!existsSync30(path))
18404
18609
  throw new UsageError(`--storage-state: no such file: ${path}`);
18405
18610
  return path;
18406
18611
  }
@@ -18413,7 +18618,7 @@ function takeSessionName(flags, storageState) {
18413
18618
  throw new UsageError(
18414
18619
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
18415
18620
  );
18416
- if (!existsSync29(sessionDir(name)))
18621
+ if (!existsSync30(sessionDir(name)))
18417
18622
  throw new UsageError(
18418
18623
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
18419
18624
  );
@@ -18429,6 +18634,63 @@ async function inSession(name, actions, fn) {
18429
18634
  }
18430
18635
  });
18431
18636
  }
18637
+ function reRecord(outDir, flags, r) {
18638
+ const wantsReuse = flags.reuse === true;
18639
+ if (wantsReuse && !existsSync30(join33(outDir, "doc.json")))
18640
+ throw new UsageError(
18641
+ `--reuse: ${outDir} has no cut to keep (no doc.json); record without --reuse`
18642
+ );
18643
+ let previousCut = null;
18644
+ return {
18645
+ prepare: async () => {
18646
+ if (existsSync30(join33(outDir, "meta.json"))) {
18647
+ const { prevDoc, kept } = await prepareReRecord(outDir);
18648
+ if (prevDoc) previousCut = join33(outDir, PREV_DOC_NAME);
18649
+ r.log(
18650
+ `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? wantsReuse ? "; the previous cut goes onto the new footage when it is recorded (--reuse)" : `; the previous cut is ${PREV_DOC_NAME}` : "")
18651
+ );
18652
+ }
18653
+ await mkdir13(outDir, { recursive: true });
18654
+ await ensureTakeDir(outDir);
18655
+ },
18656
+ plan: async (backdrop) => {
18657
+ const from = wantsReuse ? previousCut : null;
18658
+ const plan = await planTake(outDir, {
18659
+ backdrop,
18660
+ ...from ? {
18661
+ reuse: {
18662
+ from,
18663
+ doc: JSON.parse(await readFile18(from, "utf8"))
18664
+ }
18665
+ } : {}
18666
+ });
18667
+ if (!from) await notePlannedDoc(outDir);
18668
+ return plan;
18669
+ },
18670
+ previousCut: () => previousCut
18671
+ };
18672
+ }
18673
+ function previousCutReport(outDir, previousCut, plan) {
18674
+ if (plan?.reuse) {
18675
+ const s = plan.reuse;
18676
+ return {
18677
+ line: `
18678
+ reused ${s.from}: ${s.anchored} anchored + ${s.mapped} mapped span(s)` + (s.flagged.length ? `
18679
+ flagged:
18680
+ ${s.flagged.join("\n ")}` : ""),
18681
+ next: null,
18682
+ fields: { previousCut: s.from, reuse: s }
18683
+ };
18684
+ }
18685
+ if (!previousCut) return { line: "", next: null, fields: {} };
18686
+ const next = `vos plan ${outDir} --reuse`;
18687
+ return {
18688
+ line: `
18689
+ YOUR PREVIOUS CUT is kept in ${previousCut}, not on this footage. Apply it: ${next} (or record with --reuse next time)`,
18690
+ next,
18691
+ fields: { previousCut, next_step: next }
18692
+ };
18693
+ }
18432
18694
  async function takeBackdrop(flags, r) {
18433
18695
  const { backdrop, note } = await openingBackdrop(
18434
18696
  strFlag(flags, "background"),
@@ -18485,7 +18747,7 @@ async function cmdRecord(argv) {
18485
18747
  const scheme = urlSchemeProblem2(url);
18486
18748
  if (scheme) throw new UsageError(scheme);
18487
18749
  announceEnvNames(actions, r);
18488
- const outDir = resolve20(strFlag(flags, "out") ?? "take");
18750
+ const outDir = resolve21(strFlag(flags, "out") ?? "take");
18489
18751
  const storageState = takeStorageState(flags);
18490
18752
  const sessionName = takeSessionName(flags, storageState);
18491
18753
  const browserArgs = takeBrowserArgs(multi);
@@ -18505,7 +18767,7 @@ async function cmdRecord(argv) {
18505
18767
  );
18506
18768
  const hostedActions = await scriptForHost(
18507
18769
  actions,
18508
- dirname11(resolve20(actionsPath))
18770
+ dirname11(resolve21(actionsPath))
18509
18771
  ).catch((e) => {
18510
18772
  throw new UsageError(e instanceof Error ? e.message : String(e));
18511
18773
  });
@@ -18530,7 +18792,7 @@ async function cmdRecord(argv) {
18530
18792
  const { env, missing } = hostedEnv(actions, process.env);
18531
18793
  if (missing.length)
18532
18794
  throw new UsageError(
18533
- `the setup reads ${missing.map((n) => `{ "env": "${n}" }`).join(", ")} and this shell has no ${missing.length === 1 ? "value for it" : "values for them"}; export ${missing.join(", ")} and run again (the value rides the job, and is never stored)`
18795
+ `${envReaders(actions)} ${missing.map((n) => `{ "env": "${n}" }`).join(", ")} and this shell has no ${missing.length === 1 ? "value for it" : "values for them"}; export ${missing.join(", ")} and run again (the value rides the job, and is never stored)`
18534
18796
  );
18535
18797
  r.log("recording on vos.so\u2026");
18536
18798
  r.event({ event: "phase", phase: "record" });
@@ -18601,9 +18863,11 @@ async function cmdRecord(argv) {
18601
18863
  }
18602
18864
  const backdrop = await takeBackdrop(flags, r);
18603
18865
  const maxDurationSeconds = await maxDuration(flags, r);
18866
+ const cut = reRecord(outDir, flags, r);
18604
18867
  if (flags["dry-run"] === true) {
18605
18868
  void flags.strict;
18606
18869
  void flags["keep-frames"];
18870
+ void flags.reuse;
18607
18871
  const browser2 = await launchBrowser(browserArgs);
18608
18872
  try {
18609
18873
  r.log("rehearsing (nothing is captured or written)\u2026");
@@ -18613,7 +18877,7 @@ async function cmdRecord(argv) {
18613
18877
  sessionName,
18614
18878
  actions,
18615
18879
  (context2) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
18616
- actionsDir: dirname11(resolve20(actionsPath)),
18880
+ actionsDir: dirname11(resolve21(actionsPath)),
18617
18881
  storageState,
18618
18882
  headers,
18619
18883
  context: context2,
@@ -18677,7 +18941,9 @@ async function cmdRecord(argv) {
18677
18941
  ...Object.keys(headers).map((k) => `--header ${k}=\u2026`),
18678
18942
  ...sessionName ? [`--session ${sessionName}`] : [],
18679
18943
  ...flags["allow-wall"] === true ? ["--allow-wall"] : [],
18680
- ...confirm.map((c) => `--confirm ${c}`)
18944
+ ...confirm.map((c) => `--confirm ${c}`),
18945
+ // A take that already has a cut keeps it on the new footage.
18946
+ ...existsSync30(join33(outDir, "doc.json")) ? ["--reuse"] : []
18681
18947
  ].join(" ");
18682
18948
  const nextOut = strFlag(flags, "out") ?? "take";
18683
18949
  const guardNote = !unconfirmed.ok ? unconfirmed.unconfirmed.length ? ` (it refuses until you add ${unconfirmed.unconfirmed.map((id) => `--confirm ${id}`).join(" ")}: ${unconfirmed.unconfirmed.length === 1 ? "that step spends" : "those steps spend"} something real)` : ` (${unconfirmed.unknown.map((c) => `--confirm ${c}`).join(", ")} names no guarded step)` : "";
@@ -18698,11 +18964,12 @@ async function cmdRecord(argv) {
18698
18964
  exposures: rec.exposures,
18699
18965
  notices: rec.notices,
18700
18966
  masks: rec.masks,
18701
- ...rec.peers.length ? { peers: rec.peers } : {}
18967
+ ...rec.peers.length ? { peers: rec.peers } : {},
18968
+ ...teardownField(rec)
18702
18969
  },
18703
18970
  `${failed ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${resolved}/${actions.steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : rec.ready.capped ? " (the first page never stopped changing)" : ""}
18704
18971
  ${lines.join("\n")}
18705
- ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${rec.stopped ? `STOPPED: ${rec.stopped.reason}. ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict${guardNote}`}${exposureNote(rec)}${noticeNote(rec)}`
18972
+ ${maskMisses(rec).length ? `MASK hid nothing: ${maskMisses(rec).join(", ")} (the selector reached no element). ` : ""}${rec.stopped ? `STOPPED: ${rec.stopped.reason}. ` : ""}${failed ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${nextOut}${carried ? ` ${carried}` : ""} --strict${guardNote}`}${exposureNote(rec)}${noticeNote(rec)}${teardownNote(rec)}`
18706
18973
  );
18707
18974
  return rec.stopped ? EXIT_ERROR : failed ? EXIT_USAGE : EXIT_OK;
18708
18975
  } finally {
@@ -18710,16 +18977,6 @@ ${lines.join("\n")}
18710
18977
  }
18711
18978
  }
18712
18979
  const paths = takePaths(outDir);
18713
- const prepare = async () => {
18714
- if (existsSync29(join33(outDir, "meta.json"))) {
18715
- const { prevDoc, kept } = await prepareReRecord(outDir);
18716
- r.log(
18717
- `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" : "")
18718
- );
18719
- }
18720
- await mkdir13(outDir, { recursive: true });
18721
- await ensureTakeDir(outDir);
18722
- };
18723
18980
  const browser = await launchBrowser(browserArgs);
18724
18981
  try {
18725
18982
  r.log("recording\u2026");
@@ -18728,7 +18985,7 @@ ${lines.join("\n")}
18728
18985
  sessionName,
18729
18986
  actions,
18730
18987
  (context2) => recordTake(browser, url, actions, paths, r.log, {
18731
- actionsDir: dirname11(resolve20(actionsPath)),
18988
+ actionsDir: dirname11(resolve21(actionsPath)),
18732
18989
  maxDurationSeconds,
18733
18990
  storageState,
18734
18991
  headers,
@@ -18739,7 +18996,7 @@ ${lines.join("\n")}
18739
18996
  onEvent: (e) => r.event(e),
18740
18997
  onArrival: wallGate(flags, r, {
18741
18998
  strict: flags.strict === true,
18742
- prepare
18999
+ prepare: cut.prepare
18743
19000
  })
18744
19001
  })
18745
19002
  );
@@ -18752,7 +19009,8 @@ ${lines.join("\n")}
18752
19009
  );
18753
19010
  await releaseFrames(paths, flags, r.log);
18754
19011
  r.event({ event: "phase", phase: "plan" });
18755
- const plan = await planTake(outDir, { backdrop });
19012
+ const plan = await cut.plan(backdrop);
19013
+ const kept = previousCutReport(outDir, cut.previousCut(), plan);
18756
19014
  const clicks = rec.events.filter((e) => e.type === "down").length;
18757
19015
  const strict = flags.strict === true;
18758
19016
  const strictFail = strict && strictFails(rec);
@@ -18771,10 +19029,12 @@ ${lines.join("\n")}
18771
19029
  confirmed: rec.confirmed,
18772
19030
  skips: rec.skips,
18773
19031
  outputs: rec.outputs,
18774
- pageErrors: rec.pageErrors
19032
+ pageErrors: rec.pageErrors,
19033
+ ...kept.fields,
19034
+ ...teardownField(rec)
18775
19035
  },
18776
19036
  `FAILED: ${rec.stopped.reason}.
18777
- The footage up to it is in ${outDir}.${skippedNote}${stepNote}${navTimeoutNote(rec)}`
19037
+ The footage up to it is in ${outDir}.${skippedNote}${stepNote}${navTimeoutNote(rec)}${kept.line}${teardownNote(rec)}`
18778
19038
  );
18779
19039
  return EXIT_ERROR;
18780
19040
  }
@@ -18807,11 +19067,13 @@ ${lines.join("\n")}
18807
19067
  overruns: rec.overruns,
18808
19068
  pageErrors: rec.pageErrors,
18809
19069
  ...rec.peers.length ? { peers: rec.peers } : {},
18810
- ...strictFail ? { strictFailed: true } : {}
19070
+ ...teardownField(rec),
19071
+ ...strictFail ? { strictFailed: true } : {},
19072
+ ...kept.fields
18811
19073
  },
18812
- strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${exposureNote(rec)}${noticeNote(rec)}` : `Take ready: ${outDir}
18813
- ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}${stepNote}${navTimeoutNote(rec)}${pageSoundNote(rec)}
18814
- Next: edit ${join33(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}${noticeNote(rec)}`
19074
+ strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${stepNote}${navTimeoutNote(rec)}${kept.line}${exposureNote(rec)}${noticeNote(rec)}` : `Take ready: ${outDir}
19075
+ ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}${stepNote}${navTimeoutNote(rec)}${pageSoundNote(rec)}${kept.line}
19076
+ Next: ${kept.next ? `${kept.next}, then` : `edit ${join33(outDir, "doc.json")} (optional), then:`} vos render ${outDir}${exposureNote(rec)}${noticeNote(rec)}${teardownNote(rec)}`
18815
19077
  );
18816
19078
  return strictFail ? EXIT_USAGE : EXIT_OK;
18817
19079
  } finally {
@@ -18838,7 +19100,7 @@ async function cmdCreate2(argv) {
18838
19100
  const scheme = urlSchemeProblem2(url);
18839
19101
  if (scheme) throw new UsageError(scheme);
18840
19102
  announceEnvNames(actions, r);
18841
- const outDir = resolve20(strFlag(flags, "out") ?? "take");
19103
+ const outDir = resolve21(strFlag(flags, "out") ?? "take");
18842
19104
  const format = videoFormat(strFlag(flags, "format"), positionals[0]);
18843
19105
  const out = positionals[0] ?? join33(outDir, `out.${format}`);
18844
19106
  const parallel = numFlag(flags, "parallel", 1);
@@ -18853,16 +19115,7 @@ async function cmdCreate2(argv) {
18853
19115
  const headers = takeHeaders(multi);
18854
19116
  const confirm = takeConfirm(multi, actions, { dryRun: false, r });
18855
19117
  const paths = takePaths(outDir);
18856
- const prepare = async () => {
18857
- if (existsSync29(join33(outDir, "meta.json"))) {
18858
- const { prevDoc, kept } = await prepareReRecord(outDir);
18859
- r.log(
18860
- `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" : "")
18861
- );
18862
- }
18863
- await mkdir13(outDir, { recursive: true });
18864
- await ensureTakeDir(outDir);
18865
- };
19118
+ const cut = reRecord(outDir, flags, r);
18866
19119
  const browser = await launchBrowser(browserArgs);
18867
19120
  try {
18868
19121
  r.log("recording\u2026");
@@ -18871,7 +19124,7 @@ async function cmdCreate2(argv) {
18871
19124
  sessionName,
18872
19125
  actions,
18873
19126
  (context2) => recordTake(browser, url, actions, paths, r.log, {
18874
- actionsDir: dirname11(resolve20(actionsPath)),
19127
+ actionsDir: dirname11(resolve21(actionsPath)),
18875
19128
  maxDurationSeconds,
18876
19129
  storageState,
18877
19130
  headers,
@@ -18882,7 +19135,7 @@ async function cmdCreate2(argv) {
18882
19135
  onEvent: (e) => r.event(e),
18883
19136
  onArrival: wallGate(flags, r, {
18884
19137
  strict: flags.strict === true,
18885
- prepare
19138
+ prepare: cut.prepare
18886
19139
  })
18887
19140
  })
18888
19141
  );
@@ -18895,7 +19148,8 @@ async function cmdCreate2(argv) {
18895
19148
  );
18896
19149
  await releaseFrames(paths, flags, r.log);
18897
19150
  r.event({ event: "phase", phase: "plan" });
18898
- await planTake(outDir, { backdrop });
19151
+ const plan = await cut.plan(backdrop);
19152
+ const kept = previousCutReport(outDir, cut.previousCut(), plan);
18899
19153
  if (rec.stopped) {
18900
19154
  r.done(
18901
19155
  {
@@ -18903,9 +19157,11 @@ async function cmdCreate2(argv) {
18903
19157
  ok: false,
18904
19158
  stopped: rec.stopped,
18905
19159
  skipped: rec.skipped,
18906
- rendered: false
19160
+ rendered: false,
19161
+ ...kept.fields,
19162
+ ...teardownField(rec)
18907
19163
  },
18908
- `FAILED: ${rec.stopped.reason}. NOT rendering; the footage up to it is in ${outDir}.`
19164
+ `FAILED: ${rec.stopped.reason}. NOT rendering; the footage up to it is in ${outDir}.${kept.line}${teardownNote(rec)}`
18909
19165
  );
18910
19166
  return EXIT_ERROR;
18911
19167
  }
@@ -18919,9 +19175,11 @@ async function cmdCreate2(argv) {
18919
19175
  ready: rec.ready,
18920
19176
  capped: rec.capped,
18921
19177
  strictFailed: true,
18922
- rendered: false
19178
+ rendered: false,
19179
+ ...kept.fields,
19180
+ ...teardownField(rec)
18923
19181
  },
18924
- `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; NOT rendering. Fix the flow and re-run, or render the partial take with: vos render ${outDir}`
19182
+ `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; NOT rendering. Fix the flow and re-run, or render the partial take with: vos render ${outDir}${kept.line}${teardownNote(rec)}`
18925
19183
  );
18926
19184
  return EXIT_USAGE;
18927
19185
  }
@@ -18967,10 +19225,12 @@ async function cmdCreate2(argv) {
18967
19225
  ...rec.media.length ? { media: rec.media } : {},
18968
19226
  overruns: rec.overruns,
18969
19227
  pageErrors: rec.pageErrors,
18970
- ...flags.draft === true ? { draft: true } : {}
19228
+ ...teardownField(rec),
19229
+ ...flags.draft === true ? { draft: true } : {},
19230
+ ...kept.fields
18971
19231
  },
18972
19232
  `Created ${result.out} (${(result.bytes / 1024).toFixed(0)} KB, ${result.width}x${result.height}@${result.fps}, ${result.duration.toFixed(1)}s, ${result.zoomSpans} zoom spans, ${result.clicks} click effects${result.audio ? ", audio" : ""}${flags.draft === true ? ", DRAFT" : ""})
18973
- take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${stepReportNote(rec)}${navTimeoutNote(rec)}${exposureNote(rec)}${noticeNote(rec)}`
19233
+ take: ${outDir} \u2014 inspect with: vos frames ${outDir} --at-zooms; edit doc.json and re-render with: vos render ${outDir}${stepReportNote(rec)}${navTimeoutNote(rec)}${kept.line}${exposureNote(rec)}${noticeNote(rec)}${teardownNote(rec)}`
18974
19234
  );
18975
19235
  return EXIT_OK;
18976
19236
  } finally {
@@ -18996,8 +19256,8 @@ async function cmdPlan(argvIn) {
18996
19256
  }
18997
19257
  let reuse;
18998
19258
  if (flags.reuse === true) {
18999
- const from = resolve20(strFlag(flags, "from") ?? join33(dir, PREV_DOC_NAME));
19000
- if (!existsSync29(from)) {
19259
+ const from = resolve21(strFlag(flags, "from") ?? join33(dir, PREV_DOC_NAME));
19260
+ if (!existsSync30(from)) {
19001
19261
  throw new UsageError(
19002
19262
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
19003
19263
  );
@@ -19008,7 +19268,7 @@ async function cmdPlan(argvIn) {
19008
19268
  };
19009
19269
  }
19010
19270
  const style = await resolveStyleRef(flags);
19011
- const hasDoc = existsSync29(join33(dir, "doc.json"));
19271
+ const hasDoc = existsSync30(join33(dir, "doc.json"));
19012
19272
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
19013
19273
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
19014
19274
  const stepCaptions = strFlag(flags, "captions") === "steps";
@@ -19086,7 +19346,7 @@ async function cmdPlan(argvIn) {
19086
19346
  ${s.reuse.flagged.join("\n ")}` : "") : "";
19087
19347
  r.done(
19088
19348
  {
19089
- take: resolve20(dir),
19349
+ take: resolve21(dir),
19090
19350
  fresh: s.fresh,
19091
19351
  cursorKept: s.cursorKept,
19092
19352
  zoomAuto,
@@ -19466,11 +19726,11 @@ async function resolveStyleRef(flags) {
19466
19726
  }
19467
19727
  async function resolveDocRef(ref, flags, what) {
19468
19728
  const styleRef = ref;
19469
- const file = existsSync29(styleRef) ? resolve20(
19729
+ const file = existsSync30(styleRef) ? resolve21(
19470
19730
  styleRef,
19471
- existsSync29(join33(styleRef, "doc.json")) ? "doc.json" : ""
19731
+ existsSync30(join33(styleRef, "doc.json")) ? "doc.json" : ""
19472
19732
  ) : null;
19473
- if (file && existsSync29(file)) {
19733
+ if (file && existsSync30(file)) {
19474
19734
  return {
19475
19735
  from: file,
19476
19736
  doc: migrateHostedDoc4(
@@ -19585,14 +19845,14 @@ async function cmdOpen(argv) {
19585
19845
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
19586
19846
  const r = createReporter(flags.json === true);
19587
19847
  const take = await loadTake(dir);
19588
- if (!existsSync29(take.paths.recording)) {
19848
+ if (!existsSync30(take.paths.recording)) {
19589
19849
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
19590
19850
  }
19591
19851
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
19592
19852
  /\/+$/,
19593
19853
  ""
19594
19854
  );
19595
- const server = await startTakeServer(resolve20(dir), {});
19855
+ const server = await startTakeServer(resolve21(dir), {});
19596
19856
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
19597
19857
  r.event({ event: "open", server: server.base, url });
19598
19858
  r.log(`take served at ${server.base}`);
@@ -19718,7 +19978,7 @@ async function cmdValidate(argv) {
19718
19978
  const target = positionals[0];
19719
19979
  if (!target) throw new UsageError("vos validate <actions.json|take>");
19720
19980
  const r = createReporter(flags.json === true);
19721
- const kitTarget = target.endsWith("kit.json") ? target : existsSync29(join33(target, "kit.json")) && !isTakeDir(target) ? join33(target, "kit.json") : null;
19981
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync30(join33(target, "kit.json")) && !isTakeDir(target) ? join33(target, "kit.json") : null;
19722
19982
  if (kitTarget) {
19723
19983
  const picture = flags.picture === true;
19724
19984
  const verdict = await validateKit(kitTarget, { picture });
@@ -19750,7 +20010,7 @@ async function cmdValidate(argv) {
19750
20010
  r.done({ valid: true, target }, `${target}: valid actions file`);
19751
20011
  return EXIT_OK;
19752
20012
  }
19753
- if (!isTakeDir(target) && existsSync29(join33(target, "config.json"))) {
20013
+ if (!isTakeDir(target) && existsSync30(join33(target, "config.json"))) {
19754
20014
  const parsed = JSON.parse(
19755
20015
  await readFile18(join33(target, "config.json"), "utf8")
19756
20016
  );
@@ -19794,7 +20054,7 @@ async function cmdValidate(argv) {
19794
20054
  const problems = [];
19795
20055
  const warnings = [];
19796
20056
  const notes = [];
19797
- if (!existsSync29(take.paths.recording)) {
20057
+ if (!existsSync30(take.paths.recording)) {
19798
20058
  problems.push("missing recording (re-run record)");
19799
20059
  } else {
19800
20060
  const mismatch = containerMismatch(
@@ -19835,7 +20095,7 @@ async function cmdValidate(argv) {
19835
20095
  const found = localFile(rel, target);
19836
20096
  if (!("refused" in found)) continue;
19837
20097
  problems.push(
19838
- existsSync29(resolve20(target, rel)) ? `doc.json: ${ref.where}: ${ref.key} ${found.refused}` : `doc.json: ${ref.where}: ${ref.key} is not a file in the take, so a render plays the film without it (put the file there, or fix the key)`
20098
+ existsSync30(resolve21(target, rel)) ? `doc.json: ${ref.where}: ${ref.key} ${found.refused}` : `doc.json: ${ref.where}: ${ref.key} is not a file in the take, so a render plays the film without it (put the file there, or fix the key)`
19839
20099
  );
19840
20100
  }
19841
20101
  if (flags.picture === true && (take.doc.overlays ?? []).some((o) => o.kind === "html")) {
@@ -19914,7 +20174,7 @@ async function cmdPush3(argv) {
19914
20174
  );
19915
20175
  const refusal = takePushRefusal({
19916
20176
  vos: strFlag(flags, "vos"),
19917
- trackedVosId: readSyncState(resolve20(dir))?.vosId ?? null,
20177
+ trackedVosId: readSyncState(resolve21(dir))?.vosId ?? null,
19918
20178
  programOnly: ["slug", "desc", "tags", "base", "remix-of", "still"].filter(
19919
20179
  (name) => hasFlag(flags, name)
19920
20180
  )
@@ -19930,7 +20190,7 @@ async function cmdPush3(argv) {
19930
20190
  `--claimable creates a NEW claimable vos; ${iterating.map((f) => `--${f}`).join(", ")} ${iterating.length > 1 ? "apply" : "applies"} to a keyed push`
19931
20191
  );
19932
20192
  const claim = await pushTakeClaimable(
19933
- resolve20(dir),
20193
+ resolve21(dir),
19934
20194
  {
19935
20195
  api: strFlag(flags, "api"),
19936
20196
  origin: strFlag(flags, "origin"),
@@ -19953,7 +20213,7 @@ reference and the only credential.`
19953
20213
  }
19954
20214
  const overrides = multi.override;
19955
20215
  const result = await pushTake(
19956
- resolve20(dir),
20216
+ resolve21(dir),
19957
20217
  {
19958
20218
  key: strFlag(flags, "key"),
19959
20219
  api: strFlag(flags, "api"),
@@ -20008,7 +20268,7 @@ async function cmdPull2(argv) {
20008
20268
  const dir = positionals[0] ?? ".";
20009
20269
  const r = createReporter(flags.json === true);
20010
20270
  const result = await pullTake(
20011
- resolve20(dir),
20271
+ resolve21(dir),
20012
20272
  {
20013
20273
  key: strFlag(flags, "key"),
20014
20274
  api: strFlag(flags, "api"),
@@ -20183,10 +20443,13 @@ function verdictExit(v, strict, none) {
20183
20443
  return strict ? EXIT_WALL : EXIT_OK;
20184
20444
  }
20185
20445
  async function cmdSession(argv) {
20186
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS15);
20446
+ const { positionals, flags } = parseArgs(
20447
+ argv,
20448
+ /* @__PURE__ */ new Set([...BOOLEAN_FLAGS15, "headed"])
20449
+ );
20187
20450
  const [sub, arg] = positionals;
20188
20451
  const r = createReporter(flags.json === true);
20189
- const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
20452
+ const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | exec <name> <script.mjs> [--url <url>] [--headed] [--viewport WxH] | list | rm <name>";
20190
20453
  switch (sub) {
20191
20454
  case "open": {
20192
20455
  if (!arg) throw new UsageError(usage);
@@ -20227,7 +20490,7 @@ async function cmdSession(argv) {
20227
20490
  if (!arg) throw new UsageError(usage);
20228
20491
  const bad = sessionNameVerdict(arg);
20229
20492
  if (bad) throw new UsageError(bad);
20230
- if (!existsSync29(sessionDir(arg)))
20493
+ if (!existsSync30(sessionDir(arg)))
20231
20494
  throw new UsageError(`no session "${arg}" (vos session list)`);
20232
20495
  const summary3 = await summarizeSession(arg);
20233
20496
  const url = strFlag(flags, "url");
@@ -20252,10 +20515,58 @@ async function cmdSession(argv) {
20252
20515
  origins: summary3.origins,
20253
20516
  newestExpires: summary3.newestExpires
20254
20517
  },
20255
- `session "${arg}": ${verdictLine(verdict, url, arg)}${verdict.state === "not-signed-in" ? `. It expired, or the sign-in never landed. Sign in again: vos session open ${url} --name ${arg}` : `. ${summaryLine(summary3)}`}`
20518
+ `session "${arg}": ${verdictLine(verdict, url, arg)}${verdict.state === "not-signed-in" ? `. It expired, the sign-in never landed, or the profile was opened by a browser that cannot read its cookies (a script launching it itself signs it out; run scripts with vos session exec). Sign in again: vos session open ${url} --name ${arg}` : `. ${summaryLine(summary3)}`}`
20256
20519
  );
20257
20520
  return verdictExit(verdict, flags.strict === true, EXIT_WALL);
20258
20521
  }
20522
+ case "exec": {
20523
+ const scriptPath = positionals[2];
20524
+ if (!arg || !scriptPath) throw new UsageError(usage);
20525
+ const bad = sessionNameVerdict(arg);
20526
+ if (bad) throw new UsageError(bad);
20527
+ if (!existsSync30(sessionDir(arg)))
20528
+ throw new UsageError(`no session "${arg}" (vos session list)`);
20529
+ const url = strFlag(flags, "url");
20530
+ if (url && !/^https?:\/\//.test(url))
20531
+ throw new UsageError("--url wants an http(s) URL");
20532
+ const viewport = strFlag(flags, "viewport");
20533
+ const size = viewport ? /^(\d+)x(\d+)$/.exec(viewport) : null;
20534
+ if (viewport && !size)
20535
+ throw new UsageError("--viewport wants WxH, e.g. 1280x720");
20536
+ let script;
20537
+ try {
20538
+ script = await loadExecScript(scriptPath);
20539
+ } catch (e) {
20540
+ if (e instanceof ExecScriptError) throw new UsageError(e.message);
20541
+ throw new UsageError(
20542
+ `${scriptPath} could not be loaded: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`
20543
+ );
20544
+ }
20545
+ let result;
20546
+ try {
20547
+ result = await execInProfile(sessionDir(arg), script, {
20548
+ name: arg,
20549
+ ...url ? { url } : {},
20550
+ headless: flags.headed !== true,
20551
+ ...size ? { viewport: { width: +size[1], height: +size[2] } } : {}
20552
+ });
20553
+ } catch (e) {
20554
+ r.done(
20555
+ {
20556
+ name: arg,
20557
+ ok: false,
20558
+ error: e instanceof Error ? e.message.split("\n")[0] : String(e)
20559
+ },
20560
+ `the script failed in session "${arg}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)} (the session was closed cleanly)`
20561
+ );
20562
+ return EXIT_ERROR;
20563
+ }
20564
+ r.done(
20565
+ { name: arg, ok: true, ...result !== void 0 ? { result } : {} },
20566
+ result === void 0 ? `ran ${scriptPath} in session "${arg}"` : typeof result === "string" ? result : JSON.stringify(result, null, 2)
20567
+ );
20568
+ return EXIT_OK;
20569
+ }
20259
20570
  case "list": {
20260
20571
  const rows = listSessions();
20261
20572
  r.done(
@@ -20300,8 +20611,10 @@ export {
20300
20611
  HELP,
20301
20612
  strictFails,
20302
20613
  takeBrowserArgs,
20614
+ reRecord,
20615
+ previousCutReport,
20303
20616
  takeBackdrop,
20304
20617
  verbHelp,
20305
20618
  run
20306
20619
  };
20307
- //# sourceMappingURL=chunk-SP5OHTVB.js.map
20620
+ //# sourceMappingURL=chunk-S3RI2SHH.js.map