@vosjs/cli 0.48.1 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/{chunk-P3X5KVA6.js → chunk-2EHIFIYC.js} +5 -1
- package/dist/chunk-2EHIFIYC.js.map +1 -0
- package/dist/chunk-72ADTMWW.js +40 -0
- package/dist/chunk-72ADTMWW.js.map +1 -0
- package/dist/{chunk-RHPRZQRM.js → chunk-SE4F3OMQ.js} +612 -1190
- package/dist/chunk-SE4F3OMQ.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +9 -346
- package/dist/index.js +3 -3
- package/dist/manifest-IG2WSARS.js +8 -0
- package/dist/{run-DV73Z3MQ.js → run-HCAKVJZ3.js} +5 -3
- package/dist/{shotList-7F2FE3RU.js → shotList-IYFDHSIK.js} +2 -2
- package/package.json +4 -4
- package/schema/digest.schema.json +4 -1
- package/dist/chunk-AUPGJJTL.js +0 -184
- package/dist/chunk-AUPGJJTL.js.map +0 -1
- package/dist/chunk-P3X5KVA6.js.map +0 -1
- package/dist/chunk-RHPRZQRM.js.map +0 -1
- package/dist/manifest-A4R367EM.js +0 -8
- /package/dist/{manifest-A4R367EM.js.map → manifest-IG2WSARS.js.map} +0 -0
- /package/dist/{run-DV73Z3MQ.js.map → run-HCAKVJZ3.js.map} +0 -0
- /package/dist/{shotList-7F2FE3RU.js.map → shotList-IYFDHSIK.js.map} +0 -0
|
@@ -7,28 +7,14 @@ import {
|
|
|
7
7
|
trackFlags
|
|
8
8
|
} from "./chunk-FACYY7VV.js";
|
|
9
9
|
import {
|
|
10
|
-
PRESS_HOLD_MS,
|
|
11
|
-
PRESS_LEAD_MS,
|
|
12
|
-
SCROLL_SETTLE_MS,
|
|
13
|
-
TRAILING_HOLD_MS,
|
|
14
|
-
askedMs,
|
|
15
|
-
clockMotion,
|
|
16
|
-
clockTyping,
|
|
17
10
|
deadLine,
|
|
18
|
-
|
|
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
|
-
import { mkdir as
|
|
30
|
-
import { existsSync as
|
|
31
|
-
import { join as
|
|
15
|
+
import { mkdir as mkdir11, readFile as readFile16, rm as rm5 } from "fs/promises";
|
|
16
|
+
import { existsSync as existsSync19 } from "fs";
|
|
17
|
+
import { join as join22, resolve as resolve10 } from "path";
|
|
32
18
|
import {
|
|
33
19
|
END_CARD_FROM as END_CARD_FROM2,
|
|
34
20
|
docOutputDuration as docOutputDuration3,
|
|
@@ -119,238 +105,8 @@ function createReporter(json) {
|
|
|
119
105
|
};
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
// src/plugin/setup.ts
|
|
123
|
-
var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
|
|
124
|
-
function validateSetup(value) {
|
|
125
|
-
const errors = [];
|
|
126
|
-
if (!Array.isArray(value)) return ["setup must be an array of steps"];
|
|
127
|
-
value.forEach((raw, i) => {
|
|
128
|
-
const at2 = `setup[${i}]`;
|
|
129
|
-
if (typeof raw !== "object" || raw === null) {
|
|
130
|
-
errors.push(`${at2}: must be an object`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const s = raw;
|
|
134
|
-
if (typeof s.do !== "string" || !VERBS.has(s.do)) {
|
|
135
|
-
errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
|
|
139
|
-
errors.push(`${at2}: ${s.do} needs a selector`);
|
|
140
|
-
if (s.do === "wait" && typeof s.ms !== "number")
|
|
141
|
-
errors.push(`${at2}: wait needs ms`);
|
|
142
|
-
if (s.do === "press" && typeof s.key !== "string")
|
|
143
|
-
errors.push(`${at2}: press needs a key`);
|
|
144
|
-
if (s.do === "goto" && typeof s.url !== "string")
|
|
145
|
-
errors.push(`${at2}: goto needs a url`);
|
|
146
|
-
if (s.do === "type") {
|
|
147
|
-
const t = s.text;
|
|
148
|
-
const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
|
|
149
|
-
if (typeof t !== "string" && !isEnv)
|
|
150
|
-
errors.push(
|
|
151
|
-
`${at2}: type needs text, a string or { "env": "NAME" } read at run time`
|
|
152
|
-
);
|
|
153
|
-
if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
|
|
154
|
-
errors.push(
|
|
155
|
-
`${at2}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return errors;
|
|
160
|
-
}
|
|
161
|
-
function resolveSetupText(t, env = process.env) {
|
|
162
|
-
if (typeof t === "string") return { value: t, secret: false };
|
|
163
|
-
const v = env[t.env];
|
|
164
|
-
if (v === void 0 || v === "")
|
|
165
|
-
throw new SetupEnvError(
|
|
166
|
-
`setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
|
|
167
|
-
);
|
|
168
|
-
return { value: v, secret: true };
|
|
169
|
-
}
|
|
170
|
-
async function runSetup(page, steps, opts) {
|
|
171
|
-
const lines = [];
|
|
172
|
-
for (const [i, step] of steps.entries()) {
|
|
173
|
-
const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
|
|
174
|
-
try {
|
|
175
|
-
switch (step.do) {
|
|
176
|
-
case "wait":
|
|
177
|
-
await opts.sleep(step.ms);
|
|
178
|
-
lines.push(`setup #${i} wait ${step.ms}ms`);
|
|
179
|
-
break;
|
|
180
|
-
case "goto":
|
|
181
|
-
await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
182
|
-
});
|
|
183
|
-
lines.push(`setup #${i} goto ${step.url}`);
|
|
184
|
-
break;
|
|
185
|
-
case "press":
|
|
186
|
-
await page.keyboard.press(step.key);
|
|
187
|
-
await opts.sleep(settle);
|
|
188
|
-
lines.push(`setup #${i} press ${step.key}`);
|
|
189
|
-
break;
|
|
190
|
-
case "click": {
|
|
191
|
-
const loc = page.locator(step.selector).first();
|
|
192
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
193
|
-
await loc.click();
|
|
194
|
-
await opts.sleep(settle);
|
|
195
|
-
lines.push(`setup #${i} click ${step.selector}`);
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case "type": {
|
|
199
|
-
const loc = page.locator(step.selector).first();
|
|
200
|
-
await loc.waitFor({ state: "visible", timeout: 8e3 });
|
|
201
|
-
const { value, secret } = resolveSetupText(step.text, opts.env);
|
|
202
|
-
await loc.fill(value);
|
|
203
|
-
await opts.sleep(settle);
|
|
204
|
-
lines.push(
|
|
205
|
-
`setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
|
|
206
|
-
);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch (e) {
|
|
211
|
-
if (e instanceof SetupEnvError) throw e;
|
|
212
|
-
return {
|
|
213
|
-
ran: i,
|
|
214
|
-
lines,
|
|
215
|
-
failed: {
|
|
216
|
-
step: i,
|
|
217
|
-
do: step.do,
|
|
218
|
-
..."selector" in step ? { selector: step.selector } : {}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return { ran: steps.length, lines };
|
|
224
|
-
}
|
|
225
|
-
var SetupEnvError = class extends Error {
|
|
226
|
-
constructor(message) {
|
|
227
|
-
super(message);
|
|
228
|
-
this.name = "SetupEnvError";
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
var SetupError = class extends Error {
|
|
232
|
-
constructor(failed2) {
|
|
233
|
-
super(
|
|
234
|
-
`setup #${failed2.step} ${failed2.do}${failed2.selector ? ` ${failed2.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
|
|
235
|
-
);
|
|
236
|
-
this.failed = failed2;
|
|
237
|
-
this.name = "SetupError";
|
|
238
|
-
}
|
|
239
|
-
failed;
|
|
240
|
-
};
|
|
241
|
-
function parseHeaders(given) {
|
|
242
|
-
const out = {};
|
|
243
|
-
for (const h of given ?? []) {
|
|
244
|
-
const i = h.indexOf("=");
|
|
245
|
-
if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
|
|
246
|
-
out[h.slice(0, i).trim()] = h.slice(i + 1);
|
|
247
|
-
}
|
|
248
|
-
return out;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
108
|
// src/plugin/actions.ts
|
|
252
|
-
|
|
253
|
-
"wait",
|
|
254
|
-
"hover",
|
|
255
|
-
"click",
|
|
256
|
-
"type",
|
|
257
|
-
"scroll",
|
|
258
|
-
"move",
|
|
259
|
-
"drag"
|
|
260
|
-
]);
|
|
261
|
-
function validateActions(value) {
|
|
262
|
-
const errors = [];
|
|
263
|
-
if (typeof value !== "object" || value === null)
|
|
264
|
-
return ["actions file must be a JSON object"];
|
|
265
|
-
const obj = value;
|
|
266
|
-
if (obj.url !== void 0 && typeof obj.url !== "string")
|
|
267
|
-
errors.push("url must be a string");
|
|
268
|
-
if (obj.viewport !== void 0) {
|
|
269
|
-
const v = obj.viewport;
|
|
270
|
-
if (typeof v !== "object" || v === null) {
|
|
271
|
-
errors.push("viewport must be { width, height }");
|
|
272
|
-
} else {
|
|
273
|
-
const vp = v;
|
|
274
|
-
if (typeof vp.width !== "number" || typeof vp.height !== "number") {
|
|
275
|
-
errors.push("viewport must be { width, height }");
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
|
|
280
|
-
if (obj.mask !== void 0) {
|
|
281
|
-
if (!Array.isArray(obj.mask)) {
|
|
282
|
-
errors.push("mask must be an array of { selector, as?, text? }");
|
|
283
|
-
} else {
|
|
284
|
-
obj.mask.forEach((raw, i) => {
|
|
285
|
-
const at2 = `mask[${i}]`;
|
|
286
|
-
const m = raw ?? {};
|
|
287
|
-
if (typeof m.selector !== "string" || !m.selector.trim())
|
|
288
|
-
errors.push(`${at2}: needs a selector`);
|
|
289
|
-
if (m.as !== void 0 && m.as !== "blur" && m.as !== "text")
|
|
290
|
-
errors.push(`${at2}: as must be "blur" or "text"`);
|
|
291
|
-
if (m.as === "text" && (typeof m.text !== "string" || !m.text))
|
|
292
|
-
errors.push(`${at2}: as "text" needs the text to show instead`);
|
|
293
|
-
if (m.as !== "text" && m.text !== void 0)
|
|
294
|
-
errors.push(`${at2}: text is only read with as "text"`);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
|
|
299
|
-
errors.push("steps must be a non-empty array");
|
|
300
|
-
return errors;
|
|
301
|
-
}
|
|
302
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
303
|
-
obj.steps.forEach((raw, i) => {
|
|
304
|
-
const at2 = `steps[${i}]`;
|
|
305
|
-
if (typeof raw !== "object" || raw === null) {
|
|
306
|
-
errors.push(`${at2}: must be an object`);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const s = raw;
|
|
310
|
-
if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
|
|
311
|
-
errors.push(`${at2}: "do" must be one of ${[...VERBS2].join(", ")}`);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (s.id !== void 0) {
|
|
315
|
-
if (typeof s.id !== "string" || !s.id.trim()) {
|
|
316
|
-
errors.push(`${at2}: id must be a non-empty string`);
|
|
317
|
-
} else if (seenIds.has(s.id)) {
|
|
318
|
-
errors.push(
|
|
319
|
-
`${at2}: duplicate id "${s.id}" \u2014 an anchor could not tell the two steps apart`
|
|
320
|
-
);
|
|
321
|
-
} else {
|
|
322
|
-
seenIds.add(s.id);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const needSelector = s.do === "hover" || s.do === "click" || s.do === "type";
|
|
326
|
-
if (needSelector && typeof s.selector !== "string")
|
|
327
|
-
errors.push(`${at2}: ${s.do} needs a selector`);
|
|
328
|
-
if (s.do === "wait" && typeof s.ms !== "number")
|
|
329
|
-
errors.push(`${at2}: wait needs ms`);
|
|
330
|
-
if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
|
|
331
|
-
errors.push(`${at2}: ms must be a number of milliseconds \u2265 0`);
|
|
332
|
-
if (s.do === "type" && typeof s.text !== "string")
|
|
333
|
-
errors.push(`${at2}: type needs text`);
|
|
334
|
-
if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
|
|
335
|
-
errors.push(`${at2}: type focus must be true or false`);
|
|
336
|
-
if (s.do === "scroll" && typeof s.dy !== "number")
|
|
337
|
-
errors.push(`${at2}: scroll needs dy`);
|
|
338
|
-
if (s.do === "move" && (typeof s.x !== "number" || typeof s.y !== "number")) {
|
|
339
|
-
errors.push(`${at2}: move needs x and y (CSS px)`);
|
|
340
|
-
}
|
|
341
|
-
if (s.do === "drag") {
|
|
342
|
-
if (typeof s.tx !== "number" || typeof s.ty !== "number") {
|
|
343
|
-
errors.push(`${at2}: drag needs tx and ty (CSS px)`);
|
|
344
|
-
}
|
|
345
|
-
const hasStart = typeof s.selector === "string" || typeof s.x === "number" && typeof s.y === "number";
|
|
346
|
-
if (!hasStart)
|
|
347
|
-
errors.push(
|
|
348
|
-
`${at2}: drag needs a selector OR x and y as the start point`
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
return errors;
|
|
353
|
-
}
|
|
109
|
+
import { validateActions } from "@vosjs/render-core/record";
|
|
354
110
|
|
|
355
111
|
// src/plugin/validateDoc.ts
|
|
356
112
|
import {
|
|
@@ -2457,10 +2213,10 @@ function startTakeServer(rootDir, pages) {
|
|
|
2457
2213
|
res.writeHead(404).end();
|
|
2458
2214
|
});
|
|
2459
2215
|
return new Promise(
|
|
2460
|
-
(
|
|
2216
|
+
(resolve11) => server.listen(0, () => {
|
|
2461
2217
|
const addr = server.address();
|
|
2462
2218
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
2463
|
-
|
|
2219
|
+
resolve11({ base: `http://localhost:${port}`, close: () => server.close() });
|
|
2464
2220
|
})
|
|
2465
2221
|
);
|
|
2466
2222
|
}
|
|
@@ -6674,805 +6430,67 @@ async function validateKit(kitPath, opts = {}) {
|
|
|
6674
6430
|
}
|
|
6675
6431
|
|
|
6676
6432
|
// src/plugin/recorder.ts
|
|
6677
|
-
import {
|
|
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";
|
|
@@ -7536,10 +6554,10 @@ async function openSession(name, url, opts = { log: () => {
|
|
|
7536
6554
|
opts.log(
|
|
7537
6555
|
`a Chrome window opened on session "${name}": sign in to ${new URL(url).host}, ${leave}`
|
|
7538
6556
|
);
|
|
7539
|
-
return new Promise((
|
|
6557
|
+
return new Promise((resolve11, reject) => {
|
|
7540
6558
|
const child = spawn(exe, args, { stdio: "ignore" });
|
|
7541
6559
|
child.on("error", reject);
|
|
7542
|
-
child.on("exit", (code) =>
|
|
6560
|
+
child.on("exit", (code) => resolve11({ code }));
|
|
7543
6561
|
});
|
|
7544
6562
|
}
|
|
7545
6563
|
async function withSession(name, fn, opts = {}) {
|
|
@@ -7616,47 +6634,47 @@ function looksLikeStorageState(value) {
|
|
|
7616
6634
|
return Array.isArray(v.cookies) && Array.isArray(v.origins);
|
|
7617
6635
|
}
|
|
7618
6636
|
|
|
6637
|
+
// src/plugin/recordingCap.ts
|
|
6638
|
+
import {
|
|
6639
|
+
capReached,
|
|
6640
|
+
cappedLine,
|
|
6641
|
+
clampWait,
|
|
6642
|
+
formatDurationCap
|
|
6643
|
+
} from "@vosjs/render-core/record";
|
|
6644
|
+
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
6645
|
+
function parseHostedCap(body) {
|
|
6646
|
+
const limits = body && typeof body === "object" ? body.limits : void 0;
|
|
6647
|
+
const cap = limits?.recordingMaxSeconds;
|
|
6648
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
|
|
6649
|
+
}
|
|
6650
|
+
async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
|
|
6651
|
+
const ctl = new AbortController();
|
|
6652
|
+
const timer = setTimeout(() => ctl.abort(), 2e3);
|
|
6653
|
+
try {
|
|
6654
|
+
const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
|
|
6655
|
+
signal: ctl.signal,
|
|
6656
|
+
...key ? { headers: { authorization: `Bearer ${key}` } } : {}
|
|
6657
|
+
});
|
|
6658
|
+
if (!res.ok) return null;
|
|
6659
|
+
return parseHostedCap(await res.json());
|
|
6660
|
+
} catch {
|
|
6661
|
+
return null;
|
|
6662
|
+
} finally {
|
|
6663
|
+
clearTimeout(timer);
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
function defaultMaxDurationSeconds() {
|
|
6667
|
+
return HOSTED_RECORDING_CAP_SECONDS;
|
|
6668
|
+
}
|
|
6669
|
+
|
|
7619
6670
|
// src/plugin/encode.ts
|
|
7620
|
-
|
|
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
|
}
|
|
@@ -9723,7 +8741,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
9723
8741
|
}
|
|
9724
8742
|
|
|
9725
8743
|
// src/plugin/asset.ts
|
|
9726
|
-
import { readFileSync as
|
|
8744
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9727
8745
|
import { basename as basename5, extname as extname2 } from "path";
|
|
9728
8746
|
var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9729
8747
|
var MIME_BY_EXT = {
|
|
@@ -9777,7 +8795,7 @@ async function cmdPush(argv) {
|
|
|
9777
8795
|
for (const file of positionals) {
|
|
9778
8796
|
const name = basename5(file);
|
|
9779
8797
|
const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
|
|
9780
|
-
const body =
|
|
8798
|
+
const body = readFileSync4(file);
|
|
9781
8799
|
const form = new FormData();
|
|
9782
8800
|
form.set("file", new Blob([body], { type: mime }), name);
|
|
9783
8801
|
if (folder) form.set("folderId", folder.id);
|
|
@@ -9831,7 +8849,7 @@ async function cmdAsset(argv) {
|
|
|
9831
8849
|
}
|
|
9832
8850
|
|
|
9833
8851
|
// src/plugin/recipe.ts
|
|
9834
|
-
import { readFileSync as
|
|
8852
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
9835
8853
|
import { basename as basename6 } from "path";
|
|
9836
8854
|
var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9837
8855
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
@@ -9851,7 +8869,7 @@ async function cmdPush2(argv) {
|
|
|
9851
8869
|
throw new UsageError(USAGE);
|
|
9852
8870
|
}
|
|
9853
8871
|
const filename = recipeFilename(file);
|
|
9854
|
-
const body =
|
|
8872
|
+
const body = readFileSync5(file);
|
|
9855
8873
|
const r = createReporter(flags.json === true);
|
|
9856
8874
|
const origin = platformOrigin({
|
|
9857
8875
|
origin: strFlag(flags, "origin"),
|
|
@@ -10294,6 +9312,103 @@ async function cmdBrand(argv) {
|
|
|
10294
9312
|
return 0;
|
|
10295
9313
|
}
|
|
10296
9314
|
|
|
9315
|
+
// src/plugin/hostedRecord.ts
|
|
9316
|
+
import { setupEnvNames as setupEnvNames2 } from "@vosjs/render-core/record";
|
|
9317
|
+
var POLL_MS = 3e3;
|
|
9318
|
+
var DEFAULT_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
9319
|
+
function hostedEnv(actions, shell) {
|
|
9320
|
+
const env = {};
|
|
9321
|
+
const missing = [];
|
|
9322
|
+
for (const name of setupEnvNames2(actions.setup)) {
|
|
9323
|
+
const v = shell[name];
|
|
9324
|
+
if (v === void 0 || v === "") missing.push(name);
|
|
9325
|
+
else env[name] = v;
|
|
9326
|
+
}
|
|
9327
|
+
return { env, missing };
|
|
9328
|
+
}
|
|
9329
|
+
function isWallError(error) {
|
|
9330
|
+
return !!error && /^(\[\w+\] )?The page is behind a sign-in/.test(error);
|
|
9331
|
+
}
|
|
9332
|
+
async function recordHosted(input) {
|
|
9333
|
+
const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
9334
|
+
const posted = await apiJson(input.origin, "/api/record/jobs", {
|
|
9335
|
+
method: "POST",
|
|
9336
|
+
key: input.key,
|
|
9337
|
+
body: {
|
|
9338
|
+
url: input.url,
|
|
9339
|
+
actions: input.actions,
|
|
9340
|
+
viewport: input.actions.viewport,
|
|
9341
|
+
...Object.keys(input.headers).length ? { headers: input.headers } : {},
|
|
9342
|
+
...Object.keys(input.env).length ? { env: input.env } : {},
|
|
9343
|
+
...input.title ? { title: input.title } : {},
|
|
9344
|
+
...input.folderId ? { folderId: input.folderId } : {}
|
|
9345
|
+
}
|
|
9346
|
+
});
|
|
9347
|
+
if (posted.status !== 202) {
|
|
9348
|
+
return {
|
|
9349
|
+
ok: false,
|
|
9350
|
+
wall: false,
|
|
9351
|
+
error: apiError("record on vos.so", posted),
|
|
9352
|
+
vosId: null,
|
|
9353
|
+
jobId: null
|
|
9354
|
+
};
|
|
9355
|
+
}
|
|
9356
|
+
const job = posted.body.job;
|
|
9357
|
+
const vos = posted.body.vos;
|
|
9358
|
+
const cap = posted.body.cap;
|
|
9359
|
+
input.log(
|
|
9360
|
+
`queued on vos.so as job ${job.id.slice(0, 8)} (vos ${job.vosId}${cap?.seconds ? `, cap ${cap.seconds}s` : ""})`
|
|
9361
|
+
);
|
|
9362
|
+
const deadline = Date.now() + (input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
9363
|
+
let lastStatus = "queued";
|
|
9364
|
+
while (Date.now() < deadline) {
|
|
9365
|
+
await sleep(POLL_MS);
|
|
9366
|
+
const polled = await apiJson(input.origin, `/api/record/jobs/${job.id}`, {
|
|
9367
|
+
key: input.key
|
|
9368
|
+
});
|
|
9369
|
+
if (polled.status !== 200) {
|
|
9370
|
+
return {
|
|
9371
|
+
ok: false,
|
|
9372
|
+
wall: false,
|
|
9373
|
+
error: apiError(`poll record job ${job.id}`, polled),
|
|
9374
|
+
vosId: job.vosId,
|
|
9375
|
+
jobId: job.id
|
|
9376
|
+
};
|
|
9377
|
+
}
|
|
9378
|
+
const state = polled.body.job;
|
|
9379
|
+
if (state.status !== lastStatus) {
|
|
9380
|
+
input.log(`${state.status}\u2026`);
|
|
9381
|
+
lastStatus = state.status;
|
|
9382
|
+
}
|
|
9383
|
+
if (state.status === "completed") {
|
|
9384
|
+
return {
|
|
9385
|
+
ok: true,
|
|
9386
|
+
vosId: state.vosId,
|
|
9387
|
+
jobId: state.id,
|
|
9388
|
+
job: state,
|
|
9389
|
+
studio: vos?.studio ?? `${input.origin}/studio?vos=${state.vosId}`
|
|
9390
|
+
};
|
|
9391
|
+
}
|
|
9392
|
+
if (state.status === "failed" || state.status === "canceled") {
|
|
9393
|
+
const error = state.error ?? (state.status === "canceled" ? "the record job was canceled" : "the record job failed");
|
|
9394
|
+
return {
|
|
9395
|
+
ok: false,
|
|
9396
|
+
wall: isWallError(error),
|
|
9397
|
+
error,
|
|
9398
|
+
vosId: state.vosId,
|
|
9399
|
+
jobId: state.id
|
|
9400
|
+
};
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
return {
|
|
9404
|
+
ok: false,
|
|
9405
|
+
wall: false,
|
|
9406
|
+
error: `the record job ${job.id} did not finish within ${Math.round((input.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 6e4)} min; poll ${input.origin}/api/record/jobs/${job.id}`,
|
|
9407
|
+
vosId: job.vosId,
|
|
9408
|
+
jobId: job.id
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
|
|
10297
9412
|
// src/plugin/agentBrowser.ts
|
|
10298
9413
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
10299
9414
|
import { resolve as resolve8 } from "path";
|
|
@@ -10775,8 +9890,8 @@ async function cmdActions(argv) {
|
|
|
10775
9890
|
if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
|
|
10776
9891
|
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
10777
9892
|
}
|
|
10778
|
-
const { readFile:
|
|
10779
|
-
const text = await
|
|
9893
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
9894
|
+
const text = await readFile17(resolve8(input), "utf8");
|
|
10780
9895
|
const { records, problems } = parseAgentBrowserLog(text);
|
|
10781
9896
|
for (const p of problems) r.log(` ${p}`);
|
|
10782
9897
|
const result = convertAgentBrowser(records, {
|
|
@@ -10813,9 +9928,9 @@ function summary(result) {
|
|
|
10813
9928
|
}
|
|
10814
9929
|
async function cmdActionsScript(input, flags) {
|
|
10815
9930
|
const r = createReporter(flags.json === true);
|
|
10816
|
-
const { readFile:
|
|
10817
|
-
const { shotList, formatShotList } = await import("./shotList-
|
|
10818
|
-
const raw = JSON.parse(await
|
|
9931
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
9932
|
+
const { shotList, formatShotList } = await import("./shotList-IYFDHSIK.js");
|
|
9933
|
+
const raw = JSON.parse(await readFile17(resolve8(input), "utf8"));
|
|
10819
9934
|
const errors = validateActions(raw);
|
|
10820
9935
|
if (errors.length) throw new UsageError(`${input}:
|
|
10821
9936
|
${errors.join("\n ")}`);
|
|
@@ -10824,23 +9939,232 @@ async function cmdActionsScript(input, flags) {
|
|
|
10824
9939
|
return EXIT_OK;
|
|
10825
9940
|
}
|
|
10826
9941
|
|
|
9942
|
+
// src/plugin/ingest.ts
|
|
9943
|
+
import { existsSync as existsSync16 } from "fs";
|
|
9944
|
+
import { copyFile, mkdir as mkdir10, readFile as readFile15, stat as stat4 } from "fs/promises";
|
|
9945
|
+
import { basename as basename7, join as join19, resolve as resolve9 } from "path";
|
|
9946
|
+
import { inflateRawSync } from "zlib";
|
|
9947
|
+
import {
|
|
9948
|
+
cursorFromPlaywrightTrace,
|
|
9949
|
+
cursorFromTrace,
|
|
9950
|
+
playwrightTraceEntry,
|
|
9951
|
+
traceKindOf,
|
|
9952
|
+
zipEntries,
|
|
9953
|
+
zipEntryBytes
|
|
9954
|
+
} from "@vosjs/studio-core";
|
|
9955
|
+
import {
|
|
9956
|
+
ALL_FORMATS,
|
|
9957
|
+
Conversion,
|
|
9958
|
+
FilePathSource,
|
|
9959
|
+
FilePathTarget,
|
|
9960
|
+
Input,
|
|
9961
|
+
Mp4OutputFormat,
|
|
9962
|
+
Output,
|
|
9963
|
+
WebMOutputFormat
|
|
9964
|
+
} from "mediabunny";
|
|
9965
|
+
var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
9966
|
+
async function probeVideoFile(path) {
|
|
9967
|
+
const input = new Input({
|
|
9968
|
+
source: new FilePathSource(path),
|
|
9969
|
+
formats: ALL_FORMATS
|
|
9970
|
+
});
|
|
9971
|
+
try {
|
|
9972
|
+
const format = await input.getFormat();
|
|
9973
|
+
const video = await input.getPrimaryVideoTrack();
|
|
9974
|
+
if (!video) throw new UsageError(`${path} has no video track`);
|
|
9975
|
+
const [duration, stats, audio] = await Promise.all([
|
|
9976
|
+
input.computeDuration(),
|
|
9977
|
+
video.computePacketStats(240),
|
|
9978
|
+
input.getPrimaryAudioTrack()
|
|
9979
|
+
]);
|
|
9980
|
+
const mimeType = format.mimeType;
|
|
9981
|
+
const ext = mimeType.includes("mp4") ? "mp4" : mimeType.includes("quicktime") ? "mov" : "webm";
|
|
9982
|
+
return {
|
|
9983
|
+
width: video.displayWidth,
|
|
9984
|
+
height: video.displayHeight,
|
|
9985
|
+
durationMs: Math.round(duration * 1e3),
|
|
9986
|
+
fps: Math.round(stats.averagePacketRate * 100) / 100,
|
|
9987
|
+
hasAudio: !!audio,
|
|
9988
|
+
mimeType,
|
|
9989
|
+
ext
|
|
9990
|
+
};
|
|
9991
|
+
} finally {
|
|
9992
|
+
await input.dispose?.();
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
async function copyRecording(src, dest, ext) {
|
|
9996
|
+
if (ext === "mov") {
|
|
9997
|
+
await copyFile(src, dest);
|
|
9998
|
+
return false;
|
|
9999
|
+
}
|
|
10000
|
+
const input = new Input({
|
|
10001
|
+
source: new FilePathSource(src),
|
|
10002
|
+
formats: ALL_FORMATS
|
|
10003
|
+
});
|
|
10004
|
+
const target = new FilePathTarget(dest);
|
|
10005
|
+
const output = new Output({
|
|
10006
|
+
format: ext === "mp4" ? new Mp4OutputFormat({ fastStart: "in-memory" }) : new WebMOutputFormat(),
|
|
10007
|
+
target
|
|
10008
|
+
});
|
|
10009
|
+
try {
|
|
10010
|
+
const conversion = await Conversion.init({ input, output });
|
|
10011
|
+
if (!conversion.isValid || conversion.discardedTracks.length > 0) {
|
|
10012
|
+
await conversion.cancel?.();
|
|
10013
|
+
await copyFile(src, dest);
|
|
10014
|
+
return false;
|
|
10015
|
+
}
|
|
10016
|
+
await conversion.execute();
|
|
10017
|
+
return true;
|
|
10018
|
+
} catch {
|
|
10019
|
+
await copyFile(src, dest);
|
|
10020
|
+
return false;
|
|
10021
|
+
} finally {
|
|
10022
|
+
await input.dispose?.();
|
|
10023
|
+
}
|
|
10024
|
+
}
|
|
10025
|
+
async function readTraceFile(path, offsetMs) {
|
|
10026
|
+
const kind = traceKindOf(basename7(path));
|
|
10027
|
+
if (!kind) {
|
|
10028
|
+
throw new UsageError(
|
|
10029
|
+
`${path}: not a trace the recorder reads (trace.zip, .trace, .jsonl, .json or .csv)`
|
|
10030
|
+
);
|
|
10031
|
+
}
|
|
10032
|
+
if (kind === "zip") {
|
|
10033
|
+
const bytes = new Uint8Array(await readFile15(path));
|
|
10034
|
+
const entry = playwrightTraceEntry(zipEntries(bytes));
|
|
10035
|
+
if (!entry) {
|
|
10036
|
+
throw new UsageError(
|
|
10037
|
+
`${path}: no trace.trace inside; a Playwright trace.zip is what --cursor reads as a zip`
|
|
10038
|
+
);
|
|
10039
|
+
}
|
|
10040
|
+
const raw = zipEntryBytes(bytes, entry);
|
|
10041
|
+
const text2 = new TextDecoder().decode(
|
|
10042
|
+
entry.method === 8 ? new Uint8Array(inflateRawSync(raw)) : raw
|
|
10043
|
+
);
|
|
10044
|
+
return cursorFromPlaywrightTrace(text2, { offsetMs });
|
|
10045
|
+
}
|
|
10046
|
+
const text = await readFile15(path, "utf8");
|
|
10047
|
+
return cursorFromTrace(basename7(path), text, { offsetMs });
|
|
10048
|
+
}
|
|
10049
|
+
function parseViewport(v) {
|
|
10050
|
+
if (!v) return null;
|
|
10051
|
+
const m = /^(\d+)x(\d+)$/i.exec(v.trim());
|
|
10052
|
+
if (!m) throw new UsageError("--viewport expects WxH, like 1280x720");
|
|
10053
|
+
return { width: Number(m[1]), height: Number(m[2]) };
|
|
10054
|
+
}
|
|
10055
|
+
async function cmdIngest(argv) {
|
|
10056
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5, MULTI_FLAGS2);
|
|
10057
|
+
const source = positionals[0];
|
|
10058
|
+
if (!source)
|
|
10059
|
+
throw new UsageError(
|
|
10060
|
+
"vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]"
|
|
10061
|
+
);
|
|
10062
|
+
const videoPath = resolve9(source);
|
|
10063
|
+
if (!existsSync16(videoPath)) throw new UsageError(`${source}: no such file`);
|
|
10064
|
+
const r = createReporter(flags.json === true);
|
|
10065
|
+
const outDir = resolve9(strFlag(flags, "out") ?? "take");
|
|
10066
|
+
const offsetMs = numFlag(flags, "offset", 0);
|
|
10067
|
+
const viewportFlag = parseViewport(strFlag(flags, "viewport"));
|
|
10068
|
+
const cursorPath = strFlag(flags, "cursor");
|
|
10069
|
+
const backdrop = await takeBackdrop(flags, r);
|
|
10070
|
+
r.log(`probing ${basename7(videoPath)}\u2026`);
|
|
10071
|
+
const probed = await probeVideoFile(videoPath);
|
|
10072
|
+
if (!(probed.durationMs > 0)) {
|
|
10073
|
+
throw new UsageError(
|
|
10074
|
+
`${source}: the file reports no duration; a stream-shaped recording needs its container fixed first (ffmpeg -i in.webm -c copy out.webm)`
|
|
10075
|
+
);
|
|
10076
|
+
}
|
|
10077
|
+
let trace = null;
|
|
10078
|
+
if (cursorPath) {
|
|
10079
|
+
trace = await readTraceFile(resolve9(cursorPath), offsetMs);
|
|
10080
|
+
for (const n of trace.notes) r.log(`note: ${n}`);
|
|
10081
|
+
}
|
|
10082
|
+
if (existsSync16(join19(outDir, "meta.json"))) {
|
|
10083
|
+
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10084
|
+
r.log(
|
|
10085
|
+
`note: re-ingesting into ${outDir}; kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? `; apply the previous cut with: vos plan ${outDir} --reuse` : "")
|
|
10086
|
+
);
|
|
10087
|
+
}
|
|
10088
|
+
await mkdir10(outDir, { recursive: true });
|
|
10089
|
+
const paths = await ensureTakeDir(outDir);
|
|
10090
|
+
const recording = join19(outDir, `recording.${probed.ext}`);
|
|
10091
|
+
r.log("copying the recording\u2026");
|
|
10092
|
+
const remuxed = await copyRecording(videoPath, recording, probed.ext);
|
|
10093
|
+
const viewport = viewportFlag ?? trace?.viewport ?? {
|
|
10094
|
+
width: probed.width,
|
|
10095
|
+
height: probed.height
|
|
10096
|
+
};
|
|
10097
|
+
const events = trace?.events ?? [];
|
|
10098
|
+
const fileStat = await stat4(videoPath);
|
|
10099
|
+
const meta = {
|
|
10100
|
+
dpr: 1,
|
|
10101
|
+
zoom: 1,
|
|
10102
|
+
t0: trace?.t0 ?? Math.round(fileStat.mtimeMs),
|
|
10103
|
+
durationMs: probed.durationMs,
|
|
10104
|
+
width: viewport.width,
|
|
10105
|
+
height: viewport.height,
|
|
10106
|
+
captureWidth: probed.width,
|
|
10107
|
+
captureHeight: probed.height,
|
|
10108
|
+
fps: probed.fps > 0 ? probed.fps : 30,
|
|
10109
|
+
hasAudio: probed.hasAudio,
|
|
10110
|
+
producer: "ingest",
|
|
10111
|
+
...events.length ? { captureSurface: "tab" } : {}
|
|
10112
|
+
};
|
|
10113
|
+
await writeJson(paths.meta, meta, true);
|
|
10114
|
+
if (events.length) await writeJson(paths.cursor, events);
|
|
10115
|
+
const plan = await planTake(outDir, { backdrop });
|
|
10116
|
+
const clicks = events.filter((e) => e.type === "down").length;
|
|
10117
|
+
const seconds = (probed.durationMs / 1e3).toFixed(1);
|
|
10118
|
+
const facts = `${seconds}s, ${probed.width}\xD7${probed.height} @ ${meta.fps} fps${probed.hasAudio ? ", audio" : ", no audio"}${remuxed ? "" : ", copied as is"}`;
|
|
10119
|
+
const cursorLine = trace ? `${events.length} cursor events (${clicks} clicks) from ${basename7(cursorPath)}${trace.dropped ? `, ${trace.dropped} not placed` : ""}; ${plan.zoomAuto} zoom${plan.zoomAuto === 1 ? "" : "s"} planned` : "no cursor track, so nothing was planned";
|
|
10120
|
+
const next = trace ? `Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}` : `Add --cursor <trace> recorded beside the video (a Playwright trace.zip, stamped JSON records, or a t,x,y,type CSV) to plan zooms from its clicks; else place them by eye: vos frames ${outDir} --times 0,25%,50%,75%,100%, then edit doc.json`;
|
|
10121
|
+
r.done(
|
|
10122
|
+
{
|
|
10123
|
+
out: outDir,
|
|
10124
|
+
recording: basename7(recording),
|
|
10125
|
+
durationMs: probed.durationMs,
|
|
10126
|
+
width: probed.width,
|
|
10127
|
+
height: probed.height,
|
|
10128
|
+
viewport,
|
|
10129
|
+
fps: meta.fps,
|
|
10130
|
+
hasAudio: probed.hasAudio,
|
|
10131
|
+
remuxed,
|
|
10132
|
+
cursor: trace ? {
|
|
10133
|
+
file: basename7(cursorPath),
|
|
10134
|
+
kind: trace.kind,
|
|
10135
|
+
events: events.length,
|
|
10136
|
+
clicks,
|
|
10137
|
+
placed: trace.placed,
|
|
10138
|
+
dropped: trace.dropped,
|
|
10139
|
+
notes: trace.notes
|
|
10140
|
+
} : null,
|
|
10141
|
+
zooms: plan.zoomAuto,
|
|
10142
|
+
backdrop: plan.backdrop ?? null
|
|
10143
|
+
},
|
|
10144
|
+
`Ingested ${basename7(videoPath)} \u2192 ${outDir}/${basename7(recording)} (${facts})
|
|
10145
|
+
${cursorLine}
|
|
10146
|
+
${next}`
|
|
10147
|
+
);
|
|
10148
|
+
return EXIT_OK;
|
|
10149
|
+
}
|
|
10150
|
+
|
|
10827
10151
|
// src/plugin/machineCmd.ts
|
|
10828
10152
|
import { spawnSync } from "child_process";
|
|
10829
10153
|
import {
|
|
10830
10154
|
cpSync,
|
|
10831
|
-
existsSync as
|
|
10155
|
+
existsSync as existsSync18,
|
|
10832
10156
|
mkdirSync as mkdirSync3,
|
|
10833
|
-
readFileSync as
|
|
10157
|
+
readFileSync as readFileSync7,
|
|
10834
10158
|
rmSync as rmSync2,
|
|
10835
10159
|
writeFileSync as writeFileSync3
|
|
10836
10160
|
} from "fs";
|
|
10837
10161
|
import { homedir as homedir3 } from "os";
|
|
10838
|
-
import { join as
|
|
10162
|
+
import { join as join21 } from "path";
|
|
10839
10163
|
import { chromium as chromium2 } from "playwright";
|
|
10840
10164
|
|
|
10841
10165
|
// src/plugin/machine.ts
|
|
10842
|
-
import { existsSync as
|
|
10843
|
-
import { dirname as dirname5, join as
|
|
10166
|
+
import { existsSync as existsSync17, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
10167
|
+
import { dirname as dirname5, join as join20 } from "path";
|
|
10844
10168
|
var AGENT_NAMES = [
|
|
10845
10169
|
"claude",
|
|
10846
10170
|
"cursor",
|
|
@@ -10860,17 +10184,17 @@ var HOME_DIRS = {
|
|
|
10860
10184
|
copilot: [".copilot"]
|
|
10861
10185
|
};
|
|
10862
10186
|
function detectAgents(root, opts = {}) {
|
|
10863
|
-
const exists = opts.exists ??
|
|
10187
|
+
const exists = opts.exists ?? existsSync17;
|
|
10864
10188
|
const out = [];
|
|
10865
10189
|
for (const agent of AGENT_NAMES) {
|
|
10866
10190
|
for (const rel of PROJECT_DIRS[agent]) {
|
|
10867
|
-
const dir =
|
|
10191
|
+
const dir = join20(root, rel);
|
|
10868
10192
|
if (exists(dir)) out.push({ agent, dir, scope: "project" });
|
|
10869
10193
|
}
|
|
10870
|
-
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(
|
|
10194
|
+
if (agent === "copilot" && !out.some((h) => h.agent === "copilot") && exists(join20(root, ".github", "copilot-instructions.md"))) {
|
|
10871
10195
|
out.push({
|
|
10872
10196
|
agent,
|
|
10873
|
-
dir:
|
|
10197
|
+
dir: join20(root, ".github", "copilot"),
|
|
10874
10198
|
scope: "project"
|
|
10875
10199
|
});
|
|
10876
10200
|
}
|
|
@@ -10878,7 +10202,7 @@ function detectAgents(root, opts = {}) {
|
|
|
10878
10202
|
if (opts.global && opts.home) {
|
|
10879
10203
|
for (const agent of AGENT_NAMES) {
|
|
10880
10204
|
for (const rel of HOME_DIRS[agent]) {
|
|
10881
|
-
const dir =
|
|
10205
|
+
const dir = join20(opts.home, rel);
|
|
10882
10206
|
if (exists(dir)) out.push({ agent, dir, scope: "global" });
|
|
10883
10207
|
}
|
|
10884
10208
|
}
|
|
@@ -10896,18 +10220,18 @@ function agentsFromFlag(value, root) {
|
|
|
10896
10220
|
return {
|
|
10897
10221
|
homes: wanted.map((agent) => ({
|
|
10898
10222
|
agent,
|
|
10899
|
-
dir:
|
|
10223
|
+
dir: join20(root, PROJECT_DIRS[agent][0]),
|
|
10900
10224
|
scope: "project"
|
|
10901
10225
|
}))
|
|
10902
10226
|
};
|
|
10903
10227
|
}
|
|
10904
10228
|
function skillsDirFor(home) {
|
|
10905
|
-
return
|
|
10229
|
+
return join20(home.dir, "skills");
|
|
10906
10230
|
}
|
|
10907
10231
|
function fallbackSkillsHome(root) {
|
|
10908
|
-
return { agent: "codex", dir:
|
|
10232
|
+
return { agent: "codex", dir: join20(root, ".agents"), scope: "project" };
|
|
10909
10233
|
}
|
|
10910
|
-
function installedSkills(dir, exists =
|
|
10234
|
+
function installedSkills(dir, exists = existsSync17, list = (p) => {
|
|
10911
10235
|
try {
|
|
10912
10236
|
return readdirSync4(p);
|
|
10913
10237
|
} catch {
|
|
@@ -10915,7 +10239,7 @@ function installedSkills(dir, exists = existsSync16, list = (p) => {
|
|
|
10915
10239
|
}
|
|
10916
10240
|
}) {
|
|
10917
10241
|
if (!exists(dir)) return [];
|
|
10918
|
-
return list(dir).filter((name) => exists(
|
|
10242
|
+
return list(dir).filter((name) => exists(join20(dir, name, "SKILL.md"))).sort();
|
|
10919
10243
|
}
|
|
10920
10244
|
var RULES_BEGIN = "<!-- vos:begin (written by vos setup; the block is replaced on the next run) -->";
|
|
10921
10245
|
var RULES_END = "<!-- vos:end -->";
|
|
@@ -10947,10 +10271,10 @@ ${RULES_BLOCK}
|
|
|
10947
10271
|
}
|
|
10948
10272
|
return { text, changed: text !== current };
|
|
10949
10273
|
}
|
|
10950
|
-
function rulesFileFor(root, exists =
|
|
10951
|
-
const agents =
|
|
10274
|
+
function rulesFileFor(root, exists = existsSync17) {
|
|
10275
|
+
const agents = join20(root, "AGENTS.md");
|
|
10952
10276
|
if (exists(agents)) return agents;
|
|
10953
|
-
const claude =
|
|
10277
|
+
const claude = join20(root, "CLAUDE.md");
|
|
10954
10278
|
if (exists(claude)) return claude;
|
|
10955
10279
|
return agents;
|
|
10956
10280
|
}
|
|
@@ -10959,14 +10283,14 @@ function chromeCandidates(platform, env) {
|
|
|
10959
10283
|
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
|
|
10960
10284
|
if (platform === "win32")
|
|
10961
10285
|
return [
|
|
10962
|
-
|
|
10286
|
+
join20(
|
|
10963
10287
|
env["PROGRAMFILES"] ?? "C:\\Program Files",
|
|
10964
10288
|
"Google",
|
|
10965
10289
|
"Chrome",
|
|
10966
10290
|
"Application",
|
|
10967
10291
|
"chrome.exe"
|
|
10968
10292
|
),
|
|
10969
|
-
|
|
10293
|
+
join20(
|
|
10970
10294
|
env["LOCALAPPDATA"] ?? "",
|
|
10971
10295
|
"Google",
|
|
10972
10296
|
"Chrome",
|
|
@@ -11053,13 +10377,13 @@ function doctorReport(facts) {
|
|
|
11053
10377
|
function bundledSkillsDir(from) {
|
|
11054
10378
|
let dir = dirname5(from);
|
|
11055
10379
|
for (let i = 0; i < 6; i++) {
|
|
11056
|
-
const pkg =
|
|
11057
|
-
if (
|
|
10380
|
+
const pkg = join20(dir, "package.json");
|
|
10381
|
+
if (existsSync17(pkg)) {
|
|
11058
10382
|
try {
|
|
11059
|
-
const name = JSON.parse(
|
|
10383
|
+
const name = JSON.parse(readFileSync6(pkg, "utf8")).name;
|
|
11060
10384
|
if (name === "@vosjs/cli") {
|
|
11061
|
-
const skills =
|
|
11062
|
-
return
|
|
10385
|
+
const skills = join20(dir, "skills");
|
|
10386
|
+
return existsSync17(skills) && statSync4(skills).isDirectory() ? skills : null;
|
|
11063
10387
|
}
|
|
11064
10388
|
} catch {
|
|
11065
10389
|
}
|
|
@@ -11072,7 +10396,7 @@ function bundledSkillsDir(from) {
|
|
|
11072
10396
|
}
|
|
11073
10397
|
|
|
11074
10398
|
// src/plugin/machineCmd.ts
|
|
11075
|
-
var
|
|
10399
|
+
var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
|
|
11076
10400
|
"json",
|
|
11077
10401
|
"help",
|
|
11078
10402
|
"global",
|
|
@@ -11090,7 +10414,7 @@ function npx() {
|
|
|
11090
10414
|
function bundledChromium() {
|
|
11091
10415
|
try {
|
|
11092
10416
|
const p = chromium2.executablePath();
|
|
11093
|
-
return p &&
|
|
10417
|
+
return p && existsSync18(p) ? p : null;
|
|
11094
10418
|
} catch {
|
|
11095
10419
|
return null;
|
|
11096
10420
|
}
|
|
@@ -11099,7 +10423,7 @@ function probe() {
|
|
|
11099
10423
|
return probeBrowser({
|
|
11100
10424
|
platform: process.platform,
|
|
11101
10425
|
env: process.env,
|
|
11102
|
-
exists:
|
|
10426
|
+
exists: existsSync18,
|
|
11103
10427
|
bundled: bundledChromium
|
|
11104
10428
|
});
|
|
11105
10429
|
}
|
|
@@ -11150,7 +10474,7 @@ async function doctorFacts(opts) {
|
|
|
11150
10474
|
};
|
|
11151
10475
|
}
|
|
11152
10476
|
async function cmdSetup(argv) {
|
|
11153
|
-
const { flags } = parseArgs(argv,
|
|
10477
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
|
|
11154
10478
|
const r = reporterFor(flags);
|
|
11155
10479
|
const root = process.cwd();
|
|
11156
10480
|
const global = flags.global === true;
|
|
@@ -11203,7 +10527,7 @@ async function cmdSetup(argv) {
|
|
|
11203
10527
|
const dest = skillsDirFor(home);
|
|
11204
10528
|
mkdirSync3(dest, { recursive: true });
|
|
11205
10529
|
for (const name of installedSkills(bundled)) {
|
|
11206
|
-
cpSync(
|
|
10530
|
+
cpSync(join21(bundled, name), join21(dest, name), { recursive: true });
|
|
11207
10531
|
}
|
|
11208
10532
|
}
|
|
11209
10533
|
skillsVia = "bundled";
|
|
@@ -11238,7 +10562,7 @@ async function cmdSetup(argv) {
|
|
|
11238
10562
|
let rules = null;
|
|
11239
10563
|
if (!noRules) {
|
|
11240
10564
|
const file = rulesFileFor(root);
|
|
11241
|
-
const existing =
|
|
10565
|
+
const existing = existsSync18(file) ? readFileSync7(file, "utf8") : null;
|
|
11242
10566
|
const { text, changed } = applyRulesBlock(existing);
|
|
11243
10567
|
if (changed) writeFileSync3(file, text);
|
|
11244
10568
|
rules = { file, changed };
|
|
@@ -11269,7 +10593,7 @@ Next: ${report.next_step}`
|
|
|
11269
10593
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
11270
10594
|
}
|
|
11271
10595
|
async function cmdDoctor(argv) {
|
|
11272
|
-
const { flags } = parseArgs(argv,
|
|
10596
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
|
|
11273
10597
|
const r = reporterFor(flags);
|
|
11274
10598
|
const root = process.cwd();
|
|
11275
10599
|
const homes = detectAgents(root, { home: homedir3(), global: true });
|
|
@@ -11288,7 +10612,7 @@ Next: ${report.next_step}`
|
|
|
11288
10612
|
return report.ok ? EXIT_OK : EXIT_NO_BROWSER;
|
|
11289
10613
|
}
|
|
11290
10614
|
async function cmdWhoami(argv) {
|
|
11291
|
-
const { flags } = parseArgs(argv,
|
|
10615
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
|
|
11292
10616
|
const r = reporterFor(flags);
|
|
11293
10617
|
const origin = platformOrigin({
|
|
11294
10618
|
origin: strFlag(flags, "origin"),
|
|
@@ -11350,9 +10674,9 @@ async function cmdWhoami(argv) {
|
|
|
11350
10674
|
return EXIT_OK;
|
|
11351
10675
|
}
|
|
11352
10676
|
function cmdLogout(argv) {
|
|
11353
|
-
const { flags } = parseArgs(argv,
|
|
10677
|
+
const { flags } = parseArgs(argv, BOOLEAN_FLAGS6);
|
|
11354
10678
|
const r = reporterFor(flags);
|
|
11355
|
-
const had =
|
|
10679
|
+
const had = existsSync18(CREDENTIALS_PATH);
|
|
11356
10680
|
rmSync2(CREDENTIALS_PATH, { force: true });
|
|
11357
10681
|
const envKey = Boolean(process.env.VOS_API_KEY?.trim());
|
|
11358
10682
|
r.done(
|
|
@@ -11368,7 +10692,7 @@ function cmdLogout(argv) {
|
|
|
11368
10692
|
}
|
|
11369
10693
|
|
|
11370
10694
|
// src/plugin/run.ts
|
|
11371
|
-
var
|
|
10695
|
+
var BOOLEAN_FLAGS7 = /* @__PURE__ */ new Set([
|
|
11372
10696
|
"json",
|
|
11373
10697
|
"help",
|
|
11374
10698
|
"picture",
|
|
@@ -11389,7 +10713,8 @@ var BOOLEAN_FLAGS6 = /* @__PURE__ */ new Set([
|
|
|
11389
10713
|
"leader",
|
|
11390
10714
|
"keep-frames",
|
|
11391
10715
|
"dry-run",
|
|
11392
|
-
"allow-wall"
|
|
10716
|
+
"allow-wall",
|
|
10717
|
+
"hosted"
|
|
11393
10718
|
]);
|
|
11394
10719
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
|
|
11395
10720
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -11397,6 +10722,8 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
|
|
|
11397
10722
|
Take pipeline
|
|
11398
10723
|
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
11399
10724
|
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>|--session <name>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
|
|
10725
|
+
vos record --hosted --actions actions.json [--url <url>] [--out take] [--title "\u2026"] [--folder <id>] [--header name=value]... [--json]
|
|
10726
|
+
the take recorded on vos.so's fleet (no browser here): lands as a private vos with its doc and digest, then fetched into --out with its media
|
|
11400
10727
|
vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
|
|
11401
10728
|
vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
11402
10729
|
vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
|
|
@@ -11408,6 +10735,8 @@ Take pipeline
|
|
|
11408
10735
|
vos validate <actions.json|take|kit.json> [--picture] [--json]
|
|
11409
10736
|
vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
|
|
11410
10737
|
vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
|
|
10738
|
+
vos ingest <video.webm|mp4> [--cursor <trace.zip|steps.jsonl|cursor.csv>] [--out take] [--offset <ms>] [--viewport WxH] [--background <slug|url|none>] [--json]
|
|
10739
|
+
a take from a recording someone else made (a cloud agent's PR video, a Playwright run's video, a Loom export): the file becomes recording.<container>, meta.json from its own dimensions and duration, and a trace beside it becomes cursor.json so the planner has clicks to zoom on; without one, nothing is planned and the done event says so
|
|
11411
10740
|
vos session open <url> --name <app> a plain Chrome window on a vos-owned profile; sign in, close it
|
|
11412
10741
|
vos session check <name> [--url <url>] headless: does the session still open the page? exit 0/4
|
|
11413
10742
|
vos session list | rm <name> what exists and how old; delete one
|
|
@@ -11665,7 +10994,7 @@ lint-gated, so a bad override fails like a bad doc.json):
|
|
|
11665
10994
|
first ready loop (the house backdrop the studio opens on), or a flat ground offline
|
|
11666
10995
|
`;
|
|
11667
10996
|
async function loadActions(file) {
|
|
11668
|
-
const raw = JSON.parse(await
|
|
10997
|
+
const raw = JSON.parse(await readFile16(file, "utf8"));
|
|
11669
10998
|
const errors = validateActions(raw);
|
|
11670
10999
|
if (errors.length)
|
|
11671
11000
|
throw new UsageError(`invalid actions file:
|
|
@@ -11727,8 +11056,8 @@ function takeBrowserArgs(multi) {
|
|
|
11727
11056
|
function takeStorageState(flags) {
|
|
11728
11057
|
const raw = strFlag(flags, "storage-state");
|
|
11729
11058
|
if (!raw) return void 0;
|
|
11730
|
-
const path =
|
|
11731
|
-
if (!
|
|
11059
|
+
const path = resolve10(raw);
|
|
11060
|
+
if (!existsSync19(path))
|
|
11732
11061
|
throw new UsageError(`--storage-state: no such file: ${path}`);
|
|
11733
11062
|
return path;
|
|
11734
11063
|
}
|
|
@@ -11741,7 +11070,7 @@ function takeSessionName(flags, storageState) {
|
|
|
11741
11070
|
throw new UsageError(
|
|
11742
11071
|
"--session and --storage-state are two doors to one take: a persistent profile cannot take a storage state. Pass one"
|
|
11743
11072
|
);
|
|
11744
|
-
if (!
|
|
11073
|
+
if (!existsSync19(sessionDir(name)))
|
|
11745
11074
|
throw new UsageError(
|
|
11746
11075
|
`no session "${name}". Make one: vos session open <url> --name ${name} (a Chrome window opens; sign in and close it)`
|
|
11747
11076
|
);
|
|
@@ -11797,7 +11126,7 @@ function wallGate(flags, r, opts) {
|
|
|
11797
11126
|
async function cmdRecord(argv) {
|
|
11798
11127
|
const { positionals, flags, multi } = parseArgs(
|
|
11799
11128
|
argv,
|
|
11800
|
-
|
|
11129
|
+
BOOLEAN_FLAGS7,
|
|
11801
11130
|
MULTI_FLAGS2
|
|
11802
11131
|
);
|
|
11803
11132
|
const actionsPath = strFlag(flags, "actions") ?? positionals[0];
|
|
@@ -11810,13 +11139,103 @@ async function cmdRecord(argv) {
|
|
|
11810
11139
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
11811
11140
|
if (!url)
|
|
11812
11141
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
11813
|
-
const outDir =
|
|
11814
|
-
const backdrop = await takeBackdrop(flags, r);
|
|
11815
|
-
const maxDurationSeconds = await maxDuration(flags, r);
|
|
11142
|
+
const outDir = resolve10(strFlag(flags, "out") ?? "take");
|
|
11816
11143
|
const storageState = takeStorageState(flags);
|
|
11817
11144
|
const sessionName = takeSessionName(flags, storageState);
|
|
11818
11145
|
const browserArgs = takeBrowserArgs(multi);
|
|
11819
11146
|
const headers = takeHeaders(multi);
|
|
11147
|
+
if (flags.hosted === true) {
|
|
11148
|
+
if (flags["dry-run"] === true)
|
|
11149
|
+
throw new UsageError(
|
|
11150
|
+
"--dry-run rehearses on this machine; drop --hosted to rehearse, or --dry-run to record on vos.so"
|
|
11151
|
+
);
|
|
11152
|
+
if (storageState || sessionName)
|
|
11153
|
+
throw new UsageError(
|
|
11154
|
+
'a session never reaches vos.so: --hosted takes no --session or --storage-state. Put the sign-in in the setup steps with { "env": "NAME" }, or record here with vos record.'
|
|
11155
|
+
);
|
|
11156
|
+
const origin = platformOrigin({
|
|
11157
|
+
origin: strFlag(flags, "origin"),
|
|
11158
|
+
api: strFlag(flags, "api")
|
|
11159
|
+
});
|
|
11160
|
+
const key = resolveCredential(strFlag(flags, "key"));
|
|
11161
|
+
if (!key)
|
|
11162
|
+
throw new UsageError(
|
|
11163
|
+
"recording on vos.so needs a credential: vos login, or VOS_API_KEY (mint a key at https://vos.so/app/api)"
|
|
11164
|
+
);
|
|
11165
|
+
const { env, missing } = hostedEnv(actions, process.env);
|
|
11166
|
+
if (missing.length)
|
|
11167
|
+
throw new UsageError(
|
|
11168
|
+
`the setup reads ${missing.map((n) => `{ "env": "${n}" }`).join(", ")} and this shell has no ${missing.length === 1 ? "value for it" : "values for them"}; export ${missing.join(", ")} and run again (the value rides the job, and is never stored)`
|
|
11169
|
+
);
|
|
11170
|
+
r.log("recording on vos.so\u2026");
|
|
11171
|
+
r.event({ event: "phase", phase: "record" });
|
|
11172
|
+
const started = Date.now();
|
|
11173
|
+
const outcome = await recordHosted({
|
|
11174
|
+
origin,
|
|
11175
|
+
key,
|
|
11176
|
+
url,
|
|
11177
|
+
actions: { ...actions, url },
|
|
11178
|
+
headers,
|
|
11179
|
+
title: strFlag(flags, "title"),
|
|
11180
|
+
folderId: strFlag(flags, "folder"),
|
|
11181
|
+
env,
|
|
11182
|
+
log: r.log
|
|
11183
|
+
});
|
|
11184
|
+
if (!outcome.ok) {
|
|
11185
|
+
if (outcome.vosId) {
|
|
11186
|
+
const gone = await apiJson(origin, `/api/vos/${outcome.vosId}`, {
|
|
11187
|
+
method: "DELETE",
|
|
11188
|
+
key
|
|
11189
|
+
});
|
|
11190
|
+
if (gone.status === 200)
|
|
11191
|
+
r.log("the empty vos it would have landed on was removed");
|
|
11192
|
+
}
|
|
11193
|
+
r.done(
|
|
11194
|
+
{
|
|
11195
|
+
hosted: true,
|
|
11196
|
+
ok: false,
|
|
11197
|
+
wall: outcome.wall,
|
|
11198
|
+
error: outcome.error,
|
|
11199
|
+
vosId: outcome.vosId,
|
|
11200
|
+
jobId: outcome.jobId
|
|
11201
|
+
},
|
|
11202
|
+
`${outcome.wall ? "REFUSED" : "FAILED"} on vos.so: ${outcome.error}`
|
|
11203
|
+
);
|
|
11204
|
+
return outcome.wall ? EXIT_WALL : EXIT_ERROR;
|
|
11205
|
+
}
|
|
11206
|
+
r.event({ event: "phase", phase: "fetch" });
|
|
11207
|
+
r.log(`landed as ${outcome.studio}; fetching the take into ${outDir}\u2026`);
|
|
11208
|
+
const fetched = await cmdFetch([
|
|
11209
|
+
outcome.vosId,
|
|
11210
|
+
"--out",
|
|
11211
|
+
outDir,
|
|
11212
|
+
"--media",
|
|
11213
|
+
...strFlag(flags, "origin") ? ["--origin", strFlag(flags, "origin")] : [],
|
|
11214
|
+
...strFlag(flags, "key") ? ["--key", strFlag(flags, "key")] : [],
|
|
11215
|
+
...flags.json === true ? ["--json"] : []
|
|
11216
|
+
]);
|
|
11217
|
+
if (fetched !== EXIT_OK) return fetched;
|
|
11218
|
+
const t = outcome.job.telemetry ?? {};
|
|
11219
|
+
r.done(
|
|
11220
|
+
{
|
|
11221
|
+
hosted: true,
|
|
11222
|
+
ok: true,
|
|
11223
|
+
vosId: outcome.vosId,
|
|
11224
|
+
jobId: outcome.jobId,
|
|
11225
|
+
out: outDir,
|
|
11226
|
+
seconds: +((Date.now() - started) / 1e3).toFixed(1),
|
|
11227
|
+
telemetry: t
|
|
11228
|
+
},
|
|
11229
|
+
`Recorded on vos.so in ${((Date.now() - started) / 1e3).toFixed(1)}s: ${outcome.studio}
|
|
11230
|
+
${outDir}/ holds doc.json, the recording and vos.json${typeof t.durationMs === "number" ? ` (${(t.durationMs / 1e3).toFixed(1)}s of footage)` : ""}${Array.isArray(t.dead) && t.dead.length ? `
|
|
11231
|
+
DEAD TIME: ${t.dead.length} stretch(es) under a parked cursor; give those steps their ms` : ""}${Array.isArray(t.exposures) && t.exposures.length ? `
|
|
11232
|
+
EXPOSURES: ${t.exposures.length} sensitive-looking text(s) in frame; mask them in actions.json` : ""}
|
|
11233
|
+
Next: vos digest ${outDir} \u2014 look before you cut; edit doc.json; vos push ${outDir}`
|
|
11234
|
+
);
|
|
11235
|
+
return EXIT_OK;
|
|
11236
|
+
}
|
|
11237
|
+
const backdrop = await takeBackdrop(flags, r);
|
|
11238
|
+
const maxDurationSeconds = await maxDuration(flags, r);
|
|
11820
11239
|
if (flags["dry-run"] === true) {
|
|
11821
11240
|
void flags.strict;
|
|
11822
11241
|
void flags["keep-frames"];
|
|
@@ -11874,13 +11293,13 @@ ${lines.join("\n")}
|
|
|
11874
11293
|
}
|
|
11875
11294
|
const paths = takePaths(outDir);
|
|
11876
11295
|
const prepare = async () => {
|
|
11877
|
-
if (
|
|
11296
|
+
if (existsSync19(join22(outDir, "meta.json"))) {
|
|
11878
11297
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
11879
11298
|
r.log(
|
|
11880
11299
|
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
11881
11300
|
);
|
|
11882
11301
|
}
|
|
11883
|
-
await
|
|
11302
|
+
await mkdir11(outDir, { recursive: true });
|
|
11884
11303
|
await ensureTakeDir(outDir);
|
|
11885
11304
|
};
|
|
11886
11305
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -11939,7 +11358,7 @@ ${lines.join("\n")}
|
|
|
11939
11358
|
},
|
|
11940
11359
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}${exposureNote(rec)}` : `Take ready: ${outDir}
|
|
11941
11360
|
${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen \xB7 ${rec.dead.pct}% dead${deadWarns(rec.dead) ? " \u26A0 " + deadLine(rec.dead) : ""}${skippedNote}
|
|
11942
|
-
Next: edit ${
|
|
11361
|
+
Next: edit ${join22(outDir, "doc.json")} (optional), then: vos render ${outDir}${exposureNote(rec)}`
|
|
11943
11362
|
);
|
|
11944
11363
|
return strictFail ? EXIT_USAGE : EXIT_OK;
|
|
11945
11364
|
} finally {
|
|
@@ -11949,7 +11368,7 @@ ${lines.join("\n")}
|
|
|
11949
11368
|
async function cmdCreate2(argv) {
|
|
11950
11369
|
const { positionals, flags, multi } = parseArgs(
|
|
11951
11370
|
argv,
|
|
11952
|
-
|
|
11371
|
+
BOOLEAN_FLAGS7,
|
|
11953
11372
|
MULTI_FLAGS2
|
|
11954
11373
|
);
|
|
11955
11374
|
const actionsPath = strFlag(flags, "actions");
|
|
@@ -11963,12 +11382,12 @@ async function cmdCreate2(argv) {
|
|
|
11963
11382
|
const url = strFlag(flags, "url") ?? actions.url;
|
|
11964
11383
|
if (!url)
|
|
11965
11384
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
11966
|
-
const outDir =
|
|
11385
|
+
const outDir = resolve10(strFlag(flags, "out") ?? "take");
|
|
11967
11386
|
const fmtRaw = strFlag(flags, "format") ?? "webm";
|
|
11968
11387
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
11969
11388
|
throw new UsageError("--format must be webm or mp4");
|
|
11970
11389
|
const format = fmtRaw;
|
|
11971
|
-
const out = positionals[0] ??
|
|
11390
|
+
const out = positionals[0] ?? join22(outDir, `out.${format}`);
|
|
11972
11391
|
const parallel = numFlag(flags, "parallel", 1);
|
|
11973
11392
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
11974
11393
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -11981,13 +11400,13 @@ async function cmdCreate2(argv) {
|
|
|
11981
11400
|
const headers = takeHeaders(multi);
|
|
11982
11401
|
const paths = takePaths(outDir);
|
|
11983
11402
|
const prepare = async () => {
|
|
11984
|
-
if (
|
|
11403
|
+
if (existsSync19(join22(outDir, "meta.json"))) {
|
|
11985
11404
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
11986
11405
|
r.log(
|
|
11987
11406
|
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
11988
11407
|
);
|
|
11989
11408
|
}
|
|
11990
|
-
await
|
|
11409
|
+
await mkdir11(outDir, { recursive: true });
|
|
11991
11410
|
await ensureTakeDir(outDir);
|
|
11992
11411
|
};
|
|
11993
11412
|
const browser = await launchBrowser(browserArgs);
|
|
@@ -12078,7 +11497,7 @@ async function cmdCreate2(argv) {
|
|
|
12078
11497
|
async function cmdPlan(argv) {
|
|
12079
11498
|
const { positionals, flags, multi } = parseArgs(
|
|
12080
11499
|
argv,
|
|
12081
|
-
|
|
11500
|
+
BOOLEAN_FLAGS7,
|
|
12082
11501
|
/* @__PURE__ */ new Set(["with"])
|
|
12083
11502
|
);
|
|
12084
11503
|
const dir = positionals[0];
|
|
@@ -12088,24 +11507,24 @@ async function cmdPlan(argv) {
|
|
|
12088
11507
|
);
|
|
12089
11508
|
const r = createReporter(flags.json === true);
|
|
12090
11509
|
if (flags.fresh === true) {
|
|
12091
|
-
await rm5(
|
|
11510
|
+
await rm5(join22(dir, "doc.json"), { force: true });
|
|
12092
11511
|
r.log("note: --fresh discarded the existing doc.json");
|
|
12093
11512
|
}
|
|
12094
11513
|
let reuse;
|
|
12095
11514
|
if (flags.reuse === true) {
|
|
12096
|
-
const from =
|
|
12097
|
-
if (!
|
|
11515
|
+
const from = resolve10(strFlag(flags, "from") ?? join22(dir, PREV_DOC_NAME));
|
|
11516
|
+
if (!existsSync19(from)) {
|
|
12098
11517
|
throw new UsageError(
|
|
12099
11518
|
`--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
|
|
12100
11519
|
);
|
|
12101
11520
|
}
|
|
12102
11521
|
reuse = {
|
|
12103
11522
|
from,
|
|
12104
|
-
doc: JSON.parse(await
|
|
11523
|
+
doc: JSON.parse(await readFile16(from, "utf8"))
|
|
12105
11524
|
};
|
|
12106
11525
|
}
|
|
12107
11526
|
const style = await resolveStyleRef(flags);
|
|
12108
|
-
const hasDoc =
|
|
11527
|
+
const hasDoc = existsSync19(join22(dir, "doc.json"));
|
|
12109
11528
|
const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
|
|
12110
11529
|
const release = await releaseInputs(dir, flags, r, multi.with ?? []);
|
|
12111
11530
|
const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
|
|
@@ -12151,7 +11570,7 @@ async function cmdPlan(argv) {
|
|
|
12151
11570
|
${s.reuse.flagged.join("\n ")}` : "") : "";
|
|
12152
11571
|
r.done(
|
|
12153
11572
|
{
|
|
12154
|
-
take:
|
|
11573
|
+
take: resolve10(dir),
|
|
12155
11574
|
fresh: s.fresh,
|
|
12156
11575
|
cursorKept: s.cursorKept,
|
|
12157
11576
|
zoomAuto: s.zoomAuto,
|
|
@@ -12163,7 +11582,7 @@ async function cmdPlan(argv) {
|
|
|
12163
11582
|
...s.motion ? { motion: s.motion } : {},
|
|
12164
11583
|
...s.reuse ? { reuse: s.reuse } : {}
|
|
12165
11584
|
},
|
|
12166
|
-
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${
|
|
11585
|
+
`${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join22(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
|
|
12167
11586
|
);
|
|
12168
11587
|
return EXIT_OK;
|
|
12169
11588
|
}
|
|
@@ -12276,7 +11695,7 @@ async function releaseInputs(dir, flags, r, withRefs = []) {
|
|
|
12276
11695
|
async function cmdRender(argv) {
|
|
12277
11696
|
const { positionals, flags, multi } = parseArgs(
|
|
12278
11697
|
argv,
|
|
12279
|
-
|
|
11698
|
+
BOOLEAN_FLAGS7,
|
|
12280
11699
|
MULTI_FLAGS2
|
|
12281
11700
|
);
|
|
12282
11701
|
const dir = positionals[0];
|
|
@@ -12289,7 +11708,7 @@ async function cmdRender(argv) {
|
|
|
12289
11708
|
if (fmtRaw !== "webm" && fmtRaw !== "mp4")
|
|
12290
11709
|
throw new UsageError("--format must be webm or mp4");
|
|
12291
11710
|
const format = fmtRaw;
|
|
12292
|
-
const out = positionals[1] ??
|
|
11711
|
+
const out = positionals[1] ?? join22(dir, `out.${format}`);
|
|
12293
11712
|
const parallel = numFlag(flags, "parallel", 1);
|
|
12294
11713
|
if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
|
|
12295
11714
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
@@ -12347,7 +11766,7 @@ async function cmdRender(argv) {
|
|
|
12347
11766
|
async function cmdFrames(argv) {
|
|
12348
11767
|
const { positionals, flags, multi } = parseArgs(
|
|
12349
11768
|
argv,
|
|
12350
|
-
|
|
11769
|
+
BOOLEAN_FLAGS7,
|
|
12351
11770
|
MULTI_FLAGS2
|
|
12352
11771
|
);
|
|
12353
11772
|
const dir = positionals[0];
|
|
@@ -12420,7 +11839,7 @@ async function cmdFrames(argv) {
|
|
|
12420
11839
|
async function cmdDeliver(argv) {
|
|
12421
11840
|
const { positionals, flags, multi } = parseArgs(
|
|
12422
11841
|
argv,
|
|
12423
|
-
|
|
11842
|
+
BOOLEAN_FLAGS7,
|
|
12424
11843
|
MULTI_FLAGS2
|
|
12425
11844
|
);
|
|
12426
11845
|
const dir = positionals[0];
|
|
@@ -12536,15 +11955,15 @@ async function resolveStyleRef(flags) {
|
|
|
12536
11955
|
}
|
|
12537
11956
|
async function resolveDocRef(ref, flags, what) {
|
|
12538
11957
|
const styleRef = ref;
|
|
12539
|
-
const file =
|
|
11958
|
+
const file = existsSync19(styleRef) ? resolve10(
|
|
12540
11959
|
styleRef,
|
|
12541
|
-
|
|
11960
|
+
existsSync19(join22(styleRef, "doc.json")) ? "doc.json" : ""
|
|
12542
11961
|
) : null;
|
|
12543
|
-
if (file &&
|
|
11962
|
+
if (file && existsSync19(file)) {
|
|
12544
11963
|
return {
|
|
12545
11964
|
from: file,
|
|
12546
11965
|
doc: migrateHostedDoc5(
|
|
12547
|
-
JSON.parse(await
|
|
11966
|
+
JSON.parse(await readFile16(file, "utf8"))
|
|
12548
11967
|
)
|
|
12549
11968
|
};
|
|
12550
11969
|
}
|
|
@@ -12586,7 +12005,7 @@ async function officialTemplate(origin, ref) {
|
|
|
12586
12005
|
}
|
|
12587
12006
|
}
|
|
12588
12007
|
async function cmdDigest(argv) {
|
|
12589
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12008
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
12590
12009
|
const dir = positionals[0];
|
|
12591
12010
|
if (!dir)
|
|
12592
12011
|
throw new UsageError(
|
|
@@ -12596,7 +12015,7 @@ async function cmdDigest(argv) {
|
|
|
12596
12015
|
const take = await loadTake(dir);
|
|
12597
12016
|
if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
|
|
12598
12017
|
const transcriptPath = strFlag(flags, "transcript");
|
|
12599
|
-
const transcript = transcriptPath ? parseTranscript(JSON.parse(await
|
|
12018
|
+
const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile16(transcriptPath, "utf8"))) : null;
|
|
12600
12019
|
const style = await resolveStyleRef(flags);
|
|
12601
12020
|
const noFrames = flags["no-frames"] === true;
|
|
12602
12021
|
let browser = null;
|
|
@@ -12627,7 +12046,7 @@ async function cmdDigest(argv) {
|
|
|
12627
12046
|
kinds,
|
|
12628
12047
|
frames: d.images.full,
|
|
12629
12048
|
crops: d.images.crop,
|
|
12630
|
-
sheet: d.images.sheet ?
|
|
12049
|
+
sheet: d.images.sheet ? join22(result.outDir, d.images.sheet) : null,
|
|
12631
12050
|
scenes: kinds.scene ?? 0,
|
|
12632
12051
|
sourceDuration: d.take.sourceDuration,
|
|
12633
12052
|
outputDuration: d.take.outputDuration,
|
|
@@ -12650,19 +12069,19 @@ async function cmdDigest(argv) {
|
|
|
12650
12069
|
}
|
|
12651
12070
|
}
|
|
12652
12071
|
async function cmdOpen(argv) {
|
|
12653
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12072
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
12654
12073
|
const dir = positionals[0];
|
|
12655
12074
|
if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
|
|
12656
12075
|
const r = createReporter(flags.json === true);
|
|
12657
12076
|
const take = await loadTake(dir);
|
|
12658
|
-
if (!
|
|
12077
|
+
if (!existsSync19(take.paths.recording)) {
|
|
12659
12078
|
throw new UsageError(`${dir} has no recording \u2014 re-run record`);
|
|
12660
12079
|
}
|
|
12661
12080
|
const studio = (strFlag(flags, "studio") ?? platformOrigin()).replace(
|
|
12662
12081
|
/\/+$/,
|
|
12663
12082
|
""
|
|
12664
12083
|
);
|
|
12665
|
-
const server = await startTakeServer(
|
|
12084
|
+
const server = await startTakeServer(resolve10(dir), {});
|
|
12666
12085
|
const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
|
|
12667
12086
|
r.event({ event: "open", server: server.base, url });
|
|
12668
12087
|
r.log(`take served at ${server.base}`);
|
|
@@ -12688,7 +12107,7 @@ async function cmdOpen(argv) {
|
|
|
12688
12107
|
return EXIT_OK;
|
|
12689
12108
|
}
|
|
12690
12109
|
async function cmdJudge(argv) {
|
|
12691
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12110
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
12692
12111
|
const target = positionals[0];
|
|
12693
12112
|
const against = strFlag(flags, "against");
|
|
12694
12113
|
if (!target || !against)
|
|
@@ -12696,12 +12115,12 @@ async function cmdJudge(argv) {
|
|
|
12696
12115
|
"vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
|
|
12697
12116
|
);
|
|
12698
12117
|
const r = createReporter(flags.json === true);
|
|
12699
|
-
const kitFile = target.endsWith("kit.json") ? target :
|
|
12700
|
-
if (!
|
|
12701
|
-
if (!
|
|
12118
|
+
const kitFile = target.endsWith("kit.json") ? target : join22(target, "kit.json");
|
|
12119
|
+
if (!existsSync19(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
|
|
12120
|
+
if (!existsSync19(against))
|
|
12702
12121
|
throw new UsageError(`${against}: no reference manifest`);
|
|
12703
12122
|
const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
|
|
12704
|
-
const verdicts = JSON.parse(await
|
|
12123
|
+
const verdicts = JSON.parse(await readFile16(result.verdictFile, "utf8")).verdicts;
|
|
12705
12124
|
const rate = winRate(verdicts);
|
|
12706
12125
|
const lines = result.sheets.map(
|
|
12707
12126
|
(s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
|
|
@@ -12717,7 +12136,7 @@ Win rate so far: ${rate.wins} win(s), ${rate.ties} tie(s) of ${rate.judged} judg
|
|
|
12717
12136
|
return EXIT_OK;
|
|
12718
12137
|
}
|
|
12719
12138
|
async function cmdCallout(argv) {
|
|
12720
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12139
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
12721
12140
|
const dir = positionals[0];
|
|
12722
12141
|
const shape = positionals[1];
|
|
12723
12142
|
if (!dir || !shape || !isCalloutShape(shape))
|
|
@@ -12741,7 +12160,7 @@ async function cmdCallout(argv) {
|
|
|
12741
12160
|
`no register: put a BRAND.md beside the take (vos brand <url> --out BRAND.md; its bgA and accent are the ground and the accent), or pass --ground #hex --accent #hex`
|
|
12742
12161
|
);
|
|
12743
12162
|
const codeFile = strFlag(flags, "code-file");
|
|
12744
|
-
const code = codeFile ? await
|
|
12163
|
+
const code = codeFile ? await readFile16(codeFile, "utf8") : strFlag(flags, "code");
|
|
12745
12164
|
const words = {
|
|
12746
12165
|
kicker: strFlag(flags, "kicker"),
|
|
12747
12166
|
title: strFlag(flags, "title"),
|
|
@@ -12816,11 +12235,11 @@ async function cmdCallout(argv) {
|
|
|
12816
12235
|
return EXIT_OK;
|
|
12817
12236
|
}
|
|
12818
12237
|
async function cmdValidate(argv) {
|
|
12819
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12238
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
12820
12239
|
const target = positionals[0];
|
|
12821
12240
|
if (!target) throw new UsageError("vos validate <actions.json|take>");
|
|
12822
12241
|
const r = createReporter(flags.json === true);
|
|
12823
|
-
const kitTarget = target.endsWith("kit.json") ? target :
|
|
12242
|
+
const kitTarget = target.endsWith("kit.json") ? target : existsSync19(join22(target, "kit.json")) && !isTakeDir(target) ? join22(target, "kit.json") : null;
|
|
12824
12243
|
if (kitTarget) {
|
|
12825
12244
|
const picture = flags.picture === true;
|
|
12826
12245
|
const verdict = await validateKit(kitTarget, { picture });
|
|
@@ -12852,9 +12271,9 @@ async function cmdValidate(argv) {
|
|
|
12852
12271
|
r.done({ valid: true, target }, `${target}: valid actions file`);
|
|
12853
12272
|
return EXIT_OK;
|
|
12854
12273
|
}
|
|
12855
|
-
if (!isTakeDir(target) &&
|
|
12274
|
+
if (!isTakeDir(target) && existsSync19(join22(target, "config.json"))) {
|
|
12856
12275
|
const parsed = JSON.parse(
|
|
12857
|
-
await
|
|
12276
|
+
await readFile16(join22(target, "config.json"), "utf8")
|
|
12858
12277
|
);
|
|
12859
12278
|
const pre = preflightConfig(parsed);
|
|
12860
12279
|
const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
|
|
@@ -12895,7 +12314,7 @@ async function cmdValidate(argv) {
|
|
|
12895
12314
|
const take = await loadTake(target);
|
|
12896
12315
|
const problems = [];
|
|
12897
12316
|
const warnings = [];
|
|
12898
|
-
if (!
|
|
12317
|
+
if (!existsSync19(take.paths.recording)) {
|
|
12899
12318
|
problems.push("missing recording (re-run record)");
|
|
12900
12319
|
} else {
|
|
12901
12320
|
const mismatch = containerMismatch(
|
|
@@ -12978,7 +12397,7 @@ async function cmdValidate(argv) {
|
|
|
12978
12397
|
async function cmdPush3(argv) {
|
|
12979
12398
|
const { positionals, flags, multi } = parseArgs(
|
|
12980
12399
|
argv,
|
|
12981
|
-
|
|
12400
|
+
BOOLEAN_FLAGS7,
|
|
12982
12401
|
MULTI_FLAGS2
|
|
12983
12402
|
);
|
|
12984
12403
|
const dir = positionals[0];
|
|
@@ -12988,7 +12407,7 @@ async function cmdPush3(argv) {
|
|
|
12988
12407
|
);
|
|
12989
12408
|
const refusal = takePushRefusal({
|
|
12990
12409
|
vos: strFlag(flags, "vos"),
|
|
12991
|
-
trackedVosId: readSyncState(
|
|
12410
|
+
trackedVosId: readSyncState(resolve10(dir))?.vosId ?? null,
|
|
12992
12411
|
programOnly: ["slug", "desc", "tags", "base", "remix-of"].filter(
|
|
12993
12412
|
(name) => hasFlag(flags, name)
|
|
12994
12413
|
)
|
|
@@ -12997,7 +12416,7 @@ async function cmdPush3(argv) {
|
|
|
12997
12416
|
const r = createReporter(flags.json === true);
|
|
12998
12417
|
const overrides = multi.override;
|
|
12999
12418
|
const result = await pushTake(
|
|
13000
|
-
|
|
12419
|
+
resolve10(dir),
|
|
13001
12420
|
{
|
|
13002
12421
|
key: strFlag(flags, "key"),
|
|
13003
12422
|
api: strFlag(flags, "api"),
|
|
@@ -13025,11 +12444,11 @@ async function cmdPush3(argv) {
|
|
|
13025
12444
|
return EXIT_OK;
|
|
13026
12445
|
}
|
|
13027
12446
|
async function cmdPull2(argv) {
|
|
13028
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12447
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7, MULTI_FLAGS2);
|
|
13029
12448
|
const dir = positionals[0] ?? ".";
|
|
13030
12449
|
const r = createReporter(flags.json === true);
|
|
13031
12450
|
const result = await pullTake(
|
|
13032
|
-
|
|
12451
|
+
resolve10(dir),
|
|
13033
12452
|
{
|
|
13034
12453
|
key: strFlag(flags, "key"),
|
|
13035
12454
|
api: strFlag(flags, "api"),
|
|
@@ -13148,6 +12567,8 @@ async function run(argv) {
|
|
|
13148
12567
|
return await cmdSession(rest);
|
|
13149
12568
|
case "actions":
|
|
13150
12569
|
return await cmdActions(rest);
|
|
12570
|
+
case "ingest":
|
|
12571
|
+
return await cmdIngest(rest);
|
|
13151
12572
|
default:
|
|
13152
12573
|
throw new UsageError(`unknown command "${cmd}" \u2014 run: vos help`);
|
|
13153
12574
|
}
|
|
@@ -13180,7 +12601,7 @@ async function run(argv) {
|
|
|
13180
12601
|
}
|
|
13181
12602
|
}
|
|
13182
12603
|
async function cmdSession(argv) {
|
|
13183
|
-
const { positionals, flags } = parseArgs(argv,
|
|
12604
|
+
const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS7);
|
|
13184
12605
|
const [sub, arg] = positionals;
|
|
13185
12606
|
const r = createReporter(flags.json === true);
|
|
13186
12607
|
const usage = "vos session open <url> --name <app> | check <name> [--url <url>] | list | rm <name>";
|
|
@@ -13216,7 +12637,7 @@ async function cmdSession(argv) {
|
|
|
13216
12637
|
if (!arg) throw new UsageError(usage);
|
|
13217
12638
|
const bad = sessionNameVerdict(arg);
|
|
13218
12639
|
if (bad) throw new UsageError(bad);
|
|
13219
|
-
if (!
|
|
12640
|
+
if (!existsSync19(sessionDir(arg)))
|
|
13220
12641
|
throw new UsageError(`no session "${arg}" (vos session list)`);
|
|
13221
12642
|
const summary2 = await summarizeSession(arg);
|
|
13222
12643
|
const url = strFlag(flags, "url");
|
|
@@ -13323,7 +12744,8 @@ export {
|
|
|
13323
12744
|
MULTI_FLAGS2 as MULTI_FLAGS,
|
|
13324
12745
|
HELP,
|
|
13325
12746
|
takeBrowserArgs,
|
|
12747
|
+
takeBackdrop,
|
|
13326
12748
|
verbHelp,
|
|
13327
12749
|
run
|
|
13328
12750
|
};
|
|
13329
|
-
//# sourceMappingURL=chunk-
|
|
12751
|
+
//# sourceMappingURL=chunk-SE4F3OMQ.js.map
|