@vosjs/cli 0.48.1 → 0.50.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.
@@ -7,28 +7,14 @@ import {
7
7
  trackFlags
8
8
  } from "./chunk-FACYY7VV.js";
9
9
  import {
10
- PRESS_HOLD_MS,
11
- PRESS_LEAD_MS,
12
- SCROLL_SETTLE_MS,
13
- TRAILING_HOLD_MS,
14
- askedMs,
15
- clockMotion,
16
- clockTyping,
17
10
  deadLine,
18
- deadTime,
19
- deadWarns,
20
- holdLeftMs,
21
- paceLine,
22
- paceReport,
23
- pointerTravelMs,
24
- settleMs,
25
- settleVerdict
26
- } from "./chunk-AUPGJJTL.js";
11
+ deadWarns
12
+ } from "./chunk-72ADTMWW.js";
27
13
 
28
14
  // src/plugin/run.ts
29
- import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
30
- import { existsSync as existsSync18 } from "fs";
31
- import { join as join21, resolve as resolve9 } from "path";
15
+ import { mkdir as mkdir11, readFile as readFile16, rm as rm5 } from "fs/promises";
16
+ import { existsSync as existsSync19 } from "fs";
17
+ import { join as join22, resolve as resolve10 } from "path";
32
18
  import {
33
19
  END_CARD_FROM as END_CARD_FROM2,
34
20
  docOutputDuration as docOutputDuration3,
@@ -119,238 +105,8 @@ function createReporter(json) {
119
105
  };
120
106
  }
121
107
 
122
- // src/plugin/setup.ts
123
- var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
124
- function validateSetup(value) {
125
- const errors = [];
126
- if (!Array.isArray(value)) return ["setup must be an array of steps"];
127
- value.forEach((raw, i) => {
128
- const at2 = `setup[${i}]`;
129
- if (typeof raw !== "object" || raw === null) {
130
- errors.push(`${at2}: must be an object`);
131
- return;
132
- }
133
- const s = raw;
134
- if (typeof s.do !== "string" || !VERBS.has(s.do)) {
135
- errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
136
- return;
137
- }
138
- if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
139
- errors.push(`${at2}: ${s.do} needs a selector`);
140
- if (s.do === "wait" && typeof s.ms !== "number")
141
- errors.push(`${at2}: wait needs ms`);
142
- if (s.do === "press" && typeof s.key !== "string")
143
- errors.push(`${at2}: press needs a key`);
144
- if (s.do === "goto" && typeof s.url !== "string")
145
- errors.push(`${at2}: goto needs a url`);
146
- if (s.do === "type") {
147
- const t = s.text;
148
- const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
149
- if (typeof t !== "string" && !isEnv)
150
- errors.push(
151
- `${at2}: type needs text, a string or { "env": "NAME" } read at run time`
152
- );
153
- if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
154
- errors.push(
155
- `${at2}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
156
- );
157
- }
158
- });
159
- return errors;
160
- }
161
- function resolveSetupText(t, env = process.env) {
162
- if (typeof t === "string") return { value: t, secret: false };
163
- const v = env[t.env];
164
- if (v === void 0 || v === "")
165
- throw new SetupEnvError(
166
- `setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
167
- );
168
- return { value: v, secret: true };
169
- }
170
- async function runSetup(page, steps, opts) {
171
- const lines = [];
172
- for (const [i, step] of steps.entries()) {
173
- const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
174
- try {
175
- switch (step.do) {
176
- case "wait":
177
- await opts.sleep(step.ms);
178
- lines.push(`setup #${i} wait ${step.ms}ms`);
179
- break;
180
- case "goto":
181
- await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
182
- });
183
- lines.push(`setup #${i} goto ${step.url}`);
184
- break;
185
- case "press":
186
- await page.keyboard.press(step.key);
187
- await opts.sleep(settle);
188
- lines.push(`setup #${i} press ${step.key}`);
189
- break;
190
- case "click": {
191
- const loc = page.locator(step.selector).first();
192
- await loc.waitFor({ state: "visible", timeout: 8e3 });
193
- await loc.click();
194
- await opts.sleep(settle);
195
- lines.push(`setup #${i} click ${step.selector}`);
196
- break;
197
- }
198
- case "type": {
199
- const loc = page.locator(step.selector).first();
200
- await loc.waitFor({ state: "visible", timeout: 8e3 });
201
- const { value, secret } = resolveSetupText(step.text, opts.env);
202
- await loc.fill(value);
203
- await opts.sleep(settle);
204
- lines.push(
205
- `setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
206
- );
207
- break;
208
- }
209
- }
210
- } catch (e) {
211
- if (e instanceof SetupEnvError) throw e;
212
- return {
213
- ran: i,
214
- lines,
215
- failed: {
216
- step: i,
217
- do: step.do,
218
- ..."selector" in step ? { selector: step.selector } : {}
219
- }
220
- };
221
- }
222
- }
223
- return { ran: steps.length, lines };
224
- }
225
- var SetupEnvError = class extends Error {
226
- constructor(message) {
227
- super(message);
228
- this.name = "SetupEnvError";
229
- }
230
- };
231
- var SetupError = class extends Error {
232
- constructor(failed2) {
233
- super(
234
- `setup #${failed2.step} ${failed2.do}${failed2.selector ? ` ${failed2.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
235
- );
236
- this.failed = failed2;
237
- this.name = "SetupError";
238
- }
239
- failed;
240
- };
241
- function parseHeaders(given) {
242
- const out = {};
243
- for (const h of given ?? []) {
244
- const i = h.indexOf("=");
245
- if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
246
- out[h.slice(0, i).trim()] = h.slice(i + 1);
247
- }
248
- return out;
249
- }
250
-
251
108
  // src/plugin/actions.ts
252
- var VERBS2 = /* @__PURE__ */ new Set([
253
- "wait",
254
- "hover",
255
- "click",
256
- "type",
257
- "scroll",
258
- "move",
259
- "drag"
260
- ]);
261
- function validateActions(value) {
262
- const errors = [];
263
- if (typeof value !== "object" || value === null)
264
- return ["actions file must be a JSON object"];
265
- const obj = value;
266
- if (obj.url !== void 0 && typeof obj.url !== "string")
267
- errors.push("url must be a string");
268
- if (obj.viewport !== void 0) {
269
- const v = obj.viewport;
270
- if (typeof v !== "object" || v === null) {
271
- errors.push("viewport must be { width, height }");
272
- } else {
273
- const vp = v;
274
- if (typeof vp.width !== "number" || typeof vp.height !== "number") {
275
- errors.push("viewport must be { width, height }");
276
- }
277
- }
278
- }
279
- if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
280
- if (obj.mask !== void 0) {
281
- if (!Array.isArray(obj.mask)) {
282
- errors.push("mask must be an array of { selector, as?, text? }");
283
- } else {
284
- obj.mask.forEach((raw, i) => {
285
- const at2 = `mask[${i}]`;
286
- const m = raw ?? {};
287
- if (typeof m.selector !== "string" || !m.selector.trim())
288
- errors.push(`${at2}: needs a selector`);
289
- if (m.as !== void 0 && m.as !== "blur" && m.as !== "text")
290
- errors.push(`${at2}: as must be "blur" or "text"`);
291
- if (m.as === "text" && (typeof m.text !== "string" || !m.text))
292
- errors.push(`${at2}: as "text" needs the text to show instead`);
293
- if (m.as !== "text" && m.text !== void 0)
294
- errors.push(`${at2}: text is only read with as "text"`);
295
- });
296
- }
297
- }
298
- if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
299
- errors.push("steps must be a non-empty array");
300
- return errors;
301
- }
302
- const seenIds = /* @__PURE__ */ new Set();
303
- obj.steps.forEach((raw, i) => {
304
- const at2 = `steps[${i}]`;
305
- if (typeof raw !== "object" || raw === null) {
306
- errors.push(`${at2}: must be an object`);
307
- return;
308
- }
309
- const s = raw;
310
- if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
311
- errors.push(`${at2}: "do" must be one of ${[...VERBS2].join(", ")}`);
312
- return;
313
- }
314
- if (s.id !== void 0) {
315
- if (typeof s.id !== "string" || !s.id.trim()) {
316
- errors.push(`${at2}: id must be a non-empty string`);
317
- } else if (seenIds.has(s.id)) {
318
- errors.push(
319
- `${at2}: duplicate id "${s.id}" \u2014 an anchor could not tell the two steps apart`
320
- );
321
- } else {
322
- seenIds.add(s.id);
323
- }
324
- }
325
- const needSelector = s.do === "hover" || s.do === "click" || s.do === "type";
326
- if (needSelector && typeof s.selector !== "string")
327
- errors.push(`${at2}: ${s.do} needs a selector`);
328
- if (s.do === "wait" && typeof s.ms !== "number")
329
- errors.push(`${at2}: wait needs ms`);
330
- if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
331
- errors.push(`${at2}: ms must be a number of milliseconds \u2265 0`);
332
- if (s.do === "type" && typeof s.text !== "string")
333
- errors.push(`${at2}: type needs text`);
334
- if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
335
- errors.push(`${at2}: type focus must be true or false`);
336
- if (s.do === "scroll" && typeof s.dy !== "number")
337
- errors.push(`${at2}: scroll needs dy`);
338
- if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
339
- errors.push(`${at2}: move needs x and y (CSS px)`);
340
- }
341
- if (s.do === "drag") {
342
- if (typeof s.tx !== "number" || typeof s.ty !== "number") {
343
- errors.push(`${at2}: drag needs tx and ty (CSS px)`);
344
- }
345
- const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
346
- if (!hasStart)
347
- errors.push(
348
- `${at2}: drag needs a selector OR x and y as the start point`
349
- );
350
- }
351
- });
352
- return errors;
353
- }
109
+ import { validateActions } from "@vosjs/render-core/record";
354
110
 
355
111
  // src/plugin/validateDoc.ts
356
112
  import {
@@ -2457,10 +2213,10 @@ function startTakeServer(rootDir, pages) {
2457
2213
  res.writeHead(404).end();
2458
2214
  });
2459
2215
  return new Promise(
2460
- (resolve10) => server.listen(0, () => {
2216
+ (resolve11) => server.listen(0, () => {
2461
2217
  const addr = server.address();
2462
2218
  const port = typeof addr === "object" && addr ? addr.port : 0;
2463
- resolve10({ base: `http://localhost:${port}`, close: () => server.close() });
2219
+ resolve11({ base: `http://localhost:${port}`, close: () => server.close() });
2464
2220
  })
2465
2221
  );
2466
2222
  }
@@ -6674,805 +6430,67 @@ async function validateKit(kitPath, opts = {}) {
6674
6430
  }
6675
6431
 
6676
6432
  // src/plugin/recorder.ts
6677
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6433
+ import { writeFileSync as writeFileSync2 } from "fs";
6678
6434
  import { join as join13 } from "path";
6679
-
6680
- // src/plugin/recordingCap.ts
6681
- var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
6682
- function parseHostedCap(body) {
6683
- const limits = body && typeof body === "object" ? body.limits : void 0;
6684
- const cap = limits?.recordingMaxSeconds;
6685
- return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
6686
- }
6687
- async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
6688
- const ctl = new AbortController();
6689
- const timer = setTimeout(() => ctl.abort(), 2e3);
6690
- try {
6691
- const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
6692
- signal: ctl.signal,
6693
- ...key ? { headers: { authorization: `Bearer ${key}` } } : {}
6694
- });
6695
- if (!res.ok) return null;
6696
- return parseHostedCap(await res.json());
6697
- } catch {
6698
- return null;
6699
- } finally {
6700
- clearTimeout(timer);
6701
- }
6702
- }
6703
- function defaultMaxDurationSeconds() {
6704
- return HOSTED_RECORDING_CAP_SECONDS;
6705
- }
6706
- function capReached(elapsedMs, capSeconds) {
6707
- return elapsedMs >= capSeconds * 1e3;
6708
- }
6709
- function clampWait(waitMs, elapsedMs, capSeconds) {
6710
- return Math.max(0, Math.min(waitMs, capSeconds * 1e3 - elapsedMs));
6711
- }
6712
- function formatDurationCap(seconds) {
6713
- if (seconds < 60) return `${seconds} s`;
6714
- const m = Math.round(seconds / 60);
6715
- if (m < 60) return `${m} min`;
6716
- const h = Math.floor(m / 60);
6717
- const rest = m % 60;
6718
- return rest ? `${h} h ${rest} min` : `${h} h`;
6719
- }
6720
- function cappedLine(capSeconds) {
6721
- return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
6722
- }
6723
-
6724
- // src/plugin/wall.ts
6725
- var IDP_HOSTS = [
6726
- "accounts.google.com",
6727
- "login.microsoftonline.com",
6728
- "login.live.com",
6729
- "appleid.apple.com",
6730
- "auth0.com",
6731
- "okta.com",
6732
- "onelogin.com",
6733
- "clerk.accounts.dev",
6734
- "accounts.dev",
6735
- "b2clogin.com",
6736
- "amazoncognito.com",
6737
- "workos.com",
6738
- "stytch.com"
6739
- ];
6740
- var SIGNIN_SEGMENTS = /* @__PURE__ */ new Set([
6741
- "login",
6742
- "log-in",
6743
- "signin",
6744
- "sign-in",
6745
- "sign_in",
6746
- "sso",
6747
- "auth",
6748
- "authenticate",
6749
- "authorize",
6750
- "oauth",
6751
- "oauth2",
6752
- "saml"
6753
- ]);
6754
- function parse(url) {
6755
- try {
6756
- return new URL(url);
6757
- } catch {
6758
- return null;
6759
- }
6760
- }
6761
- var bareHost = (h) => h.replace(/^www\./, "").toLowerCase();
6762
- var trimSlash = (p) => p.length > 1 ? p.replace(/\/+$/, "") : p;
6763
- var place = (u) => `${u.origin}${trimSlash(u.pathname)}`;
6764
- function isIdpHost(host) {
6765
- const h = host.toLowerCase();
6766
- if (h.startsWith("clerk.")) return true;
6767
- return IDP_HOSTS.some((idp) => h === idp || h.endsWith(`.${idp}`));
6768
- }
6769
- function isSignInPath(pathname) {
6770
- const segs = pathname.toLowerCase().split("/").filter(Boolean);
6771
- return segs.some((s) => SIGNIN_SEGMENTS.has(s));
6772
- }
6773
- function redirectedAway(asked, landed) {
6774
- if (bareHost(asked.hostname) !== bareHost(landed.hostname)) return true;
6775
- const a = trimSlash(asked.pathname).toLowerCase();
6776
- const l = trimSlash(landed.pathname).toLowerCase();
6777
- if (a === l || a === "/") return false;
6778
- if (l.startsWith(`${a}/`)) return false;
6779
- if (l.endsWith(a)) return false;
6780
- return true;
6781
- }
6782
- var LADDER = 'This is a missing or expired session, not a script bug. Ways past, cheapest first: mint a session from the test auth the project already has; script the sign-in off camera and save the storage state; or have a person sign in once (npx playwright open --channel chrome --save-storage=<file> <url>). Then pass --storage-state <file>. The ladder: https://vos.so/llms-full.txt, "Sessions". A take OF this page is --allow-wall.';
6783
- function wallVerdict(a) {
6784
- const asked = parse(a.askedUrl);
6785
- const landed = parse(a.landedUrl);
6786
- if (!asked || !landed) return null;
6787
- if (!/^https?:$/.test(asked.protocol)) return null;
6788
- const away = redirectedAway(asked, landed);
6789
- const where = { asked: place(asked), landed: place(landed) };
6790
- const host = bareHost(asked.hostname);
6791
- if (a.status === 401 || a.status === 403) {
6792
- return {
6793
- kind: "status",
6794
- level: "hard",
6795
- ...where,
6796
- message: `asked for ${asked.pathname}, answered ${a.status}: no session for ${host}. ${LADDER}`
6797
- };
6798
- }
6799
- const askedForSignIn = isSignInPath(asked.pathname) || isIdpHost(asked.hostname);
6800
- if (askedForSignIn && !away) return null;
6801
- if (away && isIdpHost(landed.hostname) && !isIdpHost(asked.hostname)) {
6802
- return {
6803
- kind: "idp",
6804
- level: "hard",
6805
- ...where,
6806
- message: `asked for ${asked.pathname}, landed on ${landed.hostname}: no session for ${host}. ${LADDER}`
6807
- };
6808
- }
6809
- const signInForm = a.passwordFields === 1 && !a.newPasswordField || a.oneTimeCodeField;
6810
- if (away && isSignInPath(landed.pathname) || signInForm && !askedForSignIn) {
6811
- const seen = away ? `landed on ${landed.pathname}` : "the page is a sign-in form";
6812
- return {
6813
- kind: "signin",
6814
- level: "hard",
6815
- ...where,
6816
- message: `asked for ${asked.pathname}, ${seen}: no session for ${host}. ${LADDER}`
6817
- };
6818
- }
6819
- if (away) {
6820
- return {
6821
- kind: "redirect",
6822
- level: "soft",
6823
- ...where,
6824
- message: `asked for ${asked.pathname}, landed on ${landed.pathname}: the site sent the recorder elsewhere, which is what a page behind a login does to a stranger. If that is the page you meant, record it by its own URL. ${LADDER}`
6825
- };
6826
- }
6827
- return null;
6828
- }
6829
- var WallError = class extends Error {
6830
- constructor(verdict) {
6831
- super(verdict.message);
6832
- this.verdict = verdict;
6833
- this.name = "WallError";
6834
- }
6835
- verdict;
6836
- };
6837
- var WALL_PROBE = `(() => {
6838
- const shown = (el) => {
6839
- const r = el.getBoundingClientRect()
6840
- if (r.width < 2 || r.height < 2) return false
6841
- const s = getComputedStyle(el)
6842
- return s.visibility !== 'hidden' && s.display !== 'none'
6843
- }
6844
- const pw = [...document.querySelectorAll('input[type="password"]')].filter(shown)
6845
- const otp = [...document.querySelectorAll('input[autocomplete="one-time-code"]')].filter(shown)
6846
- return {
6847
- passwordFields: pw.length,
6848
- newPasswordField: pw.some((el) => (el.getAttribute('autocomplete') || '').includes('new-password')),
6849
- oneTimeCodeField: otp.length > 0,
6850
- }
6851
- })()`;
6852
-
6853
- // src/plugin/exposure.ts
6854
- var EXPOSURE_MATCHERS_SRC = `(() => {
6855
- const EMAIL = /[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,})/g
6856
- const SAFE_HOST = /(^|\\.)example\\.(com|org|net)$|\\.(test|example|invalid|localhost)$/i
6857
- const KEY = new RegExp([
6858
- '\\\\b[srp]k_(?:live|test)_[A-Za-z0-9]{8,}',
6859
- '\\\\bgh[pousr]_[A-Za-z0-9]{20,}',
6860
- '\\\\bgithub_pat_[A-Za-z0-9_]{20,}',
6861
- '\\\\bxox[abprs]-[A-Za-z0-9-]{10,}',
6862
- '\\\\bAKIA[0-9A-Z]{16}\\\\b',
6863
- '\\\\bAIza[0-9A-Za-z_-]{30,}',
6864
- '\\\\bvos_(?:sk|rg|ho)_[A-Za-z0-9]{8,}',
6865
- '\\\\beyJ[A-Za-z0-9_-]{8,}\\\\.eyJ[A-Za-z0-9_-]{8,}\\\\.[A-Za-z0-9_-]{8,}',
6866
- ].join('|'))
6867
- const CARD = /(?:^|[^0-9])((?:[0-9][ -]?){12,18}[0-9])(?![0-9])/g
6868
- const TAIL = /[\\u2022\\u00b7*xX]{2,}[ -]?[0-9]{4}(?![0-9])/
6869
- const luhn = (digits) => {
6870
- let sum = 0, alt = false
6871
- for (let i = digits.length - 1; i >= 0; i--) {
6872
- let d = digits.charCodeAt(i) - 48
6873
- if (alt) { d *= 2; if (d > 9) d -= 9 }
6874
- sum += d; alt = !alt
6875
- }
6876
- return sum % 10 === 0
6877
- }
6878
- const kindsIn = (text) => {
6879
- const kinds = []
6880
- if (!text || text.length < 6) return kinds
6881
- EMAIL.lastIndex = 0
6882
- for (let m; (m = EMAIL.exec(text)); ) {
6883
- if (!SAFE_HOST.test(m[1])) { kinds.push('email'); break }
6884
- }
6885
- if (KEY.test(text)) kinds.push('key')
6886
- CARD.lastIndex = 0
6887
- for (let m; (m = CARD.exec(text)); ) {
6888
- const digits = m[1].replace(/[ -]/g, '')
6889
- if (digits.length >= 13 && digits.length <= 19 && !/^0+$/.test(digits) && luhn(digits)) { kinds.push('card'); break }
6890
- }
6891
- if (TAIL.test(text)) kinds.push('card-tail')
6892
- return kinds
6893
- }
6894
- return { kindsIn }
6895
- })()`;
6896
- var EXPOSURE_PROBE = `(() => {
6897
- const { kindsIn } = ${EXPOSURE_MATCHERS_SRC}
6898
- const vw = innerWidth, vh = innerHeight
6899
- // A selector that reaches THIS element and no other, so it can be pasted
6900
- // into "mask" as it is: climb until the path is unique, and say which
6901
- // sibling wherever a tag-and-class alone would match several.
6902
- const segment = (el) => {
6903
- const tid = el.getAttribute('data-testid')
6904
- if (tid) return el.tagName.toLowerCase() + '[data-testid="' + tid + '"]'
6905
- if (el.id && /^[A-Za-z_][\\w-]*$/.test(el.id)) return el.tagName.toLowerCase() + '#' + el.id
6906
- const cls = [...el.classList].filter((c) => /^[A-Za-z_-][\\w-]*$/.test(c)).slice(0, 2)
6907
- let seg = el.tagName.toLowerCase() + (cls.length ? '.' + cls.join('.') : '')
6908
- const p = el.parentElement
6909
- if (p && [...p.children].filter((c) => c.matches(seg)).length > 1)
6910
- seg += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')'
6911
- return seg
6912
- }
6913
- const name = (el) => {
6914
- const parts = []
6915
- for (let cur = el; cur && cur !== document.body && cur !== document.documentElement; cur = cur.parentElement) {
6916
- parts.unshift(segment(cur))
6917
- let n = 2
6918
- try { n = document.querySelectorAll(parts.join(' > ')).length } catch {}
6919
- // a bare tag is unique HERE and nowhere else: give it its parent too
6920
- const bare = parts.length === 1 && !/[#.\\[]/.test(parts[0])
6921
- if ((n === 1 && !bare) || parts.length >= 5) break
6922
- }
6923
- return parts.join(' > ')
6924
- }
6925
- const seen = (el) => {
6926
- if (el.closest('[data-vos-masked]')) return null
6927
- const r = el.getBoundingClientRect()
6928
- if (r.width < 2 || r.height < 2) return null
6929
- if (r.bottom <= 0 || r.right <= 0 || r.top >= vh || r.left >= vw) return null
6930
- const s = getComputedStyle(el)
6931
- if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity === 0) return null
6932
- return r
6933
- }
6934
- const out = [], keys = new Set()
6935
- const add = (el, text) => {
6936
- const kinds = kindsIn(text)
6937
- if (!kinds.length) return
6938
- const r = seen(el)
6939
- if (!r) return
6940
- const selector = name(el)
6941
- for (const kind of kinds) {
6942
- const k = kind + '|' + selector
6943
- if (keys.has(k) || out.length >= 40) continue
6944
- keys.add(k)
6945
- out.push({ kind, selector, rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } })
6946
- }
6947
- }
6948
- const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT)
6949
- for (let n; (n = walker.nextNode()); ) {
6950
- const el = n.parentElement
6951
- if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/.test(el.tagName)) continue
6952
- add(el, n.nodeValue)
6953
- }
6954
- for (const el of document.querySelectorAll('input, textarea')) {
6955
- if (/^(password|hidden|file|checkbox|radio)$/.test(el.type)) continue
6956
- add(el, el.value)
6957
- }
6958
- const hits = {}
6959
- for (const [sel, n] of Object.entries(window.__vosMaskHits || {})) hits[sel] = n
6960
- return { exposures: out, maskHits: hits }
6961
- })()`;
6962
- function maskInitScript(masks) {
6963
- const rules = masks.map((m) => ({
6964
- selector: m.selector,
6965
- as: m.as === "text" ? "text" : "blur",
6966
- text: m.text ?? ""
6967
- }));
6968
- return `(() => {
6969
- const RULES = ${JSON.stringify(rules)}
6970
- const hits = (window.__vosMaskHits = window.__vosMaskHits || {})
6971
- for (const r of RULES) hits[r.selector] = hits[r.selector] || 0
6972
- const css = RULES.map((r) => r.selector + '[data-vos-masked="blur"]{filter:blur(7px) !important;user-select:none !important}').join('\\n')
6973
- const paint = () => {
6974
- const root = document.head || document.documentElement
6975
- if (!root || document.getElementById('__vos-mask-style')) return
6976
- const style = document.createElement('style')
6977
- style.id = '__vos-mask-style'
6978
- style.textContent = css
6979
- root.appendChild(style)
6980
- }
6981
- const apply = () => {
6982
- paint()
6983
- for (const r of RULES) {
6984
- let found
6985
- try { found = document.querySelectorAll(r.selector) } catch { continue }
6986
- if (found.length > hits[r.selector]) hits[r.selector] = found.length
6987
- for (const el of found) {
6988
- const form = /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName)
6989
- const mode = r.as === 'text' && !form ? 'text' : 'blur'
6990
- if (el.getAttribute('data-vos-masked') !== mode) el.setAttribute('data-vos-masked', mode)
6991
- if (mode !== 'text') continue
6992
- const texts = []
6993
- const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
6994
- for (let n; (n = w.nextNode()); ) texts.push(n)
6995
- if (!texts.length) { if (el.textContent !== r.text) el.textContent = r.text; continue }
6996
- texts.forEach((n, i) => { const want = i === 0 ? r.text : ''; if (n.nodeValue !== want) n.nodeValue = want })
6997
- }
6998
- }
6999
- }
7000
- new MutationObserver(apply).observe(document, { childList: true, subtree: true, characterData: true })
7001
- document.addEventListener('DOMContentLoaded', apply)
7002
- apply()
7003
- })()`;
7004
- }
7005
- var ExposureLog = class {
7006
- byKey = /* @__PURE__ */ new Map();
7007
- maskHits = /* @__PURE__ */ new Map();
7008
- constructor(masks = []) {
7009
- for (const m of masks) this.maskHits.set(m.selector, 0);
7010
- }
7011
- add(step, scan) {
7012
- if (!scan) return;
7013
- for (const e of scan.exposures ?? []) {
7014
- const key = `${e.kind}|${e.selector}`;
7015
- const had = this.byKey.get(key);
7016
- if (had) had.seen += 1;
7017
- else this.byKey.set(key, { step, ...e, seen: 1 });
7018
- }
7019
- for (const [sel, n] of Object.entries(scan.maskHits ?? {})) {
7020
- if (n > (this.maskHits.get(sel) ?? 0)) this.maskHits.set(sel, n);
7021
- }
7022
- }
7023
- exposures() {
7024
- return [...this.byKey.values()];
7025
- }
7026
- masks(rules) {
7027
- return rules.map((m) => ({
7028
- selector: m.selector,
7029
- as: m.as === "text" ? "text" : "blur",
7030
- hits: this.maskHits.get(m.selector) ?? 0
7031
- }));
7032
- }
7033
- };
7034
- var KIND_WORDS = {
7035
- email: "an email address",
7036
- key: "something shaped like an API key or token",
7037
- card: "a card number",
7038
- "card-tail": "a card's last digits"
7039
- };
7040
- function exposureLine(e) {
7041
- const when = e.step < 0 ? "as the page opened" : `at step #${e.step}`;
7042
- return `${KIND_WORDS[e.kind]} in ${e.selector} (${when}, ${e.rect.x},${e.rect.y} ${e.rect.w}\xD7${e.rect.h})`;
7043
- }
7044
- var EXPOSURE_ADVICE = 'The recording shows these. Record from a demo account, or hide them before the camera rolls with "mask" in actions.json: { "selector": "\u2026", "as": "text", "text": "demo@acme.test" } swaps the words, "as": "blur" blurs. Then re-record.';
7045
-
7046
- // src/plugin/recorder.ts
7047
- var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
7048
- var DRY_PAUSE_MS = 120;
7049
- function jpegDims(buf) {
7050
- let i = 2;
7051
- while (i < buf.length - 9) {
7052
- if (buf[i] !== 255) return null;
7053
- const marker = buf[i + 1];
7054
- const len = buf.readUInt16BE(i + 2);
7055
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
7056
- return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
7057
- }
7058
- i += 2 + len;
7059
- }
7060
- return null;
7061
- }
6435
+ import { recordTake as recordTakeCore } from "@vosjs/render-core/record";
7062
6436
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
7063
6437
  const dry = opts.dryRun === true;
7064
- const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
7065
- const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
7066
- const vw = actions.viewport?.width ?? 1280;
7067
- const vh = actions.viewport?.height ?? 720;
7068
- const context = opts.context ?? await browser.newContext({
7069
- viewport: { width: vw, height: vh },
7070
- deviceScaleFactor: 1,
7071
- ...opts.storageState ? { storageState: opts.storageState } : {},
7072
- ...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
7073
- });
7074
- if (opts.context && opts.headers && Object.keys(opts.headers).length)
7075
- await context.setExtraHTTPHeaders(opts.headers);
7076
- const masks = actions.mask ?? [];
7077
- if (masks.length) await context.addInitScript(maskInitScript(masks));
7078
- const exposureLog = new ExposureLog(masks);
7079
- const page = await context.newPage();
7080
- const scanExposures = async (step) => {
7081
- const scan = await page.evaluate(EXPOSURE_PROBE).catch(() => null);
7082
- exposureLog.add(step, scan);
7083
- };
7084
- page.on("console", (m) => {
7085
- if (m.type() === "error") log(` [page error] ${m.text()}`);
7086
- });
7087
- log(`goto ${url}`);
7088
- let navTimeout = false;
7089
- let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
7090
- navTimeout = true;
7091
- log(" (networkidle timeout \u2014 continuing)");
7092
- return null;
7093
- });
7094
- await sleep(800);
7095
- const setup = actions.setup ?? [];
7096
- if (setup.length) {
7097
- log(`setup: ${setup.length} step(s), off camera`);
7098
- const result = await runSetup(page, setup, { sleep: realSleep });
7099
- for (const line of result.lines) log(` ${line}`);
7100
- if (result.failed) {
7101
- await context.close().catch(() => {
7102
- });
7103
- throw new SetupError(result.failed);
7104
- }
7105
- log(`goto ${url} (after setup)`);
7106
- response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
7107
- await sleep(800);
7108
- }
7109
- let wall = null;
7110
- if (opts.onArrival) {
7111
- const seen = await page.evaluate(WALL_PROBE).catch(() => null);
7112
- try {
7113
- wall = await opts.onArrival({
7114
- askedUrl: url,
7115
- landedUrl: page.url(),
7116
- ...response ? { status: response.status() } : {},
7117
- passwordFields: seen?.passwordFields ?? 0,
7118
- newPasswordField: seen?.newPasswordField ?? false,
7119
- oneTimeCodeField: seen?.oneTimeCodeField ?? false
7120
- }) ?? null;
7121
- } catch (e) {
7122
- await context.close().catch(() => {
7123
- });
7124
- throw e;
7125
- }
7126
- }
7127
- await scanExposures(-1);
7128
- const cdp = await context.newCDPSession(page);
7129
- const frames = [];
7130
- let frameIdx = 0;
7131
- let t0 = 0;
7132
- if (!dry)
7133
- cdp.on("Page.screencastFrame", (ev) => {
7134
- const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
7135
- const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
7136
- writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
7137
- frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
7138
- cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
7139
- });
7140
- });
7141
- t0 = Date.now();
7142
- if (!dry)
7143
- await cdp.send("Page.startScreencast", {
7144
- format: "jpeg",
7145
- quality: 90,
7146
- maxWidth: vw,
7147
- maxHeight: vh,
7148
- everyNthFrame: 1
7149
- });
7150
- const events = [];
7151
- const cur = { x: 48, y: 48 };
7152
- const now = () => Date.now() - t0;
7153
- const emit = (e) => events.push({ t: now(), ...e });
7154
- await page.mouse.move(cur.x, cur.y);
7155
- emit({ x: cur.x, y: cur.y, type: "move" });
7156
- const clock = { now: () => Date.now(), sleep };
7157
- const moveTo = async (tx, ty) => {
7158
- const dist = Math.hypot(tx - cur.x, ty - cur.y);
7159
- const from = { ...cur };
7160
- await clockMotion(
7161
- dry ? 0 : pointerTravelMs(dist),
7162
- async (u) => {
7163
- cur.x = from.x + (tx - from.x) * u;
7164
- cur.y = from.y + (ty - from.y) * u;
7165
- await page.mouse.move(cur.x, cur.y);
7166
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
7167
- },
7168
- clock
7169
- );
7170
- };
7171
- const boxOf = async (selector) => {
7172
- const loc = page.locator(selector).first();
7173
- try {
7174
- await loc.waitFor({ state: "visible", timeout: 8e3 });
7175
- const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
7176
- await loc.scrollIntoViewIfNeeded();
7177
- const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
7178
- if (before[0] !== after[0] || before[1] !== after[1])
7179
- await sleep(SCROLL_SETTLE_MS);
7180
- const bb = await loc.boundingBox();
7181
- if (!bb) return null;
7182
- return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
7183
- } catch {
7184
- log(` (selector not found, skipping: ${selector})`);
7185
- return null;
7186
- }
7187
- };
7188
- const clickAt = async (rect) => {
7189
- await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
7190
- await sleep(PRESS_LEAD_MS);
7191
- emit({
7192
- x: Math.round(cur.x),
7193
- y: Math.round(cur.y),
7194
- type: "down",
7195
- button: 0,
7196
- rect
7197
- });
7198
- await page.mouse.down();
7199
- await sleep(PRESS_HOLD_MS);
7200
- await page.mouse.up();
7201
- emit({
7202
- x: Math.round(cur.x),
7203
- y: Math.round(cur.y),
7204
- type: "up",
7205
- button: 0,
7206
- rect
7207
- });
7208
- };
7209
- const waitSettled = async () => {
7210
- if (dry) return null;
7211
- const pressAt = now();
7212
- const before = frames.length;
7213
- for (; ; ) {
7214
- const t = now();
7215
- const last = frames.length > before ? frames[frames.length - 1] : null;
7216
- const sinceChange = last ? t - last.tMs : null;
7217
- if (settleVerdict({ sincePress: t - pressAt, sinceChange }) === "settled")
7218
- return sinceChange;
7219
- await sleep(40);
7220
- }
7221
- };
7222
- const skipped = [];
7223
- const stepSpans = [];
7224
- const paces = [];
7225
- let capped = false;
7226
- for (const [stepIdx, step] of actions.steps.entries()) {
7227
- if (capReached(now(), maxSeconds)) {
7228
- capped = true;
7229
- break;
7230
- }
7231
- const stepStart = now();
7232
- let gestureMs = 0;
7233
- const gesture = async (fn) => {
7234
- const t = now();
7235
- const out = await fn();
7236
- gestureMs += now() - t;
7237
- return out;
7238
- };
7239
- const skippedBefore = skipped.length;
7240
- const urlBefore = page.url();
7241
- let stepRect = null;
7242
- switch (step.do) {
7243
- case "wait":
7244
- await sleep(clampWait(step.ms, now(), maxSeconds));
7245
- break;
7246
- case "move":
7247
- await moveTo(step.x, step.y);
7248
- break;
7249
- case "hover": {
7250
- const rect = await boxOf(step.selector);
7251
- stepRect = rect;
7252
- if (rect) {
7253
- await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
7254
- log(`hover ${step.selector}`);
7255
- await sleep(step.ms ?? 700);
7256
- } else
7257
- skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
7258
- break;
7259
- }
7260
- case "click": {
7261
- const rect = await boxOf(step.selector);
7262
- stepRect = rect;
7263
- if (rect) {
7264
- await gesture(() => clickAt(rect));
7265
- log(`click ${step.selector}`);
7266
- const sinceChange = await gesture(() => waitSettled());
7267
- await sleep(holdLeftMs(settleMs(step), sinceChange));
7268
- } else
7269
- skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
7270
- break;
7271
- }
7272
- case "type": {
7273
- const rect = await boxOf(step.selector);
7274
- stepRect = rect;
7275
- if (rect) {
7276
- if (step.focus !== false) await gesture(() => clickAt(rect));
7277
- const delay = step.delayMs ?? 40;
7278
- const ping = () => emit({
7279
- x: Math.round(rect.x + rect.w / 2),
7280
- y: Math.round(rect.y + rect.h / 2),
7281
- type: "key",
7282
- rect
7283
- });
7284
- let lastPing = -Infinity;
7285
- await clockTyping(
7286
- [...step.text],
7287
- delay,
7288
- async (ch) => {
7289
- if (now() - lastPing >= 350) {
7290
- lastPing = now();
7291
- ping();
7292
- }
7293
- await page.keyboard.type(ch);
7294
- },
7295
- clock
7296
- );
7297
- ping();
7298
- log(`type "${step.text}" into ${step.selector}`);
7299
- await gesture(() => sleep(settleMs(step)));
7300
- } else
7301
- skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
7302
- break;
7303
- }
7304
- case "scroll": {
7305
- const total = Math.abs(step.dy);
7306
- const dir = Math.sign(step.dy);
7307
- for (let done = 0; done < total; done += 120) {
7308
- await page.mouse.wheel(0, dir * Math.min(120, total - done));
7309
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
7310
- await sleep(40);
7311
- }
7312
- await gesture(() => sleep(settleMs(step)));
7313
- break;
7314
- }
7315
- case "drag": {
7316
- let start = null;
7317
- let rect;
7318
- if (step.selector) {
7319
- const box = await boxOf(step.selector);
7320
- stepRect = box;
7321
- if (box) {
7322
- rect = box;
7323
- start = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
7324
- } else {
7325
- skipped.push({
7326
- step: stepIdx,
7327
- do: step.do,
7328
- selector: step.selector
7329
- });
7330
- break;
7331
- }
7332
- } else if (typeof step.x === "number" && typeof step.y === "number") {
7333
- start = { x: step.x, y: step.y };
7334
- }
7335
- if (!start) break;
7336
- await gesture(() => moveTo(start.x, start.y));
7337
- await gesture(() => sleep(PRESS_LEAD_MS));
7338
- emit({
7339
- x: Math.round(cur.x),
7340
- y: Math.round(cur.y),
7341
- type: "down",
7342
- button: 0,
7343
- rect
7344
- });
7345
- await page.mouse.down();
7346
- await gesture(() => sleep(PRESS_HOLD_MS));
7347
- const dur = step.ms ?? 700;
7348
- const from = { ...cur };
7349
- await clockMotion(
7350
- dur,
7351
- async (u) => {
7352
- cur.x = from.x + (step.tx - from.x) * u;
7353
- cur.y = from.y + (step.ty - from.y) * u;
7354
- await page.mouse.move(cur.x, cur.y);
7355
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
7356
- },
7357
- clock
7358
- );
7359
- await gesture(() => sleep(settleMs({ do: "drag" })));
7360
- await page.mouse.up();
7361
- emit({
7362
- x: Math.round(cur.x),
7363
- y: Math.round(cur.y),
7364
- type: "up",
7365
- button: 0,
7366
- rect
7367
- });
7368
- log(
7369
- `drag ${step.selector ?? `${start.x},${start.y}`} \u2192 ${step.tx},${step.ty}`
7370
- );
7371
- await sleep(400);
7372
- break;
6438
+ const result = await recordTakeCore(
6439
+ browser,
6440
+ url,
6441
+ actions,
6442
+ {
6443
+ frame: (file, bytes) => {
6444
+ writeFileSync2(join13(paths.framesDir, file), bytes);
7373
6445
  }
6446
+ },
6447
+ log,
6448
+ {
6449
+ ...opts,
6450
+ env: process.env,
6451
+ platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux"
7374
6452
  }
7375
- const stepInfo = step;
7376
- stepSpans.push({
7377
- step: stepIdx,
7378
- ...stepInfo.id ? { id: stepInfo.id } : {},
7379
- do: step.do,
7380
- ...stepInfo.selector ? { selector: stepInfo.selector } : {},
7381
- tStart: +(stepStart / 1e3).toFixed(3),
7382
- tEnd: +(now() / 1e3).toFixed(3),
7383
- ...skipped.length > skippedBefore ? { skipped: true } : {},
7384
- ...page.url() !== urlBefore ? { navigated: true } : {},
7385
- ...stepRect ? {
7386
- rect: {
7387
- x: +stepRect.x.toFixed(1),
7388
- y: +stepRect.y.toFixed(1),
7389
- w: +stepRect.w.toFixed(1),
7390
- h: +stepRect.h.toFixed(1)
7391
- }
7392
- } : {}
7393
- });
7394
- paces.push({
7395
- step: stepIdx,
7396
- do: step.do,
7397
- askedMs: askedMs(step),
7398
- gestureMs,
7399
- wallMs: now() - stepStart
7400
- });
7401
- await scanExposures(stepIdx);
7402
- }
7403
- if (!capped) await sleep(TRAILING_HOLD_MS);
7404
- if (capped || capReached(now(), maxSeconds)) {
7405
- capped = true;
7406
- log(` ${cappedLine(maxSeconds)}`);
7407
- }
7408
- const durationMs = capped ? Math.min(now(), maxSeconds * 1e3) : now();
7409
- await cdp.send("Page.stopScreencast").catch(() => {
7410
- });
7411
- await sleep(200);
7412
- const pageTitle = await page.title().catch(() => "");
7413
- await context.close();
7414
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
7415
- const meta = {
7416
- dpr: 1,
7417
- zoom: 1,
7418
- t0,
7419
- durationMs,
7420
- width: vw,
7421
- height: vh,
7422
- fps: 30,
7423
- hasAudio: false,
7424
- captureWidth: firstFrame?.w ?? vw,
7425
- captureHeight: firstFrame?.h ?? vh,
7426
- captureSurface: "tab",
7427
- pageUrl: url,
7428
- pageTitle,
7429
- platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux",
7430
- producer: "cli",
7431
- steps: stepSpans,
7432
- ...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {},
7433
- ...exposureLog.exposures().length ? { exposures: exposureLog.exposures() } : {},
7434
- ...masks.length ? { masks: exposureLog.masks(masks) } : {}
7435
- };
7436
- const freezes = [];
7437
- for (let i = 1; i < frames.length; i++) {
7438
- const gap = frames[i].tMs - frames[i - 1].tMs;
7439
- if (gap >= 400) {
7440
- freezes.push({
7441
- from: +(frames[i - 1].tMs / 1e3).toFixed(2),
7442
- to: +(frames[i].tMs / 1e3).toFixed(2),
7443
- ms: gap
7444
- });
7445
- }
7446
- }
7447
- const frozenMs = freezes.reduce((sum, f) => sum + f.ms, 0);
7448
- const freezePct = durationMs > 0 ? Math.round(frozenMs / durationMs * 100) : 0;
7449
- const dead = deadTime(stepSpans, frames, events, durationMs);
7450
- if (deadWarns(dead)) log(` WARNING: ${deadLine(dead)}`);
7451
- const pace = paceReport(paces);
6453
+ );
7452
6454
  if (!dry) {
7453
- await writeJson(paths.cursor, events);
7454
- await writeJson(paths.meta, meta, true);
7455
- await writeJson(paths.framesIndex, frames);
7456
- await writeJson(paths.actions, { ...actions, url }, true);
7457
- log(` ${paceLine(pace)}`);
6455
+ await writeJson(paths.cursor, result.events);
6456
+ await writeJson(paths.meta, result.meta, true);
6457
+ await writeJson(paths.framesIndex, result.frames);
6458
+ await writeJson(paths.actions, result.actions, true);
7458
6459
  }
7459
- return {
7460
- events,
7461
- frames,
7462
- meta,
7463
- pace,
7464
- skipped,
7465
- navTimeout,
7466
- wall,
7467
- exposures: exposureLog.exposures(),
7468
- masks: exposureLog.masks(masks),
7469
- freezes,
7470
- freezePct,
7471
- dead,
7472
- capped
7473
- };
6460
+ return result;
7474
6461
  }
7475
6462
 
6463
+ // src/plugin/wall.ts
6464
+ import {
6465
+ WALL_PROBE,
6466
+ WallError,
6467
+ isIdpHost,
6468
+ isSignInPath,
6469
+ redirectedAway,
6470
+ wallVerdict
6471
+ } from "@vosjs/render-core/record";
6472
+
6473
+ // src/plugin/exposure.ts
6474
+ import {
6475
+ EXPOSURE_ADVICE,
6476
+ EXPOSURE_MATCHERS_SRC,
6477
+ EXPOSURE_PROBE,
6478
+ ExposureLog,
6479
+ exposureLine,
6480
+ maskInitScript
6481
+ } from "@vosjs/render-core/record";
6482
+
6483
+ // src/plugin/setup.ts
6484
+ import {
6485
+ SetupEnvError,
6486
+ SetupError,
6487
+ parseHeaders,
6488
+ resolveSetupText,
6489
+ runSetup,
6490
+ setupEnvNames,
6491
+ validateSetup
6492
+ } from "@vosjs/render-core/record";
6493
+
7476
6494
  // src/plugin/session.ts
7477
6495
  import { spawn } from "child_process";
7478
6496
  import { existsSync as existsSync12, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync3 } from "fs";
@@ -7536,10 +6554,10 @@ async function openSession(name, url, opts = { log: () => {
7536
6554
  opts.log(
7537
6555
  `a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
7538
6556
  );
7539
- return new Promise((resolve10, reject) => {
6557
+ return new Promise((resolve11, reject) => {
7540
6558
  const child = spawn(exe, args, { stdio: "ignore" });
7541
6559
  child.on("error", reject);
7542
- child.on("exit", (code) => resolve10({ code }));
6560
+ child.on("exit", (code) => resolve11({ code }));
7543
6561
  });
7544
6562
  }
7545
6563
  async function withSession(name, fn, opts = {}) {
@@ -7616,47 +6634,47 @@ function looksLikeStorageState(value) {
7616
6634
  return Array.isArray(v.cookies) && Array.isArray(v.origins);
7617
6635
  }
7618
6636
 
6637
+ // src/plugin/recordingCap.ts
6638
+ import {
6639
+ capReached,
6640
+ cappedLine,
6641
+ clampWait,
6642
+ formatDurationCap
6643
+ } from "@vosjs/render-core/record";
6644
+ var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
6645
+ function parseHostedCap(body) {
6646
+ const limits = body && typeof body === "object" ? body.limits : void 0;
6647
+ const cap = limits?.recordingMaxSeconds;
6648
+ return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
6649
+ }
6650
+ async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
6651
+ const ctl = new AbortController();
6652
+ const timer = setTimeout(() => ctl.abort(), 2e3);
6653
+ try {
6654
+ const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
6655
+ signal: ctl.signal,
6656
+ ...key ? { headers: { authorization: `Bearer ${key}` } } : {}
6657
+ });
6658
+ if (!res.ok) return null;
6659
+ return parseHostedCap(await res.json());
6660
+ } catch {
6661
+ return null;
6662
+ } finally {
6663
+ clearTimeout(timer);
6664
+ }
6665
+ }
6666
+ function defaultMaxDurationSeconds() {
6667
+ return HOSTED_RECORDING_CAP_SECONDS;
6668
+ }
6669
+
7619
6670
  // src/plugin/encode.ts
7620
- var MEDIABUNNY_URL2 = "https://esm.sh/mediabunny@1.55.7?target=es2022";
7621
- var ENCODE_HTML = `<!doctype html><html><head><meta charset="utf-8"></head><body>
7622
- <script type="module">
7623
- try {
7624
- const MB = await import('${MEDIABUNNY_URL2}')
7625
- const frames = await (await fetch('/frames.json')).json()
7626
- const meta = await (await fetch('/meta.json')).json()
7627
- const W = meta.captureWidth ?? meta.width, H = meta.captureHeight ?? meta.height
7628
- const FPS = 30
7629
- const durSec = meta.durationMs / 1000
7630
- const cv = document.createElement('canvas')
7631
- cv.width = W; cv.height = H
7632
- const c2 = cv.getContext('2d')
7633
- const output = new MB.Output({ format: new MB.WebMOutputFormat(), target: new MB.BufferTarget() })
7634
- const src = new MB.CanvasSource(cv, { codec: 'vp9', bitrate: 8_000_000 })
7635
- output.addVideoTrack(src, { frameRate: FPS })
7636
- await output.start()
7637
- let fi = -1
7638
- const total = Math.ceil(durSec * FPS)
7639
- for (let i = 0; i < total; i++) {
7640
- const t = i / FPS
7641
- let next = fi
7642
- while (next + 1 < frames.length && frames[next + 1].tMs <= t * 1000) next++
7643
- if (next < 0) next = 0
7644
- if (next !== fi) {
7645
- fi = next
7646
- const blob = await (await fetch('/frames/' + frames[fi].file)).blob()
7647
- const bmp = await createImageBitmap(blob)
7648
- c2.drawImage(bmp, 0, 0, W, H)
7649
- bmp.close()
7650
- }
7651
- await src.add(t, 1 / FPS)
7652
- window.__progress = i / total
7653
- }
7654
- await output.finalize()
7655
- const buf = output.target.buffer
7656
- await fetch('/save?name=${RECORDING_NAME}', { method: 'POST', body: buf })
7657
- window.__done = { ok: true, bytes: buf.byteLength }
7658
- } catch (e) { window.__error = String(e && e.stack || e) }
7659
- </script></body></html>`;
6671
+ import { buildEncodePage } from "@vosjs/render-core/record";
6672
+ var ENCODE_HTML = buildEncodePage({
6673
+ framesIndexUrl: "/frames.json",
6674
+ metaUrl: "/meta.json",
6675
+ framesBaseUrl: "/frames/",
6676
+ save: { method: "POST", url: `/save?name=${RECORDING_NAME}` }
6677
+ });
7660
6678
  async function encodeRecording(browser, takeDir, onProgress) {
7661
6679
  const server = await startTakeServer(takeDir, { "/encode.html": ENCODE_HTML });
7662
6680
  const page = await browser.newPage();
@@ -8112,7 +7130,7 @@ async function planTake(dir, opts = {}) {
8112
7130
 
8113
7131
  // src/plugin/sync.ts
8114
7132
  import { createHash } from "crypto";
8115
- import { existsSync as existsSync14, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
7133
+ import { existsSync as existsSync14, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
8116
7134
  import { readFile as readFile13 } from "fs/promises";
8117
7135
  import { createInterface } from "readline/promises";
8118
7136
  import { basename as basename3, join as join17 } from "path";
@@ -8616,7 +7634,7 @@ function storageStateInDir(dir) {
8616
7634
  if (!f.endsWith(".json") || f === "doc.json" || f === "meta.json" || f === "actions.json" || f === "cursor.json" || f === "vos.json" || f === "doc.prev.json")
8617
7635
  continue;
8618
7636
  try {
8619
- const v = JSON.parse(readFileSync4(join17(dir, f), "utf8"));
7637
+ const v = JSON.parse(readFileSync3(join17(dir, f), "utf8"));
8620
7638
  if (looksLikeStorageState(v)) return join17(dir, f);
8621
7639
  } catch {
8622
7640
  }
@@ -9723,7 +8741,7 @@ async function writeProgramDoc(dir, hosted) {
9723
8741
  }
9724
8742
 
9725
8743
  // src/plugin/asset.ts
9726
- import { readFileSync as readFileSync5 } from "fs";
8744
+ import { readFileSync as readFileSync4 } from "fs";
9727
8745
  import { basename as basename5, extname as extname2 } from "path";
9728
8746
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
9729
8747
  var MIME_BY_EXT = {
@@ -9777,7 +8795,7 @@ async function cmdPush(argv) {
9777
8795
  for (const file of positionals) {
9778
8796
  const name = basename5(file);
9779
8797
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
9780
- const body = readFileSync5(file);
8798
+ const body = readFileSync4(file);
9781
8799
  const form = new FormData();
9782
8800
  form.set("file", new Blob([body], { type: mime }), name);
9783
8801
  if (folder) form.set("folderId", folder.id);
@@ -9831,7 +8849,7 @@ async function cmdAsset(argv) {
9831
8849
  }
9832
8850
 
9833
8851
  // src/plugin/recipe.ts
9834
- import { readFileSync as readFileSync6 } from "fs";
8852
+ import { readFileSync as readFileSync5 } from "fs";
9835
8853
  import { basename as basename6 } from "path";
9836
8854
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
9837
8855
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -9851,7 +8869,7 @@ async function cmdPush2(argv) {
9851
8869
  throw new UsageError(USAGE);
9852
8870
  }
9853
8871
  const filename = recipeFilename(file);
9854
- const body = readFileSync6(file);
8872
+ const body = readFileSync5(file);
9855
8873
  const r = createReporter(flags.json === true);
9856
8874
  const origin = platformOrigin({
9857
8875
  origin: strFlag(flags, "origin"),
@@ -10294,6 +9312,103 @@ async function cmdBrand(argv) {
10294
9312
  return 0;
10295
9313
  }
10296
9314
 
9315
+ // src/plugin/hostedRecord.ts
9316
+ import { setupEnvNames as setupEnvNames2 } from "@vosjs/render-core/record";
9317
+ var POLL_MS = 3e3;
9318
+ var DEFAULT_TIMEOUT_MS = 20 * 60 * 1e3;
9319
+ function hostedEnv(actions, shell) {
9320
+ const env = {};
9321
+ const missing = [];
9322
+ for (const name of setupEnvNames2(actions.setup)) {
9323
+ const v = shell[name];
9324
+ if (v === void 0 || v === "") missing.push(name);
9325
+ else env[name] = v;
9326
+ }
9327
+ return { env, missing };
9328
+ }
9329
+ function isWallError(error) {
9330
+ return !!error && /^(\[\w+\] )?The page is behind a sign-in/.test(error);
9331
+ }
9332
+ async function recordHosted(input) {
9333
+ const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
9334
+ const posted = await apiJson(input.origin, "/api/record/jobs", {
9335
+ method: "POST",
9336
+ key: input.key,
9337
+ body: {
9338
+ url: input.url,
9339
+ actions: input.actions,
9340
+ viewport: input.actions.viewport,
9341
+ ...Object.keys(input.headers).length ? { headers: input.headers } : {},
9342
+ ...Object.keys(input.env).length ? { env: input.env } : {},
9343
+ ...input.title ? { title: input.title } : {},
9344
+ ...input.folderId ? { folderId: input.folderId } : {}
9345
+ }
9346
+ });
9347
+ if (posted.status !== 202) {
9348
+ return {
9349
+ ok: false,
9350
+ wall: false,
9351
+ error: apiError("record on vos.so", posted),
9352
+ vosId: null,
9353
+ jobId: null
9354
+ };
9355
+ }
9356
+ const job = posted.body.job;
9357
+ const vos = posted.body.vos;
9358
+ const cap = posted.body.cap;
9359
+ input.log(
9360
+ `queued on vos.so as job ${job.id.slice(0, 8)} (vos ${job.vosId}${cap?.seconds ? `, cap ${cap.seconds}s` : ""})`
9361
+ );
9362
+ const deadline = Date.now() + (input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
9363
+ let lastStatus = "queued";
9364
+ while (Date.now() < deadline) {
9365
+ await sleep(POLL_MS);
9366
+ const polled = await apiJson(input.origin, `/api/record/jobs/${job.id}`, {
9367
+ key: input.key
9368
+ });
9369
+ if (polled.status !== 200) {
9370
+ return {
9371
+ ok: false,
9372
+ wall: false,
9373
+ error: apiError(`poll record job ${job.id}`, polled),
9374
+ vosId: job.vosId,
9375
+ jobId: job.id
9376
+ };
9377
+ }
9378
+ const state = polled.body.job;
9379
+ if (state.status !== lastStatus) {
9380
+ input.log(`${state.status}\u2026`);
9381
+ lastStatus = state.status;
9382
+ }
9383
+ if (state.status === "completed") {
9384
+ return {
9385
+ ok: true,
9386
+ vosId: state.vosId,
9387
+ jobId: state.id,
9388
+ job: state,
9389
+ studio: vos?.studio ?? `${input.origin}/studio?vos=${state.vosId}`
9390
+ };
9391
+ }
9392
+ if (state.status === "failed" || state.status === "canceled") {
9393
+ const error = state.error ?? (state.status === "canceled" ? "the record job was canceled" : "the record job failed");
9394
+ return {
9395
+ ok: false,
9396
+ wall: isWallError(error),
9397
+ error,
9398
+ vosId: state.vosId,
9399
+ jobId: state.id
9400
+ };
9401
+ }
9402
+ }
9403
+ return {
9404
+ ok: false,
9405
+ wall: false,
9406
+ error: `the record job ${job.id} did not finish within ${Math.round((input.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 6e4)} min; poll ${input.origin}/api/record/jobs/${job.id}`,
9407
+ vosId: job.vosId,
9408
+ jobId: job.id
9409
+ };
9410
+ }
9411
+
10297
9412
  // src/plugin/agentBrowser.ts
10298
9413
  import { writeFile as writeFile13 } from "fs/promises";
10299
9414
  import { resolve as resolve8 } from "path";
@@ -10775,8 +9890,8 @@ async function cmdActions(argv) {
10775
9890
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
10776
9891
  viewport = { width: Number(m[1]), height: Number(m[2]) };
10777
9892
  }
10778
- const { readFile: readFile16 } = await import("fs/promises");
10779
- const text = await readFile16(resolve8(input), "utf8");
9893
+ const { readFile: readFile17 } = await import("fs/promises");
9894
+ const text = await readFile17(resolve8(input), "utf8");
10780
9895
  const { records, problems } = parseAgentBrowserLog(text);
10781
9896
  for (const p of problems) r.log(` ${p}`);
10782
9897
  const result = convertAgentBrowser(records, {
@@ -10813,9 +9928,9 @@ function summary(result) {
10813
9928
  }
10814
9929
  async function cmdActionsScript(input, flags) {
10815
9930
  const r = createReporter(flags.json === true);
10816
- const { readFile: readFile16 } = await import("fs/promises");
10817
- const { shotList, formatShotList } = await import("./shotList-7F2FE3RU.js");
10818
- const raw = JSON.parse(await readFile16(resolve8(input), "utf8"));
9931
+ const { readFile: readFile17 } = await import("fs/promises");
9932
+ const { shotList, formatShotList } = await import("./shotList-IYFDHSIK.js");
9933
+ const raw = JSON.parse(await readFile17(resolve8(input), "utf8"));
10819
9934
  const errors = validateActions(raw);
10820
9935
  if (errors.length) throw new UsageError(`${input}:
10821
9936
  ${errors.join("\n ")}`);
@@ -10824,23 +9939,232 @@ async function cmdActionsScript(input, flags) {
10824
9939
  return EXIT_OK;
10825
9940
  }
10826
9941
 
9942
+ // src/plugin/ingest.ts
9943
+ import { existsSync as existsSync16 } from "fs";
9944
+ import { copyFile, mkdir as mkdir10, readFile as readFile15, stat as stat4 } from "fs/promises";
9945
+ import { basename as basename7, join as join19, resolve as resolve9 } from "path";
9946
+ import { inflateRawSync } from "zlib";
9947
+ import {
9948
+ cursorFromPlaywrightTrace,
9949
+ cursorFromTrace,
9950
+ playwrightTraceEntry,
9951
+ traceKindOf,
9952
+ zipEntries,
9953
+ zipEntryBytes
9954
+ } from "@vosjs/studio-core";
9955
+ import {
9956
+ ALL_FORMATS,
9957
+ Conversion,
9958
+ FilePathSource,
9959
+ FilePathTarget,
9960
+ Input,
9961
+ Mp4OutputFormat,
9962
+ Output,
9963
+ WebMOutputFormat
9964
+ } from "mediabunny";
9965
+ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set(["json", "help"]);
9966
+ async function probeVideoFile(path) {
9967
+ const input = new Input({
9968
+ source: new FilePathSource(path),
9969
+ formats: ALL_FORMATS
9970
+ });
9971
+ try {
9972
+ const format = await input.getFormat();
9973
+ const video = await input.getPrimaryVideoTrack();
9974
+ if (!video) throw new UsageError(`${path} has no video track`);
9975
+ const [duration, stats, audio] = await Promise.all([
9976
+ input.computeDuration(),
9977
+ video.computePacketStats(240),
9978
+ input.getPrimaryAudioTrack()
9979
+ ]);
9980
+ const mimeType = format.mimeType;
9981
+ const ext = mimeType.includes("mp4") ? "mp4" : mimeType.includes("quicktime") ? "mov" : "webm";
9982
+ return {
9983
+ width: video.displayWidth,
9984
+ height: video.displayHeight,
9985
+ durationMs: Math.round(duration * 1e3),
9986
+ fps: Math.round(stats.averagePacketRate * 100) / 100,
9987
+ hasAudio: !!audio,
9988
+ mimeType,
9989
+ ext
9990
+ };
9991
+ } finally {
9992
+ await input.dispose?.();
9993
+ }
9994
+ }
9995
+ async function copyRecording(src, dest, ext) {
9996
+ if (ext === "mov") {
9997
+ await copyFile(src, dest);
9998
+ return false;
9999
+ }
10000
+ const input = new Input({
10001
+ source: new FilePathSource(src),
10002
+ formats: ALL_FORMATS
10003
+ });
10004
+ const target = new FilePathTarget(dest);
10005
+ const output = new Output({
10006
+ format: ext === "mp4" ? new Mp4OutputFormat({ fastStart: "in-memory" }) : new WebMOutputFormat(),
10007
+ target
10008
+ });
10009
+ try {
10010
+ const conversion = await Conversion.init({ input, output });
10011
+ if (!conversion.isValid || conversion.discardedTracks.length > 0) {
10012
+ await conversion.cancel?.();
10013
+ await copyFile(src, dest);
10014
+ return false;
10015
+ }
10016
+ await conversion.execute();
10017
+ return true;
10018
+ } catch {
10019
+ await copyFile(src, dest);
10020
+ return false;
10021
+ } finally {
10022
+ await input.dispose?.();
10023
+ }
10024
+ }
10025
+ async function readTraceFile(path, offsetMs) {
10026
+ const kind = traceKindOf(basename7(path));
10027
+ if (!kind) {
10028
+ throw new UsageError(
10029
+ `${path}: not a trace the recorder reads (trace.zip, .trace, .jsonl, .json or .csv)`
10030
+ );
10031
+ }
10032
+ if (kind === "zip") {
10033
+ const bytes = new Uint8Array(await readFile15(path));
10034
+ const entry = playwrightTraceEntry(zipEntries(bytes));
10035
+ if (!entry) {
10036
+ throw new UsageError(
10037
+ `${path}: no trace.trace inside; a Playwright trace.zip is what --cursor reads as a zip`
10038
+ );
10039
+ }
10040
+ const raw = zipEntryBytes(bytes, entry);
10041
+ const text2 = new TextDecoder().decode(
10042
+ entry.method === 8 ? new Uint8Array(inflateRawSync(raw)) : raw
10043
+ );
10044
+ return cursorFromPlaywrightTrace(text2, { offsetMs });
10045
+ }
10046
+ const text = await readFile15(path, "utf8");
10047
+ return cursorFromTrace(basename7(path), text, { offsetMs });
10048
+ }
10049
+ function parseViewport(v) {
10050
+ if (!v) return null;
10051
+ const m = /^(\d+)x(\d+)$/i.exec(v.trim());
10052
+ if (!m) throw new UsageError("--viewport expects WxH, like 1280x720");
10053
+ return { width: Number(m[1]), height: Number(m[2]) };
10054
+ }
10055
+ async function cmdIngest(argv) {
10056
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5, MULTI_FLAGS2);
10057
+ const source = positionals[0];
10058
+ if (!source)
10059
+ throw new UsageError(
10060
+ "vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]"
10061
+ );
10062
+ const videoPath = resolve9(source);
10063
+ if (!existsSync16(videoPath)) throw new UsageError(`${source}: no such file`);
10064
+ const r = createReporter(flags.json === true);
10065
+ const outDir = resolve9(strFlag(flags, "out") ?? "take");
10066
+ const offsetMs = numFlag(flags, "offset", 0);
10067
+ const viewportFlag = parseViewport(strFlag(flags, "viewport"));
10068
+ const cursorPath = strFlag(flags, "cursor");
10069
+ const backdrop = await takeBackdrop(flags, r);
10070
+ r.log(`probing ${basename7(videoPath)}\u2026`);
10071
+ const probed = await probeVideoFile(videoPath);
10072
+ if (!(probed.durationMs > 0)) {
10073
+ throw new UsageError(
10074
+ `${source}: the file reports no duration; a stream-shaped recording needs its container fixed first (ffmpeg -i in.webm -c copy out.webm)`
10075
+ );
10076
+ }
10077
+ let trace = null;
10078
+ if (cursorPath) {
10079
+ trace = await readTraceFile(resolve9(cursorPath), offsetMs);
10080
+ for (const n of trace.notes) r.log(`note: ${n}`);
10081
+ }
10082
+ if (existsSync16(join19(outDir, "meta.json"))) {
10083
+ const { prevDoc, kept } = await prepareReRecord(outDir);
10084
+ r.log(
10085
+ `note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
10086
+ );
10087
+ }
10088
+ await mkdir10(outDir, { recursive: true });
10089
+ const paths = await ensureTakeDir(outDir);
10090
+ const recording = join19(outDir, `recording.${probed.ext}`);
10091
+ r.log("copying the recording\u2026");
10092
+ const remuxed = await copyRecording(videoPath, recording, probed.ext);
10093
+ const viewport = viewportFlag ?? trace?.viewport ?? {
10094
+ width: probed.width,
10095
+ height: probed.height
10096
+ };
10097
+ const events = trace?.events ?? [];
10098
+ const fileStat = await stat4(videoPath);
10099
+ const meta = {
10100
+ dpr: 1,
10101
+ zoom: 1,
10102
+ t0: trace?.t0 ?? Math.round(fileStat.mtimeMs),
10103
+ durationMs: probed.durationMs,
10104
+ width: viewport.width,
10105
+ height: viewport.height,
10106
+ captureWidth: probed.width,
10107
+ captureHeight: probed.height,
10108
+ fps: probed.fps > 0 ? probed.fps : 30,
10109
+ hasAudio: probed.hasAudio,
10110
+ producer: "ingest",
10111
+ ...events.length ? { captureSurface: "tab" } : {}
10112
+ };
10113
+ await writeJson(paths.meta, meta, true);
10114
+ if (events.length) await writeJson(paths.cursor, events);
10115
+ const plan = await planTake(outDir, { backdrop });
10116
+ const clicks = events.filter((e) => e.type === "down").length;
10117
+ const seconds = (probed.durationMs / 1e3).toFixed(1);
10118
+ const facts = `${seconds}s, ${probed.width}\xD7${probed.height} @ ${meta.fps} fps${probed.hasAudio ? ", audio" : ", no audio"}${remuxed ? "" : ", copied as is"}`;
10119
+ const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename7(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
10120
+ const next = trace ? `Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}` : `Add --cursor <trace> recorded beside the video (a Playwright trace.zip, stamped JSON records, or a t,x,y,type CSV) to plan zooms from its clicks; else place them by eye: vos frames ${outDir} --times 0,25%,50%,75%,100%, then edit doc.json`;
10121
+ r.done(
10122
+ {
10123
+ out: outDir,
10124
+ recording: basename7(recording),
10125
+ durationMs: probed.durationMs,
10126
+ width: probed.width,
10127
+ height: probed.height,
10128
+ viewport,
10129
+ fps: meta.fps,
10130
+ hasAudio: probed.hasAudio,
10131
+ remuxed,
10132
+ cursor: trace ? {
10133
+ file: basename7(cursorPath),
10134
+ kind: trace.kind,
10135
+ events: events.length,
10136
+ clicks,
10137
+ placed: trace.placed,
10138
+ dropped: trace.dropped,
10139
+ notes: trace.notes
10140
+ } : null,
10141
+ zooms: plan.zoomAuto,
10142
+ backdrop: plan.backdrop ?? null
10143
+ },
10144
+ `Ingested ${basename7(videoPath)} \u2192 ${outDir}/${basename7(recording)} (${facts})
10145
+ ${cursorLine}
10146
+ ${next}`
10147
+ );
10148
+ return EXIT_OK;
10149
+ }
10150
+
10827
10151
  // src/plugin/machineCmd.ts
10828
10152
  import { spawnSync } from "child_process";
10829
10153
  import {
10830
10154
  cpSync,
10831
- existsSync as existsSync17,
10155
+ existsSync as existsSync18,
10832
10156
  mkdirSync as mkdirSync3,
10833
- readFileSync as readFileSync8,
10157
+ readFileSync as readFileSync7,
10834
10158
  rmSync as rmSync2,
10835
10159
  writeFileSync as writeFileSync3
10836
10160
  } from "fs";
10837
10161
  import { homedir as homedir3 } from "os";
10838
- import { join as join20 } from "path";
10162
+ import { join as join21 } from "path";
10839
10163
  import { chromium as chromium2 } from "playwright";
10840
10164
 
10841
10165
  // src/plugin/machine.ts
10842
- import { existsSync as existsSync16, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
10843
- import { dirname as dirname5, join as join19 } from "path";
10166
+ import { existsSync as existsSync17, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
10167
+ import { dirname as dirname5, join as join20 } from "path";
10844
10168
  var AGENT_NAMES = [
10845
10169
  "claude",
10846
10170
  "cursor",
@@ -10860,17 +10184,17 @@ var HOME_DIRS = {
10860
10184
  copilot: [".copilot"]
10861
10185
  };
10862
10186
  function detectAgents(root, opts = {}) {
10863
- const exists = opts.exists ?? existsSync16;
10187
+ const exists = opts.exists ?? existsSync17;
10864
10188
  const out = [];
10865
10189
  for (const agent of AGENT_NAMES) {
10866
10190
  for (const rel of PROJECT_DIRS[agent]) {
10867
- const dir = join19(root, rel);
10191
+ const dir = join20(root, rel);
10868
10192
  if (exists(dir)) out.push({ agent, dir, scope: "project" });
10869
10193
  }
10870
- if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join19(root, ".github", "copilot-instructions.md"))) {
10194
+ if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join20(root, ".github", "copilot-instructions.md"))) {
10871
10195
  out.push({
10872
10196
  agent,
10873
- dir: join19(root, ".github", "copilot"),
10197
+ dir: join20(root, ".github", "copilot"),
10874
10198
  scope: "project"
10875
10199
  });
10876
10200
  }
@@ -10878,7 +10202,7 @@ function detectAgents(root, opts = {}) {
10878
10202
  if (opts.global && opts.home) {
10879
10203
  for (const agent of AGENT_NAMES) {
10880
10204
  for (const rel of HOME_DIRS[agent]) {
10881
- const dir = join19(opts.home, rel);
10205
+ const dir = join20(opts.home, rel);
10882
10206
  if (exists(dir)) out.push({ agent, dir, scope: "global" });
10883
10207
  }
10884
10208
  }
@@ -10896,18 +10220,18 @@ function agentsFromFlag(value, root) {
10896
10220
  return {
10897
10221
  homes: wanted.map((agent) => ({
10898
10222
  agent,
10899
- dir: join19(root, PROJECT_DIRS[agent][0]),
10223
+ dir: join20(root, PROJECT_DIRS[agent][0]),
10900
10224
  scope: "project"
10901
10225
  }))
10902
10226
  };
10903
10227
  }
10904
10228
  function skillsDirFor(home) {
10905
- return join19(home.dir, "skills");
10229
+ return join20(home.dir, "skills");
10906
10230
  }
10907
10231
  function fallbackSkillsHome(root) {
10908
- return { agent: "codex", dir: join19(root, ".agents"), scope: "project" };
10232
+ return { agent: "codex", dir: join20(root, ".agents"), scope: "project" };
10909
10233
  }
10910
- function installedSkills(dir, exists = existsSync16, list = (p) => {
10234
+ function installedSkills(dir, exists = existsSync17, list = (p) => {
10911
10235
  try {
10912
10236
  return readdirSync4(p);
10913
10237
  } catch {
@@ -10915,7 +10239,7 @@ function installedSkills(dir, exists = existsSync16, list = (p) => {
10915
10239
  }
10916
10240
  }) {
10917
10241
  if (!exists(dir)) return [];
10918
- return list(dir).filter((name) => exists(join19(dir, name, "SKILL.md"))).sort();
10242
+ return list(dir).filter((name) => exists(join20(dir, name, "SKILL.md"))).sort();
10919
10243
  }
10920
10244
  var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
10921
10245
  var RULES_END = "<!-- vos:end -->";
@@ -10947,10 +10271,10 @@ ${RULES_BLOCK}
10947
10271
  }
10948
10272
  return { text, changed: text !== current };
10949
10273
  }
10950
- function rulesFileFor(root, exists = existsSync16) {
10951
- const agents = join19(root, "AGENTS.md");
10274
+ function rulesFileFor(root, exists = existsSync17) {
10275
+ const agents = join20(root, "AGENTS.md");
10952
10276
  if (exists(agents)) return agents;
10953
- const claude = join19(root, "CLAUDE.md");
10277
+ const claude = join20(root, "CLAUDE.md");
10954
10278
  if (exists(claude)) return claude;
10955
10279
  return agents;
10956
10280
  }
@@ -10959,14 +10283,14 @@ function chromeCandidates(platform, env) {
10959
10283
  return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
10960
10284
  if (platform === "win32")
10961
10285
  return [
10962
- join19(
10286
+ join20(
10963
10287
  env["PROGRAMFILES"] ?? "C:\\Program Files",
10964
10288
  "Google",
10965
10289
  "Chrome",
10966
10290
  "Application",
10967
10291
  "chrome.exe"
10968
10292
  ),
10969
- join19(
10293
+ join20(
10970
10294
  env["LOCALAPPDATA"] ?? "",
10971
10295
  "Google",
10972
10296
  "Chrome",
@@ -11053,13 +10377,13 @@ function doctorReport(facts) {
11053
10377
  function bundledSkillsDir(from) {
11054
10378
  let dir = dirname5(from);
11055
10379
  for (let i = 0; i < 6; i++) {
11056
- const pkg = join19(dir, "package.json");
11057
- if (existsSync16(pkg)) {
10380
+ const pkg = join20(dir, "package.json");
10381
+ if (existsSync17(pkg)) {
11058
10382
  try {
11059
- const name = JSON.parse(readFileSync7(pkg, "utf8")).name;
10383
+ const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
11060
10384
  if (name === "@vosjs/cli") {
11061
- const skills = join19(dir, "skills");
11062
- return existsSync16(skills) && statSync4(skills).isDirectory() ? skills : null;
10385
+ const skills = join20(dir, "skills");
10386
+ return existsSync17(skills) && statSync4(skills).isDirectory() ? skills : null;
11063
10387
  }
11064
10388
  } catch {
11065
10389
  }
@@ -11072,7 +10396,7 @@ function bundledSkillsDir(from) {
11072
10396
  }
11073
10397
 
11074
10398
  // src/plugin/machineCmd.ts
11075
- var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
10399
+ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
11076
10400
  "json",
11077
10401
  "help",
11078
10402
  "global",
@@ -11090,7 +10414,7 @@ function npx() {
11090
10414
  function bundledChromium() {
11091
10415
  try {
11092
10416
  const p = chromium2.executablePath();
11093
- return p && existsSync17(p) ? p : null;
10417
+ return p && existsSync18(p) ? p : null;
11094
10418
  } catch {
11095
10419
  return null;
11096
10420
  }
@@ -11099,7 +10423,7 @@ function probe() {
11099
10423
  return probeBrowser({
11100
10424
  platform: process.platform,
11101
10425
  env: process.env,
11102
- exists: existsSync17,
10426
+ exists: existsSync18,
11103
10427
  bundled: bundledChromium
11104
10428
  });
11105
10429
  }
@@ -11150,7 +10474,7 @@ async function doctorFacts(opts) {
11150
10474
  };
11151
10475
  }
11152
10476
  async function cmdSetup(argv) {
11153
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10477
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
11154
10478
  const r = reporterFor(flags);
11155
10479
  const root = process.cwd();
11156
10480
  const global = flags.global === true;
@@ -11203,7 +10527,7 @@ async function cmdSetup(argv) {
11203
10527
  const dest = skillsDirFor(home);
11204
10528
  mkdirSync3(dest, { recursive: true });
11205
10529
  for (const name of installedSkills(bundled)) {
11206
- cpSync(join20(bundled, name), join20(dest, name), { recursive: true });
10530
+ cpSync(join21(bundled, name), join21(dest, name), { recursive: true });
11207
10531
  }
11208
10532
  }
11209
10533
  skillsVia = "bundled";
@@ -11238,7 +10562,7 @@ async function cmdSetup(argv) {
11238
10562
  let rules = null;
11239
10563
  if (!noRules) {
11240
10564
  const file = rulesFileFor(root);
11241
- const existing = existsSync17(file) ? readFileSync8(file, "utf8") : null;
10565
+ const existing = existsSync18(file) ? readFileSync7(file, "utf8") : null;
11242
10566
  const { text, changed } = applyRulesBlock(existing);
11243
10567
  if (changed) writeFileSync3(file, text);
11244
10568
  rules = { file, changed };
@@ -11269,7 +10593,7 @@ Next: ${report.next_step}`
11269
10593
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
11270
10594
  }
11271
10595
  async function cmdDoctor(argv) {
11272
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10596
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
11273
10597
  const r = reporterFor(flags);
11274
10598
  const root = process.cwd();
11275
10599
  const homes = detectAgents(root, { home: homedir3(), global: true });
@@ -11288,7 +10612,7 @@ Next: ${report.next_step}`
11288
10612
  return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
11289
10613
  }
11290
10614
  async function cmdWhoami(argv) {
11291
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10615
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
11292
10616
  const r = reporterFor(flags);
11293
10617
  const origin = platformOrigin({
11294
10618
  origin: strFlag(flags, "origin"),
@@ -11350,9 +10674,9 @@ async function cmdWhoami(argv) {
11350
10674
  return EXIT_OK;
11351
10675
  }
11352
10676
  function cmdLogout(argv) {
11353
- const { flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10677
+ const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
11354
10678
  const r = reporterFor(flags);
11355
- const had = existsSync17(CREDENTIALS_PATH);
10679
+ const had = existsSync18(CREDENTIALS_PATH);
11356
10680
  rmSync2(CREDENTIALS_PATH, { force: true });
11357
10681
  const envKey = Boolean(process.env.VOS_API_KEY?.trim());
11358
10682
  r.done(
@@ -11368,7 +10692,7 @@ function cmdLogout(argv) {
11368
10692
  }
11369
10693
 
11370
10694
  // src/plugin/run.ts
11371
- var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
10695
+ var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set([
11372
10696
  "json",
11373
10697
  "help",
11374
10698
  "picture",
@@ -11389,7 +10713,8 @@ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
11389
10713
  "leader",
11390
10714
  "keep-frames",
11391
10715
  "dry-run",
11392
- "allow-wall"
10716
+ "allow-wall",
10717
+ "hosted"
11393
10718
  ]);
11394
10719
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
11395
10720
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -11397,6 +10722,8 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
11397
10722
  Take pipeline
11398
10723
  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]
11399
10724
  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]
10725
+ vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
10726
+ the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
11400
10727
  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]
11401
10728
  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]
11402
10729
  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]
@@ -11408,6 +10735,8 @@ Take pipeline
11408
10735
  vos validate <actions.json|take|kit.json> [--picture] [--json]
11409
10736
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
11410
10737
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
10738
+ vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
10739
+ a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so
11411
10740
  vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
11412
10741
  vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
11413
10742
  vos session list | rm <name> what exists and how old; delete one
@@ -11665,7 +10994,7 @@ lint-gated, so a bad override fails like a bad doc.json):
11665
10994
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
11666
10995
  `;
11667
10996
  async function loadActions(file) {
11668
- const raw = JSON.parse(await readFile15(file, "utf8"));
10997
+ const raw = JSON.parse(await readFile16(file, "utf8"));
11669
10998
  const errors = validateActions(raw);
11670
10999
  if (errors.length)
11671
11000
  throw new UsageError(`invalid actions file:
@@ -11727,8 +11056,8 @@ function takeBrowserArgs(multi) {
11727
11056
  function takeStorageState(flags) {
11728
11057
  const raw = strFlag(flags, "storage-state");
11729
11058
  if (!raw) return void 0;
11730
- const path = resolve9(raw);
11731
- if (!existsSync18(path))
11059
+ const path = resolve10(raw);
11060
+ if (!existsSync19(path))
11732
11061
  throw new UsageError(`--storage-state: no such file: ${path}`);
11733
11062
  return path;
11734
11063
  }
@@ -11741,7 +11070,7 @@ function takeSessionName(flags, storageState) {
11741
11070
  throw new UsageError(
11742
11071
  "--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
11743
11072
  );
11744
- if (!existsSync18(sessionDir(name)))
11073
+ if (!existsSync19(sessionDir(name)))
11745
11074
  throw new UsageError(
11746
11075
  `no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
11747
11076
  );
@@ -11797,7 +11126,7 @@ function wallGate(flags, r, opts) {
11797
11126
  async function cmdRecord(argv) {
11798
11127
  const { positionals, flags, multi } = parseArgs(
11799
11128
  argv,
11800
- BOOLEAN_FLAGS6,
11129
+ BOOLEAN_FLAGS7,
11801
11130
  MULTI_FLAGS2
11802
11131
  );
11803
11132
  const actionsPath = strFlag(flags, "actions") ?? positionals[0];
@@ -11810,13 +11139,103 @@ async function cmdRecord(argv) {
11810
11139
  const url = strFlag(flags, "url") ?? actions.url;
11811
11140
  if (!url)
11812
11141
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
11813
- const outDir = resolve9(strFlag(flags, "out") ?? "take");
11814
- const backdrop = await takeBackdrop(flags, r);
11815
- const maxDurationSeconds = await maxDuration(flags, r);
11142
+ const outDir = resolve10(strFlag(flags, "out") ?? "take");
11816
11143
  const storageState = takeStorageState(flags);
11817
11144
  const sessionName = takeSessionName(flags, storageState);
11818
11145
  const browserArgs = takeBrowserArgs(multi);
11819
11146
  const headers = takeHeaders(multi);
11147
+ if (flags.hosted === true) {
11148
+ if (flags["dry-run"] === true)
11149
+ throw new UsageError(
11150
+ "--dry-run rehearses on this machine; drop --hosted to rehearse, or --dry-run to record on vos.so"
11151
+ );
11152
+ if (storageState || sessionName)
11153
+ throw new UsageError(
11154
+ 'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
11155
+ );
11156
+ const origin = platformOrigin({
11157
+ origin: strFlag(flags, "origin"),
11158
+ api: strFlag(flags, "api")
11159
+ });
11160
+ const key = resolveCredential(strFlag(flags, "key"));
11161
+ if (!key)
11162
+ throw new UsageError(
11163
+ "recording on vos.so needs a credential: vos login, or VOS_API_KEY (mint a key at https://vos.so/app/api)"
11164
+ );
11165
+ const { env, missing } = hostedEnv(actions, process.env);
11166
+ if (missing.length)
11167
+ throw new UsageError(
11168
+ `the setup reads ${missing.map((n) => `{ "env": "${n}" }`).join(", ")} and this shell has no ${missing.length === 1 ? "value for it" : "values for them"}; export ${missing.join(", ")} and run again (the value rides the job, and is never stored)`
11169
+ );
11170
+ r.log("recording on vos.so\u2026");
11171
+ r.event({ event: "phase", phase: "record" });
11172
+ const started = Date.now();
11173
+ const outcome = await recordHosted({
11174
+ origin,
11175
+ key,
11176
+ url,
11177
+ actions: { ...actions, url },
11178
+ headers,
11179
+ title: strFlag(flags, "title"),
11180
+ folderId: strFlag(flags, "folder"),
11181
+ env,
11182
+ log: r.log
11183
+ });
11184
+ if (!outcome.ok) {
11185
+ if (outcome.vosId) {
11186
+ const gone = await apiJson(origin, `/api/vos/${outcome.vosId}`, {
11187
+ method: "DELETE",
11188
+ key
11189
+ });
11190
+ if (gone.status === 200)
11191
+ r.log("the empty vos it would have landed on was removed");
11192
+ }
11193
+ r.done(
11194
+ {
11195
+ hosted: true,
11196
+ ok: false,
11197
+ wall: outcome.wall,
11198
+ error: outcome.error,
11199
+ vosId: outcome.vosId,
11200
+ jobId: outcome.jobId
11201
+ },
11202
+ `${outcome.wall ? "REFUSED" : "FAILED"} on vos.so: ${outcome.error}`
11203
+ );
11204
+ return outcome.wall ? EXIT_WALL : EXIT_ERROR;
11205
+ }
11206
+ r.event({ event: "phase", phase: "fetch" });
11207
+ r.log(`landed as ${outcome.studio}; fetching the take into ${outDir}\u2026`);
11208
+ const fetched = await cmdFetch([
11209
+ outcome.vosId,
11210
+ "--out",
11211
+ outDir,
11212
+ "--media",
11213
+ ...strFlag(flags, "origin") ? ["--origin", strFlag(flags, "origin")] : [],
11214
+ ...strFlag(flags, "key") ? ["--key", strFlag(flags, "key")] : [],
11215
+ ...flags.json === true ? ["--json"] : []
11216
+ ]);
11217
+ if (fetched !== EXIT_OK) return fetched;
11218
+ const t = outcome.job.telemetry ?? {};
11219
+ r.done(
11220
+ {
11221
+ hosted: true,
11222
+ ok: true,
11223
+ vosId: outcome.vosId,
11224
+ jobId: outcome.jobId,
11225
+ out: outDir,
11226
+ seconds: +((Date.now() - started) / 1e3).toFixed(1),
11227
+ telemetry: t
11228
+ },
11229
+ `Recorded on vos.so in ${((Date.now() - started) / 1e3).toFixed(1)}s: ${outcome.studio}
11230
+ ${outDir}/ holds doc.json, the recording and vos.json${typeof t.durationMs === "number" ? ` (${(t.durationMs / 1e3).toFixed(1)}s of footage)` : ""}${Array.isArray(t.dead) && t.dead.length ? `
11231
+ DEAD TIME: ${t.dead.length} stretch(es) under a parked cursor; give those steps their ms` : ""}${Array.isArray(t.exposures) && t.exposures.length ? `
11232
+ EXPOSURES: ${t.exposures.length} sensitive-looking text(s) in frame; mask them in actions.json` : ""}
11233
+ Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}`
11234
+ );
11235
+ return EXIT_OK;
11236
+ }
11237
+ const backdrop = await takeBackdrop(flags, r);
11238
+ const maxDurationSeconds = await maxDuration(flags, r);
11820
11239
  if (flags["dry-run"] === true) {
11821
11240
  void flags.strict;
11822
11241
  void flags["keep-frames"];
@@ -11874,13 +11293,13 @@ ${lines.join("\n")}
11874
11293
  }
11875
11294
  const paths = takePaths(outDir);
11876
11295
  const prepare = async () => {
11877
- if (existsSync18(join21(outDir, "meta.json"))) {
11296
+ if (existsSync19(join22(outDir, "meta.json"))) {
11878
11297
  const { prevDoc, kept } = await prepareReRecord(outDir);
11879
11298
  r.log(
11880
11299
  `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" : "")
11881
11300
  );
11882
11301
  }
11883
- await mkdir10(outDir, { recursive: true });
11302
+ await mkdir11(outDir, { recursive: true });
11884
11303
  await ensureTakeDir(outDir);
11885
11304
  };
11886
11305
  const browser = await launchBrowser(browserArgs);
@@ -11939,7 +11358,7 @@ ${lines.join("\n")}
11939
11358
  },
11940
11359
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
11941
11360
  ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
11942
- Next: edit ${join21(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11361
+ Next: edit ${join22(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
11943
11362
  );
11944
11363
  return strictFail ? EXIT_USAGE : EXIT_OK;
11945
11364
  } finally {
@@ -11949,7 +11368,7 @@ ${lines.join("\n")}
11949
11368
  async function cmdCreate2(argv) {
11950
11369
  const { positionals, flags, multi } = parseArgs(
11951
11370
  argv,
11952
- BOOLEAN_FLAGS6,
11371
+ BOOLEAN_FLAGS7,
11953
11372
  MULTI_FLAGS2
11954
11373
  );
11955
11374
  const actionsPath = strFlag(flags, "actions");
@@ -11963,12 +11382,12 @@ async function cmdCreate2(argv) {
11963
11382
  const url = strFlag(flags, "url") ?? actions.url;
11964
11383
  if (!url)
11965
11384
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
11966
- const outDir = resolve9(strFlag(flags, "out") ?? "take");
11385
+ const outDir = resolve10(strFlag(flags, "out") ?? "take");
11967
11386
  const fmtRaw = strFlag(flags, "format") ?? "webm";
11968
11387
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
11969
11388
  throw new UsageError("--format must be webm or mp4");
11970
11389
  const format = fmtRaw;
11971
- const out = positionals[0] ?? join21(outDir, `out.${format}`);
11390
+ const out = positionals[0] ?? join22(outDir, `out.${format}`);
11972
11391
  const parallel = numFlag(flags, "parallel", 1);
11973
11392
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
11974
11393
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -11981,13 +11400,13 @@ async function cmdCreate2(argv) {
11981
11400
  const headers = takeHeaders(multi);
11982
11401
  const paths = takePaths(outDir);
11983
11402
  const prepare = async () => {
11984
- if (existsSync18(join21(outDir, "meta.json"))) {
11403
+ if (existsSync19(join22(outDir, "meta.json"))) {
11985
11404
  const { prevDoc, kept } = await prepareReRecord(outDir);
11986
11405
  r.log(
11987
11406
  `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" : "")
11988
11407
  );
11989
11408
  }
11990
- await mkdir10(outDir, { recursive: true });
11409
+ await mkdir11(outDir, { recursive: true });
11991
11410
  await ensureTakeDir(outDir);
11992
11411
  };
11993
11412
  const browser = await launchBrowser(browserArgs);
@@ -12078,7 +11497,7 @@ async function cmdCreate2(argv) {
12078
11497
  async function cmdPlan(argv) {
12079
11498
  const { positionals, flags, multi } = parseArgs(
12080
11499
  argv,
12081
- BOOLEAN_FLAGS6,
11500
+ BOOLEAN_FLAGS7,
12082
11501
  /* @__PURE__ */ new Set(["with"])
12083
11502
  );
12084
11503
  const dir = positionals[0];
@@ -12088,24 +11507,24 @@ async function cmdPlan(argv) {
12088
11507
  );
12089
11508
  const r = createReporter(flags.json === true);
12090
11509
  if (flags.fresh === true) {
12091
- await rm5(join21(dir, "doc.json"), { force: true });
11510
+ await rm5(join22(dir, "doc.json"), { force: true });
12092
11511
  r.log("note: --fresh discarded the existing doc.json");
12093
11512
  }
12094
11513
  let reuse;
12095
11514
  if (flags.reuse === true) {
12096
- const from = resolve9(strFlag(flags, "from") ?? join21(dir, PREV_DOC_NAME));
12097
- if (!existsSync18(from)) {
11515
+ const from = resolve10(strFlag(flags, "from") ?? join22(dir, PREV_DOC_NAME));
11516
+ if (!existsSync19(from)) {
12098
11517
  throw new UsageError(
12099
11518
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
12100
11519
  );
12101
11520
  }
12102
11521
  reuse = {
12103
11522
  from,
12104
- doc: JSON.parse(await readFile15(from, "utf8"))
11523
+ doc: JSON.parse(await readFile16(from, "utf8"))
12105
11524
  };
12106
11525
  }
12107
11526
  const style = await resolveStyleRef(flags);
12108
- const hasDoc = existsSync18(join21(dir, "doc.json"));
11527
+ const hasDoc = existsSync19(join22(dir, "doc.json"));
12109
11528
  const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
12110
11529
  const release = await releaseInputs(dir, flags, r, multi.with ?? []);
12111
11530
  const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
@@ -12151,7 +11570,7 @@ async function cmdPlan(argv) {
12151
11570
  ${s.reuse.flagged.join("\n ")}` : "") : "";
12152
11571
  r.done(
12153
11572
  {
12154
- take: resolve9(dir),
11573
+ take: resolve10(dir),
12155
11574
  fresh: s.fresh,
12156
11575
  cursorKept: s.cursorKept,
12157
11576
  zoomAuto: s.zoomAuto,
@@ -12163,7 +11582,7 @@ async function cmdPlan(argv) {
12163
11582
  ...s.motion ? { motion: s.motion } : {},
12164
11583
  ...s.reuse ? { reuse: s.reuse } : {}
12165
11584
  },
12166
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join21(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
11585
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join22(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
12167
11586
  );
12168
11587
  return EXIT_OK;
12169
11588
  }
@@ -12276,7 +11695,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
12276
11695
  async function cmdRender(argv) {
12277
11696
  const { positionals, flags, multi } = parseArgs(
12278
11697
  argv,
12279
- BOOLEAN_FLAGS6,
11698
+ BOOLEAN_FLAGS7,
12280
11699
  MULTI_FLAGS2
12281
11700
  );
12282
11701
  const dir = positionals[0];
@@ -12289,7 +11708,7 @@ async function cmdRender(argv) {
12289
11708
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
12290
11709
  throw new UsageError("--format must be webm or mp4");
12291
11710
  const format = fmtRaw;
12292
- const out = positionals[1] ?? join21(dir, `out.${format}`);
11711
+ const out = positionals[1] ?? join22(dir, `out.${format}`);
12293
11712
  const parallel = numFlag(flags, "parallel", 1);
12294
11713
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
12295
11714
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -12347,7 +11766,7 @@ async function cmdRender(argv) {
12347
11766
  async function cmdFrames(argv) {
12348
11767
  const { positionals, flags, multi } = parseArgs(
12349
11768
  argv,
12350
- BOOLEAN_FLAGS6,
11769
+ BOOLEAN_FLAGS7,
12351
11770
  MULTI_FLAGS2
12352
11771
  );
12353
11772
  const dir = positionals[0];
@@ -12420,7 +11839,7 @@ async function cmdFrames(argv) {
12420
11839
  async function cmdDeliver(argv) {
12421
11840
  const { positionals, flags, multi } = parseArgs(
12422
11841
  argv,
12423
- BOOLEAN_FLAGS6,
11842
+ BOOLEAN_FLAGS7,
12424
11843
  MULTI_FLAGS2
12425
11844
  );
12426
11845
  const dir = positionals[0];
@@ -12536,15 +11955,15 @@ async function resolveStyleRef(flags) {
12536
11955
  }
12537
11956
  async function resolveDocRef(ref, flags, what) {
12538
11957
  const styleRef = ref;
12539
- const file = existsSync18(styleRef) ? resolve9(
11958
+ const file = existsSync19(styleRef) ? resolve10(
12540
11959
  styleRef,
12541
- existsSync18(join21(styleRef, "doc.json")) ? "doc.json" : ""
11960
+ existsSync19(join22(styleRef, "doc.json")) ? "doc.json" : ""
12542
11961
  ) : null;
12543
- if (file && existsSync18(file)) {
11962
+ if (file && existsSync19(file)) {
12544
11963
  return {
12545
11964
  from: file,
12546
11965
  doc: migrateHostedDoc5(
12547
- JSON.parse(await readFile15(file, "utf8"))
11966
+ JSON.parse(await readFile16(file, "utf8"))
12548
11967
  )
12549
11968
  };
12550
11969
  }
@@ -12586,7 +12005,7 @@ async function officialTemplate(origin, ref) {
12586
12005
  }
12587
12006
  }
12588
12007
  async function cmdDigest(argv) {
12589
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12008
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
12590
12009
  const dir = positionals[0];
12591
12010
  if (!dir)
12592
12011
  throw new UsageError(
@@ -12596,7 +12015,7 @@ async function cmdDigest(argv) {
12596
12015
  const take = await loadTake(dir);
12597
12016
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
12598
12017
  const transcriptPath = strFlag(flags, "transcript");
12599
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile15(transcriptPath, "utf8"))) : null;
12018
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile16(transcriptPath, "utf8"))) : null;
12600
12019
  const style = await resolveStyleRef(flags);
12601
12020
  const noFrames = flags["no-frames"] === true;
12602
12021
  let browser = null;
@@ -12627,7 +12046,7 @@ async function cmdDigest(argv) {
12627
12046
  kinds,
12628
12047
  frames: d.images.full,
12629
12048
  crops: d.images.crop,
12630
- sheet: d.images.sheet ? join21(result.outDir, d.images.sheet) : null,
12049
+ sheet: d.images.sheet ? join22(result.outDir, d.images.sheet) : null,
12631
12050
  scenes: kinds.scene ?? 0,
12632
12051
  sourceDuration: d.take.sourceDuration,
12633
12052
  outputDuration: d.take.outputDuration,
@@ -12650,19 +12069,19 @@ async function cmdDigest(argv) {
12650
12069
  }
12651
12070
  }
12652
12071
  async function cmdOpen(argv) {
12653
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12072
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
12654
12073
  const dir = positionals[0];
12655
12074
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
12656
12075
  const r = createReporter(flags.json === true);
12657
12076
  const take = await loadTake(dir);
12658
- if (!existsSync18(take.paths.recording)) {
12077
+ if (!existsSync19(take.paths.recording)) {
12659
12078
  throw new UsageError(`${dir} has no recording \u2014 re-run record`);
12660
12079
  }
12661
12080
  const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
12662
12081
  /\/+$/,
12663
12082
  ""
12664
12083
  );
12665
- const server = await startTakeServer(resolve9(dir), {});
12084
+ const server = await startTakeServer(resolve10(dir), {});
12666
12085
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
12667
12086
  r.event({ event: "open", server: server.base, url });
12668
12087
  r.log(`take served at ${server.base}`);
@@ -12688,7 +12107,7 @@ async function cmdOpen(argv) {
12688
12107
  return EXIT_OK;
12689
12108
  }
12690
12109
  async function cmdJudge(argv) {
12691
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12110
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
12692
12111
  const target = positionals[0];
12693
12112
  const against = strFlag(flags, "against");
12694
12113
  if (!target || !against)
@@ -12696,12 +12115,12 @@ async function cmdJudge(argv) {
12696
12115
  "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)"
12697
12116
  );
12698
12117
  const r = createReporter(flags.json === true);
12699
- const kitFile = target.endsWith("kit.json") ? target : join21(target, "kit.json");
12700
- if (!existsSync18(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
12701
- if (!existsSync18(against))
12118
+ const kitFile = target.endsWith("kit.json") ? target : join22(target, "kit.json");
12119
+ if (!existsSync19(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
12120
+ if (!existsSync19(against))
12702
12121
  throw new UsageError(`${against}: no reference manifest`);
12703
12122
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
12704
- const verdicts = JSON.parse(await readFile15(result.verdictFile, "utf8")).verdicts;
12123
+ const verdicts = JSON.parse(await readFile16(result.verdictFile, "utf8")).verdicts;
12705
12124
  const rate = winRate(verdicts);
12706
12125
  const lines = result.sheets.map(
12707
12126
  (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
@@ -12717,7 +12136,7 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
12717
12136
  return EXIT_OK;
12718
12137
  }
12719
12138
  async function cmdCallout(argv) {
12720
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12139
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
12721
12140
  const dir = positionals[0];
12722
12141
  const shape = positionals[1];
12723
12142
  if (!dir || !shape || !isCalloutShape(shape))
@@ -12741,7 +12160,7 @@ async function cmdCallout(argv) {
12741
12160
  `no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
12742
12161
  );
12743
12162
  const codeFile = strFlag(flags, "code-file");
12744
- const code = codeFile ? await readFile15(codeFile, "utf8") : strFlag(flags, "code");
12163
+ const code = codeFile ? await readFile16(codeFile, "utf8") : strFlag(flags, "code");
12745
12164
  const words = {
12746
12165
  kicker: strFlag(flags, "kicker"),
12747
12166
  title: strFlag(flags, "title"),
@@ -12816,11 +12235,11 @@ async function cmdCallout(argv) {
12816
12235
  return EXIT_OK;
12817
12236
  }
12818
12237
  async function cmdValidate(argv) {
12819
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12238
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
12820
12239
  const target = positionals[0];
12821
12240
  if (!target) throw new UsageError("vos validate <actions.json|take>");
12822
12241
  const r = createReporter(flags.json === true);
12823
- const kitTarget = target.endsWith("kit.json") ? target : existsSync18(join21(target, "kit.json")) && !isTakeDir(target) ? join21(target, "kit.json") : null;
12242
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync19(join22(target, "kit.json")) && !isTakeDir(target) ? join22(target, "kit.json") : null;
12824
12243
  if (kitTarget) {
12825
12244
  const picture = flags.picture === true;
12826
12245
  const verdict = await validateKit(kitTarget, { picture });
@@ -12852,9 +12271,9 @@ async function cmdValidate(argv) {
12852
12271
  r.done({ valid: true, target }, `${target}: valid actions file`);
12853
12272
  return EXIT_OK;
12854
12273
  }
12855
- if (!isTakeDir(target) && existsSync18(join21(target, "config.json"))) {
12274
+ if (!isTakeDir(target) && existsSync19(join22(target, "config.json"))) {
12856
12275
  const parsed = JSON.parse(
12857
- await readFile15(join21(target, "config.json"), "utf8")
12276
+ await readFile16(join22(target, "config.json"), "utf8")
12858
12277
  );
12859
12278
  const pre = preflightConfig(parsed);
12860
12279
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -12895,7 +12314,7 @@ async function cmdValidate(argv) {
12895
12314
  const take = await loadTake(target);
12896
12315
  const problems = [];
12897
12316
  const warnings = [];
12898
- if (!existsSync18(take.paths.recording)) {
12317
+ if (!existsSync19(take.paths.recording)) {
12899
12318
  problems.push("missing recording (re-run record)");
12900
12319
  } else {
12901
12320
  const mismatch = containerMismatch(
@@ -12978,7 +12397,7 @@ async function cmdValidate(argv) {
12978
12397
  async function cmdPush3(argv) {
12979
12398
  const { positionals, flags, multi } = parseArgs(
12980
12399
  argv,
12981
- BOOLEAN_FLAGS6,
12400
+ BOOLEAN_FLAGS7,
12982
12401
  MULTI_FLAGS2
12983
12402
  );
12984
12403
  const dir = positionals[0];
@@ -12988,7 +12407,7 @@ async function cmdPush3(argv) {
12988
12407
  );
12989
12408
  const refusal = takePushRefusal({
12990
12409
  vos: strFlag(flags, "vos"),
12991
- trackedVosId: readSyncState(resolve9(dir))?.vosId ?? null,
12410
+ trackedVosId: readSyncState(resolve10(dir))?.vosId ?? null,
12992
12411
  programOnly: ["slug", "desc", "tags", "base", "remix-of"].filter(
12993
12412
  (name) => hasFlag(flags, name)
12994
12413
  )
@@ -12997,7 +12416,7 @@ async function cmdPush3(argv) {
12997
12416
  const r = createReporter(flags.json === true);
12998
12417
  const overrides = multi.override;
12999
12418
  const result = await pushTake(
13000
- resolve9(dir),
12419
+ resolve10(dir),
13001
12420
  {
13002
12421
  key: strFlag(flags, "key"),
13003
12422
  api: strFlag(flags, "api"),
@@ -13025,11 +12444,11 @@ async function cmdPush3(argv) {
13025
12444
  return EXIT_OK;
13026
12445
  }
13027
12446
  async function cmdPull2(argv) {
13028
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6, MULTI_FLAGS2);
12447
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7, MULTI_FLAGS2);
13029
12448
  const dir = positionals[0] ?? ".";
13030
12449
  const r = createReporter(flags.json === true);
13031
12450
  const result = await pullTake(
13032
- resolve9(dir),
12451
+ resolve10(dir),
13033
12452
  {
13034
12453
  key: strFlag(flags, "key"),
13035
12454
  api: strFlag(flags, "api"),
@@ -13148,6 +12567,8 @@ async function run(argv) {
13148
12567
  return await cmdSession(rest);
13149
12568
  case "actions":
13150
12569
  return await cmdActions(rest);
12570
+ case "ingest":
12571
+ return await cmdIngest(rest);
13151
12572
  default:
13152
12573
  throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
13153
12574
  }
@@ -13180,7 +12601,7 @@ async function run(argv) {
13180
12601
  }
13181
12602
  }
13182
12603
  async function cmdSession(argv) {
13183
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS6);
12604
+ const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
13184
12605
  const [sub, arg] = positionals;
13185
12606
  const r = createReporter(flags.json === true);
13186
12607
  const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
@@ -13216,7 +12637,7 @@ async function cmdSession(argv) {
13216
12637
  if (!arg) throw new UsageError(usage);
13217
12638
  const bad = sessionNameVerdict(arg);
13218
12639
  if (bad) throw new UsageError(bad);
13219
- if (!existsSync18(sessionDir(arg)))
12640
+ if (!existsSync19(sessionDir(arg)))
13220
12641
  throw new UsageError(`no session "${arg}" (vos session list)`);
13221
12642
  const summary2 = await summarizeSession(arg);
13222
12643
  const url = strFlag(flags, "url");
@@ -13323,7 +12744,8 @@ export {
13323
12744
  MULTI_FLAGS2 as MULTI_FLAGS,
13324
12745
  HELP,
13325
12746
  takeBrowserArgs,
12747
+ takeBackdrop,
13326
12748
  verbHelp,
13327
12749
  run
13328
12750
  };
13329
- //# sourceMappingURL=chunk-RHPRZQRM.js.map
12751
+ //# sourceMappingURL=chunk-SE4F3OMQ.js.map