@vosjs/cli 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,11 +6,24 @@ import {
6
6
  noteGivenFlag,
7
7
  trackFlags
8
8
  } from "./chunk-FACYY7VV.js";
9
+ import {
10
+ PRESS_HOLD_MS,
11
+ PRESS_LEAD_MS,
12
+ SCROLL_SETTLE_MS,
13
+ TRAILING_HOLD_MS,
14
+ askedMs,
15
+ clockMotion,
16
+ clockTyping,
17
+ paceLine,
18
+ paceReport,
19
+ pointerTravelMs,
20
+ settleMs
21
+ } from "./chunk-RDQYYRWN.js";
9
22
 
10
23
  // src/plugin/run.ts
11
24
  import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
12
- import { existsSync as existsSync15 } from "fs";
13
- import { join as join18, resolve as resolve9 } from "path";
25
+ import { existsSync as existsSync16 } from "fs";
26
+ import { join as join19, resolve as resolve9 } from "path";
14
27
  import {
15
28
  END_CARD_FROM as END_CARD_FROM2,
16
29
  docOutputDuration as docOutputDuration3,
@@ -6686,97 +6699,6 @@ function cappedLine(capSeconds) {
6686
6699
  return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
6687
6700
  }
6688
6701
 
6689
- // src/plugin/pace.ts
6690
- var POINTER_MS_PER_PX = 0.7;
6691
- var POINTER_MIN_MS = 250;
6692
- var POINTER_MAX_MS = 800;
6693
- var MOTION_TICK_MS = 16;
6694
- var SETTLE_MS = {
6695
- click: 150,
6696
- type: 150,
6697
- scroll: 200,
6698
- drag: 80
6699
- };
6700
- var PRESS_LEAD_MS = 80;
6701
- var PRESS_HOLD_MS = 70;
6702
- var SCROLL_SETTLE_MS = 120;
6703
- var TRAILING_HOLD_MS = 400;
6704
- function pointerTravelMs(dist) {
6705
- return Math.min(
6706
- POINTER_MAX_MS,
6707
- Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
6708
- );
6709
- }
6710
- function settleMs(step) {
6711
- if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
6712
- const d = step.do;
6713
- return SETTLE_MS[d] ?? 0;
6714
- }
6715
- var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
6716
- async function clockMotion(dur, at2, clock) {
6717
- const start = clock.now();
6718
- let samples = 0;
6719
- for (; ; ) {
6720
- const elapsed = clock.now() - start;
6721
- if (elapsed >= dur) break;
6722
- await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6723
- samples++;
6724
- const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6725
- const wait2 = Math.min(next, start + dur) - clock.now();
6726
- if (wait2 > 0) await clock.sleep(wait2);
6727
- }
6728
- await at2(1);
6729
- return samples + 1;
6730
- }
6731
- async function clockTyping(chars, delay, type, clock) {
6732
- const start = clock.now();
6733
- for (let i = 0; i < chars.length; i++) {
6734
- await type(chars[i], i);
6735
- const due = start + (i + 1) * delay;
6736
- const wait2 = due - clock.now();
6737
- if (wait2 > 0 && i < chars.length - 1) await clock.sleep(wait2);
6738
- }
6739
- }
6740
- function askedMs(step) {
6741
- switch (step.do) {
6742
- case "wait":
6743
- return step.ms ?? 0;
6744
- case "hover":
6745
- return step.ms ?? 700;
6746
- case "drag":
6747
- return step.ms ?? 700;
6748
- case "type":
6749
- return (step.text?.length ?? 0) * (step.delayMs ?? 40);
6750
- default:
6751
- return 0;
6752
- }
6753
- }
6754
- function paceReport(steps) {
6755
- const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
6756
- const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
6757
- const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
6758
- const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
6759
- const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
6760
- step: s.step,
6761
- do: s.do,
6762
- askedMs: s.askedMs,
6763
- wallMs: s.wallMs
6764
- }));
6765
- return {
6766
- askedMs: askedMs2,
6767
- gestureMs,
6768
- wallMs,
6769
- overheadMs,
6770
- overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
6771
- slow
6772
- };
6773
- }
6774
- function paceLine(r) {
6775
- const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
6776
- const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
6777
- return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
6778
- }
6779
-
6780
6702
  // src/plugin/wall.ts
6781
6703
  var IDP_HOSTS = [
6782
6704
  "accounts.google.com",
@@ -7121,12 +7043,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7121
7043
  const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
7122
7044
  const vw = actions.viewport?.width ?? 1280;
7123
7045
  const vh = actions.viewport?.height ?? 720;
7124
- const context = await browser.newContext({
7046
+ const context = opts.context ?? await browser.newContext({
7125
7047
  viewport: { width: vw, height: vh },
7126
7048
  deviceScaleFactor: 1,
7127
7049
  ...opts.storageState ? { storageState: opts.storageState } : {},
7128
7050
  ...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
7129
7051
  });
7052
+ if (opts.context && opts.headers && Object.keys(opts.headers).length)
7053
+ await context.setExtraHTTPHeaders(opts.headers);
7130
7054
  const masks = actions.mask ?? [];
7131
7055
  if (masks.length) await context.addInitScript(maskInitScript(masks));
7132
7056
  const exposureLog = new ExposureLog(masks);
@@ -7515,6 +7439,148 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7515
7439
  };
7516
7440
  }
7517
7441
 
7442
+ // src/plugin/session.ts
7443
+ import { spawn } from "child_process";
7444
+ import { existsSync as existsSync12, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync3 } from "fs";
7445
+ import { homedir as homedir2 } from "os";
7446
+ import { join as join14 } from "path";
7447
+ import { chromium } from "playwright";
7448
+ var SESSIONS_DIR = join14(homedir2(), ".config", "vos", "sessions");
7449
+ var NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
7450
+ function sessionNameVerdict(name) {
7451
+ if (!name) return "a session needs a name: --name <app>";
7452
+ if (!NAME.test(name) || name.includes(".."))
7453
+ return `"${name}" is not a session name: letters, digits, . _ - only, up to 64`;
7454
+ return null;
7455
+ }
7456
+ function sessionDir(name) {
7457
+ return join14(SESSIONS_DIR, name);
7458
+ }
7459
+ function systemChromePath() {
7460
+ const explicit = process.env.VOS_BROWSER_PATH;
7461
+ if (explicit) return explicit;
7462
+ const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
7463
+ join14(
7464
+ process.env["PROGRAMFILES"] ?? "C:\\Program Files",
7465
+ "Google",
7466
+ "Chrome",
7467
+ "Application",
7468
+ "chrome.exe"
7469
+ ),
7470
+ join14(
7471
+ process.env["LOCALAPPDATA"] ?? "",
7472
+ "Google",
7473
+ "Chrome",
7474
+ "Application",
7475
+ "chrome.exe"
7476
+ )
7477
+ ] : [
7478
+ "/usr/bin/google-chrome",
7479
+ "/usr/bin/google-chrome-stable",
7480
+ "/opt/google/chrome/chrome"
7481
+ ];
7482
+ const found = candidates.find((c) => existsSync12(c));
7483
+ if (!found)
7484
+ throw new Error(
7485
+ "no Google Chrome found: a session needs the system Chrome (the bundled Chromium cannot read a profile Chrome wrote). Install Chrome, or set VOS_BROWSER_PATH"
7486
+ );
7487
+ return found;
7488
+ }
7489
+ async function openSession(name, url, opts = { log: () => {
7490
+ } }) {
7491
+ const dir = sessionDir(name);
7492
+ mkdirSync2(dir, { recursive: true, mode: 448 });
7493
+ const exe = systemChromePath();
7494
+ const args = [
7495
+ `--user-data-dir=${dir}`,
7496
+ "--no-first-run",
7497
+ "--no-default-browser-check",
7498
+ "--window-size=1100,800",
7499
+ url
7500
+ ];
7501
+ opts.log(
7502
+ `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, then close the window`
7503
+ );
7504
+ return new Promise((resolve10, reject) => {
7505
+ const child = spawn(exe, args, { stdio: "ignore" });
7506
+ child.on("error", reject);
7507
+ child.on("exit", (code) => resolve10({ code }));
7508
+ });
7509
+ }
7510
+ async function withSession(name, fn, opts = {}) {
7511
+ const dir = sessionDir(name);
7512
+ if (!existsSync12(dir))
7513
+ throw new Error(`no session "${name}" (vos session list)`);
7514
+ const explicit = process.env.VOS_BROWSER_PATH;
7515
+ const ctx = await chromium.launchPersistentContext(dir, {
7516
+ headless: opts.headless ?? true,
7517
+ ...explicit ? { executablePath: explicit } : { channel: "chrome" },
7518
+ // The real keychain: the whole reason a profile Chrome wrote can be read.
7519
+ ignoreDefaultArgs: ["--use-mock-keychain"],
7520
+ ...opts.viewport ? { viewport: opts.viewport, deviceScaleFactor: 1 } : {}
7521
+ });
7522
+ try {
7523
+ return await fn(ctx);
7524
+ } finally {
7525
+ await ctx.close().catch(() => {
7526
+ });
7527
+ }
7528
+ }
7529
+ async function summarizeSession(name) {
7530
+ const dir = sessionDir(name);
7531
+ const created = statSync3(dir).birthtimeMs || statSync3(dir).mtimeMs;
7532
+ return withSession(name, async (ctx) => {
7533
+ const cookies = await ctx.cookies();
7534
+ const byHost = /* @__PURE__ */ new Map();
7535
+ let newest = 0;
7536
+ let sessionOnly = 0;
7537
+ for (const c of cookies) {
7538
+ const host = c.domain.replace(/^\./, "");
7539
+ byHost.set(host, (byHost.get(host) ?? 0) + 1);
7540
+ if (c.expires && c.expires > 0) newest = Math.max(newest, c.expires);
7541
+ else sessionOnly++;
7542
+ }
7543
+ return {
7544
+ name,
7545
+ origins: [...byHost.entries()].map(([host, n]) => ({ host, cookies: n })).sort((a, b) => b.cookies - a.cookies),
7546
+ newestExpires: newest ? new Date(newest * 1e3).toISOString() : null,
7547
+ sessionOnly,
7548
+ ageDays: Math.floor((Date.now() - created) / 864e5)
7549
+ };
7550
+ });
7551
+ }
7552
+ function summaryLine(s) {
7553
+ if (!s.origins.length)
7554
+ return `session "${s.name}" holds no cookies: the window closed before a sign-in landed, or the sign-in never happened`;
7555
+ const hosts = s.origins.slice(0, 4).map((o) => `${o.host} (${o.cookies})`).join(", ");
7556
+ const more = s.origins.length > 4 ? ` and ${s.origins.length - 4} more` : "";
7557
+ const exp = s.newestExpires ? `newest expires in ${Math.max(0, Math.round((new Date(s.newestExpires).getTime() - Date.now()) / 864e5))} days` : "every cookie dies with the window (the app set no expiry), so this session cannot be reused";
7558
+ return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
7559
+ }
7560
+ function listSessions() {
7561
+ if (!existsSync12(SESSIONS_DIR)) return [];
7562
+ return readdirSync2(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
7563
+ const st = statSync3(join14(SESSIONS_DIR, d.name));
7564
+ return {
7565
+ name: d.name,
7566
+ ageDays: Math.floor(
7567
+ (Date.now() - (st.birthtimeMs || st.mtimeMs)) / 864e5
7568
+ )
7569
+ };
7570
+ }).sort((a, b) => a.name.localeCompare(b.name));
7571
+ }
7572
+ function removeSession(name) {
7573
+ const dir = sessionDir(name);
7574
+ if (!existsSync12(dir)) return false;
7575
+ rmSync(dir, { recursive: true, force: true });
7576
+ return true;
7577
+ }
7578
+ function looksLikeStorageState(value) {
7579
+ if (typeof value !== "object" || value === null) return false;
7580
+ const v = value;
7581
+ return Array.isArray(v.cookies) && Array.isArray(v.origins);
7582
+ }
7583
+
7518
7584
  // src/plugin/encode.ts
7519
7585
  var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.55.7?target=es2022";
7520
7586
  var ENCODE_HTML = `<!doctype html><html><head><meta charset="utf-8"></head><body>
@@ -7570,9 +7636,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
7570
7636
  }
7571
7637
 
7572
7638
  // src/plugin/plan.ts
7573
- import { existsSync as existsSync12 } from "fs";
7639
+ import { existsSync as existsSync13 } from "fs";
7574
7640
  import { readFile as readFile12 } from "fs/promises";
7575
- import { join as join14 } from "path";
7641
+ import { join as join15 } from "path";
7576
7642
  import {
7577
7643
  DEFAULT_FRAME_STYLE,
7578
7644
  STYLE_FIELDS,
@@ -7794,8 +7860,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
7794
7860
  // src/plugin/plan.ts
7795
7861
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
7796
7862
  async function readDigestActivity(dir) {
7797
- const file = join14(dir, "digest", "digest.json");
7798
- if (!existsSync12(file)) return null;
7863
+ const file = join15(dir, "digest", "digest.json");
7864
+ if (!existsSync13(file)) return null;
7799
7865
  try {
7800
7866
  const d = JSON.parse(await readFile12(file, "utf8"));
7801
7867
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -8007,15 +8073,15 @@ async function planTake(dir, opts = {}) {
8007
8073
 
8008
8074
  // src/plugin/sync.ts
8009
8075
  import { createHash } from "crypto";
8010
- import { existsSync as existsSync13 } from "fs";
8076
+ import { existsSync as existsSync14, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
8011
8077
  import { readFile as readFile13 } from "fs/promises";
8012
8078
  import { createInterface } from "readline/promises";
8013
- import { basename as basename3, join as join16 } from "path";
8079
+ import { basename as basename3, join as join17 } from "path";
8014
8080
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
8015
8081
 
8016
8082
  // src/plugin/folder.ts
8017
8083
  import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
8018
- import { join as join15 } from "path";
8084
+ import { join as join16 } from "path";
8019
8085
  import { recipeHints } from "@vosjs/shared/frontmatter";
8020
8086
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
8021
8087
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
@@ -8191,18 +8257,18 @@ async function cmdPull(argv) {
8191
8257
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
8192
8258
  const payload = res.body;
8193
8259
  const out = strFlag(flags, "out") ?? folder.slug;
8194
- await mkdir8(join15(out, "recipes"), { recursive: true });
8260
+ await mkdir8(join16(out, "recipes"), { recursive: true });
8195
8261
  const lines = [];
8196
8262
  const recipes = [];
8197
8263
  for (const rec of payload.recipes) {
8198
- const path = join15(out, "recipes", safeName(rec.filename));
8264
+ const path = join16(out, "recipes", safeName(rec.filename));
8199
8265
  await writeFile10(path, rec.body ?? "");
8200
8266
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
8201
8267
  }
8202
8268
  for (const rec of payload.inheritedRecipes) {
8203
8269
  const from = safeName(rec.folderSlug ?? "inherited");
8204
- await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
8205
- const path = join15(
8270
+ await mkdir8(join16(out, "recipes", "_inherited", from), { recursive: true });
8271
+ const path = join16(
8206
8272
  out,
8207
8273
  "recipes",
8208
8274
  "_inherited",
@@ -8217,7 +8283,7 @@ async function cmdPull(argv) {
8217
8283
  }
8218
8284
  const members = [];
8219
8285
  for (const v of payload.voses) {
8220
- const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
8286
+ const dir = join16(out, "members", safeName(v.slug || v.title || v.id));
8221
8287
  await mkdir8(dir, { recursive: true });
8222
8288
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
8223
8289
  if (cfg.status !== 200) {
@@ -8225,7 +8291,7 @@ async function cmdPull(argv) {
8225
8291
  continue;
8226
8292
  }
8227
8293
  await writeFile10(
8228
- join15(dir, "config.json"),
8294
+ join16(dir, "config.json"),
8229
8295
  JSON.stringify(cfg.body.config, null, 2)
8230
8296
  );
8231
8297
  let take = false;
@@ -8237,7 +8303,7 @@ async function cmdPull(argv) {
8237
8303
  );
8238
8304
  if (doc.status === 200) {
8239
8305
  await writeFile10(
8240
- join15(dir, "doc.json"),
8306
+ join16(dir, "doc.json"),
8241
8307
  JSON.stringify(doc.body, null, 2)
8242
8308
  );
8243
8309
  take = true;
@@ -8506,7 +8572,24 @@ function programPushTarget(ask) {
8506
8572
  }
8507
8573
  return { kind: "version", vosId: tracked, tracked: true };
8508
8574
  }
8575
+ function storageStateInDir(dir) {
8576
+ for (const f of readdirSync3(dir)) {
8577
+ if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
8578
+ continue;
8579
+ try {
8580
+ const v = JSON.parse(readFileSync4(join17(dir, f), "utf8"));
8581
+ if (looksLikeStorageState(v)) return join17(dir, f);
8582
+ } catch {
8583
+ }
8584
+ }
8585
+ return null;
8586
+ }
8509
8587
  async function pushTake(dir, flags, r) {
8588
+ const stateFile = storageStateInDir(dir);
8589
+ if (stateFile)
8590
+ throw new Error(
8591
+ `${stateFile} looks like a Playwright storage state (cookies + origins): a live session in the take directory would be uploaded with it. Move it out of the take (a session belongs under ~/.config/vos/sessions or a temp dir) and push again`
8592
+ );
8510
8593
  const take = await loadTake(dir);
8511
8594
  if (!take.doc) {
8512
8595
  throw new Error("no doc.json in this take \u2014 run `vos plan` first");
@@ -8516,7 +8599,7 @@ async function pushTake(dir, flags, r) {
8516
8599
  throw new Error(`doc.json fails lint:
8517
8600
  ${lint.problems.join("\n ")}`);
8518
8601
  }
8519
- if (!existsSync13(take.paths.recording)) {
8602
+ if (!existsSync14(take.paths.recording)) {
8520
8603
  throw new Error(
8521
8604
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
8522
8605
  );
@@ -8608,8 +8691,8 @@ async function pushTake(dir, flags, r) {
8608
8691
  delete docForPush.source.micKey;
8609
8692
  }
8610
8693
  for (const ref of docMediaRefs(docForPush)) {
8611
- const file = join16(dir, takeRelativeFile(ref.key));
8612
- if (!existsSync13(file)) {
8694
+ const file = join17(dir, takeRelativeFile(ref.key));
8695
+ if (!existsSync14(file)) {
8613
8696
  r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
8614
8697
  continue;
8615
8698
  }
@@ -8753,9 +8836,9 @@ async function pullTake(dir, flags, r) {
8753
8836
  };
8754
8837
  }
8755
8838
  let media2;
8756
- if (flags.media && existsSync13(join16(dir, "doc.json"))) {
8839
+ if (flags.media && existsSync14(join17(dir, "doc.json"))) {
8757
8840
  const local = JSON.parse(
8758
- await readFile13(join16(dir, "doc.json"), "utf8")
8841
+ await readFile13(join17(dir, "doc.json"), "utf8")
8759
8842
  );
8760
8843
  media2 = await pullMedia(ctx, dir, local, r.log);
8761
8844
  }
@@ -8816,10 +8899,10 @@ async function pullTake(dir, flags, r) {
8816
8899
  const hostedDoc = migrateHostedDoc3(docRes.json);
8817
8900
  const doc = hostedDoc;
8818
8901
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8819
- if (existsSync13(localRecording)) {
8902
+ if (existsSync14(localRecording)) {
8820
8903
  doc.source.videoKey = basename3(localRecording);
8821
8904
  }
8822
- await writeJson(join16(dir, "doc.json"), doc, true);
8905
+ await writeJson(join17(dir, "doc.json"), doc, true);
8823
8906
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
8824
8907
  const versions = await api(ctx, `/vos/${vosId}/versions`);
8825
8908
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -8838,10 +8921,10 @@ async function pullTake(dir, flags, r) {
8838
8921
  }
8839
8922
 
8840
8923
  // src/plugin/program.ts
8841
- import { existsSync as existsSync14 } from "fs";
8924
+ import { existsSync as existsSync15 } from "fs";
8842
8925
  import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8843
8926
  import { createInterface as createInterface2 } from "readline/promises";
8844
- import { basename as basename4, dirname as dirname4, join as join17 } from "path";
8927
+ import { basename as basename4, dirname as dirname4, join as join18 } from "path";
8845
8928
  import {
8846
8929
  CURRENT_CONFIG_VERSION,
8847
8930
  migrateConfig,
@@ -9011,8 +9094,8 @@ function preflightConfig(parsed) {
9011
9094
  }
9012
9095
  function resolveConfigPath(target) {
9013
9096
  if (target.endsWith(".json")) return target;
9014
- const inDir = join17(target, "config.json");
9015
- if (existsSync14(inDir)) return inDir;
9097
+ const inDir = join18(target, "config.json");
9098
+ if (existsSync15(inDir)) return inDir;
9016
9099
  throw new UsageError(
9017
9100
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
9018
9101
  );
@@ -9040,7 +9123,7 @@ async function cmdFetch(argv) {
9040
9123
  const out = strFlag(flags, "out") ?? slug;
9041
9124
  await mkdir9(out, { recursive: true });
9042
9125
  await writeFile11(
9043
- join17(out, "config.json"),
9126
+ join18(out, "config.json"),
9044
9127
  JSON.stringify(cfg.body.config, null, 2)
9045
9128
  );
9046
9129
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -9065,12 +9148,12 @@ async function cmdFetch(argv) {
9065
9148
  const hosted = migrateHostedDoc4(doc.body);
9066
9149
  const { config: own } = await writeProgramDoc(out, hosted);
9067
9150
  if (own) {
9068
- await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
9151
+ await writeFile11(join18(out, "config.json"), JSON.stringify(own, null, 2));
9069
9152
  }
9070
9153
  } else if (doc.status === 200) {
9071
9154
  take = true;
9072
9155
  const hosted = migrateHostedDoc4(doc.body);
9073
- await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
9156
+ await writeFile11(join18(out, "doc.json"), JSON.stringify(hosted, null, 2));
9074
9157
  if (flags.media === true) {
9075
9158
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
9076
9159
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -9433,10 +9516,10 @@ async function cmdPullProgram(argv) {
9433
9516
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
9434
9517
  if (cfg.status !== 200)
9435
9518
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
9436
- const configPath = join17(dir, "config.json");
9519
+ const configPath = join18(dir, "config.json");
9437
9520
  let backedUp = false;
9438
- if (existsSync14(configPath)) {
9439
- await writeFile11(join17(dir, "config.backup.json"), await readFile14(configPath));
9521
+ if (existsSync15(configPath)) {
9522
+ await writeFile11(join18(dir, "config.backup.json"), await readFile14(configPath));
9440
9523
  backedUp = true;
9441
9524
  }
9442
9525
  let headConfig = cfg.body.config;
@@ -9469,7 +9552,7 @@ async function cmdPullProgram(argv) {
9469
9552
  protected: protectedIds,
9470
9553
  changes,
9471
9554
  out: configPath,
9472
- backup: backedUp ? join17(dir, "config.backup.json") : null
9555
+ backup: backedUp ? join18(dir, "config.backup.json") : null
9473
9556
  },
9474
9557
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
9475
9558
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -9531,8 +9614,8 @@ function storedProgramConfig(config, programDoc) {
9531
9614
  return lowerProgramDoc(doc).config;
9532
9615
  }
9533
9616
  async function readProgramDoc(dir, config) {
9534
- const docPath = join17(dir, "doc.json");
9535
- if (!existsSync14(docPath)) return null;
9617
+ const docPath = join18(dir, "doc.json");
9618
+ if (!existsSync15(docPath)) return null;
9536
9619
  const parsed = JSON.parse(await readFile14(docPath, "utf8"));
9537
9620
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
9538
9621
  return null;
@@ -9544,14 +9627,14 @@ async function writeProgramDoc(dir, hosted) {
9544
9627
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
9545
9628
  const { config, ...rest } = program;
9546
9629
  const onDisk = { ...hosted, program: rest };
9547
- await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9630
+ await writeFile11(join18(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9548
9631
  return {
9549
9632
  config: config && typeof config === "object" ? config : null
9550
9633
  };
9551
9634
  }
9552
9635
 
9553
9636
  // src/plugin/asset.ts
9554
- import { readFileSync as readFileSync4 } from "fs";
9637
+ import { readFileSync as readFileSync5 } from "fs";
9555
9638
  import { basename as basename5, extname as extname2 } from "path";
9556
9639
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
9557
9640
  var MIME_BY_EXT = {
@@ -9605,7 +9688,7 @@ async function cmdPush(argv) {
9605
9688
  for (const file of positionals) {
9606
9689
  const name = basename5(file);
9607
9690
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
9608
- const body = readFileSync4(file);
9691
+ const body = readFileSync5(file);
9609
9692
  const form = new FormData();
9610
9693
  form.set("file", new Blob([body], { type: mime }), name);
9611
9694
  if (folder) form.set("folderId", folder.id);
@@ -9659,7 +9742,7 @@ async function cmdAsset(argv) {
9659
9742
  }
9660
9743
 
9661
9744
  // src/plugin/recipe.ts
9662
- import { readFileSync as readFileSync5 } from "fs";
9745
+ import { readFileSync as readFileSync6 } from "fs";
9663
9746
  import { basename as basename6 } from "path";
9664
9747
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
9665
9748
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -9679,7 +9762,7 @@ async function cmdPush2(argv) {
9679
9762
  throw new UsageError(USAGE);
9680
9763
  }
9681
9764
  const filename = recipeFilename(file);
9682
- const body = readFileSync5(file);
9765
+ const body = readFileSync6(file);
9683
9766
  const r = createReporter(flags.json === true);
9684
9767
  const origin = platformOrigin({
9685
9768
  origin: strFlag(flags, "origin"),
@@ -10588,9 +10671,10 @@ function convertAgentBrowser(records, opts = {}) {
10588
10671
  async function cmdActions(argv) {
10589
10672
  const { positionals, flags } = parseArgs(argv, /* @__PURE__ */ new Set(["json"]));
10590
10673
  const [sub, input] = positionals;
10674
+ if (sub === "script" && input) return cmdActionsScript(input, flags);
10591
10675
  if (sub !== "from-agent-browser" || !input) {
10592
10676
  throw new UsageError(
10593
- "vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]"
10677
+ "vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]\n vos actions script <actions.json> [--json]"
10594
10678
  );
10595
10679
  }
10596
10680
  const r = createReporter(flags.json === true);
@@ -10638,6 +10722,18 @@ function summary(result) {
10638
10722
  notes: result.notes
10639
10723
  };
10640
10724
  }
10725
+ async function cmdActionsScript(input, flags) {
10726
+ const r = createReporter(flags.json === true);
10727
+ const { readFile: readFile16 } = await import("fs/promises");
10728
+ const { shotList, formatShotList } = await import("./shotList-FYDHO5AI.js");
10729
+ const raw = JSON.parse(await readFile16(resolve8(input), "utf8"));
10730
+ const errors = validateActions(raw);
10731
+ if (errors.length) throw new UsageError(`${input}:
10732
+ ${errors.join("\n ")}`);
10733
+ const list = shotList(raw);
10734
+ r.done({ ...list, text: formatShotList(list) }, formatShotList(list));
10735
+ return EXIT_OK;
10736
+ }
10641
10737
 
10642
10738
  // src/plugin/run.ts
10643
10739
  var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
@@ -10667,8 +10763,8 @@ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "h
10667
10763
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10668
10764
 
10669
10765
  Take pipeline
10670
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10671
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10766
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10767
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10672
10768
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
10673
10769
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
10674
10770
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
@@ -10680,6 +10776,11 @@ Take pipeline
10680
10776
  vos validate <actions.json|take|kit.json> [--picture] [--json]
10681
10777
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
10682
10778
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
10779
+ vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
10780
+ vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
10781
+ vos session list | rm <name> what exists and how old; delete one
10782
+ vos actions script <actions.json> [--json]
10783
+ the flow as numbered beats in words, for a person recording it by hand
10683
10784
 
10684
10785
  Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
10685
10786
  vos login [--key <vos_sk_\u2026>] [--label <name>] [--no-browser]
@@ -10976,10 +11077,35 @@ function takeStorageState(flags) {
10976
11077
  const raw = strFlag(flags, "storage-state");
10977
11078
  if (!raw) return void 0;
10978
11079
  const path = resolve9(raw);
10979
- if (!existsSync15(path))
11080
+ if (!existsSync16(path))
10980
11081
  throw new UsageError(`--storage-state: no such file: ${path}`);
10981
11082
  return path;
10982
11083
  }
11084
+ function takeSessionName(flags, storageState) {
11085
+ const name = strFlag(flags, "session");
11086
+ if (!name) return void 0;
11087
+ const bad = sessionNameVerdict(name);
11088
+ if (bad) throw new UsageError(bad);
11089
+ if (storageState)
11090
+ throw new UsageError(
11091
+ "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
11092
+ );
11093
+ if (!existsSync16(sessionDir(name)))
11094
+ throw new UsageError(
11095
+ `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
11096
+ );
11097
+ return name;
11098
+ }
11099
+ async function inSession(name, actions, fn) {
11100
+ if (!name) return fn(void 0);
11101
+ return withSession(name, (ctx) => fn(ctx), {
11102
+ headless: true,
11103
+ viewport: {
11104
+ width: actions.viewport?.width ?? 1280,
11105
+ height: actions.viewport?.height ?? 720
11106
+ }
11107
+ });
11108
+ }
10983
11109
  async function takeBackdrop(flags, r) {
10984
11110
  const { backdrop, note } = await openingBackdrop(
10985
11111
  strFlag(flags, "background"),
@@ -11037,6 +11163,7 @@ async function cmdRecord(argv) {
11037
11163
  const backdrop = await takeBackdrop(flags, r);
11038
11164
  const maxDurationSeconds = await maxDuration(flags, r);
11039
11165
  const storageState = takeStorageState(flags);
11166
+ const sessionName = takeSessionName(flags, storageState);
11040
11167
  const browserArgs = takeBrowserArgs(multi);
11041
11168
  const headers = takeHeaders(multi);
11042
11169
  if (flags["dry-run"] === true) {
@@ -11047,19 +11174,17 @@ async function cmdRecord(argv) {
11047
11174
  r.log("rehearsing (nothing is captured or written)\u2026");
11048
11175
  r.event({ event: "phase", phase: "rehearse" });
11049
11176
  const started = Date.now();
11050
- const rec = await recordTake(
11051
- browser2,
11052
- url,
11177
+ const rec = await inSession(
11178
+ sessionName,
11053
11179
  actions,
11054
- takePaths(outDir),
11055
- r.log,
11056
- {
11180
+ (context) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
11057
11181
  storageState,
11058
11182
  headers,
11183
+ context,
11059
11184
  dryRun: true,
11060
11185
  // A rehearsal is always strict, and it touches no directory.
11061
11186
  onArrival: wallGate(flags, r, { strict: true })
11062
- }
11187
+ })
11063
11188
  );
11064
11189
  const steps = rec.meta.steps ?? [];
11065
11190
  const lines = steps.map((s) => {
@@ -11072,6 +11197,7 @@ async function cmdRecord(argv) {
11072
11197
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
11073
11198
  ...browserArgs.map((a) => `--browser-arg=${a}`),
11074
11199
  ...Object.entries(headers).map(([k, v]) => `--header ${k}=${v}`),
11200
+ ...sessionName ? [`--session ${sessionName}`] : [],
11075
11201
  ...flags["allow-wall"] === true ? ["--allow-wall"] : []
11076
11202
  ].join(" ");
11077
11203
  const nextOut = strFlag(flags, "out") ?? "take";
@@ -11097,7 +11223,7 @@ ${lines.join("\n")}
11097
11223
  }
11098
11224
  const paths = takePaths(outDir);
11099
11225
  const prepare = async () => {
11100
- if (existsSync15(join18(outDir, "meta.json"))) {
11226
+ if (existsSync16(join19(outDir, "meta.json"))) {
11101
11227
  const { prevDoc, kept } = await prepareReRecord(outDir);
11102
11228
  r.log(
11103
11229
  `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" : "")
@@ -11110,15 +11236,20 @@ ${lines.join("\n")}
11110
11236
  try {
11111
11237
  r.log("recording\u2026");
11112
11238
  r.event({ event: "phase", phase: "record" });
11113
- const rec = await recordTake(browser, url, actions, paths, r.log, {
11114
- maxDurationSeconds,
11115
- storageState,
11116
- headers,
11117
- onArrival: wallGate(flags, r, {
11118
- strict: flags.strict === true,
11119
- prepare
11239
+ const rec = await inSession(
11240
+ sessionName,
11241
+ actions,
11242
+ (context) => recordTake(browser, url, actions, paths, r.log, {
11243
+ maxDurationSeconds,
11244
+ storageState,
11245
+ headers,
11246
+ context,
11247
+ onArrival: wallGate(flags, r, {
11248
+ strict: flags.strict === true,
11249
+ prepare
11250
+ })
11120
11251
  })
11121
- });
11252
+ );
11122
11253
  r.event({ event: "phase", phase: "encode" });
11123
11254
  r.log("encoding\u2026");
11124
11255
  const enc = await encodeRecording(
@@ -11156,7 +11287,7 @@ ${lines.join("\n")}
11156
11287
  },
11157
11288
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
11158
11289
  ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
11159
- Next: edit ${join18(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11290
+ Next: edit ${join19(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11160
11291
  );
11161
11292
  return strictFail ? EXIT_USAGE : EXIT_OK;
11162
11293
  } finally {
@@ -11185,7 +11316,7 @@ async function cmdCreate2(argv) {
11185
11316
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11186
11317
  throw new UsageError("--format must be webm or mp4");
11187
11318
  const format = fmtRaw;
11188
- const out = positionals[0] ?? join18(outDir, `out.${format}`);
11319
+ const out = positionals[0] ?? join19(outDir, `out.${format}`);
11189
11320
  const parallel = numFlag(flags, "parallel", 1);
11190
11321
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11191
11322
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11193,11 +11324,12 @@ async function cmdCreate2(argv) {
11193
11324
  const backdrop = await takeBackdrop(flags, r);
11194
11325
  const maxDurationSeconds = await maxDuration(flags, r);
11195
11326
  const storageState = takeStorageState(flags);
11327
+ const sessionName = takeSessionName(flags, storageState);
11196
11328
  const browserArgs = takeBrowserArgs(multi);
11197
11329
  const headers = takeHeaders(multi);
11198
11330
  const paths = takePaths(outDir);
11199
11331
  const prepare = async () => {
11200
- if (existsSync15(join18(outDir, "meta.json"))) {
11332
+ if (existsSync16(join19(outDir, "meta.json"))) {
11201
11333
  const { prevDoc, kept } = await prepareReRecord(outDir);
11202
11334
  r.log(
11203
11335
  `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" : "")
@@ -11210,15 +11342,20 @@ async function cmdCreate2(argv) {
11210
11342
  try {
11211
11343
  r.log("recording\u2026");
11212
11344
  r.event({ event: "phase", phase: "record" });
11213
- const rec = await recordTake(browser, url, actions, paths, r.log, {
11214
- maxDurationSeconds,
11215
- storageState,
11216
- headers,
11217
- onArrival: wallGate(flags, r, {
11218
- strict: flags.strict === true,
11219
- prepare
11345
+ const rec = await inSession(
11346
+ sessionName,
11347
+ actions,
11348
+ (context) => recordTake(browser, url, actions, paths, r.log, {
11349
+ maxDurationSeconds,
11350
+ storageState,
11351
+ headers,
11352
+ context,
11353
+ onArrival: wallGate(flags, r, {
11354
+ strict: flags.strict === true,
11355
+ prepare
11356
+ })
11220
11357
  })
11221
- });
11358
+ );
11222
11359
  r.event({ event: "phase", phase: "encode" });
11223
11360
  r.log("encoding\u2026");
11224
11361
  await encodeRecording(
@@ -11298,13 +11435,13 @@ async function cmdPlan(argv) {
11298
11435
  );
11299
11436
  const r = createReporter(flags.json === true);
11300
11437
  if (flags.fresh === true) {
11301
- await rm5(join18(dir, "doc.json"), { force: true });
11438
+ await rm5(join19(dir, "doc.json"), { force: true });
11302
11439
  r.log("note: --fresh discarded the existing doc.json");
11303
11440
  }
11304
11441
  let reuse;
11305
11442
  if (flags.reuse === true) {
11306
- const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
11307
- if (!existsSync15(from)) {
11443
+ const from = resolve9(strFlag(flags, "from") ?? join19(dir, PREV_DOC_NAME));
11444
+ if (!existsSync16(from)) {
11308
11445
  throw new UsageError(
11309
11446
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
11310
11447
  );
@@ -11315,7 +11452,7 @@ async function cmdPlan(argv) {
11315
11452
  };
11316
11453
  }
11317
11454
  const style = await resolveStyleRef(flags);
11318
- const hasDoc = existsSync15(join18(dir, "doc.json"));
11455
+ const hasDoc = existsSync16(join19(dir, "doc.json"));
11319
11456
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
11320
11457
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
11321
11458
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -11373,7 +11510,7 @@ async function cmdPlan(argv) {
11373
11510
  ...s.motion ? { motion: s.motion } : {},
11374
11511
  ...s.reuse ? { reuse: s.reuse } : {}
11375
11512
  },
11376
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join18(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
11513
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join19(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
11377
11514
  );
11378
11515
  return EXIT_OK;
11379
11516
  }
@@ -11499,7 +11636,7 @@ async function cmdRender(argv) {
11499
11636
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11500
11637
  throw new UsageError("--format must be webm or mp4");
11501
11638
  const format = fmtRaw;
11502
- const out = positionals[1] ?? join18(dir, `out.${format}`);
11639
+ const out = positionals[1] ?? join19(dir, `out.${format}`);
11503
11640
  const parallel = numFlag(flags, "parallel", 1);
11504
11641
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11505
11642
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11746,11 +11883,11 @@ async function resolveStyleRef(flags) {
11746
11883
  }
11747
11884
  async function resolveDocRef(ref, flags, what) {
11748
11885
  const styleRef = ref;
11749
- const file = existsSync15(styleRef) ? resolve9(
11886
+ const file = existsSync16(styleRef) ? resolve9(
11750
11887
  styleRef,
11751
- existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
11888
+ existsSync16(join19(styleRef, "doc.json")) ? "doc.json" : ""
11752
11889
  ) : null;
11753
- if (file && existsSync15(file)) {
11890
+ if (file && existsSync16(file)) {
11754
11891
  return {
11755
11892
  from: file,
11756
11893
  doc: migrateHostedDoc5(
@@ -11837,7 +11974,7 @@ async function cmdDigest(argv) {
11837
11974
  kinds,
11838
11975
  frames: d.images.full,
11839
11976
  crops: d.images.crop,
11840
- sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
11977
+ sheet: d.images.sheet ? join19(result.outDir, d.images.sheet) : null,
11841
11978
  scenes: kinds.scene ?? 0,
11842
11979
  sourceDuration: d.take.sourceDuration,
11843
11980
  outputDuration: d.take.outputDuration,
@@ -11865,7 +12002,7 @@ async function cmdOpen(argv) {
11865
12002
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
11866
12003
  const r = createReporter(flags.json === true);
11867
12004
  const take = await loadTake(dir);
11868
- if (!existsSync15(take.paths.recording)) {
12005
+ if (!existsSync16(take.paths.recording)) {
11869
12006
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
11870
12007
  }
11871
12008
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
@@ -11906,9 +12043,9 @@ async function cmdJudge(argv) {
11906
12043
  "vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
11907
12044
  );
11908
12045
  const r = createReporter(flags.json === true);
11909
- const kitFile = target.endsWith("kit.json") ? target : join18(target, "kit.json");
11910
- if (!existsSync15(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
11911
- if (!existsSync15(against))
12046
+ const kitFile = target.endsWith("kit.json") ? target : join19(target, "kit.json");
12047
+ if (!existsSync16(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
12048
+ if (!existsSync16(against))
11912
12049
  throw new UsageError(`${against}: no reference manifest`);
11913
12050
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
11914
12051
  const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
@@ -12030,7 +12167,7 @@ async function cmdValidate(argv) {
12030
12167
  const target = positionals[0];
12031
12168
  if (!target) throw new UsageError("vos validate <actions.json|take>");
12032
12169
  const r = createReporter(flags.json === true);
12033
- const kitTarget = target.endsWith("kit.json") ? target : existsSync15(join18(target, "kit.json")) && !isTakeDir(target) ? join18(target, "kit.json") : null;
12170
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync16(join19(target, "kit.json")) && !isTakeDir(target) ? join19(target, "kit.json") : null;
12034
12171
  if (kitTarget) {
12035
12172
  const picture = flags.picture === true;
12036
12173
  const verdict = await validateKit(kitTarget, { picture });
@@ -12062,9 +12199,9 @@ async function cmdValidate(argv) {
12062
12199
  r.done({ valid: true, target }, `${target}: valid actions file`);
12063
12200
  return EXIT_OK;
12064
12201
  }
12065
- if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
12202
+ if (!isTakeDir(target) && existsSync16(join19(target, "config.json"))) {
12066
12203
  const parsed = JSON.parse(
12067
- await readFile15(join18(target, "config.json"), "utf8")
12204
+ await readFile15(join19(target, "config.json"), "utf8")
12068
12205
  );
12069
12206
  const pre = preflightConfig(parsed);
12070
12207
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -12105,7 +12242,7 @@ async function cmdValidate(argv) {
12105
12242
  const take = await loadTake(target);
12106
12243
  const problems = [];
12107
12244
  const warnings = [];
12108
- if (!existsSync15(take.paths.recording)) {
12245
+ if (!existsSync16(take.paths.recording)) {
12109
12246
  problems.push("missing recording (re-run record)");
12110
12247
  } else {
12111
12248
  const mismatch = containerMismatch(
@@ -12346,6 +12483,8 @@ async function run(argv) {
12346
12483
  return await cmdRecipe(rest);
12347
12484
  case "brand":
12348
12485
  return await cmdBrand(rest);
12486
+ case "session":
12487
+ return await cmdSession(rest);
12349
12488
  case "actions":
12350
12489
  return await cmdActions(rest);
12351
12490
  default:
@@ -12379,6 +12518,121 @@ async function run(argv) {
12379
12518
  return EXIT_ERROR;
12380
12519
  }
12381
12520
  }
12521
+ async function cmdSession(argv) {
12522
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
12523
+ const [sub, arg] = positionals;
12524
+ const r = createReporter(flags.json === true);
12525
+ const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
12526
+ switch (sub) {
12527
+ case "open": {
12528
+ if (!arg) throw new UsageError(usage);
12529
+ const name = strFlag(flags, "name");
12530
+ const bad = sessionNameVerdict(name);
12531
+ if (bad) throw new UsageError(bad);
12532
+ let url;
12533
+ try {
12534
+ url = new URL(arg);
12535
+ } catch {
12536
+ throw new UsageError(`vos session open wants a URL, got "${arg}"`);
12537
+ }
12538
+ if (!/^https?:$/.test(url.protocol))
12539
+ throw new UsageError("vos session open wants an http(s) URL");
12540
+ await openSession(name, url.href, { log: r.log });
12541
+ const summary2 = await summarizeSession(name);
12542
+ r.done(
12543
+ {
12544
+ name: summary2.name,
12545
+ origins: summary2.origins,
12546
+ newestExpires: summary2.newestExpires,
12547
+ sessionOnly: summary2.sessionOnly
12548
+ },
12549
+ `${summaryLine(summary2)}
12550
+ Record on it: vos record --actions actions.json --out take --session ${name} --strict`
12551
+ );
12552
+ return summary2.origins.length ? EXIT_OK : EXIT_ERROR;
12553
+ }
12554
+ case "check": {
12555
+ if (!arg) throw new UsageError(usage);
12556
+ const bad = sessionNameVerdict(arg);
12557
+ if (bad) throw new UsageError(bad);
12558
+ if (!existsSync16(sessionDir(arg)))
12559
+ throw new UsageError(`no session "${arg}" (vos session list)`);
12560
+ const summary2 = await summarizeSession(arg);
12561
+ const url = strFlag(flags, "url");
12562
+ if (!url) {
12563
+ r.done(
12564
+ {
12565
+ name: arg,
12566
+ origins: summary2.origins,
12567
+ newestExpires: summary2.newestExpires,
12568
+ sessionOnly: summary2.sessionOnly
12569
+ },
12570
+ summaryLine(summary2)
12571
+ );
12572
+ return summary2.origins.length ? EXIT_OK : EXIT_WALL;
12573
+ }
12574
+ const verdict = await withSession(arg, async (ctx) => {
12575
+ const page = await ctx.newPage();
12576
+ const response = await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => null);
12577
+ const seen = await page.evaluate(WALL_PROBE).catch(() => null);
12578
+ return wallVerdict({
12579
+ askedUrl: url,
12580
+ landedUrl: page.url(),
12581
+ ...response ? { status: response.status() } : {},
12582
+ passwordFields: seen?.passwordFields ?? 0,
12583
+ newPasswordField: seen?.newPasswordField ?? false,
12584
+ oneTimeCodeField: seen?.oneTimeCodeField ?? false
12585
+ });
12586
+ });
12587
+ if (verdict) {
12588
+ r.done(
12589
+ {
12590
+ name: arg,
12591
+ ok: false,
12592
+ wall: verdict,
12593
+ origins: summary2.origins,
12594
+ newestExpires: summary2.newestExpires
12595
+ },
12596
+ `session "${arg}" no longer opens ${url}: ${verdict.message.split(". ")[0]}. It expired, or the sign-in never landed. Sign in again: vos session open ${url} --name ${arg}`
12597
+ );
12598
+ return EXIT_WALL;
12599
+ }
12600
+ r.done(
12601
+ {
12602
+ name: arg,
12603
+ ok: true,
12604
+ origins: summary2.origins,
12605
+ newestExpires: summary2.newestExpires
12606
+ },
12607
+ `session "${arg}" opens ${url} signed in. ${summaryLine(summary2)}`
12608
+ );
12609
+ return EXIT_OK;
12610
+ }
12611
+ case "list": {
12612
+ const rows = listSessions();
12613
+ r.done(
12614
+ { sessions: rows },
12615
+ rows.length ? rows.map(
12616
+ (x) => ` ${x.name} ${x.ageDays} day${x.ageDays === 1 ? "" : "s"} old`
12617
+ ).join("\n") : "no sessions. Make one: vos session open <url> --name <app>"
12618
+ );
12619
+ return EXIT_OK;
12620
+ }
12621
+ case "rm": {
12622
+ if (!arg) throw new UsageError(usage);
12623
+ const bad = sessionNameVerdict(arg);
12624
+ if (bad) throw new UsageError(bad);
12625
+ const gone = removeSession(arg);
12626
+ r.done(
12627
+ { name: arg, removed: gone },
12628
+ gone ? `removed session "${arg}"` : `no session "${arg}"`
12629
+ );
12630
+ return gone ? EXIT_OK : EXIT_ERROR;
12631
+ }
12632
+ default:
12633
+ throw new UsageError(usage);
12634
+ }
12635
+ }
12382
12636
 
12383
12637
  export {
12384
12638
  validateActions,
@@ -12411,4 +12665,4 @@ export {
12411
12665
  verbHelp,
12412
12666
  run
12413
12667
  };
12414
- //# sourceMappingURL=chunk-6PJPGBKH.js.map
12668
+ //# sourceMappingURL=chunk-JRRM7P5G.js.map