@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.
- package/README.md +10 -1
- package/dist/chunk-72ADTMWW.js +40 -0
- package/dist/chunk-72ADTMWW.js.map +1 -0
- package/dist/{chunk-ANKECV7F.js → chunk-TAWRT7UM.js} +354 -1094
- package/dist/chunk-TAWRT7UM.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +9 -346
- package/dist/index.js +2 -2
- package/dist/{run-CYHH56KA.js → run-EQZXEPXZ.js} +3 -3
- package/dist/{shotList-7F2FE3RU.js → shotList-IYFDHSIK.js} +2 -2
- package/package.json +5 -5
- package/dist/chunk-ANKECV7F.js.map +0 -1
- package/dist/chunk-AUPGJJTL.js +0 -184
- package/dist/chunk-AUPGJJTL.js.map +0 -1
- /package/dist/{run-CYHH56KA.js.map → run-EQZXEPXZ.js.map} +0 -0
- /package/dist/{shotList-7F2FE3RU.js.map → shotList-IYFDHSIK.js.map} +0 -0
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
7621
|
-
var ENCODE_HTML =
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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-
|
|
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
|
|
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
|
|
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(
|
|
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) ?
|
|
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-
|
|
12537
|
+
//# sourceMappingURL=chunk-TAWRT7UM.js.map
|