@vosjs/cli 0.48.0 → 0.49.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,23 +7,9 @@ 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
15
  import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
@@ -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 {
@@ -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";
@@ -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
  }
@@ -9059,6 +8077,41 @@ async function browserLogin(origin, r, opts) {
9059
8077
  "no approval within 15 minutes \u2014 run `vos login` again, or mint a key at https://vos.so/app/api and pass --key"
9060
8078
  );
9061
8079
  }
8080
+ var HANDOFF_URL = /^(https?:\/\/[^/]+)\/h\/(vos_ho_[A-Za-z0-9_-]+)$/;
8081
+ function parseHandoffUrl(url) {
8082
+ const m = HANDOFF_URL.exec(url.trim());
8083
+ return m ? { origin: m[1], token: m[2] } : null;
8084
+ }
8085
+ async function handoffLogin(url, r, opts = {}) {
8086
+ const parsed = parseHandoffUrl(url);
8087
+ if (!parsed)
8088
+ throw new Error(
8089
+ "--handoff wants the link a person copied from vos.so, https://vos.so/h/vos_ho_\u2026"
8090
+ );
8091
+ r.log(`exchanging the setup link at ${parsed.origin}\u2026`);
8092
+ const res = await apiJson(parsed.origin, "/api/cli/login/exchange", {
8093
+ method: "POST",
8094
+ body: { token: parsed.token, hostname: hostname().slice(0, 64) }
8095
+ });
8096
+ if (res.status === 200 && res.body.status === "ok") {
8097
+ const path = (opts.store ?? writeCredential)(res.body.key);
8098
+ const user = typeof res.body.user === "object" && res.body.user !== null ? res.body.user.name ?? "" : "";
8099
+ return {
8100
+ path,
8101
+ user,
8102
+ keyName: String(res.body.keyName ?? ""),
8103
+ origin: parsed.origin
8104
+ };
8105
+ }
8106
+ if (res.status === 404 && !res.body.status) {
8107
+ throw new Error(
8108
+ `${parsed.origin} does not support setup links yet \u2014 run \`vos login\` instead`
8109
+ );
8110
+ }
8111
+ const said = typeof res.body.error === "string" ? res.body.error : apiError("exchange", res);
8112
+ const hint = typeof res.body.hint === "string" ? ` ${res.body.hint}` : "";
8113
+ throw new Error(`${said}${hint}`);
8114
+ }
9062
8115
 
9063
8116
  // src/plugin/program.ts
9064
8117
  var BOOLEAN_FLAGS2 = /* @__PURE__ */ new Set([
@@ -9605,6 +8658,21 @@ async function cmdLogin(argv) {
9605
8658
  origin: strFlag(flags, "origin"),
9606
8659
  api: strFlag(flags, "api")
9607
8660
  });
8661
+ const handoffUrl = strFlag(flags, "handoff");
8662
+ if (handoffUrl) {
8663
+ const done = await handoffLogin(handoffUrl, r);
8664
+ r.done(
8665
+ {
8666
+ path: done.path,
8667
+ origin: done.origin,
8668
+ user: done.user,
8669
+ keyName: done.keyName,
8670
+ next_step: "the link is spent; vos push when you want the work hosted"
8671
+ },
8672
+ `Signed in${done.user ? ` as ${done.user}` : ""} \u2014 key "${done.keyName}" stored at ${done.path} (used by every vos platform verb). The link is spent.`
8673
+ );
8674
+ return EXIT_OK;
8675
+ }
9608
8676
  const explicit = strFlag(flags, "key") ?? process.env.VOS_API_KEY?.trim();
9609
8677
  if (explicit) return storeKey(explicit, origin, r);
9610
8678
  try {
@@ -9673,7 +8741,7 @@ async function writeProgramDoc(dir, hosted) {
9673
8741
  }
9674
8742
 
9675
8743
  // src/plugin/asset.ts
9676
- import { readFileSync as readFileSync5 } from "fs";
8744
+ import { readFileSync as readFileSync4 } from "fs";
9677
8745
  import { basename as basename5, extname as extname2 } from "path";
9678
8746
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
9679
8747
  var MIME_BY_EXT = {
@@ -9727,7 +8795,7 @@ async function cmdPush(argv) {
9727
8795
  for (const file of positionals) {
9728
8796
  const name = basename5(file);
9729
8797
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
9730
- const body = readFileSync5(file);
8798
+ const body = readFileSync4(file);
9731
8799
  const form = new FormData();
9732
8800
  form.set("file", new Blob([body], { type: mime }), name);
9733
8801
  if (folder) form.set("folderId", folder.id);
@@ -9781,7 +8849,7 @@ async function cmdAsset(argv) {
9781
8849
  }
9782
8850
 
9783
8851
  // src/plugin/recipe.ts
9784
- import { readFileSync as readFileSync6 } from "fs";
8852
+ import { readFileSync as readFileSync5 } from "fs";
9785
8853
  import { basename as basename6 } from "path";
9786
8854
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
9787
8855
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -9801,7 +8869,7 @@ async function cmdPush2(argv) {
9801
8869
  throw new UsageError(USAGE);
9802
8870
  }
9803
8871
  const filename = recipeFilename(file);
9804
- const body = readFileSync6(file);
8872
+ const body = readFileSync5(file);
9805
8873
  const r = createReporter(flags.json === true);
9806
8874
  const origin = platformOrigin({
9807
8875
  origin: strFlag(flags, "origin"),
@@ -10244,6 +9312,103 @@ async function cmdBrand(argv) {
10244
9312
  return 0;
10245
9313
  }
10246
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
+
10247
9412
  // src/plugin/agentBrowser.ts
10248
9413
  import { writeFile as writeFile13 } from "fs/promises";
10249
9414
  import { resolve as resolve8 } from "path";
@@ -10764,7 +9929,7 @@ function summary(result) {
10764
9929
  async function cmdActionsScript(input, flags) {
10765
9930
  const r = createReporter(flags.json === true);
10766
9931
  const { readFile: readFile16 } = await import("fs/promises");
10767
- const { shotList, formatShotList } = await import("./shotList-7F2FE3RU.js");
9932
+ const { shotList, formatShotList } = await import("./shotList-IYFDHSIK.js");
10768
9933
  const raw = JSON.parse(await readFile16(resolve8(input), "utf8"));
10769
9934
  const errors = validateActions(raw);
10770
9935
  if (errors.length) throw new UsageError(`${input}:
@@ -10780,7 +9945,7 @@ import {
10780
9945
  cpSync,
10781
9946
  existsSync as existsSync17,
10782
9947
  mkdirSync as mkdirSync3,
10783
- readFileSync as readFileSync8,
9948
+ readFileSync as readFileSync7,
10784
9949
  rmSync as rmSync2,
10785
9950
  writeFileSync as writeFileSync3
10786
9951
  } from "fs";
@@ -10789,7 +9954,7 @@ import { join as join20 } from "path";
10789
9954
  import { chromium as chromium2 } from "playwright";
10790
9955
 
10791
9956
  // src/plugin/machine.ts
10792
- import { existsSync as existsSync16, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
9957
+ import { existsSync as existsSync16, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
10793
9958
  import { dirname as dirname5, join as join19 } from "path";
10794
9959
  var AGENT_NAMES = [
10795
9960
  "claude",
@@ -11006,7 +10171,7 @@ function bundledSkillsDir(from) {
11006
10171
  const pkg = join19(dir, "package.json");
11007
10172
  if (existsSync16(pkg)) {
11008
10173
  try {
11009
- const name = JSON.parse(readFileSync7(pkg, "utf8")).name;
10174
+ const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
11010
10175
  if (name === "@vosjs/cli") {
11011
10176
  const skills = join19(dir, "skills");
11012
10177
  return existsSync16(skills) && statSync4(skills).isDirectory() ? skills : null;
@@ -11188,7 +10353,7 @@ async function cmdSetup(argv) {
11188
10353
  let rules = null;
11189
10354
  if (!noRules) {
11190
10355
  const file = rulesFileFor(root);
11191
- const existing = existsSync17(file) ? readFileSync8(file, "utf8") : null;
10356
+ const existing = existsSync17(file) ? readFileSync7(file, "utf8") : null;
11192
10357
  const { text, changed } = applyRulesBlock(existing);
11193
10358
  if (changed) writeFileSync3(file, text);
11194
10359
  rules = { file, changed };
@@ -11339,7 +10504,8 @@ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
11339
10504
  "leader",
11340
10505
  "keep-frames",
11341
10506
  "dry-run",
11342
- "allow-wall"
10507
+ "allow-wall",
10508
+ "hosted"
11343
10509
  ]);
11344
10510
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
11345
10511
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -11347,6 +10513,8 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
11347
10513
  Take pipeline
11348
10514
  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]
11349
10515
  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]
10516
+ vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
10517
+ 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
11350
10518
  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]
11351
10519
  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]
11352
10520
  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]
@@ -11381,10 +10549,12 @@ This machine (no account)
11381
10549
  remove ~/.config/vos/credentials (VOS_API_KEY still resolves)
11382
10550
 
11383
10551
  Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
11384
- vos login [--key <vos_sk_\u2026>] [--label <name>] [--no-browser]
10552
+ vos login [--key <vos_sk_\u2026>] [--handoff <url>] [--label <name>] [--no-browser]
11385
10553
  sign in via the browser (approve at vos.so/cli/auth; works
11386
10554
  headless \u2014 print the URL, a human approves, the key stores
11387
- itself). --key skips the browser (mint at vos.so/app/api)
10555
+ itself). --key skips the browser (mint at vos.so/app/api).
10556
+ --handoff https://vos.so/h/\u2026 exchanges the setup link a
10557
+ signed-in person's line carries for a key, once, no click
11388
10558
  vos fetch <vosId|watch-url> [--out dir] [--media] [--json]
11389
10559
  writes config.json + vos.json (no auth needed for public programs);
11390
10560
  a take vos also gets its doc.json, and --media its footage
@@ -11759,12 +10929,102 @@ async function cmdRecord(argv) {
11759
10929
  if (!url)
11760
10930
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
11761
10931
  const outDir = resolve9(strFlag(flags, "out") ?? "take");
11762
- const backdrop = await takeBackdrop(flags, r);
11763
- const maxDurationSeconds = await maxDuration(flags, r);
11764
10932
  const storageState = takeStorageState(flags);
11765
10933
  const sessionName = takeSessionName(flags, storageState);
11766
10934
  const browserArgs = takeBrowserArgs(multi);
11767
10935
  const headers = takeHeaders(multi);
10936
+ if (flags.hosted === true) {
10937
+ if (flags["dry-run"] === true)
10938
+ throw new UsageError(
10939
+ "--dry-run rehearses on this machine; drop --hosted to rehearse, or --dry-run to record on vos.so"
10940
+ );
10941
+ if (storageState || sessionName)
10942
+ throw new UsageError(
10943
+ '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.'
10944
+ );
10945
+ const origin = platformOrigin({
10946
+ origin: strFlag(flags, "origin"),
10947
+ api: strFlag(flags, "api")
10948
+ });
10949
+ const key = resolveCredential(strFlag(flags, "key"));
10950
+ if (!key)
10951
+ throw new UsageError(
10952
+ "recording on vos.so needs a credential: vos login, or VOS_API_KEY (mint a key at https://vos.so/app/api)"
10953
+ );
10954
+ const { env, missing } = hostedEnv(actions, process.env);
10955
+ if (missing.length)
10956
+ throw new UsageError(
10957
+ `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)`
10958
+ );
10959
+ r.log("recording on vos.so\u2026");
10960
+ r.event({ event: "phase", phase: "record" });
10961
+ const started = Date.now();
10962
+ const outcome = await recordHosted({
10963
+ origin,
10964
+ key,
10965
+ url,
10966
+ actions: { ...actions, url },
10967
+ headers,
10968
+ title: strFlag(flags, "title"),
10969
+ folderId: strFlag(flags, "folder"),
10970
+ env,
10971
+ log: r.log
10972
+ });
10973
+ if (!outcome.ok) {
10974
+ if (outcome.vosId) {
10975
+ const gone = await apiJson(origin, `/api/vos/${outcome.vosId}`, {
10976
+ method: "DELETE",
10977
+ key
10978
+ });
10979
+ if (gone.status === 200)
10980
+ r.log("the empty vos it would have landed on was removed");
10981
+ }
10982
+ r.done(
10983
+ {
10984
+ hosted: true,
10985
+ ok: false,
10986
+ wall: outcome.wall,
10987
+ error: outcome.error,
10988
+ vosId: outcome.vosId,
10989
+ jobId: outcome.jobId
10990
+ },
10991
+ `${outcome.wall ? "REFUSED" : "FAILED"} on vos.so: ${outcome.error}`
10992
+ );
10993
+ return outcome.wall ? EXIT_WALL : EXIT_ERROR;
10994
+ }
10995
+ r.event({ event: "phase", phase: "fetch" });
10996
+ r.log(`landed as ${outcome.studio}; fetching the take into ${outDir}\u2026`);
10997
+ const fetched = await cmdFetch([
10998
+ outcome.vosId,
10999
+ "--out",
11000
+ outDir,
11001
+ "--media",
11002
+ ...strFlag(flags, "origin") ? ["--origin", strFlag(flags, "origin")] : [],
11003
+ ...strFlag(flags, "key") ? ["--key", strFlag(flags, "key")] : [],
11004
+ ...flags.json === true ? ["--json"] : []
11005
+ ]);
11006
+ if (fetched !== EXIT_OK) return fetched;
11007
+ const t = outcome.job.telemetry ?? {};
11008
+ r.done(
11009
+ {
11010
+ hosted: true,
11011
+ ok: true,
11012
+ vosId: outcome.vosId,
11013
+ jobId: outcome.jobId,
11014
+ out: outDir,
11015
+ seconds: +((Date.now() - started) / 1e3).toFixed(1),
11016
+ telemetry: t
11017
+ },
11018
+ `Recorded on vos.so in ${((Date.now() - started) / 1e3).toFixed(1)}s: ${outcome.studio}
11019
+ ${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 ? `
11020
+ DEAD TIME: ${t.dead.length} stretch(es) under a parked cursor; give those steps their ms` : ""}${Array.isArray(t.exposures) && t.exposures.length ? `
11021
+ EXPOSURES: ${t.exposures.length} sensitive-looking text(s) in frame; mask them in actions.json` : ""}
11022
+ Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}`
11023
+ );
11024
+ return EXIT_OK;
11025
+ }
11026
+ const backdrop = await takeBackdrop(flags, r);
11027
+ const maxDurationSeconds = await maxDuration(flags, r);
11768
11028
  if (flags["dry-run"] === true) {
11769
11029
  void flags.strict;
11770
11030
  void flags["keep-frames"];
@@ -13274,4 +12534,4 @@ export {
13274
12534
  verbHelp,
13275
12535
  run
13276
12536
  };
13277
- //# sourceMappingURL=chunk-ANKECV7F.js.map
12537
+ //# sourceMappingURL=chunk-TAWRT7UM.js.map