@vosjs/cli 0.44.0 → 0.45.1

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,149 @@ 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
+ const leave = process.platform === "darwin" ? "then quit Chrome with \u2318Q (closing the window alone leaves it running, and this waiting)" : "then close the window";
7502
+ opts.log(
7503
+ `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
7504
+ );
7505
+ return new Promise((resolve10, reject) => {
7506
+ const child = spawn(exe, args, { stdio: "ignore" });
7507
+ child.on("error", reject);
7508
+ child.on("exit", (code) => resolve10({ code }));
7509
+ });
7510
+ }
7511
+ async function withSession(name, fn, opts = {}) {
7512
+ const dir = sessionDir(name);
7513
+ if (!existsSync12(dir))
7514
+ throw new Error(`no session "${name}" (vos session list)`);
7515
+ const explicit = process.env.VOS_BROWSER_PATH;
7516
+ const ctx = await chromium.launchPersistentContext(dir, {
7517
+ headless: opts.headless ?? true,
7518
+ ...explicit ? { executablePath: explicit } : { channel: "chrome" },
7519
+ // The real keychain: the whole reason a profile Chrome wrote can be read.
7520
+ ignoreDefaultArgs: ["--use-mock-keychain"],
7521
+ ...opts.viewport ? { viewport: opts.viewport, deviceScaleFactor: 1 } : {}
7522
+ });
7523
+ try {
7524
+ return await fn(ctx);
7525
+ } finally {
7526
+ await ctx.close().catch(() => {
7527
+ });
7528
+ }
7529
+ }
7530
+ async function summarizeSession(name) {
7531
+ const dir = sessionDir(name);
7532
+ const created = statSync3(dir).birthtimeMs || statSync3(dir).mtimeMs;
7533
+ return withSession(name, async (ctx) => {
7534
+ const cookies = await ctx.cookies();
7535
+ const byHost = /* @__PURE__ */ new Map();
7536
+ let newest = 0;
7537
+ let sessionOnly = 0;
7538
+ for (const c of cookies) {
7539
+ const host = c.domain.replace(/^\./, "");
7540
+ byHost.set(host, (byHost.get(host) ?? 0) + 1);
7541
+ if (c.expires && c.expires > 0) newest = Math.max(newest, c.expires);
7542
+ else sessionOnly++;
7543
+ }
7544
+ return {
7545
+ name,
7546
+ origins: [...byHost.entries()].map(([host, n]) => ({ host, cookies: n })).sort((a, b) => b.cookies - a.cookies),
7547
+ newestExpires: newest ? new Date(newest * 1e3).toISOString() : null,
7548
+ sessionOnly,
7549
+ ageDays: Math.floor((Date.now() - created) / 864e5)
7550
+ };
7551
+ });
7552
+ }
7553
+ function summaryLine(s) {
7554
+ if (!s.origins.length)
7555
+ return `session "${s.name}" holds no cookies: the window closed before a sign-in landed, or the sign-in never happened`;
7556
+ const hosts = s.origins.slice(0, 4).map((o) => `${o.host} (${o.cookies})`).join(", ");
7557
+ const more = s.origins.length > 4 ? ` and ${s.origins.length - 4} more` : "";
7558
+ 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";
7559
+ return `session "${s.name}" holds cookies for ${hosts}${more}; ${exp}`;
7560
+ }
7561
+ function listSessions() {
7562
+ if (!existsSync12(SESSIONS_DIR)) return [];
7563
+ return readdirSync2(SESSIONS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && NAME.test(d.name)).map((d) => {
7564
+ const st = statSync3(join14(SESSIONS_DIR, d.name));
7565
+ return {
7566
+ name: d.name,
7567
+ ageDays: Math.floor(
7568
+ (Date.now() - (st.birthtimeMs || st.mtimeMs)) / 864e5
7569
+ )
7570
+ };
7571
+ }).sort((a, b) => a.name.localeCompare(b.name));
7572
+ }
7573
+ function removeSession(name) {
7574
+ const dir = sessionDir(name);
7575
+ if (!existsSync12(dir)) return false;
7576
+ rmSync(dir, { recursive: true, force: true });
7577
+ return true;
7578
+ }
7579
+ function looksLikeStorageState(value) {
7580
+ if (typeof value !== "object" || value === null) return false;
7581
+ const v = value;
7582
+ return Array.isArray(v.cookies) && Array.isArray(v.origins);
7583
+ }
7584
+
7440
7585
  // src/plugin/encode.ts
7441
7586
  var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.55.7?target=es2022";
7442
7587
  var ENCODE_HTML = `<!doctype html><html><head><meta charset="utf-8"></head><body>
@@ -7492,9 +7637,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
7492
7637
  }
7493
7638
 
7494
7639
  // src/plugin/plan.ts
7495
- import { existsSync as existsSync12 } from "fs";
7640
+ import { existsSync as existsSync13 } from "fs";
7496
7641
  import { readFile as readFile12 } from "fs/promises";
7497
- import { join as join14 } from "path";
7642
+ import { join as join15 } from "path";
7498
7643
  import {
7499
7644
  DEFAULT_FRAME_STYLE,
7500
7645
  STYLE_FIELDS,
@@ -7716,8 +7861,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
7716
7861
  // src/plugin/plan.ts
7717
7862
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
7718
7863
  async function readDigestActivity(dir) {
7719
- const file = join14(dir, "digest", "digest.json");
7720
- if (!existsSync12(file)) return null;
7864
+ const file = join15(dir, "digest", "digest.json");
7865
+ if (!existsSync13(file)) return null;
7721
7866
  try {
7722
7867
  const d = JSON.parse(await readFile12(file, "utf8"));
7723
7868
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -7929,15 +8074,15 @@ async function planTake(dir, opts = {}) {
7929
8074
 
7930
8075
  // src/plugin/sync.ts
7931
8076
  import { createHash } from "crypto";
7932
- import { existsSync as existsSync13 } from "fs";
8077
+ import { existsSync as existsSync14, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
7933
8078
  import { readFile as readFile13 } from "fs/promises";
7934
8079
  import { createInterface } from "readline/promises";
7935
- import { basename as basename3, join as join16 } from "path";
8080
+ import { basename as basename3, join as join17 } from "path";
7936
8081
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7937
8082
 
7938
8083
  // src/plugin/folder.ts
7939
8084
  import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
7940
- import { join as join15 } from "path";
8085
+ import { join as join16 } from "path";
7941
8086
  import { recipeHints } from "@vosjs/shared/frontmatter";
7942
8087
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
7943
8088
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
@@ -8113,18 +8258,18 @@ async function cmdPull(argv) {
8113
8258
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
8114
8259
  const payload = res.body;
8115
8260
  const out = strFlag(flags, "out") ?? folder.slug;
8116
- await mkdir8(join15(out, "recipes"), { recursive: true });
8261
+ await mkdir8(join16(out, "recipes"), { recursive: true });
8117
8262
  const lines = [];
8118
8263
  const recipes = [];
8119
8264
  for (const rec of payload.recipes) {
8120
- const path = join15(out, "recipes", safeName(rec.filename));
8265
+ const path = join16(out, "recipes", safeName(rec.filename));
8121
8266
  await writeFile10(path, rec.body ?? "");
8122
8267
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
8123
8268
  }
8124
8269
  for (const rec of payload.inheritedRecipes) {
8125
8270
  const from = safeName(rec.folderSlug ?? "inherited");
8126
- await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
8127
- const path = join15(
8271
+ await mkdir8(join16(out, "recipes", "_inherited", from), { recursive: true });
8272
+ const path = join16(
8128
8273
  out,
8129
8274
  "recipes",
8130
8275
  "_inherited",
@@ -8139,7 +8284,7 @@ async function cmdPull(argv) {
8139
8284
  }
8140
8285
  const members = [];
8141
8286
  for (const v of payload.voses) {
8142
- const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
8287
+ const dir = join16(out, "members", safeName(v.slug || v.title || v.id));
8143
8288
  await mkdir8(dir, { recursive: true });
8144
8289
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
8145
8290
  if (cfg.status !== 200) {
@@ -8147,7 +8292,7 @@ async function cmdPull(argv) {
8147
8292
  continue;
8148
8293
  }
8149
8294
  await writeFile10(
8150
- join15(dir, "config.json"),
8295
+ join16(dir, "config.json"),
8151
8296
  JSON.stringify(cfg.body.config, null, 2)
8152
8297
  );
8153
8298
  let take = false;
@@ -8159,7 +8304,7 @@ async function cmdPull(argv) {
8159
8304
  );
8160
8305
  if (doc.status === 200) {
8161
8306
  await writeFile10(
8162
- join15(dir, "doc.json"),
8307
+ join16(dir, "doc.json"),
8163
8308
  JSON.stringify(doc.body, null, 2)
8164
8309
  );
8165
8310
  take = true;
@@ -8428,7 +8573,24 @@ function programPushTarget(ask) {
8428
8573
  }
8429
8574
  return { kind: "version", vosId: tracked, tracked: true };
8430
8575
  }
8576
+ function storageStateInDir(dir) {
8577
+ for (const f of readdirSync3(dir)) {
8578
+ if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
8579
+ continue;
8580
+ try {
8581
+ const v = JSON.parse(readFileSync4(join17(dir, f), "utf8"));
8582
+ if (looksLikeStorageState(v)) return join17(dir, f);
8583
+ } catch {
8584
+ }
8585
+ }
8586
+ return null;
8587
+ }
8431
8588
  async function pushTake(dir, flags, r) {
8589
+ const stateFile = storageStateInDir(dir);
8590
+ if (stateFile)
8591
+ throw new Error(
8592
+ `${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`
8593
+ );
8432
8594
  const take = await loadTake(dir);
8433
8595
  if (!take.doc) {
8434
8596
  throw new Error("no doc.json in this take \u2014 run `vos plan` first");
@@ -8438,7 +8600,7 @@ async function pushTake(dir, flags, r) {
8438
8600
  throw new Error(`doc.json fails lint:
8439
8601
  ${lint.problems.join("\n ")}`);
8440
8602
  }
8441
- if (!existsSync13(take.paths.recording)) {
8603
+ if (!existsSync14(take.paths.recording)) {
8442
8604
  throw new Error(
8443
8605
  "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
8444
8606
  );
@@ -8530,8 +8692,8 @@ async function pushTake(dir, flags, r) {
8530
8692
  delete docForPush.source.micKey;
8531
8693
  }
8532
8694
  for (const ref of docMediaRefs(docForPush)) {
8533
- const file = join16(dir, takeRelativeFile(ref.key));
8534
- if (!existsSync13(file)) {
8695
+ const file = join17(dir, takeRelativeFile(ref.key));
8696
+ if (!existsSync14(file)) {
8535
8697
  r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
8536
8698
  continue;
8537
8699
  }
@@ -8675,9 +8837,9 @@ async function pullTake(dir, flags, r) {
8675
8837
  };
8676
8838
  }
8677
8839
  let media2;
8678
- if (flags.media && existsSync13(join16(dir, "doc.json"))) {
8840
+ if (flags.media && existsSync14(join17(dir, "doc.json"))) {
8679
8841
  const local = JSON.parse(
8680
- await readFile13(join16(dir, "doc.json"), "utf8")
8842
+ await readFile13(join17(dir, "doc.json"), "utf8")
8681
8843
  );
8682
8844
  media2 = await pullMedia(ctx, dir, local, r.log);
8683
8845
  }
@@ -8738,10 +8900,10 @@ async function pullTake(dir, flags, r) {
8738
8900
  const hostedDoc = migrateHostedDoc3(docRes.json);
8739
8901
  const doc = hostedDoc;
8740
8902
  const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8741
- if (existsSync13(localRecording)) {
8903
+ if (existsSync14(localRecording)) {
8742
8904
  doc.source.videoKey = basename3(localRecording);
8743
8905
  }
8744
- await writeJson(join16(dir, "doc.json"), doc, true);
8906
+ await writeJson(join17(dir, "doc.json"), doc, true);
8745
8907
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
8746
8908
  const versions = await api(ctx, `/vos/${vosId}/versions`);
8747
8909
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -8760,10 +8922,10 @@ async function pullTake(dir, flags, r) {
8760
8922
  }
8761
8923
 
8762
8924
  // src/plugin/program.ts
8763
- import { existsSync as existsSync14 } from "fs";
8925
+ import { existsSync as existsSync15 } from "fs";
8764
8926
  import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8765
8927
  import { createInterface as createInterface2 } from "readline/promises";
8766
- import { basename as basename4, dirname as dirname4, join as join17 } from "path";
8928
+ import { basename as basename4, dirname as dirname4, join as join18 } from "path";
8767
8929
  import {
8768
8930
  CURRENT_CONFIG_VERSION,
8769
8931
  migrateConfig,
@@ -8933,8 +9095,8 @@ function preflightConfig(parsed) {
8933
9095
  }
8934
9096
  function resolveConfigPath(target) {
8935
9097
  if (target.endsWith(".json")) return target;
8936
- const inDir = join17(target, "config.json");
8937
- if (existsSync14(inDir)) return inDir;
9098
+ const inDir = join18(target, "config.json");
9099
+ if (existsSync15(inDir)) return inDir;
8938
9100
  throw new UsageError(
8939
9101
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
8940
9102
  );
@@ -8962,7 +9124,7 @@ async function cmdFetch(argv) {
8962
9124
  const out = strFlag(flags, "out") ?? slug;
8963
9125
  await mkdir9(out, { recursive: true });
8964
9126
  await writeFile11(
8965
- join17(out, "config.json"),
9127
+ join18(out, "config.json"),
8966
9128
  JSON.stringify(cfg.body.config, null, 2)
8967
9129
  );
8968
9130
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -8987,12 +9149,12 @@ async function cmdFetch(argv) {
8987
9149
  const hosted = migrateHostedDoc4(doc.body);
8988
9150
  const { config: own } = await writeProgramDoc(out, hosted);
8989
9151
  if (own) {
8990
- await writeFile11(join17(out, "config.json"), JSON.stringify(own, null, 2));
9152
+ await writeFile11(join18(out, "config.json"), JSON.stringify(own, null, 2));
8991
9153
  }
8992
9154
  } else if (doc.status === 200) {
8993
9155
  take = true;
8994
9156
  const hosted = migrateHostedDoc4(doc.body);
8995
- await writeFile11(join17(out, "doc.json"), JSON.stringify(hosted, null, 2));
9157
+ await writeFile11(join18(out, "doc.json"), JSON.stringify(hosted, null, 2));
8996
9158
  if (flags.media === true) {
8997
9159
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
8998
9160
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -9355,10 +9517,10 @@ async function cmdPullProgram(argv) {
9355
9517
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
9356
9518
  if (cfg.status !== 200)
9357
9519
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
9358
- const configPath = join17(dir, "config.json");
9520
+ const configPath = join18(dir, "config.json");
9359
9521
  let backedUp = false;
9360
- if (existsSync14(configPath)) {
9361
- await writeFile11(join17(dir, "config.backup.json"), await readFile14(configPath));
9522
+ if (existsSync15(configPath)) {
9523
+ await writeFile11(join18(dir, "config.backup.json"), await readFile14(configPath));
9362
9524
  backedUp = true;
9363
9525
  }
9364
9526
  let headConfig = cfg.body.config;
@@ -9391,7 +9553,7 @@ async function cmdPullProgram(argv) {
9391
9553
  protected: protectedIds,
9392
9554
  changes,
9393
9555
  out: configPath,
9394
- backup: backedUp ? join17(dir, "config.backup.json") : null
9556
+ backup: backedUp ? join18(dir, "config.backup.json") : null
9395
9557
  },
9396
9558
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
9397
9559
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -9453,8 +9615,8 @@ function storedProgramConfig(config, programDoc) {
9453
9615
  return lowerProgramDoc(doc).config;
9454
9616
  }
9455
9617
  async function readProgramDoc(dir, config) {
9456
- const docPath = join17(dir, "doc.json");
9457
- if (!existsSync14(docPath)) return null;
9618
+ const docPath = join18(dir, "doc.json");
9619
+ if (!existsSync15(docPath)) return null;
9458
9620
  const parsed = JSON.parse(await readFile14(docPath, "utf8"));
9459
9621
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
9460
9622
  return null;
@@ -9466,14 +9628,14 @@ async function writeProgramDoc(dir, hosted) {
9466
9628
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
9467
9629
  const { config, ...rest } = program;
9468
9630
  const onDisk = { ...hosted, program: rest };
9469
- await writeFile11(join17(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9631
+ await writeFile11(join18(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
9470
9632
  return {
9471
9633
  config: config && typeof config === "object" ? config : null
9472
9634
  };
9473
9635
  }
9474
9636
 
9475
9637
  // src/plugin/asset.ts
9476
- import { readFileSync as readFileSync4 } from "fs";
9638
+ import { readFileSync as readFileSync5 } from "fs";
9477
9639
  import { basename as basename5, extname as extname2 } from "path";
9478
9640
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
9479
9641
  var MIME_BY_EXT = {
@@ -9527,7 +9689,7 @@ async function cmdPush(argv) {
9527
9689
  for (const file of positionals) {
9528
9690
  const name = basename5(file);
9529
9691
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
9530
- const body = readFileSync4(file);
9692
+ const body = readFileSync5(file);
9531
9693
  const form = new FormData();
9532
9694
  form.set("file", new Blob([body], { type: mime }), name);
9533
9695
  if (folder) form.set("folderId", folder.id);
@@ -9581,7 +9743,7 @@ async function cmdAsset(argv) {
9581
9743
  }
9582
9744
 
9583
9745
  // src/plugin/recipe.ts
9584
- import { readFileSync as readFileSync5 } from "fs";
9746
+ import { readFileSync as readFileSync6 } from "fs";
9585
9747
  import { basename as basename6 } from "path";
9586
9748
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
9587
9749
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -9601,7 +9763,7 @@ async function cmdPush2(argv) {
9601
9763
  throw new UsageError(USAGE);
9602
9764
  }
9603
9765
  const filename = recipeFilename(file);
9604
- const body = readFileSync5(file);
9766
+ const body = readFileSync6(file);
9605
9767
  const r = createReporter(flags.json === true);
9606
9768
  const origin = platformOrigin({
9607
9769
  origin: strFlag(flags, "origin"),
@@ -10602,8 +10764,8 @@ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "h
10602
10764
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10603
10765
 
10604
10766
  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]
10767
+ 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]
10768
+ 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
10769
  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
10770
  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
10771
  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 +10777,9 @@ Take pipeline
10615
10777
  vos validate <actions.json|take|kit.json> [--picture] [--json]
10616
10778
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
10617
10779
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
10780
+ vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
10781
+ vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
10782
+ vos session list | rm <name> what exists and how old; delete one
10618
10783
  vos actions script <actions.json> [--json]
10619
10784
  the flow as numbered beats in words, for a person recording it by hand
10620
10785
 
@@ -10913,10 +11078,35 @@ function takeStorageState(flags) {
10913
11078
  const raw = strFlag(flags, "storage-state");
10914
11079
  if (!raw) return void 0;
10915
11080
  const path = resolve9(raw);
10916
- if (!existsSync15(path))
11081
+ if (!existsSync16(path))
10917
11082
  throw new UsageError(`--storage-state: no such file: ${path}`);
10918
11083
  return path;
10919
11084
  }
11085
+ function takeSessionName(flags, storageState) {
11086
+ const name = strFlag(flags, "session");
11087
+ if (!name) return void 0;
11088
+ const bad = sessionNameVerdict(name);
11089
+ if (bad) throw new UsageError(bad);
11090
+ if (storageState)
11091
+ throw new UsageError(
11092
+ "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
11093
+ );
11094
+ if (!existsSync16(sessionDir(name)))
11095
+ throw new UsageError(
11096
+ `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
11097
+ );
11098
+ return name;
11099
+ }
11100
+ async function inSession(name, actions, fn) {
11101
+ if (!name) return fn(void 0);
11102
+ return withSession(name, (ctx) => fn(ctx), {
11103
+ headless: true,
11104
+ viewport: {
11105
+ width: actions.viewport?.width ?? 1280,
11106
+ height: actions.viewport?.height ?? 720
11107
+ }
11108
+ });
11109
+ }
10920
11110
  async function takeBackdrop(flags, r) {
10921
11111
  const { backdrop, note } = await openingBackdrop(
10922
11112
  strFlag(flags, "background"),
@@ -10974,6 +11164,7 @@ async function cmdRecord(argv) {
10974
11164
  const backdrop = await takeBackdrop(flags, r);
10975
11165
  const maxDurationSeconds = await maxDuration(flags, r);
10976
11166
  const storageState = takeStorageState(flags);
11167
+ const sessionName = takeSessionName(flags, storageState);
10977
11168
  const browserArgs = takeBrowserArgs(multi);
10978
11169
  const headers = takeHeaders(multi);
10979
11170
  if (flags["dry-run"] === true) {
@@ -10984,19 +11175,17 @@ async function cmdRecord(argv) {
10984
11175
  r.log("rehearsing (nothing is captured or written)\u2026");
10985
11176
  r.event({ event: "phase", phase: "rehearse" });
10986
11177
  const started = Date.now();
10987
- const rec = await recordTake(
10988
- browser2,
10989
- url,
11178
+ const rec = await inSession(
11179
+ sessionName,
10990
11180
  actions,
10991
- takePaths(outDir),
10992
- r.log,
10993
- {
11181
+ (context) => recordTake(browser2, url, actions, takePaths(outDir), r.log, {
10994
11182
  storageState,
10995
11183
  headers,
11184
+ context,
10996
11185
  dryRun: true,
10997
11186
  // A rehearsal is always strict, and it touches no directory.
10998
11187
  onArrival: wallGate(flags, r, { strict: true })
10999
- }
11188
+ })
11000
11189
  );
11001
11190
  const steps = rec.meta.steps ?? [];
11002
11191
  const lines = steps.map((s) => {
@@ -11009,6 +11198,7 @@ async function cmdRecord(argv) {
11009
11198
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
11010
11199
  ...browserArgs.map((a) => `--browser-arg=${a}`),
11011
11200
  ...Object.entries(headers).map(([k, v]) => `--header ${k}=${v}`),
11201
+ ...sessionName ? [`--session ${sessionName}`] : [],
11012
11202
  ...flags["allow-wall"] === true ? ["--allow-wall"] : []
11013
11203
  ].join(" ");
11014
11204
  const nextOut = strFlag(flags, "out") ?? "take";
@@ -11034,7 +11224,7 @@ ${lines.join("\n")}
11034
11224
  }
11035
11225
  const paths = takePaths(outDir);
11036
11226
  const prepare = async () => {
11037
- if (existsSync15(join18(outDir, "meta.json"))) {
11227
+ if (existsSync16(join19(outDir, "meta.json"))) {
11038
11228
  const { prevDoc, kept } = await prepareReRecord(outDir);
11039
11229
  r.log(
11040
11230
  `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 +11237,20 @@ ${lines.join("\n")}
11047
11237
  try {
11048
11238
  r.log("recording\u2026");
11049
11239
  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
11240
+ const rec = await inSession(
11241
+ sessionName,
11242
+ actions,
11243
+ (context) => recordTake(browser, url, actions, paths, r.log, {
11244
+ maxDurationSeconds,
11245
+ storageState,
11246
+ headers,
11247
+ context,
11248
+ onArrival: wallGate(flags, r, {
11249
+ strict: flags.strict === true,
11250
+ prepare
11251
+ })
11057
11252
  })
11058
- });
11253
+ );
11059
11254
  r.event({ event: "phase", phase: "encode" });
11060
11255
  r.log("encoding\u2026");
11061
11256
  const enc = await encodeRecording(
@@ -11093,7 +11288,7 @@ ${lines.join("\n")}
11093
11288
  },
11094
11289
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
11095
11290
  ${(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)}`
11291
+ Next: edit ${join19(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11097
11292
  );
11098
11293
  return strictFail ? EXIT_USAGE : EXIT_OK;
11099
11294
  } finally {
@@ -11122,7 +11317,7 @@ async function cmdCreate2(argv) {
11122
11317
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11123
11318
  throw new UsageError("--format must be webm or mp4");
11124
11319
  const format = fmtRaw;
11125
- const out = positionals[0] ?? join18(outDir, `out.${format}`);
11320
+ const out = positionals[0] ?? join19(outDir, `out.${format}`);
11126
11321
  const parallel = numFlag(flags, "parallel", 1);
11127
11322
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11128
11323
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11130,11 +11325,12 @@ async function cmdCreate2(argv) {
11130
11325
  const backdrop = await takeBackdrop(flags, r);
11131
11326
  const maxDurationSeconds = await maxDuration(flags, r);
11132
11327
  const storageState = takeStorageState(flags);
11328
+ const sessionName = takeSessionName(flags, storageState);
11133
11329
  const browserArgs = takeBrowserArgs(multi);
11134
11330
  const headers = takeHeaders(multi);
11135
11331
  const paths = takePaths(outDir);
11136
11332
  const prepare = async () => {
11137
- if (existsSync15(join18(outDir, "meta.json"))) {
11333
+ if (existsSync16(join19(outDir, "meta.json"))) {
11138
11334
  const { prevDoc, kept } = await prepareReRecord(outDir);
11139
11335
  r.log(
11140
11336
  `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 +11343,20 @@ async function cmdCreate2(argv) {
11147
11343
  try {
11148
11344
  r.log("recording\u2026");
11149
11345
  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
11346
+ const rec = await inSession(
11347
+ sessionName,
11348
+ actions,
11349
+ (context) => recordTake(browser, url, actions, paths, r.log, {
11350
+ maxDurationSeconds,
11351
+ storageState,
11352
+ headers,
11353
+ context,
11354
+ onArrival: wallGate(flags, r, {
11355
+ strict: flags.strict === true,
11356
+ prepare
11357
+ })
11157
11358
  })
11158
- });
11359
+ );
11159
11360
  r.event({ event: "phase", phase: "encode" });
11160
11361
  r.log("encoding\u2026");
11161
11362
  await encodeRecording(
@@ -11235,13 +11436,13 @@ async function cmdPlan(argv) {
11235
11436
  );
11236
11437
  const r = createReporter(flags.json === true);
11237
11438
  if (flags.fresh === true) {
11238
- await rm5(join18(dir, "doc.json"), { force: true });
11439
+ await rm5(join19(dir, "doc.json"), { force: true });
11239
11440
  r.log("note: --fresh discarded the existing doc.json");
11240
11441
  }
11241
11442
  let reuse;
11242
11443
  if (flags.reuse === true) {
11243
- const from = resolve9(strFlag(flags, "from") ?? join18(dir, PREV_DOC_NAME));
11244
- if (!existsSync15(from)) {
11444
+ const from = resolve9(strFlag(flags, "from") ?? join19(dir, PREV_DOC_NAME));
11445
+ if (!existsSync16(from)) {
11245
11446
  throw new UsageError(
11246
11447
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
11247
11448
  );
@@ -11252,7 +11453,7 @@ async function cmdPlan(argv) {
11252
11453
  };
11253
11454
  }
11254
11455
  const style = await resolveStyleRef(flags);
11255
- const hasDoc = existsSync15(join18(dir, "doc.json"));
11456
+ const hasDoc = existsSync16(join19(dir, "doc.json"));
11256
11457
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
11257
11458
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
11258
11459
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -11310,7 +11511,7 @@ async function cmdPlan(argv) {
11310
11511
  ...s.motion ? { motion: s.motion } : {},
11311
11512
  ...s.reuse ? { reuse: s.reuse } : {}
11312
11513
  },
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}`
11514
+ `${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
11515
  );
11315
11516
  return EXIT_OK;
11316
11517
  }
@@ -11436,7 +11637,7 @@ async function cmdRender(argv) {
11436
11637
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11437
11638
  throw new UsageError("--format must be webm or mp4");
11438
11639
  const format = fmtRaw;
11439
- const out = positionals[1] ?? join18(dir, `out.${format}`);
11640
+ const out = positionals[1] ?? join19(dir, `out.${format}`);
11440
11641
  const parallel = numFlag(flags, "parallel", 1);
11441
11642
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11442
11643
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11683,11 +11884,11 @@ async function resolveStyleRef(flags) {
11683
11884
  }
11684
11885
  async function resolveDocRef(ref, flags, what) {
11685
11886
  const styleRef = ref;
11686
- const file = existsSync15(styleRef) ? resolve9(
11887
+ const file = existsSync16(styleRef) ? resolve9(
11687
11888
  styleRef,
11688
- existsSync15(join18(styleRef, "doc.json")) ? "doc.json" : ""
11889
+ existsSync16(join19(styleRef, "doc.json")) ? "doc.json" : ""
11689
11890
  ) : null;
11690
- if (file && existsSync15(file)) {
11891
+ if (file && existsSync16(file)) {
11691
11892
  return {
11692
11893
  from: file,
11693
11894
  doc: migrateHostedDoc5(
@@ -11774,7 +11975,7 @@ async function cmdDigest(argv) {
11774
11975
  kinds,
11775
11976
  frames: d.images.full,
11776
11977
  crops: d.images.crop,
11777
- sheet: d.images.sheet ? join18(result.outDir, d.images.sheet) : null,
11978
+ sheet: d.images.sheet ? join19(result.outDir, d.images.sheet) : null,
11778
11979
  scenes: kinds.scene ?? 0,
11779
11980
  sourceDuration: d.take.sourceDuration,
11780
11981
  outputDuration: d.take.outputDuration,
@@ -11802,7 +12003,7 @@ async function cmdOpen(argv) {
11802
12003
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
11803
12004
  const r = createReporter(flags.json === true);
11804
12005
  const take = await loadTake(dir);
11805
- if (!existsSync15(take.paths.recording)) {
12006
+ if (!existsSync16(take.paths.recording)) {
11806
12007
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
11807
12008
  }
11808
12009
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
@@ -11843,9 +12044,9 @@ async function cmdJudge(argv) {
11843
12044
  "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
12045
  );
11845
12046
  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))
12047
+ const kitFile = target.endsWith("kit.json") ? target : join19(target, "kit.json");
12048
+ if (!existsSync16(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
12049
+ if (!existsSync16(against))
11849
12050
  throw new UsageError(`${against}: no reference manifest`);
11850
12051
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
11851
12052
  const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
@@ -11967,7 +12168,7 @@ async function cmdValidate(argv) {
11967
12168
  const target = positionals[0];
11968
12169
  if (!target) throw new UsageError("vos validate <actions.json|take>");
11969
12170
  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;
12171
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync16(join19(target, "kit.json")) && !isTakeDir(target) ? join19(target, "kit.json") : null;
11971
12172
  if (kitTarget) {
11972
12173
  const picture = flags.picture === true;
11973
12174
  const verdict = await validateKit(kitTarget, { picture });
@@ -11999,9 +12200,9 @@ async function cmdValidate(argv) {
11999
12200
  r.done({ valid: true, target }, `${target}: valid actions file`);
12000
12201
  return EXIT_OK;
12001
12202
  }
12002
- if (!isTakeDir(target) && existsSync15(join18(target, "config.json"))) {
12203
+ if (!isTakeDir(target) && existsSync16(join19(target, "config.json"))) {
12003
12204
  const parsed = JSON.parse(
12004
- await readFile15(join18(target, "config.json"), "utf8")
12205
+ await readFile15(join19(target, "config.json"), "utf8")
12005
12206
  );
12006
12207
  const pre = preflightConfig(parsed);
12007
12208
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -12042,7 +12243,7 @@ async function cmdValidate(argv) {
12042
12243
  const take = await loadTake(target);
12043
12244
  const problems = [];
12044
12245
  const warnings = [];
12045
- if (!existsSync15(take.paths.recording)) {
12246
+ if (!existsSync16(take.paths.recording)) {
12046
12247
  problems.push("missing recording (re-run record)");
12047
12248
  } else {
12048
12249
  const mismatch = containerMismatch(
@@ -12283,6 +12484,8 @@ async function run(argv) {
12283
12484
  return await cmdRecipe(rest);
12284
12485
  case "brand":
12285
12486
  return await cmdBrand(rest);
12487
+ case "session":
12488
+ return await cmdSession(rest);
12286
12489
  case "actions":
12287
12490
  return await cmdActions(rest);
12288
12491
  default:
@@ -12316,6 +12519,121 @@ async function run(argv) {
12316
12519
  return EXIT_ERROR;
12317
12520
  }
12318
12521
  }
12522
+ async function cmdSession(argv) {
12523
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
12524
+ const [sub, arg] = positionals;
12525
+ const r = createReporter(flags.json === true);
12526
+ const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
12527
+ switch (sub) {
12528
+ case "open": {
12529
+ if (!arg) throw new UsageError(usage);
12530
+ const name = strFlag(flags, "name");
12531
+ const bad = sessionNameVerdict(name);
12532
+ if (bad) throw new UsageError(bad);
12533
+ let url;
12534
+ try {
12535
+ url = new URL(arg);
12536
+ } catch {
12537
+ throw new UsageError(`vos session open wants a URL, got "${arg}"`);
12538
+ }
12539
+ if (!/^https?:$/.test(url.protocol))
12540
+ throw new UsageError("vos session open wants an http(s) URL");
12541
+ await openSession(name, url.href, { log: r.log });
12542
+ const summary2 = await summarizeSession(name);
12543
+ r.done(
12544
+ {
12545
+ name: summary2.name,
12546
+ origins: summary2.origins,
12547
+ newestExpires: summary2.newestExpires,
12548
+ sessionOnly: summary2.sessionOnly
12549
+ },
12550
+ `${summaryLine(summary2)}
12551
+ Record on it: vos record --actions actions.json --out take --session ${name} --strict`
12552
+ );
12553
+ return summary2.origins.length ? EXIT_OK : EXIT_ERROR;
12554
+ }
12555
+ case "check": {
12556
+ if (!arg) throw new UsageError(usage);
12557
+ const bad = sessionNameVerdict(arg);
12558
+ if (bad) throw new UsageError(bad);
12559
+ if (!existsSync16(sessionDir(arg)))
12560
+ throw new UsageError(`no session "${arg}" (vos session list)`);
12561
+ const summary2 = await summarizeSession(arg);
12562
+ const url = strFlag(flags, "url");
12563
+ if (!url) {
12564
+ r.done(
12565
+ {
12566
+ name: arg,
12567
+ origins: summary2.origins,
12568
+ newestExpires: summary2.newestExpires,
12569
+ sessionOnly: summary2.sessionOnly
12570
+ },
12571
+ summaryLine(summary2)
12572
+ );
12573
+ return summary2.origins.length ? EXIT_OK : EXIT_WALL;
12574
+ }
12575
+ const verdict = await withSession(arg, async (ctx) => {
12576
+ const page = await ctx.newPage();
12577
+ const response = await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => null);
12578
+ const seen = await page.evaluate(WALL_PROBE).catch(() => null);
12579
+ return wallVerdict({
12580
+ askedUrl: url,
12581
+ landedUrl: page.url(),
12582
+ ...response ? { status: response.status() } : {},
12583
+ passwordFields: seen?.passwordFields ?? 0,
12584
+ newPasswordField: seen?.newPasswordField ?? false,
12585
+ oneTimeCodeField: seen?.oneTimeCodeField ?? false
12586
+ });
12587
+ });
12588
+ if (verdict) {
12589
+ r.done(
12590
+ {
12591
+ name: arg,
12592
+ ok: false,
12593
+ wall: verdict,
12594
+ origins: summary2.origins,
12595
+ newestExpires: summary2.newestExpires
12596
+ },
12597
+ `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}`
12598
+ );
12599
+ return EXIT_WALL;
12600
+ }
12601
+ r.done(
12602
+ {
12603
+ name: arg,
12604
+ ok: true,
12605
+ origins: summary2.origins,
12606
+ newestExpires: summary2.newestExpires
12607
+ },
12608
+ `session "${arg}" opens ${url} signed in. ${summaryLine(summary2)}`
12609
+ );
12610
+ return EXIT_OK;
12611
+ }
12612
+ case "list": {
12613
+ const rows = listSessions();
12614
+ r.done(
12615
+ { sessions: rows },
12616
+ rows.length ? rows.map(
12617
+ (x) => ` ${x.name} ${x.ageDays} day${x.ageDays === 1 ? "" : "s"} old`
12618
+ ).join("\n") : "no sessions. Make one: vos session open <url> --name <app>"
12619
+ );
12620
+ return EXIT_OK;
12621
+ }
12622
+ case "rm": {
12623
+ if (!arg) throw new UsageError(usage);
12624
+ const bad = sessionNameVerdict(arg);
12625
+ if (bad) throw new UsageError(bad);
12626
+ const gone = removeSession(arg);
12627
+ r.done(
12628
+ { name: arg, removed: gone },
12629
+ gone ? `removed session "${arg}"` : `no session "${arg}"`
12630
+ );
12631
+ return gone ? EXIT_OK : EXIT_ERROR;
12632
+ }
12633
+ default:
12634
+ throw new UsageError(usage);
12635
+ }
12636
+ }
12319
12637
 
12320
12638
  export {
12321
12639
  validateActions,
@@ -12348,4 +12666,4 @@ export {
12348
12666
  verbHelp,
12349
12667
  run
12350
12668
  };
12351
- //# sourceMappingURL=chunk-WCSAECM4.js.map
12669
+ //# sourceMappingURL=chunk-4HW2XR5K.js.map