@vosjs/cli 0.44.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.
@@ -22,8 +22,8 @@ import {
22
22
 
23
23
  // src/plugin/run.ts
24
24
  import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
25
- import { existsSync as existsSync15 } from "fs";
26
- 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";
27
27
  import {
28
28
  END_CARD_FROM as END_CARD_FROM2,
29
29
  docOutputDuration as docOutputDuration3,
@@ -7043,12 +7043,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7043
7043
  const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
7044
7044
  const vw = actions.viewport?.width ?? 1280;
7045
7045
  const vh = actions.viewport?.height ?? 720;
7046
- const context = await browser.newContext({
7046
+ const context = opts.context ?? await browser.newContext({
7047
7047
  viewport: { width: vw, height: vh },
7048
7048
  deviceScaleFactor: 1,
7049
7049
  ...opts.storageState ? { storageState: opts.storageState } : {},
7050
7050
  ...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
7051
7051
  });
7052
+ if (opts.context && opts.headers && Object.keys(opts.headers).length)
7053
+ await context.setExtraHTTPHeaders(opts.headers);
7052
7054
  const masks = actions.mask ?? [];
7053
7055
  if (masks.length) await context.addInitScript(maskInitScript(masks));
7054
7056
  const exposureLog = new ExposureLog(masks);
@@ -7437,6 +7439,148 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7437
7439
  };
7438
7440
  }
7439
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
+
7440
7584
  // src/plugin/encode.ts
7441
7585
  var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.55.7?target=es2022";
7442
7586
  var ENCODE_HTML = `<!doctype html><html><head><meta charset="utf-8"></head><body>
@@ -7492,9 +7636,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
7492
7636
  }
7493
7637
 
7494
7638
  // src/plugin/plan.ts
7495
- import { existsSync as existsSync12 } from "fs";
7639
+ import { existsSync as existsSync13 } from "fs";
7496
7640
  import { readFile as readFile12 } from "fs/promises";
7497
- import { join as join14 } from "path";
7641
+ import { join as join15 } from "path";
7498
7642
  import {
7499
7643
  DEFAULT_FRAME_STYLE,
7500
7644
  STYLE_FIELDS,
@@ -7716,8 +7860,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
7716
7860
  // src/plugin/plan.ts
7717
7861
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
7718
7862
  async function readDigestActivity(dir) {
7719
- const file = join14(dir, "digest", "digest.json");
7720
- if (!existsSync12(file)) return null;
7863
+ const file = join15(dir, "digest", "digest.json");
7864
+ if (!existsSync13(file)) return null;
7721
7865
  try {
7722
7866
  const d = JSON.parse(await readFile12(file, "utf8"));
7723
7867
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -7929,15 +8073,15 @@ async function planTake(dir, opts = {}) {
7929
8073
 
7930
8074
  // src/plugin/sync.ts
7931
8075
  import { createHash } from "crypto";
7932
- import { existsSync as existsSync13 } from "fs";
8076
+ import { existsSync as existsSync14, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
7933
8077
  import { readFile as readFile13 } from "fs/promises";
7934
8078
  import { createInterface } from "readline/promises";
7935
- import { basename as basename3, join as join16 } from "path";
8079
+ import { basename as basename3, join as join17 } from "path";
7936
8080
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7937
8081
 
7938
8082
  // src/plugin/folder.ts
7939
8083
  import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
7940
- import { join as join15 } from "path";
8084
+ import { join as join16 } from "path";
7941
8085
  import { recipeHints } from "@vosjs/shared/frontmatter";
7942
8086
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
7943
8087
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
@@ -8113,18 +8257,18 @@ async function cmdPull(argv) {
8113
8257
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
8114
8258
  const payload = res.body;
8115
8259
  const out = strFlag(flags, "out") ?? folder.slug;
8116
- await mkdir8(join15(out, "recipes"), { recursive: true });
8260
+ await mkdir8(join16(out, "recipes"), { recursive: true });
8117
8261
  const lines = [];
8118
8262
  const recipes = [];
8119
8263
  for (const rec of payload.recipes) {
8120
- const path = join15(out, "recipes", safeName(rec.filename));
8264
+ const path = join16(out, "recipes", safeName(rec.filename));
8121
8265
  await writeFile10(path, rec.body ?? "");
8122
8266
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
8123
8267
  }
8124
8268
  for (const rec of payload.inheritedRecipes) {
8125
8269
  const from = safeName(rec.folderSlug ?? "inherited");
8126
- await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
8127
- const path = join15(
8270
+ await mkdir8(join16(out, "recipes", "_inherited", from), { recursive: true });
8271
+ const path = join16(
8128
8272
  out,
8129
8273
  "recipes",
8130
8274
  "_inherited",
@@ -8139,7 +8283,7 @@ async function cmdPull(argv) {
8139
8283
  }
8140
8284
  const members = [];
8141
8285
  for (const v of payload.voses) {
8142
- 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));
8143
8287
  await mkdir8(dir, { recursive: true });
8144
8288
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
8145
8289
  if (cfg.status !== 200) {
@@ -8147,7 +8291,7 @@ async function cmdPull(argv) {
8147
8291
  continue;
8148
8292
  }
8149
8293
  await writeFile10(
8150
- join15(dir, "config.json"),
8294
+ join16(dir, "config.json"),
8151
8295
  JSON.stringify(cfg.body.config, null, 2)
8152
8296
  );
8153
8297
  let take = false;
@@ -8159,7 +8303,7 @@ async function cmdPull(argv) {
8159
8303
  );
8160
8304
  if (doc.status === 200) {
8161
8305
  await writeFile10(
8162
- join15(dir, "doc.json"),
8306
+ join16(dir, "doc.json"),
8163
8307
  JSON.stringify(doc.body, null, 2)
8164
8308
  );
8165
8309
  take = true;
@@ -8428,7 +8572,24 @@ function programPushTarget(ask) {
8428
8572
  }
8429
8573
  return { kind: "version", vosId: tracked, tracked: true };
8430
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
+ }
8431
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
+ );
8432
8593
  const take = await loadTake(dir);
8433
8594
  if (!take.doc) {
8434
8595
  throw new Error("no doc.json in this take \u2014 run `vos plan` first");
@@ -8438,7 +8599,7 @@ async function pushTake(dir, flags, r) {
8438
8599
  throw new Error(`doc.json fails lint:
8439
8600
  ${lint.problems.join("\n ")}`);
8440
8601
  }
8441
- if (!existsSync13(take.paths.recording)) {
8602
+ if (!existsSync14(take.paths.recording)) {
8442
8603
  throw new Error(
8443
8604
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
8444
8605
  );
@@ -8530,8 +8691,8 @@ async function pushTake(dir, flags, r) {
8530
8691
  delete docForPush.source.micKey;
8531
8692
  }
8532
8693
  for (const ref of docMediaRefs(docForPush)) {
8533
- const file = join16(dir, takeRelativeFile(ref.key));
8534
- if (!existsSync13(file)) {
8694
+ const file = join17(dir, takeRelativeFile(ref.key));
8695
+ if (!existsSync14(file)) {
8535
8696
  r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
8536
8697
  continue;
8537
8698
  }
@@ -8675,9 +8836,9 @@ async function pullTake(dir, flags, r) {
8675
8836
  };
8676
8837
  }
8677
8838
  let media2;
8678
- if (flags.media && existsSync13(join16(dir, "doc.json"))) {
8839
+ if (flags.media && existsSync14(join17(dir, "doc.json"))) {
8679
8840
  const local = JSON.parse(
8680
- await readFile13(join16(dir, "doc.json"), "utf8")
8841
+ await readFile13(join17(dir, "doc.json"), "utf8")
8681
8842
  );
8682
8843
  media2 = await pullMedia(ctx, dir, local, r.log);
8683
8844
  }
@@ -8738,10 +8899,10 @@ async function pullTake(dir, flags, r) {
8738
8899
  const hostedDoc = migrateHostedDoc3(docRes.json);
8739
8900
  const doc = hostedDoc;
8740
8901
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8741
- if (existsSync13(localRecording)) {
8902
+ if (existsSync14(localRecording)) {
8742
8903
  doc.source.videoKey = basename3(localRecording);
8743
8904
  }
8744
- await writeJson(join16(dir, "doc.json"), doc, true);
8905
+ await writeJson(join17(dir, "doc.json"), doc, true);
8745
8906
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
8746
8907
  const versions = await api(ctx, `/vos/${vosId}/versions`);
8747
8908
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -8760,10 +8921,10 @@ async function pullTake(dir, flags, r) {
8760
8921
  }
8761
8922
 
8762
8923
  // src/plugin/program.ts
8763
- import { existsSync as existsSync14 } from "fs";
8924
+ import { existsSync as existsSync15 } from "fs";
8764
8925
  import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8765
8926
  import { createInterface as createInterface2 } from "readline/promises";
8766
- 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";
8767
8928
  import {
8768
8929
  CURRENT_CONFIG_VERSION,
8769
8930
  migrateConfig,
@@ -8933,8 +9094,8 @@ function preflightConfig(parsed) {
8933
9094
  }
8934
9095
  function resolveConfigPath(target) {
8935
9096
  if (target.endsWith(".json")) return target;
8936
- const inDir = join17(target, "config.json");
8937
- if (existsSync14(inDir)) return inDir;
9097
+ const inDir = join18(target, "config.json");
9098
+ if (existsSync15(inDir)) return inDir;
8938
9099
  throw new UsageError(
8939
9100
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
8940
9101
  );
@@ -8962,7 +9123,7 @@ async function cmdFetch(argv) {
8962
9123
  const out = strFlag(flags, "out") ?? slug;
8963
9124
  await mkdir9(out, { recursive: true });
8964
9125
  await writeFile11(
8965
- join17(out, "config.json"),
9126
+ join18(out, "config.json"),
8966
9127
  JSON.stringify(cfg.body.config, null, 2)
8967
9128
  );
8968
9129
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -8987,12 +9148,12 @@ async function cmdFetch(argv) {
8987
9148
  const hosted = migrateHostedDoc4(doc.body);
8988
9149
  const { config: own } = await writeProgramDoc(out, hosted);
8989
9150
  if (own) {
8990
- await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
9151
+ await writeFile11(join18(out, "config.json"), JSON.stringify(own, null, 2));
8991
9152
  }
8992
9153
  } else if (doc.status === 200) {
8993
9154
  take = true;
8994
9155
  const hosted = migrateHostedDoc4(doc.body);
8995
- await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
9156
+ await writeFile11(join18(out, "doc.json"), JSON.stringify(hosted, null, 2));
8996
9157
  if (flags.media === true) {
8997
9158
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
8998
9159
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -9355,10 +9516,10 @@ async function cmdPullProgram(argv) {
9355
9516
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
9356
9517
  if (cfg.status !== 200)
9357
9518
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
9358
- const configPath = join17(dir, "config.json");
9519
+ const configPath = join18(dir, "config.json");
9359
9520
  let backedUp = false;
9360
- if (existsSync14(configPath)) {
9361
- 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));
9362
9523
  backedUp = true;
9363
9524
  }
9364
9525
  let headConfig = cfg.body.config;
@@ -9391,7 +9552,7 @@ async function cmdPullProgram(argv) {
9391
9552
  protected: protectedIds,
9392
9553
  changes,
9393
9554
  out: configPath,
9394
- backup: backedUp ? join17(dir, "config.backup.json") : null
9555
+ backup: backedUp ? join18(dir, "config.backup.json") : null
9395
9556
  },
9396
9557
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
9397
9558
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -9453,8 +9614,8 @@ function storedProgramConfig(config, programDoc) {
9453
9614
  return lowerProgramDoc(doc).config;
9454
9615
  }
9455
9616
  async function readProgramDoc(dir, config) {
9456
- const docPath = join17(dir, "doc.json");
9457
- if (!existsSync14(docPath)) return null;
9617
+ const docPath = join18(dir, "doc.json");
9618
+ if (!existsSync15(docPath)) return null;
9458
9619
  const parsed = JSON.parse(await readFile14(docPath, "utf8"));
9459
9620
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
9460
9621
  return null;
@@ -9466,14 +9627,14 @@ async function writeProgramDoc(dir, hosted) {
9466
9627
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
9467
9628
  const { config, ...rest } = program;
9468
9629
  const onDisk = { ...hosted, program: rest };
9469
- await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9630
+ await writeFile11(join18(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9470
9631
  return {
9471
9632
  config: config && typeof config === "object" ? config : null
9472
9633
  };
9473
9634
  }
9474
9635
 
9475
9636
  // src/plugin/asset.ts
9476
- import { readFileSync as readFileSync4 } from "fs";
9637
+ import { readFileSync as readFileSync5 } from "fs";
9477
9638
  import { basename as basename5, extname as extname2 } from "path";
9478
9639
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
9479
9640
  var MIME_BY_EXT = {
@@ -9527,7 +9688,7 @@ async function cmdPush(argv) {
9527
9688
  for (const file of positionals) {
9528
9689
  const name = basename5(file);
9529
9690
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
9530
- const body = readFileSync4(file);
9691
+ const body = readFileSync5(file);
9531
9692
  const form = new FormData();
9532
9693
  form.set("file", new Blob([body], { type: mime }), name);
9533
9694
  if (folder) form.set("folderId", folder.id);
@@ -9581,7 +9742,7 @@ async function cmdAsset(argv) {
9581
9742
  }
9582
9743
 
9583
9744
  // src/plugin/recipe.ts
9584
- import { readFileSync as readFileSync5 } from "fs";
9745
+ import { readFileSync as readFileSync6 } from "fs";
9585
9746
  import { basename as basename6 } from "path";
9586
9747
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
9587
9748
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -9601,7 +9762,7 @@ async function cmdPush2(argv) {
9601
9762
  throw new UsageError(USAGE);
9602
9763
  }
9603
9764
  const filename = recipeFilename(file);
9604
- const body = readFileSync5(file);
9765
+ const body = readFileSync6(file);
9605
9766
  const r = createReporter(flags.json === true);
9606
9767
  const origin = platformOrigin({
9607
9768
  origin: strFlag(flags, "origin"),
@@ -10602,8 +10763,8 @@ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "h
10602
10763
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10603
10764
 
10604
10765
  Take pipeline
10605
- 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]
10606
- 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]
10607
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]
10608
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]
10609
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]
@@ -10615,6 +10776,9 @@ Take pipeline
10615
10776
  vos validate <actions.json|take|kit.json> [--picture] [--json]
10616
10777
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
10617
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
10618
10782
  vos actions script <actions.json> [--json]
10619
10783
  the flow as numbered beats in words, for a person recording it by hand
10620
10784
 
@@ -10913,10 +11077,35 @@ function takeStorageState(flags) {
10913
11077
  const raw = strFlag(flags, "storage-state");
10914
11078
  if (!raw) return void 0;
10915
11079
  const path = resolve9(raw);
10916
- if (!existsSync15(path))
11080
+ if (!existsSync16(path))
10917
11081
  throw new UsageError(`--storage-state: no such file: ${path}`);
10918
11082
  return path;
10919
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
+ }
10920
11109
  async function takeBackdrop(flags, r) {
10921
11110
  const { backdrop, note } = await openingBackdrop(
10922
11111
  strFlag(flags, "background"),
@@ -10974,6 +11163,7 @@ async function cmdRecord(argv) {
10974
11163
  const backdrop = await takeBackdrop(flags, r);
10975
11164
  const maxDurationSeconds = await maxDuration(flags, r);
10976
11165
  const storageState = takeStorageState(flags);
11166
+ const sessionName = takeSessionName(flags, storageState);
10977
11167
  const browserArgs = takeBrowserArgs(multi);
10978
11168
  const headers = takeHeaders(multi);
10979
11169
  if (flags["dry-run"] === true) {
@@ -10984,19 +11174,17 @@ async function cmdRecord(argv) {
10984
11174
  r.log("rehearsing (nothing is captured or written)\u2026");
10985
11175
  r.event({ event: "phase", phase: "rehearse" });
10986
11176
  const started = Date.now();
10987
- const rec = await recordTake(
10988
- browser2,
10989
- url,
11177
+ const rec = await inSession(
11178
+ sessionName,
10990
11179
  actions,
10991
- takePaths(outDir),
10992
- r.log,
10993
- {
11180
+ (context) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
10994
11181
  storageState,
10995
11182
  headers,
11183
+ context,
10996
11184
  dryRun: true,
10997
11185
  // A rehearsal is always strict, and it touches no directory.
10998
11186
  onArrival: wallGate(flags, r, { strict: true })
10999
- }
11187
+ })
11000
11188
  );
11001
11189
  const steps = rec.meta.steps ?? [];
11002
11190
  const lines = steps.map((s) => {
@@ -11009,6 +11197,7 @@ async function cmdRecord(argv) {
11009
11197
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
11010
11198
  ...browserArgs.map((a) => `--browser-arg=${a}`),
11011
11199
  ...Object.entries(headers).map(([k, v]) => `--header ${k}=${v}`),
11200
+ ...sessionName ? [`--session ${sessionName}`] : [],
11012
11201
  ...flags["allow-wall"] === true ? ["--allow-wall"] : []
11013
11202
  ].join(" ");
11014
11203
  const nextOut = strFlag(flags, "out") ?? "take";
@@ -11034,7 +11223,7 @@ ${lines.join("\n")}
11034
11223
  }
11035
11224
  const paths = takePaths(outDir);
11036
11225
  const prepare = async () => {
11037
- if (existsSync15(join18(outDir, "meta.json"))) {
11226
+ if (existsSync16(join19(outDir, "meta.json"))) {
11038
11227
  const { prevDoc, kept } = await prepareReRecord(outDir);
11039
11228
  r.log(
11040
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" : "")
@@ -11047,15 +11236,20 @@ ${lines.join("\n")}
11047
11236
  try {
11048
11237
  r.log("recording\u2026");
11049
11238
  r.event({ event: "phase", phase: "record" });
11050
- const rec = await recordTake(browser, url, actions, paths, r.log, {
11051
- maxDurationSeconds,
11052
- storageState,
11053
- headers,
11054
- onArrival: wallGate(flags, r, {
11055
- strict: flags.strict === true,
11056
- 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
+ })
11057
11251
  })
11058
- });
11252
+ );
11059
11253
  r.event({ event: "phase", phase: "encode" });
11060
11254
  r.log("encoding\u2026");
11061
11255
  const enc = await encodeRecording(
@@ -11093,7 +11287,7 @@ ${lines.join("\n")}
11093
11287
  },
11094
11288
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
11095
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}
11096
- 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)}`
11097
11291
  );
11098
11292
  return strictFail ? EXIT_USAGE : EXIT_OK;
11099
11293
  } finally {
@@ -11122,7 +11316,7 @@ async function cmdCreate2(argv) {
11122
11316
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11123
11317
  throw new UsageError("--format must be webm or mp4");
11124
11318
  const format = fmtRaw;
11125
- const out = positionals[0] ?? join18(outDir, `out.${format}`);
11319
+ const out = positionals[0] ?? join19(outDir, `out.${format}`);
11126
11320
  const parallel = numFlag(flags, "parallel", 1);
11127
11321
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11128
11322
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11130,11 +11324,12 @@ async function cmdCreate2(argv) {
11130
11324
  const backdrop = await takeBackdrop(flags, r);
11131
11325
  const maxDurationSeconds = await maxDuration(flags, r);
11132
11326
  const storageState = takeStorageState(flags);
11327
+ const sessionName = takeSessionName(flags, storageState);
11133
11328
  const browserArgs = takeBrowserArgs(multi);
11134
11329
  const headers = takeHeaders(multi);
11135
11330
  const paths = takePaths(outDir);
11136
11331
  const prepare = async () => {
11137
- if (existsSync15(join18(outDir, "meta.json"))) {
11332
+ if (existsSync16(join19(outDir, "meta.json"))) {
11138
11333
  const { prevDoc, kept } = await prepareReRecord(outDir);
11139
11334
  r.log(
11140
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" : "")
@@ -11147,15 +11342,20 @@ async function cmdCreate2(argv) {
11147
11342
  try {
11148
11343
  r.log("recording\u2026");
11149
11344
  r.event({ event: "phase", phase: "record" });
11150
- const rec = await recordTake(browser, url, actions, paths, r.log, {
11151
- maxDurationSeconds,
11152
- storageState,
11153
- headers,
11154
- onArrival: wallGate(flags, r, {
11155
- strict: flags.strict === true,
11156
- 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
+ })
11157
11357
  })
11158
- });
11358
+ );
11159
11359
  r.event({ event: "phase", phase: "encode" });
11160
11360
  r.log("encoding\u2026");
11161
11361
  await encodeRecording(
@@ -11235,13 +11435,13 @@ async function cmdPlan(argv) {
11235
11435
  );
11236
11436
  const r = createReporter(flags.json === true);
11237
11437
  if (flags.fresh === true) {
11238
- await rm5(join18(dir, "doc.json"), { force: true });
11438
+ await rm5(join19(dir, "doc.json"), { force: true });
11239
11439
  r.log("note: --fresh discarded the existing doc.json");
11240
11440
  }
11241
11441
  let reuse;
11242
11442
  if (flags.reuse === true) {
11243
- const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
11244
- if (!existsSync15(from)) {
11443
+ const from = resolve9(strFlag(flags, "from") ?? join19(dir, PREV_DOC_NAME));
11444
+ if (!existsSync16(from)) {
11245
11445
  throw new UsageError(
11246
11446
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
11247
11447
  );
@@ -11252,7 +11452,7 @@ async function cmdPlan(argv) {
11252
11452
  };
11253
11453
  }
11254
11454
  const style = await resolveStyleRef(flags);
11255
- const hasDoc = existsSync15(join18(dir, "doc.json"));
11455
+ const hasDoc = existsSync16(join19(dir, "doc.json"));
11256
11456
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
11257
11457
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
11258
11458
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -11310,7 +11510,7 @@ async function cmdPlan(argv) {
11310
11510
  ...s.motion ? { motion: s.motion } : {},
11311
11511
  ...s.reuse ? { reuse: s.reuse } : {}
11312
11512
  },
11313
- `${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}`
11314
11514
  );
11315
11515
  return EXIT_OK;
11316
11516
  }
@@ -11436,7 +11636,7 @@ async function cmdRender(argv) {
11436
11636
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11437
11637
  throw new UsageError("--format must be webm or mp4");
11438
11638
  const format = fmtRaw;
11439
- const out = positionals[1] ?? join18(dir, `out.${format}`);
11639
+ const out = positionals[1] ?? join19(dir, `out.${format}`);
11440
11640
  const parallel = numFlag(flags, "parallel", 1);
11441
11641
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11442
11642
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11683,11 +11883,11 @@ async function resolveStyleRef(flags) {
11683
11883
  }
11684
11884
  async function resolveDocRef(ref, flags, what) {
11685
11885
  const styleRef = ref;
11686
- const file = existsSync15(styleRef) ? resolve9(
11886
+ const file = existsSync16(styleRef) ? resolve9(
11687
11887
  styleRef,
11688
- existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
11888
+ existsSync16(join19(styleRef, "doc.json")) ? "doc.json" : ""
11689
11889
  ) : null;
11690
- if (file && existsSync15(file)) {
11890
+ if (file && existsSync16(file)) {
11691
11891
  return {
11692
11892
  from: file,
11693
11893
  doc: migrateHostedDoc5(
@@ -11774,7 +11974,7 @@ async function cmdDigest(argv) {
11774
11974
  kinds,
11775
11975
  frames: d.images.full,
11776
11976
  crops: d.images.crop,
11777
- sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
11977
+ sheet: d.images.sheet ? join19(result.outDir, d.images.sheet) : null,
11778
11978
  scenes: kinds.scene ?? 0,
11779
11979
  sourceDuration: d.take.sourceDuration,
11780
11980
  outputDuration: d.take.outputDuration,
@@ -11802,7 +12002,7 @@ async function cmdOpen(argv) {
11802
12002
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
11803
12003
  const r = createReporter(flags.json === true);
11804
12004
  const take = await loadTake(dir);
11805
- if (!existsSync15(take.paths.recording)) {
12005
+ if (!existsSync16(take.paths.recording)) {
11806
12006
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
11807
12007
  }
11808
12008
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
@@ -11843,9 +12043,9 @@ async function cmdJudge(argv) {
11843
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)"
11844
12044
  );
11845
12045
  const r = createReporter(flags.json === true);
11846
- const kitFile = target.endsWith("kit.json") ? target : join18(target, "kit.json");
11847
- if (!existsSync15(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
11848
- 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))
11849
12049
  throw new UsageError(`${against}: no reference manifest`);
11850
12050
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
11851
12051
  const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
@@ -11967,7 +12167,7 @@ async function cmdValidate(argv) {
11967
12167
  const target = positionals[0];
11968
12168
  if (!target) throw new UsageError("vos validate <actions.json|take>");
11969
12169
  const r = createReporter(flags.json === true);
11970
- 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;
11971
12171
  if (kitTarget) {
11972
12172
  const picture = flags.picture === true;
11973
12173
  const verdict = await validateKit(kitTarget, { picture });
@@ -11999,9 +12199,9 @@ async function cmdValidate(argv) {
11999
12199
  r.done({ valid: true, target }, `${target}: valid actions file`);
12000
12200
  return EXIT_OK;
12001
12201
  }
12002
- if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
12202
+ if (!isTakeDir(target) && existsSync16(join19(target, "config.json"))) {
12003
12203
  const parsed = JSON.parse(
12004
- await readFile15(join18(target, "config.json"), "utf8")
12204
+ await readFile15(join19(target, "config.json"), "utf8")
12005
12205
  );
12006
12206
  const pre = preflightConfig(parsed);
12007
12207
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -12042,7 +12242,7 @@ async function cmdValidate(argv) {
12042
12242
  const take = await loadTake(target);
12043
12243
  const problems = [];
12044
12244
  const warnings = [];
12045
- if (!existsSync15(take.paths.recording)) {
12245
+ if (!existsSync16(take.paths.recording)) {
12046
12246
  problems.push("missing recording (re-run record)");
12047
12247
  } else {
12048
12248
  const mismatch = containerMismatch(
@@ -12283,6 +12483,8 @@ async function run(argv) {
12283
12483
  return await cmdRecipe(rest);
12284
12484
  case "brand":
12285
12485
  return await cmdBrand(rest);
12486
+ case "session":
12487
+ return await cmdSession(rest);
12286
12488
  case "actions":
12287
12489
  return await cmdActions(rest);
12288
12490
  default:
@@ -12316,6 +12518,121 @@ async function run(argv) {
12316
12518
  return EXIT_ERROR;
12317
12519
  }
12318
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
+ }
12319
12636
 
12320
12637
  export {
12321
12638
  validateActions,
@@ -12348,4 +12665,4 @@ export {
12348
12665
  verbHelp,
12349
12666
  run
12350
12667
  };
12351
- //# sourceMappingURL=chunk-WCSAECM4.js.map
12668
+ //# sourceMappingURL=chunk-JRRM7P5G.js.map