@runalabs/rill-cli 0.1.13 → 0.1.15
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 +42 -9
- package/dist/cli.js +1081 -392
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,23 +5,109 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// src/windows-credentials.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { createHash, randomUUID } from "crypto";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join, win32 } from "path";
|
|
13
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
14
|
+
var script = String.raw`$ErrorActionPreference='Stop'; try {
|
|
15
|
+
Add-Type -AssemblyName System.Security
|
|
16
|
+
$inputData=[Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
17
|
+
$bytes=[Convert]::FromBase64String($inputData.data)
|
|
18
|
+
$entropy=[Convert]::FromBase64String($inputData.entropy)
|
|
19
|
+
if ($inputData.operation -eq 'protect') {
|
|
20
|
+
$result=[System.Security.Cryptography.ProtectedData]::Protect($bytes,$entropy,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
21
|
+
} elseif ($inputData.operation -eq 'unprotect') {
|
|
22
|
+
$result=[System.Security.Cryptography.ProtectedData]::Unprotect($bytes,$entropy,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
23
|
+
} else { throw 'Invalid operation' }
|
|
24
|
+
[Console]::Out.Write([Convert]::ToBase64String($result))
|
|
25
|
+
} catch { [Console]::Error.Write('Windows credential protection failed.'); exit 1 }`;
|
|
26
|
+
var transformWindowsSecret = async (operation, data, entropy) => {
|
|
27
|
+
if (process.platform !== "win32") throw new Error("Windows credential protection requires Windows.");
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const executable = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
30
|
+
const child = spawn(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
31
|
+
let output = "";
|
|
32
|
+
let failed = false;
|
|
33
|
+
const fail2 = () => {
|
|
34
|
+
if (failed) return;
|
|
35
|
+
failed = true;
|
|
36
|
+
child.kill();
|
|
37
|
+
reject(new Error("Cannot access Windows protected credentials. Your saved identity has not been replaced."));
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(fail2, 15e3);
|
|
40
|
+
child.stdout.on("data", (chunk) => {
|
|
41
|
+
output += chunk.toString("ascii");
|
|
42
|
+
if (output.length > 256 * 1024) fail2();
|
|
43
|
+
});
|
|
44
|
+
child.stderr.resume();
|
|
45
|
+
child.once("error", fail2);
|
|
46
|
+
child.stdin.once("error", fail2);
|
|
47
|
+
child.once("close", (code) => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
if (failed) return;
|
|
50
|
+
if (code !== 0 || !output || !/^[A-Za-z0-9+/]+={0,2}$/.test(output)) {
|
|
51
|
+
fail2();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
resolve(Buffer.from(output, "base64"));
|
|
55
|
+
});
|
|
56
|
+
child.stdin.end(JSON.stringify({ operation, data: data.toString("base64"), entropy: entropy.toString("base64") }));
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
var credentialRoot = () => join(process.env.RILL_HOME ?? join(homedir(), ".rill"), "windows-credentials");
|
|
60
|
+
var key = (apiUrl) => createHash("sha256").update(apiUrl).digest();
|
|
61
|
+
async function saveWindowsCredential(apiUrl, value, root = credentialRoot(), transform2 = transformWindowsSecret) {
|
|
62
|
+
if (!value || Buffer.byteLength(value) > 64 * 1024) throw new Error("The Windows credential is empty or too large.");
|
|
63
|
+
const entropy = key(apiUrl), protectedBytes = await transform2("protect", Buffer.from(value, "utf8"), entropy);
|
|
64
|
+
if (!protectedBytes.length || protectedBytes.length > 192 * 1024) throw new Error("Windows credential protection returned invalid data.");
|
|
65
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
66
|
+
const path = join(root, entropy.toString("hex") + ".dpapi"), temporary = path + "." + randomUUID() + ".tmp";
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(temporary, JSON.stringify({ version: 1, ciphertext: protectedBytes.toString("base64") }), { mode: 384, flag: "wx" });
|
|
69
|
+
await rename(temporary, path);
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(temporary, { force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function loadWindowsCredential(apiUrl, root = credentialRoot(), transform2 = transformWindowsSecret) {
|
|
75
|
+
const entropy = key(apiUrl), path = join(root, entropy.toString("hex") + ".dpapi");
|
|
76
|
+
try {
|
|
77
|
+
const info = await stat(path);
|
|
78
|
+
if (!info.isFile() || info.size > 256 * 1024) throw new Error("Invalid credential file.");
|
|
79
|
+
const stored = JSON.parse(await readFile(path, "utf8"));
|
|
80
|
+
if (stored.version !== 1 || typeof stored.ciphertext !== "string" || !stored.ciphertext || !/^[A-Za-z0-9+/]+={0,2}$/.test(stored.ciphertext)) throw new Error("Invalid protected credential.");
|
|
81
|
+
const plain = await transform2("unprotect", Buffer.from(stored.ciphertext, "base64"), entropy);
|
|
82
|
+
if (!plain.length || plain.length > 64 * 1024) throw new Error("Invalid credential size.");
|
|
83
|
+
return plain.toString("utf8");
|
|
84
|
+
} catch (error51) {
|
|
85
|
+
if (error51.code === "ENOENT") return null;
|
|
86
|
+
throw new Error("Cannot read Windows protected credentials. Your saved identity has not been replaced.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
8
90
|
// src/agent-session.ts
|
|
9
|
-
import { hostname, homedir } from "os";
|
|
10
|
-
import { mkdir, rm, stat } from "fs/promises";
|
|
11
|
-
import { join } from "path";
|
|
12
|
-
import { createHash, randomBytes } from "crypto";
|
|
91
|
+
import { hostname, homedir as homedir2 } from "os";
|
|
92
|
+
import { mkdir as mkdir2, rm as rm2, stat as stat2 } from "fs/promises";
|
|
93
|
+
import { join as join2 } from "path";
|
|
94
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
13
95
|
|
|
14
96
|
// src/recording-guidance.ts
|
|
15
|
-
function
|
|
16
|
-
const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
97
|
+
function cliCommandForPlatform(platform, apiUrl, ...args) {
|
|
98
|
+
const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
17
99
|
return ["rill", "--api-url", apiUrl, ...args].map(quote).join(" ");
|
|
18
100
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
101
|
+
function cliCommand(apiUrl, ...args) {
|
|
102
|
+
return cliCommandForPlatform(process.platform, apiUrl, ...args);
|
|
103
|
+
}
|
|
104
|
+
var transportRecovery = "Unset RILL_SOCKET on Windows so Rill can use its authenticated named pipe. Use RILL_HOME to choose a separate recorder identity directory.";
|
|
105
|
+
function recorderTransportCheck(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
|
|
106
|
+
if (platform === "win32" && socketOverride) return { name: "recorder_transport", status: "fail", message: "Windows requires the authenticated recorder pipe.", recovery: transportRecovery };
|
|
107
|
+
return { name: "recorder_transport", status: "pass", message: platform === "win32" ? "The local recorder uses an authenticated Windows named pipe." : "The local recorder uses a Unix-domain socket." };
|
|
22
108
|
}
|
|
23
|
-
function assertRecorderTransport(platform = process.platform) {
|
|
24
|
-
const check2 = recorderTransportCheck(platform);
|
|
109
|
+
function assertRecorderTransport(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
|
|
110
|
+
const check2 = recorderTransportCheck(platform, socketOverride);
|
|
25
111
|
if (check2.status === "fail") throw Object.assign(new Error(`unsupported_platform: ${check2.message}`), { code: "unsupported_platform", recovery: check2.recovery });
|
|
26
112
|
}
|
|
27
113
|
function recordingNextStep(apiUrl, recordingId, status, options = {}) {
|
|
@@ -30,12 +116,13 @@ function recordingNextStep(apiUrl, recordingId, status, options = {}) {
|
|
|
30
116
|
if (["uploading", "queued", "encoding", "processing", "finalizing"].includes(status)) return { nextCommand: cliCommand(apiUrl, "record", "status", recordingId), message: "Finalizing recordings hold a concurrency slot. Wait for ready; cancel only if abandoning the recording." };
|
|
31
117
|
return {};
|
|
32
118
|
}
|
|
33
|
-
function browserAttachment(cdpUrl) {
|
|
119
|
+
function browserAttachment(cdpUrl, tabId) {
|
|
34
120
|
return {
|
|
35
121
|
protocol: "cdp",
|
|
36
122
|
cdpUrl,
|
|
123
|
+
...tabId ? { tabId } : {},
|
|
37
124
|
playwrightMethod: "chromium.connectOverCDP",
|
|
38
|
-
instructions: "Attach to this endpoint and use the existing browser context. A browser tool must explicitly support CDP attachment; do not drive a separate browser. Disconnect the automation client without closing the recorded browser, then run record stop.",
|
|
125
|
+
instructions: (tabId ? "Record only the returned tabId. Match it using a page CDP session and Target.getTargetInfo; do not pick the first page. " : "") + "Attach to this endpoint and use the existing browser context. A browser tool must explicitly support CDP attachment; do not drive a separate browser. Disconnect the automation client without closing the recorded browser, then run record stop.",
|
|
39
126
|
shellFallback: "See the shell-only Playwright example in the Rill CLI README; install playwright-core in a temporary directory outside the project."
|
|
40
127
|
};
|
|
41
128
|
}
|
|
@@ -48,6 +135,7 @@ var AgentAuthError = class extends Error {
|
|
|
48
135
|
super(`${code}: ${message}`);
|
|
49
136
|
this.code = code;
|
|
50
137
|
}
|
|
138
|
+
code;
|
|
51
139
|
};
|
|
52
140
|
function parseAuthorization(value) {
|
|
53
141
|
if (!value?.startsWith("{")) return null;
|
|
@@ -63,7 +151,7 @@ async function request(apiUrl, path, body, form = false) {
|
|
|
63
151
|
redirect: "error",
|
|
64
152
|
signal: AbortSignal.timeout(15e3),
|
|
65
153
|
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
66
|
-
body: form ? new URLSearchParams(Object.entries(body).map(([
|
|
154
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
|
|
67
155
|
});
|
|
68
156
|
const value = await response.json();
|
|
69
157
|
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
@@ -107,19 +195,19 @@ async function ensureAnonymousCredential(apiUrl) {
|
|
|
107
195
|
return withAuthorizationLock(apiUrl, () => createAnonymousCredential(apiUrl));
|
|
108
196
|
}
|
|
109
197
|
async function withAuthorizationLock(apiUrl, action) {
|
|
110
|
-
const root =
|
|
111
|
-
await
|
|
112
|
-
const lock =
|
|
198
|
+
const root = join2(homedir2(), ".rill", "auth-locks");
|
|
199
|
+
await mkdir2(root, { recursive: true, mode: 448 });
|
|
200
|
+
const lock = join2(root, createHash2("sha256").update(apiUrl).digest("hex"));
|
|
113
201
|
const deadline = Date.now() + 3e4;
|
|
114
202
|
while (true) {
|
|
115
203
|
try {
|
|
116
|
-
await
|
|
204
|
+
await mkdir2(lock, { mode: 448 });
|
|
117
205
|
break;
|
|
118
206
|
} catch (error51) {
|
|
119
207
|
if (error51.code !== "EEXIST") throw error51;
|
|
120
|
-
const info = await
|
|
208
|
+
const info = await stat2(lock).catch(() => null);
|
|
121
209
|
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
122
|
-
await
|
|
210
|
+
await rm2(lock, { recursive: true, force: true });
|
|
123
211
|
continue;
|
|
124
212
|
}
|
|
125
213
|
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
@@ -129,7 +217,7 @@ async function withAuthorizationLock(apiUrl, action) {
|
|
|
129
217
|
try {
|
|
130
218
|
return await action();
|
|
131
219
|
} finally {
|
|
132
|
-
await
|
|
220
|
+
await rm2(lock, { recursive: true, force: true });
|
|
133
221
|
}
|
|
134
222
|
}
|
|
135
223
|
async function createAnonymousCredential(apiUrl) {
|
|
@@ -246,9 +334,10 @@ async function pollSavedConnection(apiUrl, authorizationId) {
|
|
|
246
334
|
}
|
|
247
335
|
|
|
248
336
|
// src/keychain.ts
|
|
249
|
-
import { spawn } from "child_process";
|
|
337
|
+
import { spawn as spawn2 } from "child_process";
|
|
250
338
|
var service = "rill";
|
|
251
339
|
async function saveCredential(apiUrl, token) {
|
|
340
|
+
if (process.platform === "win32") return saveWindowsCredential(apiUrl, token);
|
|
252
341
|
if (process.platform === "darwin") {
|
|
253
342
|
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
254
343
|
return;
|
|
@@ -263,6 +352,7 @@ function loadCredential(apiUrl) {
|
|
|
263
352
|
return loadAgentCredential(apiUrl);
|
|
264
353
|
}
|
|
265
354
|
async function loadStoredCredential(apiUrl) {
|
|
355
|
+
if (process.platform === "win32") return loadWindowsCredential(apiUrl);
|
|
266
356
|
try {
|
|
267
357
|
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
268
358
|
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
@@ -276,7 +366,7 @@ async function loadStoredCredential(apiUrl) {
|
|
|
276
366
|
}
|
|
277
367
|
function run(command, args, input) {
|
|
278
368
|
return new Promise((resolve, reject) => {
|
|
279
|
-
const child =
|
|
369
|
+
const child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
280
370
|
const stdout = [];
|
|
281
371
|
const stderr = [];
|
|
282
372
|
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
@@ -290,7 +380,7 @@ function run(command, args, input) {
|
|
|
290
380
|
|
|
291
381
|
// src/control-plane.ts
|
|
292
382
|
import { createReadStream } from "fs";
|
|
293
|
-
import { readFile, stat as
|
|
383
|
+
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
294
384
|
import { Upload } from "tus-js-client";
|
|
295
385
|
var ControlPlaneError = class extends Error {
|
|
296
386
|
constructor(code, message, recovery, details, httpStatus) {
|
|
@@ -301,6 +391,10 @@ var ControlPlaneError = class extends Error {
|
|
|
301
391
|
this.httpStatus = httpStatus;
|
|
302
392
|
this.name = "ControlPlaneError";
|
|
303
393
|
}
|
|
394
|
+
code;
|
|
395
|
+
recovery;
|
|
396
|
+
details;
|
|
397
|
+
httpStatus;
|
|
304
398
|
};
|
|
305
399
|
var ControlPlaneHttpError = class extends Error {
|
|
306
400
|
constructor(httpStatus) {
|
|
@@ -308,12 +402,15 @@ var ControlPlaneHttpError = class extends Error {
|
|
|
308
402
|
this.httpStatus = httpStatus;
|
|
309
403
|
this.name = "ControlPlaneHttpError";
|
|
310
404
|
}
|
|
405
|
+
httpStatus;
|
|
311
406
|
};
|
|
312
407
|
var ControlPlaneClient = class {
|
|
313
408
|
constructor(apiUrl, token) {
|
|
314
409
|
this.apiUrl = apiUrl;
|
|
315
410
|
this.token = token;
|
|
316
411
|
}
|
|
412
|
+
apiUrl;
|
|
413
|
+
token;
|
|
317
414
|
recordingStatusCommand(id) {
|
|
318
415
|
return cliCommand(this.apiUrl, "record", "status", id);
|
|
319
416
|
}
|
|
@@ -356,6 +453,9 @@ var ControlPlaneClient = class {
|
|
|
356
453
|
createRecording(input) {
|
|
357
454
|
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
358
455
|
}
|
|
456
|
+
failLocalCapture(recordingId) {
|
|
457
|
+
return this.request(`/api/recordings/${encodeURIComponent(recordingId)}/local-failed`, { method: "POST", body: "{}" });
|
|
458
|
+
}
|
|
359
459
|
markRecordingStarted(recordingId, input) {
|
|
360
460
|
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
361
461
|
method: "POST",
|
|
@@ -402,7 +502,7 @@ var ControlPlaneClient = class {
|
|
|
402
502
|
throw new ControlPlaneError("processing_pending", "Upload complete; the recording is not playable yet.", `Run ${cliCommand(this.apiUrl, "record", "status", recordingId)} to continue checking.`, { recordingId, nextCommand: cliCommand(this.apiUrl, "record", "status", recordingId) });
|
|
403
503
|
}
|
|
404
504
|
async uploadVideo(uploadUrl, filePath) {
|
|
405
|
-
const file2 = await
|
|
505
|
+
const file2 = await stat3(filePath);
|
|
406
506
|
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
407
507
|
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
408
508
|
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
@@ -424,7 +524,7 @@ var ControlPlaneClient = class {
|
|
|
424
524
|
process.stderr.write("\nUpload complete; playback processing may continue.\n");
|
|
425
525
|
}
|
|
426
526
|
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
427
|
-
const body = await
|
|
527
|
+
const body = await readFile2(filePath);
|
|
428
528
|
return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
|
|
429
529
|
}
|
|
430
530
|
listRecordings() {
|
|
@@ -436,16 +536,33 @@ var ControlPlaneClient = class {
|
|
|
436
536
|
pollDeviceAuthorization(deviceCode) {
|
|
437
537
|
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
438
538
|
}
|
|
439
|
-
inspect(shareUrl, cursor) {
|
|
539
|
+
async inspect(shareUrl, cursor) {
|
|
440
540
|
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
441
541
|
const url2 = new URL(shareUrl);
|
|
442
542
|
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
443
543
|
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
444
|
-
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers:
|
|
544
|
+
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: await this.reviewHeaders(url2.origin) }).then(async (response) => {
|
|
445
545
|
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
446
546
|
return response.json();
|
|
447
547
|
});
|
|
448
548
|
}
|
|
549
|
+
async reviewHeaders(origin) {
|
|
550
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
551
|
+
if (origin !== new URL(this.apiUrl).origin) return headers;
|
|
552
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
553
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
554
|
+
this.identifyAgent(headers);
|
|
555
|
+
const id = process.env.RILL_AGENT_ID;
|
|
556
|
+
if (id && /^[a-zA-Z0-9_-]{8,80}$/.test(id)) headers.set("x-rill-agent-id", id);
|
|
557
|
+
if (id && process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1") headers.set("x-rill-analytics", "1");
|
|
558
|
+
return headers;
|
|
559
|
+
}
|
|
560
|
+
identifyAgent(headers) {
|
|
561
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.RILL_ANONYMOUS_ANALYTICS === "0") headers.set("x-rill-analytics", "0");
|
|
562
|
+
const client = process.env.RILL_AGENT_CLIENT || (process.env.CODEX_THREAD_ID ? "Codex" : process.env.CLAUDECODE ? "Claude" : process.env.CURSOR_TRACE_ID ? "Cursor" : void 0);
|
|
563
|
+
if (client) headers.set("x-rill-agent-client", client.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 80));
|
|
564
|
+
if (process.env.RILL_AGENT_NAME) headers.set("x-rill-agent-name", process.env.RILL_AGENT_NAME.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 48));
|
|
565
|
+
}
|
|
449
566
|
reviewPath(shareUrl) {
|
|
450
567
|
const url2 = new URL(shareUrl);
|
|
451
568
|
if (url2.origin !== new URL(this.apiUrl).origin) throw new Error("The share URL must match --api-url before sending agent credentials.");
|
|
@@ -461,6 +578,7 @@ var ControlPlaneClient = class {
|
|
|
461
578
|
}
|
|
462
579
|
async request(path, init = {}, authenticated = true) {
|
|
463
580
|
const headers = new Headers(init.headers);
|
|
581
|
+
if (authenticated) this.identifyAgent(headers);
|
|
464
582
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
465
583
|
if (authenticated) {
|
|
466
584
|
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
@@ -500,8 +618,8 @@ async function whoami(apiUrl) {
|
|
|
500
618
|
}
|
|
501
619
|
|
|
502
620
|
// src/remote-encoding.ts
|
|
503
|
-
import { open, readFile as
|
|
504
|
-
import { join as
|
|
621
|
+
import { open, readFile as readFile3 } from "fs/promises";
|
|
622
|
+
import { join as join3 } from "path";
|
|
505
623
|
|
|
506
624
|
// ../../node_modules/zod/v4/classic/external.js
|
|
507
625
|
var external_exports = {};
|
|
@@ -1225,9 +1343,9 @@ function floatSafeRemainder(val, step) {
|
|
|
1225
1343
|
return ratio - roundedRatio;
|
|
1226
1344
|
}
|
|
1227
1345
|
var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
|
|
1228
|
-
function defineLazy(object2,
|
|
1346
|
+
function defineLazy(object2, key2, getter) {
|
|
1229
1347
|
let value = void 0;
|
|
1230
|
-
Object.defineProperty(object2,
|
|
1348
|
+
Object.defineProperty(object2, key2, {
|
|
1231
1349
|
get() {
|
|
1232
1350
|
if (value === EVALUATING) {
|
|
1233
1351
|
return void 0;
|
|
@@ -1239,7 +1357,7 @@ function defineLazy(object2, key, getter) {
|
|
|
1239
1357
|
return value;
|
|
1240
1358
|
},
|
|
1241
1359
|
set(v) {
|
|
1242
|
-
Object.defineProperty(object2,
|
|
1360
|
+
Object.defineProperty(object2, key2, {
|
|
1243
1361
|
value: v
|
|
1244
1362
|
// configurable: true,
|
|
1245
1363
|
});
|
|
@@ -1272,11 +1390,11 @@ function cloneDef(schema) {
|
|
|
1272
1390
|
function getElementAtPath(obj, path) {
|
|
1273
1391
|
if (!path)
|
|
1274
1392
|
return obj;
|
|
1275
|
-
return path.reduce((acc,
|
|
1393
|
+
return path.reduce((acc, key2) => acc?.[key2], obj);
|
|
1276
1394
|
}
|
|
1277
1395
|
function promiseAllObject(promisesObj) {
|
|
1278
1396
|
const keys = Object.keys(promisesObj);
|
|
1279
|
-
const promises = keys.map((
|
|
1397
|
+
const promises = keys.map((key2) => promisesObj[key2]);
|
|
1280
1398
|
return Promise.all(promises).then((results) => {
|
|
1281
1399
|
const resolvedObj = {};
|
|
1282
1400
|
for (let i = 0; i < keys.length; i++) {
|
|
@@ -1348,8 +1466,8 @@ function shallowClone(o) {
|
|
|
1348
1466
|
}
|
|
1349
1467
|
function numKeys(data) {
|
|
1350
1468
|
let keyCount = 0;
|
|
1351
|
-
for (const
|
|
1352
|
-
if (Object.prototype.hasOwnProperty.call(data,
|
|
1469
|
+
for (const key2 in data) {
|
|
1470
|
+
if (Object.prototype.hasOwnProperty.call(data, key2)) {
|
|
1353
1471
|
keyCount++;
|
|
1354
1472
|
}
|
|
1355
1473
|
}
|
|
@@ -1499,13 +1617,13 @@ function pick(schema, mask) {
|
|
|
1499
1617
|
const def = mergeDefs(schema._zod.def, {
|
|
1500
1618
|
get shape() {
|
|
1501
1619
|
const newShape = {};
|
|
1502
|
-
for (const
|
|
1503
|
-
if (!(
|
|
1504
|
-
throw new Error(`Unrecognized key: "${
|
|
1620
|
+
for (const key2 in mask) {
|
|
1621
|
+
if (!(key2 in currDef.shape)) {
|
|
1622
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1505
1623
|
}
|
|
1506
|
-
if (!mask[
|
|
1624
|
+
if (!mask[key2])
|
|
1507
1625
|
continue;
|
|
1508
|
-
newShape[
|
|
1626
|
+
newShape[key2] = currDef.shape[key2];
|
|
1509
1627
|
}
|
|
1510
1628
|
assignProp(this, "shape", newShape);
|
|
1511
1629
|
return newShape;
|
|
@@ -1524,13 +1642,13 @@ function omit(schema, mask) {
|
|
|
1524
1642
|
const def = mergeDefs(schema._zod.def, {
|
|
1525
1643
|
get shape() {
|
|
1526
1644
|
const newShape = { ...schema._zod.def.shape };
|
|
1527
|
-
for (const
|
|
1528
|
-
if (!(
|
|
1529
|
-
throw new Error(`Unrecognized key: "${
|
|
1645
|
+
for (const key2 in mask) {
|
|
1646
|
+
if (!(key2 in currDef.shape)) {
|
|
1647
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1530
1648
|
}
|
|
1531
|
-
if (!mask[
|
|
1649
|
+
if (!mask[key2])
|
|
1532
1650
|
continue;
|
|
1533
|
-
delete newShape[
|
|
1651
|
+
delete newShape[key2];
|
|
1534
1652
|
}
|
|
1535
1653
|
assignProp(this, "shape", newShape);
|
|
1536
1654
|
return newShape;
|
|
@@ -1547,8 +1665,8 @@ function extend(schema, shape) {
|
|
|
1547
1665
|
const hasChecks = checks && checks.length > 0;
|
|
1548
1666
|
if (hasChecks) {
|
|
1549
1667
|
const existingShape = schema._zod.def.shape;
|
|
1550
|
-
for (const
|
|
1551
|
-
if (Object.getOwnPropertyDescriptor(existingShape,
|
|
1668
|
+
for (const key2 in shape) {
|
|
1669
|
+
if (Object.getOwnPropertyDescriptor(existingShape, key2) !== void 0) {
|
|
1552
1670
|
throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
|
|
1553
1671
|
}
|
|
1554
1672
|
}
|
|
@@ -1604,23 +1722,23 @@ function partial(Class2, schema, mask) {
|
|
|
1604
1722
|
const oldShape = schema._zod.def.shape;
|
|
1605
1723
|
const shape = { ...oldShape };
|
|
1606
1724
|
if (mask) {
|
|
1607
|
-
for (const
|
|
1608
|
-
if (!(
|
|
1609
|
-
throw new Error(`Unrecognized key: "${
|
|
1725
|
+
for (const key2 in mask) {
|
|
1726
|
+
if (!(key2 in oldShape)) {
|
|
1727
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1610
1728
|
}
|
|
1611
|
-
if (!mask[
|
|
1729
|
+
if (!mask[key2])
|
|
1612
1730
|
continue;
|
|
1613
|
-
shape[
|
|
1731
|
+
shape[key2] = Class2 ? new Class2({
|
|
1614
1732
|
type: "optional",
|
|
1615
|
-
innerType: oldShape[
|
|
1616
|
-
}) : oldShape[
|
|
1733
|
+
innerType: oldShape[key2]
|
|
1734
|
+
}) : oldShape[key2];
|
|
1617
1735
|
}
|
|
1618
1736
|
} else {
|
|
1619
|
-
for (const
|
|
1620
|
-
shape[
|
|
1737
|
+
for (const key2 in oldShape) {
|
|
1738
|
+
shape[key2] = Class2 ? new Class2({
|
|
1621
1739
|
type: "optional",
|
|
1622
|
-
innerType: oldShape[
|
|
1623
|
-
}) : oldShape[
|
|
1740
|
+
innerType: oldShape[key2]
|
|
1741
|
+
}) : oldShape[key2];
|
|
1624
1742
|
}
|
|
1625
1743
|
}
|
|
1626
1744
|
assignProp(this, "shape", shape);
|
|
@@ -1636,22 +1754,22 @@ function required(Class2, schema, mask) {
|
|
|
1636
1754
|
const oldShape = schema._zod.def.shape;
|
|
1637
1755
|
const shape = { ...oldShape };
|
|
1638
1756
|
if (mask) {
|
|
1639
|
-
for (const
|
|
1640
|
-
if (!(
|
|
1641
|
-
throw new Error(`Unrecognized key: "${
|
|
1757
|
+
for (const key2 in mask) {
|
|
1758
|
+
if (!(key2 in shape)) {
|
|
1759
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1642
1760
|
}
|
|
1643
|
-
if (!mask[
|
|
1761
|
+
if (!mask[key2])
|
|
1644
1762
|
continue;
|
|
1645
|
-
shape[
|
|
1763
|
+
shape[key2] = new Class2({
|
|
1646
1764
|
type: "nonoptional",
|
|
1647
|
-
innerType: oldShape[
|
|
1765
|
+
innerType: oldShape[key2]
|
|
1648
1766
|
});
|
|
1649
1767
|
}
|
|
1650
1768
|
} else {
|
|
1651
|
-
for (const
|
|
1652
|
-
shape[
|
|
1769
|
+
for (const key2 in oldShape) {
|
|
1770
|
+
shape[key2] = new Class2({
|
|
1653
1771
|
type: "nonoptional",
|
|
1654
|
-
innerType: oldShape[
|
|
1772
|
+
innerType: oldShape[key2]
|
|
1655
1773
|
});
|
|
1656
1774
|
}
|
|
1657
1775
|
}
|
|
@@ -3432,13 +3550,13 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
|
|
|
3432
3550
|
return payload;
|
|
3433
3551
|
};
|
|
3434
3552
|
});
|
|
3435
|
-
function handlePropertyResult(result, final,
|
|
3436
|
-
const isPresent =
|
|
3553
|
+
function handlePropertyResult(result, final, key2, input, isOptionalIn, isOptionalOut) {
|
|
3554
|
+
const isPresent = key2 in input;
|
|
3437
3555
|
if (result.issues.length) {
|
|
3438
3556
|
if (isOptionalIn && isOptionalOut && !isPresent) {
|
|
3439
3557
|
return;
|
|
3440
3558
|
}
|
|
3441
|
-
final.issues.push(...prefixIssues(
|
|
3559
|
+
final.issues.push(...prefixIssues(key2, result.issues));
|
|
3442
3560
|
}
|
|
3443
3561
|
if (!isPresent && !isOptionalIn) {
|
|
3444
3562
|
if (!result.issues.length) {
|
|
@@ -3446,17 +3564,17 @@ function handlePropertyResult(result, final, key, input, isOptionalIn, isOptiona
|
|
|
3446
3564
|
code: "invalid_type",
|
|
3447
3565
|
expected: "nonoptional",
|
|
3448
3566
|
input: void 0,
|
|
3449
|
-
path: [
|
|
3567
|
+
path: [key2]
|
|
3450
3568
|
});
|
|
3451
3569
|
}
|
|
3452
3570
|
return;
|
|
3453
3571
|
}
|
|
3454
3572
|
if (result.value === void 0) {
|
|
3455
3573
|
if (isPresent) {
|
|
3456
|
-
final.value[
|
|
3574
|
+
final.value[key2] = void 0;
|
|
3457
3575
|
}
|
|
3458
3576
|
} else {
|
|
3459
|
-
final.value[
|
|
3577
|
+
final.value[key2] = result.value;
|
|
3460
3578
|
}
|
|
3461
3579
|
}
|
|
3462
3580
|
function normalizeDef(def) {
|
|
@@ -3482,20 +3600,20 @@ function handleCatchall(proms, input, payload, ctx, def, inst) {
|
|
|
3482
3600
|
const t = _catchall.def.type;
|
|
3483
3601
|
const isOptionalIn = _catchall.optin === "optional";
|
|
3484
3602
|
const isOptionalOut = _catchall.optout === "optional";
|
|
3485
|
-
for (const
|
|
3486
|
-
if (
|
|
3603
|
+
for (const key2 in input) {
|
|
3604
|
+
if (key2 === "__proto__")
|
|
3487
3605
|
continue;
|
|
3488
|
-
if (keySet.has(
|
|
3606
|
+
if (keySet.has(key2))
|
|
3489
3607
|
continue;
|
|
3490
3608
|
if (t === "never") {
|
|
3491
|
-
unrecognized.push(
|
|
3609
|
+
unrecognized.push(key2);
|
|
3492
3610
|
continue;
|
|
3493
3611
|
}
|
|
3494
|
-
const r = _catchall.run({ value: input[
|
|
3612
|
+
const r = _catchall.run({ value: input[key2], issues: [] }, ctx);
|
|
3495
3613
|
if (r instanceof Promise) {
|
|
3496
|
-
proms.push(r.then((r2) => handlePropertyResult(r2, payload,
|
|
3614
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
|
|
3497
3615
|
} else {
|
|
3498
|
-
handlePropertyResult(r, payload,
|
|
3616
|
+
handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
|
|
3499
3617
|
}
|
|
3500
3618
|
}
|
|
3501
3619
|
if (unrecognized.length) {
|
|
@@ -3531,12 +3649,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3531
3649
|
defineLazy(inst._zod, "propValues", () => {
|
|
3532
3650
|
const shape = def.shape;
|
|
3533
3651
|
const propValues = {};
|
|
3534
|
-
for (const
|
|
3535
|
-
const field = shape[
|
|
3652
|
+
for (const key2 in shape) {
|
|
3653
|
+
const field = shape[key2]._zod;
|
|
3536
3654
|
if (field.values) {
|
|
3537
|
-
propValues[
|
|
3655
|
+
propValues[key2] ?? (propValues[key2] = /* @__PURE__ */ new Set());
|
|
3538
3656
|
for (const v of field.values)
|
|
3539
|
-
propValues[
|
|
3657
|
+
propValues[key2].add(v);
|
|
3540
3658
|
}
|
|
3541
3659
|
}
|
|
3542
3660
|
return propValues;
|
|
@@ -3559,15 +3677,15 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3559
3677
|
payload.value = {};
|
|
3560
3678
|
const proms = [];
|
|
3561
3679
|
const shape = value.shape;
|
|
3562
|
-
for (const
|
|
3563
|
-
const el = shape[
|
|
3680
|
+
for (const key2 of value.keys) {
|
|
3681
|
+
const el = shape[key2];
|
|
3564
3682
|
const isOptionalIn = el._zod.optin === "optional";
|
|
3565
3683
|
const isOptionalOut = el._zod.optout === "optional";
|
|
3566
|
-
const r = el._zod.run({ value: input[
|
|
3684
|
+
const r = el._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
3567
3685
|
if (r instanceof Promise) {
|
|
3568
|
-
proms.push(r.then((r2) => handlePropertyResult(r2, payload,
|
|
3686
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
|
|
3569
3687
|
} else {
|
|
3570
|
-
handlePropertyResult(r, payload,
|
|
3688
|
+
handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
|
|
3571
3689
|
}
|
|
3572
3690
|
}
|
|
3573
3691
|
if (!catchall) {
|
|
@@ -3583,24 +3701,24 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
3583
3701
|
const generateFastpass = (shape) => {
|
|
3584
3702
|
const doc = new Doc(["shape", "payload", "ctx"]);
|
|
3585
3703
|
const normalized = _normalized.value;
|
|
3586
|
-
const parseStr = (
|
|
3587
|
-
const k = esc(
|
|
3704
|
+
const parseStr = (key2) => {
|
|
3705
|
+
const k = esc(key2);
|
|
3588
3706
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
3589
3707
|
};
|
|
3590
3708
|
doc.write(`const input = payload.value;`);
|
|
3591
3709
|
const ids = /* @__PURE__ */ Object.create(null);
|
|
3592
3710
|
let counter = 0;
|
|
3593
|
-
for (const
|
|
3594
|
-
ids[
|
|
3711
|
+
for (const key2 of normalized.keys) {
|
|
3712
|
+
ids[key2] = `key_${counter++}`;
|
|
3595
3713
|
}
|
|
3596
3714
|
doc.write(`const newResult = {};`);
|
|
3597
|
-
for (const
|
|
3598
|
-
const id = ids[
|
|
3599
|
-
const k = esc(
|
|
3600
|
-
const schema = shape[
|
|
3715
|
+
for (const key2 of normalized.keys) {
|
|
3716
|
+
const id = ids[key2];
|
|
3717
|
+
const k = esc(key2);
|
|
3718
|
+
const schema = shape[key2];
|
|
3601
3719
|
const isOptionalIn = schema?._zod?.optin === "optional";
|
|
3602
3720
|
const isOptionalOut = schema?._zod?.optout === "optional";
|
|
3603
|
-
doc.write(`const ${id} = ${parseStr(
|
|
3721
|
+
doc.write(`const ${id} = ${parseStr(key2)};`);
|
|
3604
3722
|
if (isOptionalIn && isOptionalOut) {
|
|
3605
3723
|
doc.write(`
|
|
3606
3724
|
if (${id}.issues.length) {
|
|
@@ -3912,17 +4030,17 @@ function mergeValues(a, b) {
|
|
|
3912
4030
|
}
|
|
3913
4031
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
3914
4032
|
const bKeys = Object.keys(b);
|
|
3915
|
-
const sharedKeys = Object.keys(a).filter((
|
|
4033
|
+
const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
|
|
3916
4034
|
const newObj = { ...a, ...b };
|
|
3917
|
-
for (const
|
|
3918
|
-
const sharedValue = mergeValues(a[
|
|
4035
|
+
for (const key2 of sharedKeys) {
|
|
4036
|
+
const sharedValue = mergeValues(a[key2], b[key2]);
|
|
3919
4037
|
if (!sharedValue.valid) {
|
|
3920
4038
|
return {
|
|
3921
4039
|
valid: false,
|
|
3922
|
-
mergeErrorPath: [
|
|
4040
|
+
mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
|
|
3923
4041
|
};
|
|
3924
4042
|
}
|
|
3925
|
-
newObj[
|
|
4043
|
+
newObj[key2] = sharedValue.data;
|
|
3926
4044
|
}
|
|
3927
4045
|
return { valid: true, data: newObj };
|
|
3928
4046
|
}
|
|
@@ -4057,9 +4175,9 @@ var $ZodTuple = /* @__PURE__ */ $constructor("$ZodTuple", (inst, def) => {
|
|
|
4057
4175
|
return handleTupleResults(itemResults, payload, items, input, optoutStart);
|
|
4058
4176
|
};
|
|
4059
4177
|
});
|
|
4060
|
-
function getTupleOptStart(items,
|
|
4178
|
+
function getTupleOptStart(items, key2) {
|
|
4061
4179
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
4062
|
-
if (items[i]._zod[
|
|
4180
|
+
if (items[i]._zod[key2] !== "optional")
|
|
4063
4181
|
return i + 1;
|
|
4064
4182
|
}
|
|
4065
4183
|
return 0;
|
|
@@ -4110,10 +4228,10 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4110
4228
|
if (values) {
|
|
4111
4229
|
payload.value = {};
|
|
4112
4230
|
const recordKeys = /* @__PURE__ */ new Set();
|
|
4113
|
-
for (const
|
|
4114
|
-
if (typeof
|
|
4115
|
-
recordKeys.add(typeof
|
|
4116
|
-
const keyResult = def.keyType._zod.run({ value:
|
|
4231
|
+
for (const key2 of values) {
|
|
4232
|
+
if (typeof key2 === "string" || typeof key2 === "number" || typeof key2 === "symbol") {
|
|
4233
|
+
recordKeys.add(typeof key2 === "number" ? key2.toString() : key2);
|
|
4234
|
+
const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4117
4235
|
if (keyResult instanceof Promise) {
|
|
4118
4236
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4119
4237
|
}
|
|
@@ -4122,34 +4240,34 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4122
4240
|
code: "invalid_key",
|
|
4123
4241
|
origin: "record",
|
|
4124
4242
|
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4125
|
-
input:
|
|
4126
|
-
path: [
|
|
4243
|
+
input: key2,
|
|
4244
|
+
path: [key2],
|
|
4127
4245
|
inst
|
|
4128
4246
|
});
|
|
4129
4247
|
continue;
|
|
4130
4248
|
}
|
|
4131
4249
|
const outKey = keyResult.value;
|
|
4132
|
-
const result = def.valueType._zod.run({ value: input[
|
|
4250
|
+
const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
4133
4251
|
if (result instanceof Promise) {
|
|
4134
4252
|
proms.push(result.then((result2) => {
|
|
4135
4253
|
if (result2.issues.length) {
|
|
4136
|
-
payload.issues.push(...prefixIssues(
|
|
4254
|
+
payload.issues.push(...prefixIssues(key2, result2.issues));
|
|
4137
4255
|
}
|
|
4138
4256
|
payload.value[outKey] = result2.value;
|
|
4139
4257
|
}));
|
|
4140
4258
|
} else {
|
|
4141
4259
|
if (result.issues.length) {
|
|
4142
|
-
payload.issues.push(...prefixIssues(
|
|
4260
|
+
payload.issues.push(...prefixIssues(key2, result.issues));
|
|
4143
4261
|
}
|
|
4144
4262
|
payload.value[outKey] = result.value;
|
|
4145
4263
|
}
|
|
4146
4264
|
}
|
|
4147
4265
|
}
|
|
4148
4266
|
let unrecognized;
|
|
4149
|
-
for (const
|
|
4150
|
-
if (!recordKeys.has(
|
|
4267
|
+
for (const key2 in input) {
|
|
4268
|
+
if (!recordKeys.has(key2)) {
|
|
4151
4269
|
unrecognized = unrecognized ?? [];
|
|
4152
|
-
unrecognized.push(
|
|
4270
|
+
unrecognized.push(key2);
|
|
4153
4271
|
}
|
|
4154
4272
|
}
|
|
4155
4273
|
if (unrecognized && unrecognized.length > 0) {
|
|
@@ -4162,18 +4280,18 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4162
4280
|
}
|
|
4163
4281
|
} else {
|
|
4164
4282
|
payload.value = {};
|
|
4165
|
-
for (const
|
|
4166
|
-
if (
|
|
4283
|
+
for (const key2 of Reflect.ownKeys(input)) {
|
|
4284
|
+
if (key2 === "__proto__")
|
|
4167
4285
|
continue;
|
|
4168
|
-
if (!Object.prototype.propertyIsEnumerable.call(input,
|
|
4286
|
+
if (!Object.prototype.propertyIsEnumerable.call(input, key2))
|
|
4169
4287
|
continue;
|
|
4170
|
-
let keyResult = def.keyType._zod.run({ value:
|
|
4288
|
+
let keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4171
4289
|
if (keyResult instanceof Promise) {
|
|
4172
4290
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4173
4291
|
}
|
|
4174
|
-
const checkNumericKey = typeof
|
|
4292
|
+
const checkNumericKey = typeof key2 === "string" && number.test(key2) && keyResult.issues.length;
|
|
4175
4293
|
if (checkNumericKey) {
|
|
4176
|
-
const retryResult = def.keyType._zod.run({ value: Number(
|
|
4294
|
+
const retryResult = def.keyType._zod.run({ value: Number(key2), issues: [] }, ctx);
|
|
4177
4295
|
if (retryResult instanceof Promise) {
|
|
4178
4296
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4179
4297
|
}
|
|
@@ -4183,30 +4301,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4183
4301
|
}
|
|
4184
4302
|
if (keyResult.issues.length) {
|
|
4185
4303
|
if (def.mode === "loose") {
|
|
4186
|
-
payload.value[
|
|
4304
|
+
payload.value[key2] = input[key2];
|
|
4187
4305
|
} else {
|
|
4188
4306
|
payload.issues.push({
|
|
4189
4307
|
code: "invalid_key",
|
|
4190
4308
|
origin: "record",
|
|
4191
4309
|
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4192
|
-
input:
|
|
4193
|
-
path: [
|
|
4310
|
+
input: key2,
|
|
4311
|
+
path: [key2],
|
|
4194
4312
|
inst
|
|
4195
4313
|
});
|
|
4196
4314
|
}
|
|
4197
4315
|
continue;
|
|
4198
4316
|
}
|
|
4199
|
-
const result = def.valueType._zod.run({ value: input[
|
|
4317
|
+
const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
4200
4318
|
if (result instanceof Promise) {
|
|
4201
4319
|
proms.push(result.then((result2) => {
|
|
4202
4320
|
if (result2.issues.length) {
|
|
4203
|
-
payload.issues.push(...prefixIssues(
|
|
4321
|
+
payload.issues.push(...prefixIssues(key2, result2.issues));
|
|
4204
4322
|
}
|
|
4205
4323
|
payload.value[keyResult.value] = result2.value;
|
|
4206
4324
|
}));
|
|
4207
4325
|
} else {
|
|
4208
4326
|
if (result.issues.length) {
|
|
4209
|
-
payload.issues.push(...prefixIssues(
|
|
4327
|
+
payload.issues.push(...prefixIssues(key2, result.issues));
|
|
4210
4328
|
}
|
|
4211
4329
|
payload.value[keyResult.value] = result.value;
|
|
4212
4330
|
}
|
|
@@ -4233,15 +4351,15 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
|
|
|
4233
4351
|
}
|
|
4234
4352
|
const proms = [];
|
|
4235
4353
|
payload.value = /* @__PURE__ */ new Map();
|
|
4236
|
-
for (const [
|
|
4237
|
-
const keyResult = def.keyType._zod.run({ value:
|
|
4354
|
+
for (const [key2, value] of input) {
|
|
4355
|
+
const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4238
4356
|
const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
|
|
4239
4357
|
if (keyResult instanceof Promise || valueResult instanceof Promise) {
|
|
4240
4358
|
proms.push(Promise.all([keyResult, valueResult]).then(([keyResult2, valueResult2]) => {
|
|
4241
|
-
handleMapResult(keyResult2, valueResult2, payload,
|
|
4359
|
+
handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
|
|
4242
4360
|
}));
|
|
4243
4361
|
} else {
|
|
4244
|
-
handleMapResult(keyResult, valueResult, payload,
|
|
4362
|
+
handleMapResult(keyResult, valueResult, payload, key2, input, inst, ctx);
|
|
4245
4363
|
}
|
|
4246
4364
|
}
|
|
4247
4365
|
if (proms.length)
|
|
@@ -4249,10 +4367,10 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
|
|
|
4249
4367
|
return payload;
|
|
4250
4368
|
};
|
|
4251
4369
|
});
|
|
4252
|
-
function handleMapResult(keyResult, valueResult, final,
|
|
4370
|
+
function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
|
|
4253
4371
|
if (keyResult.issues.length) {
|
|
4254
|
-
if (propertyKeyTypes.has(typeof
|
|
4255
|
-
final.issues.push(...prefixIssues(
|
|
4372
|
+
if (propertyKeyTypes.has(typeof key2)) {
|
|
4373
|
+
final.issues.push(...prefixIssues(key2, keyResult.issues));
|
|
4256
4374
|
} else {
|
|
4257
4375
|
final.issues.push({
|
|
4258
4376
|
code: "invalid_key",
|
|
@@ -4264,15 +4382,15 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
|
|
|
4264
4382
|
}
|
|
4265
4383
|
}
|
|
4266
4384
|
if (valueResult.issues.length) {
|
|
4267
|
-
if (propertyKeyTypes.has(typeof
|
|
4268
|
-
final.issues.push(...prefixIssues(
|
|
4385
|
+
if (propertyKeyTypes.has(typeof key2)) {
|
|
4386
|
+
final.issues.push(...prefixIssues(key2, valueResult.issues));
|
|
4269
4387
|
} else {
|
|
4270
4388
|
final.issues.push({
|
|
4271
4389
|
origin: "map",
|
|
4272
4390
|
code: "invalid_element",
|
|
4273
4391
|
input,
|
|
4274
4392
|
inst,
|
|
4275
|
-
key,
|
|
4393
|
+
key: key2,
|
|
4276
4394
|
issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config()))
|
|
4277
4395
|
});
|
|
4278
4396
|
}
|
|
@@ -12033,8 +12151,8 @@ function extractDefs(ctx, schema) {
|
|
|
12033
12151
|
if (defId)
|
|
12034
12152
|
seen.defId = defId;
|
|
12035
12153
|
const schema2 = seen.schema;
|
|
12036
|
-
for (const
|
|
12037
|
-
delete schema2[
|
|
12154
|
+
for (const key2 in schema2) {
|
|
12155
|
+
delete schema2[key2];
|
|
12038
12156
|
}
|
|
12039
12157
|
schema2.$ref = ref;
|
|
12040
12158
|
};
|
|
@@ -12103,20 +12221,20 @@ function finalize(ctx, schema) {
|
|
|
12103
12221
|
Object.assign(schema2, _cached);
|
|
12104
12222
|
const isParentRef = zodSchema._zod.parent === ref;
|
|
12105
12223
|
if (isParentRef) {
|
|
12106
|
-
for (const
|
|
12107
|
-
if (
|
|
12224
|
+
for (const key2 in schema2) {
|
|
12225
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
12108
12226
|
continue;
|
|
12109
|
-
if (!(
|
|
12110
|
-
delete schema2[
|
|
12227
|
+
if (!(key2 in _cached)) {
|
|
12228
|
+
delete schema2[key2];
|
|
12111
12229
|
}
|
|
12112
12230
|
}
|
|
12113
12231
|
}
|
|
12114
12232
|
if (refSchema.$ref && refSeen.def) {
|
|
12115
|
-
for (const
|
|
12116
|
-
if (
|
|
12233
|
+
for (const key2 in schema2) {
|
|
12234
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
12117
12235
|
continue;
|
|
12118
|
-
if (
|
|
12119
|
-
delete schema2[
|
|
12236
|
+
if (key2 in refSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(refSeen.def[key2])) {
|
|
12237
|
+
delete schema2[key2];
|
|
12120
12238
|
}
|
|
12121
12239
|
}
|
|
12122
12240
|
}
|
|
@@ -12128,11 +12246,11 @@ function finalize(ctx, schema) {
|
|
|
12128
12246
|
if (parentSeen?.schema.$ref) {
|
|
12129
12247
|
schema2.$ref = parentSeen.schema.$ref;
|
|
12130
12248
|
if (parentSeen.def) {
|
|
12131
|
-
for (const
|
|
12132
|
-
if (
|
|
12249
|
+
for (const key2 in schema2) {
|
|
12250
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
12133
12251
|
continue;
|
|
12134
|
-
if (
|
|
12135
|
-
delete schema2[
|
|
12252
|
+
if (key2 in parentSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(parentSeen.def[key2])) {
|
|
12253
|
+
delete schema2[key2];
|
|
12136
12254
|
}
|
|
12137
12255
|
}
|
|
12138
12256
|
}
|
|
@@ -12233,8 +12351,8 @@ function isTransforming(_schema, _ctx) {
|
|
|
12233
12351
|
return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
|
|
12234
12352
|
}
|
|
12235
12353
|
if (def.type === "object") {
|
|
12236
|
-
for (const
|
|
12237
|
-
if (isTransforming(def.shape[
|
|
12354
|
+
for (const key2 in def.shape) {
|
|
12355
|
+
if (isTransforming(def.shape[key2], ctx))
|
|
12238
12356
|
return true;
|
|
12239
12357
|
}
|
|
12240
12358
|
return false;
|
|
@@ -12523,15 +12641,15 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
12523
12641
|
json3.type = "object";
|
|
12524
12642
|
json3.properties = {};
|
|
12525
12643
|
const shape = def.shape;
|
|
12526
|
-
for (const
|
|
12527
|
-
json3.properties[
|
|
12644
|
+
for (const key2 in shape) {
|
|
12645
|
+
json3.properties[key2] = process2(shape[key2], ctx, {
|
|
12528
12646
|
...params,
|
|
12529
|
-
path: [...params.path, "properties",
|
|
12647
|
+
path: [...params.path, "properties", key2]
|
|
12530
12648
|
});
|
|
12531
12649
|
}
|
|
12532
12650
|
const allKeys = new Set(Object.keys(shape));
|
|
12533
|
-
const requiredKeys = new Set([...allKeys].filter((
|
|
12534
|
-
const v = def.shape[
|
|
12651
|
+
const requiredKeys = new Set([...allKeys].filter((key2) => {
|
|
12652
|
+
const v = def.shape[key2]._zod;
|
|
12535
12653
|
if (ctx.io === "input") {
|
|
12536
12654
|
return v.optin === void 0;
|
|
12537
12655
|
} else {
|
|
@@ -12797,9 +12915,9 @@ function toJSONSchema(input, params) {
|
|
|
12797
12915
|
};
|
|
12798
12916
|
ctx2.external = external;
|
|
12799
12917
|
for (const entry of registry2._idmap.entries()) {
|
|
12800
|
-
const [
|
|
12918
|
+
const [key2, schema] = entry;
|
|
12801
12919
|
extractDefs(ctx2, schema);
|
|
12802
|
-
schemas[
|
|
12920
|
+
schemas[key2] = finalize(ctx2, schema);
|
|
12803
12921
|
}
|
|
12804
12922
|
if (Object.keys(defs).length > 0) {
|
|
12805
12923
|
const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
|
|
@@ -13205,14 +13323,14 @@ function _installLazyMethods(inst, group, methods) {
|
|
|
13205
13323
|
if (installed.has(group))
|
|
13206
13324
|
return;
|
|
13207
13325
|
installed.add(group);
|
|
13208
|
-
for (const
|
|
13209
|
-
const fn = methods[
|
|
13210
|
-
Object.defineProperty(proto,
|
|
13326
|
+
for (const key2 in methods) {
|
|
13327
|
+
const fn = methods[key2];
|
|
13328
|
+
Object.defineProperty(proto, key2, {
|
|
13211
13329
|
configurable: true,
|
|
13212
13330
|
enumerable: false,
|
|
13213
13331
|
get() {
|
|
13214
13332
|
const bound = fn.bind(this);
|
|
13215
|
-
Object.defineProperty(this,
|
|
13333
|
+
Object.defineProperty(this, key2, {
|
|
13216
13334
|
configurable: true,
|
|
13217
13335
|
writable: true,
|
|
13218
13336
|
enumerable: true,
|
|
@@ -13221,7 +13339,7 @@ function _installLazyMethods(inst, group, methods) {
|
|
|
13221
13339
|
return bound;
|
|
13222
13340
|
},
|
|
13223
13341
|
set(v) {
|
|
13224
|
-
Object.defineProperty(this,
|
|
13342
|
+
Object.defineProperty(this, key2, {
|
|
13225
13343
|
configurable: true,
|
|
13226
13344
|
writable: true,
|
|
13227
13345
|
enumerable: true,
|
|
@@ -14609,11 +14727,11 @@ function resolveRef(ref, ctx) {
|
|
|
14609
14727
|
}
|
|
14610
14728
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
14611
14729
|
if (path[0] === defsKey) {
|
|
14612
|
-
const
|
|
14613
|
-
if (!
|
|
14730
|
+
const key2 = path[1];
|
|
14731
|
+
if (!key2 || !ctx.defs[key2]) {
|
|
14614
14732
|
throw new Error(`Reference not found: ${ref}`);
|
|
14615
14733
|
}
|
|
14616
|
-
return ctx.defs[
|
|
14734
|
+
return ctx.defs[key2];
|
|
14617
14735
|
}
|
|
14618
14736
|
throw new Error(`Reference not found: ${ref}`);
|
|
14619
14737
|
}
|
|
@@ -14799,9 +14917,9 @@ function convertBaseSchema(schema, ctx) {
|
|
|
14799
14917
|
const shape = {};
|
|
14800
14918
|
const properties = schema.properties || {};
|
|
14801
14919
|
const requiredSet = new Set(schema.required || []);
|
|
14802
|
-
for (const [
|
|
14920
|
+
for (const [key2, propSchema] of Object.entries(properties)) {
|
|
14803
14921
|
const propZodSchema = convertSchema(propSchema, ctx);
|
|
14804
|
-
shape[
|
|
14922
|
+
shape[key2] = requiredSet.has(key2) ? propZodSchema : propZodSchema.optional();
|
|
14805
14923
|
}
|
|
14806
14924
|
if (schema.propertyNames) {
|
|
14807
14925
|
const keySchema = convertSchema(schema.propertyNames, ctx);
|
|
@@ -14942,20 +15060,20 @@ function convertSchema(schema, ctx) {
|
|
|
14942
15060
|
}
|
|
14943
15061
|
const extraMeta = {};
|
|
14944
15062
|
const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
|
|
14945
|
-
for (const
|
|
14946
|
-
if (
|
|
14947
|
-
extraMeta[
|
|
15063
|
+
for (const key2 of coreMetadataKeys) {
|
|
15064
|
+
if (key2 in schema) {
|
|
15065
|
+
extraMeta[key2] = schema[key2];
|
|
14948
15066
|
}
|
|
14949
15067
|
}
|
|
14950
15068
|
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
14951
|
-
for (const
|
|
14952
|
-
if (
|
|
14953
|
-
extraMeta[
|
|
15069
|
+
for (const key2 of contentMetadataKeys) {
|
|
15070
|
+
if (key2 in schema) {
|
|
15071
|
+
extraMeta[key2] = schema[key2];
|
|
14954
15072
|
}
|
|
14955
15073
|
}
|
|
14956
|
-
for (const
|
|
14957
|
-
if (!RECOGNIZED_KEYS.has(
|
|
14958
|
-
extraMeta[
|
|
15074
|
+
for (const key2 of Object.keys(schema)) {
|
|
15075
|
+
if (!RECOGNIZED_KEYS.has(key2)) {
|
|
15076
|
+
extraMeta[key2] = schema[key2];
|
|
14959
15077
|
}
|
|
14960
15078
|
}
|
|
14961
15079
|
if (Object.keys(extraMeta).length > 0) {
|
|
@@ -15033,47 +15151,78 @@ var frameManifestSchema = external_exports.object({
|
|
|
15033
15151
|
});
|
|
15034
15152
|
|
|
15035
15153
|
// src/remote-encoding.ts
|
|
15036
|
-
async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
15154
|
+
async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), canRetry = () => true) {
|
|
15037
15155
|
for (let attempt = 0; ; attempt++) {
|
|
15038
15156
|
try {
|
|
15039
15157
|
return await upload();
|
|
15040
15158
|
} catch (error51) {
|
|
15041
15159
|
const status = error51 instanceof ControlPlaneError || error51 instanceof ControlPlaneHttpError ? error51.httpStatus : void 0;
|
|
15042
15160
|
const transient = status !== void 0 ? status === 408 || status === 429 || status >= 500 : error51 instanceof TypeError || error51 instanceof DOMException && ["TimeoutError", "AbortError"].includes(error51.name);
|
|
15043
|
-
if (!transient || attempt >= 7) throw error51;
|
|
15161
|
+
if (!transient || attempt >= 7 || !canRetry()) throw error51;
|
|
15044
15162
|
const delay = Math.min(15e3, 1e3 * 2 ** attempt);
|
|
15045
15163
|
process.stderr.write(`
|
|
15046
15164
|
Frame upload interrupted; retrying in ${delay / 1e3}s.
|
|
15047
15165
|
`);
|
|
15048
15166
|
await sleep(delay);
|
|
15167
|
+
if (!canRetry()) throw error51;
|
|
15049
15168
|
}
|
|
15050
15169
|
}
|
|
15051
15170
|
}
|
|
15052
15171
|
async function submitFrameCapture(client, id, directory) {
|
|
15053
|
-
const manifest = frameManifestSchema.parse(JSON.parse(await
|
|
15172
|
+
const manifest = frameManifestSchema.parse(JSON.parse(await readFile3(join3(directory, "capture-manifest.json"), "utf8")));
|
|
15054
15173
|
let status = await client.beginEncoding(id, manifest);
|
|
15055
15174
|
if (status.status === "failed") status = await client.retryEncoding(id);
|
|
15056
15175
|
if (status.status !== "uploading") {
|
|
15057
15176
|
if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
|
|
15058
15177
|
return status;
|
|
15059
15178
|
}
|
|
15060
|
-
const file2 = await open(
|
|
15179
|
+
const file2 = await open(join3(directory, "frames.bin"), "r");
|
|
15061
15180
|
try {
|
|
15062
15181
|
const total = manifest.frames.reduce((n, f) => n + f.size, 0);
|
|
15063
15182
|
if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
|
|
15064
|
-
const
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15183
|
+
const parts = Math.ceil(total / ENCODING_PART_BYTES);
|
|
15184
|
+
const uploaded = new Set(status.uploadedParts.filter((part) => Number.isInteger(part) && part >= 0 && part < parts));
|
|
15185
|
+
const missing = Array.from({ length: parts }, (_, part) => part).filter((part) => !uploaded.has(part));
|
|
15186
|
+
let next = 0, failed = false;
|
|
15187
|
+
let firstFailure;
|
|
15188
|
+
let acceptedBytes = [...uploaded].reduce((sum, part) => sum + Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES), 0);
|
|
15189
|
+
let transferredBytes = 0;
|
|
15190
|
+
const startedAt = Date.now();
|
|
15191
|
+
const progress = () => {
|
|
15192
|
+
const seconds = (Date.now() - startedAt) / 1e3;
|
|
15193
|
+
const rate = seconds > 0 ? transferredBytes / seconds : 0;
|
|
15194
|
+
const remaining = rate > 0 ? ` \xB7 about ${Math.ceil((total - acceptedBytes) / rate)}s left` : "";
|
|
15195
|
+
process.stderr.write(`\ruploading frames ${Math.floor(acceptedBytes / total * 100)}% \xB7 ${(acceptedBytes / 1048576).toFixed(2)}/${(total / 1048576).toFixed(2)} MiB${rate > 0 ? ` \xB7 ${(rate / 1048576).toFixed(2)} MiB/s` : ""}${remaining}`);
|
|
15196
|
+
};
|
|
15197
|
+
progress();
|
|
15198
|
+
const transfer = async () => {
|
|
15199
|
+
while (!failed) {
|
|
15200
|
+
const part = missing[next++];
|
|
15201
|
+
if (part === void 0) return;
|
|
15202
|
+
try {
|
|
15203
|
+
const bytes = Buffer.alloc(Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES));
|
|
15204
|
+
let offset = 0;
|
|
15205
|
+
while (offset < bytes.length) {
|
|
15206
|
+
const r = await file2.read(bytes, offset, bytes.length - offset, part * ENCODING_PART_BYTES + offset);
|
|
15207
|
+
if (!r.bytesRead) throw new Error("Incomplete capture bytes.");
|
|
15208
|
+
offset += r.bytesRead;
|
|
15209
|
+
}
|
|
15210
|
+
if (failed) return;
|
|
15211
|
+
await withFrameUploadRetry(() => client.uploadEncodingPart(id, part, bytes), void 0, () => !failed);
|
|
15212
|
+
acceptedBytes += bytes.length;
|
|
15213
|
+
transferredBytes += bytes.length;
|
|
15214
|
+
progress();
|
|
15215
|
+
} catch (error51) {
|
|
15216
|
+
if (!failed) {
|
|
15217
|
+
firstFailure = error51;
|
|
15218
|
+
failed = true;
|
|
15219
|
+
}
|
|
15220
|
+
throw error51;
|
|
15221
|
+
}
|
|
15222
|
+
}
|
|
15223
|
+
};
|
|
15224
|
+
await Promise.allSettled(Array.from({ length: Math.min(3, missing.length) }, transfer));
|
|
15225
|
+
if (failed) throw firstFailure;
|
|
15077
15226
|
} finally {
|
|
15078
15227
|
await file2.close();
|
|
15079
15228
|
}
|
|
@@ -15093,41 +15242,244 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
15093
15242
|
}
|
|
15094
15243
|
|
|
15095
15244
|
// src/cli.ts
|
|
15096
|
-
import { spawn as
|
|
15097
|
-
import { hostname as hostname4 } from "os";
|
|
15245
|
+
import { spawn as spawn8 } from "child_process";
|
|
15246
|
+
import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
|
|
15098
15247
|
import { Command } from "commander";
|
|
15099
|
-
import { basename, dirname as dirname2 } from "path";
|
|
15100
|
-
import { readdir as readdir2, rm as
|
|
15248
|
+
import { basename, dirname as dirname2, join as join11 } from "path";
|
|
15249
|
+
import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
|
|
15250
|
+
|
|
15251
|
+
// ../recorder/src/windows-ipc.ts
|
|
15252
|
+
import { spawn as spawn3 } from "child_process";
|
|
15253
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
15254
|
+
import { link, lstat, mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
15255
|
+
import { join as join5, win32 as win322 } from "path";
|
|
15256
|
+
|
|
15257
|
+
// ../recorder/src/paths.ts
|
|
15258
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
15259
|
+
import { join as join4 } from "path";
|
|
15260
|
+
var rillDirectory = () => process.env.RILL_HOME ?? join4(homedir3(), ".rill");
|
|
15261
|
+
var daemonSocketPath = () => process.env.RILL_SOCKET ?? join4(rillDirectory(), "recorder-v2.sock");
|
|
15262
|
+
var daemonStatePath = () => join4(rillDirectory(), "sessions-v2.json");
|
|
15263
|
+
var recordingTempRoot = () => process.env.RILL_TEMP ?? join4(tmpdir(), "rill-recordings");
|
|
15264
|
+
|
|
15265
|
+
// ../recorder/src/windows-ipc.ts
|
|
15266
|
+
var aclScript = String.raw`$ErrorActionPreference='Stop';try {
|
|
15267
|
+
$path=([Console]::In.ReadToEnd() | ConvertFrom-Json).path
|
|
15268
|
+
$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
15269
|
+
$acl=[System.Security.AccessControl.DirectorySecurity]::new()
|
|
15270
|
+
$acl.SetOwner($sid)
|
|
15271
|
+
$acl.SetAccessRuleProtection($true,$false)
|
|
15272
|
+
$rule=[System.Security.AccessControl.FileSystemAccessRule]::new($sid,[System.Security.AccessControl.FileSystemRights]::FullControl,[System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)
|
|
15273
|
+
$acl.AddAccessRule($rule)
|
|
15274
|
+
([System.IO.DirectoryInfo]::new($path)).SetAccessControl($acl)
|
|
15275
|
+
[Console]::Out.Write('secured')
|
|
15276
|
+
}catch{[Console]::Error.Write('Recorder directory protection failed.');exit 1}`;
|
|
15277
|
+
async function secureWindowsIpcDirectory(path) {
|
|
15278
|
+
if (process.platform !== "win32") throw new Error("Windows IPC directory protection requires Windows.");
|
|
15279
|
+
await new Promise((resolve, reject) => {
|
|
15280
|
+
const executable = win322.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
15281
|
+
const child = spawn3(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", aclScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
15282
|
+
let output = "", failed = false;
|
|
15283
|
+
const fail2 = () => {
|
|
15284
|
+
if (failed) return;
|
|
15285
|
+
failed = true;
|
|
15286
|
+
child.kill();
|
|
15287
|
+
reject(new Error("Cannot secure the Windows recorder directory. Recording has not started."));
|
|
15288
|
+
};
|
|
15289
|
+
const timer = setTimeout(fail2, 15e3);
|
|
15290
|
+
child.once("error", fail2);
|
|
15291
|
+
child.stdin.once("error", fail2);
|
|
15292
|
+
child.stderr.resume();
|
|
15293
|
+
child.stdout.on("data", (chunk) => {
|
|
15294
|
+
output += chunk.toString("ascii");
|
|
15295
|
+
if (output.length > 1024) fail2();
|
|
15296
|
+
});
|
|
15297
|
+
child.once("close", (code) => {
|
|
15298
|
+
clearTimeout(timer);
|
|
15299
|
+
if (failed) return;
|
|
15300
|
+
if (code !== 0 || output !== "secured") {
|
|
15301
|
+
fail2();
|
|
15302
|
+
return;
|
|
15303
|
+
}
|
|
15304
|
+
resolve();
|
|
15305
|
+
});
|
|
15306
|
+
child.stdin.end(JSON.stringify({ path }));
|
|
15307
|
+
});
|
|
15308
|
+
}
|
|
15309
|
+
function validWindowsIpcIdentity(value) {
|
|
15310
|
+
const v = value;
|
|
15311
|
+
return v?.version === 1 && typeof v.pipe === "string" && /^\\\\\.\\pipe\\rill-v3-[a-f0-9]{64}$/.test(v.pipe) && typeof v.token === "string" && /^[a-f0-9]{64}$/.test(v.token);
|
|
15312
|
+
}
|
|
15313
|
+
function acceptsWindowsIpcToken(expected, header) {
|
|
15314
|
+
if (typeof header !== "string" || !/^Bearer [a-f0-9]{64}$/.test(header)) return false;
|
|
15315
|
+
const supplied = header.slice(7);
|
|
15316
|
+
return supplied.length === expected.length && timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
|
|
15317
|
+
}
|
|
15318
|
+
async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ipc-v3"), secure = secureWindowsIpcDirectory) {
|
|
15319
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
15320
|
+
if ((await lstat(directory)).isSymbolicLink()) throw new Error("Recorder directory cannot be a symbolic link.");
|
|
15321
|
+
await secure(directory);
|
|
15322
|
+
const path = join5(directory, "identity.json");
|
|
15323
|
+
const load = async () => {
|
|
15324
|
+
const info = await lstat(path);
|
|
15325
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 4096) throw new Error("Invalid recorder identity.");
|
|
15326
|
+
const value = JSON.parse(await readFile4(path, "utf8"));
|
|
15327
|
+
if (!validWindowsIpcIdentity(value)) throw new Error("Invalid recorder identity.");
|
|
15328
|
+
return value;
|
|
15329
|
+
};
|
|
15330
|
+
try {
|
|
15331
|
+
return await load();
|
|
15332
|
+
} catch (e) {
|
|
15333
|
+
if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
|
|
15334
|
+
}
|
|
15335
|
+
const identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
|
|
15336
|
+
const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
|
|
15337
|
+
try {
|
|
15338
|
+
await writeFile2(temporary, JSON.stringify(identity), { mode: 384, flag: "wx" });
|
|
15339
|
+
try {
|
|
15340
|
+
await link(temporary, path);
|
|
15341
|
+
} catch (e) {
|
|
15342
|
+
if (e.code !== "EEXIST") throw e;
|
|
15343
|
+
}
|
|
15344
|
+
return await load();
|
|
15345
|
+
} finally {
|
|
15346
|
+
await rm3(temporary, { force: true });
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15101
15349
|
|
|
15102
15350
|
// ../recorder/src/client.ts
|
|
15103
15351
|
import { request as request2 } from "http";
|
|
15104
|
-
import { spawn as
|
|
15352
|
+
import { spawn as spawn4 } from "child_process";
|
|
15105
15353
|
|
|
15106
|
-
// ../recorder/src/
|
|
15107
|
-
import {
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
var
|
|
15111
|
-
var
|
|
15112
|
-
|
|
15354
|
+
// ../recorder/src/browser-tabs.ts
|
|
15355
|
+
import { chromium } from "playwright-core";
|
|
15356
|
+
|
|
15357
|
+
// ../recorder/src/redaction.ts
|
|
15358
|
+
var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
|
|
15359
|
+
var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
|
|
15360
|
+
function sanitizeUrl(input) {
|
|
15361
|
+
try {
|
|
15362
|
+
const url2 = new URL(input);
|
|
15363
|
+
url2.username = "";
|
|
15364
|
+
url2.password = "";
|
|
15365
|
+
url2.hash = "";
|
|
15366
|
+
for (const key2 of [...url2.searchParams.keys()]) url2.searchParams.set(key2, "[REDACTED]");
|
|
15367
|
+
return url2.toString();
|
|
15368
|
+
} catch {
|
|
15369
|
+
return "[invalid-url]";
|
|
15370
|
+
}
|
|
15371
|
+
}
|
|
15372
|
+
function redactText(value, maxLength = 8e3) {
|
|
15373
|
+
return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
|
|
15374
|
+
}
|
|
15375
|
+
function redactBody(value, contentType, maxBytes = 65536) {
|
|
15376
|
+
if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
|
|
15377
|
+
if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
|
|
15378
|
+
if (contentType?.includes("json")) {
|
|
15379
|
+
try {
|
|
15380
|
+
const parsed = JSON.parse(value);
|
|
15381
|
+
return { body: JSON.stringify(redactJson(parsed)) };
|
|
15382
|
+
} catch {
|
|
15383
|
+
return { body: redactText(value) };
|
|
15384
|
+
}
|
|
15385
|
+
}
|
|
15386
|
+
return { body: redactText(value) };
|
|
15387
|
+
}
|
|
15388
|
+
function isAllowedBodyType(contentType) {
|
|
15389
|
+
if (!contentType) return false;
|
|
15390
|
+
const normalized = contentType.toLowerCase();
|
|
15391
|
+
return normalized.includes("application/json") || normalized.includes("application/problem+json") || normalized.includes("application/graphql-response+json") || normalized.includes("text/plain") || normalized.includes("application/xml") || normalized.includes("text/xml");
|
|
15392
|
+
}
|
|
15393
|
+
function redactJson(value, depth = 0) {
|
|
15394
|
+
if (depth > 12) return "[TRUNCATED]";
|
|
15395
|
+
if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
|
|
15396
|
+
if (value && typeof value === "object") {
|
|
15397
|
+
return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key2, child]) => [key2, secretKeyPattern.test(key2) ? "[REDACTED]" : redactJson(child, depth + 1)]));
|
|
15398
|
+
}
|
|
15399
|
+
if (typeof value === "string") return redactText(value, 8e3);
|
|
15400
|
+
return value;
|
|
15401
|
+
}
|
|
15402
|
+
|
|
15403
|
+
// ../recorder/src/browser-tabs.ts
|
|
15404
|
+
function validateTabSelection(options) {
|
|
15405
|
+
if (options.tabId && !options.cdpUrl) throw new Error("validation_failed: --tab-id requires --cdp-url.");
|
|
15406
|
+
}
|
|
15407
|
+
async function tabs(browser) {
|
|
15408
|
+
const result = [];
|
|
15409
|
+
for (const context of browser.contexts()) for (const page of context.pages()) {
|
|
15410
|
+
if (page.isClosed()) continue;
|
|
15411
|
+
const session = await context.newCDPSession(page).catch(() => null);
|
|
15412
|
+
if (!session) {
|
|
15413
|
+
if (page.isClosed()) continue;
|
|
15414
|
+
throw new Error("browser_unavailable: Could not inspect an open tab.");
|
|
15415
|
+
}
|
|
15416
|
+
try {
|
|
15417
|
+
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
15418
|
+
result.push({ page, context, tab: { tabId: targetInfo.targetId, url: /^(?:https?:|about:)/i.test(page.url()) ? sanitizeUrl(page.url()) : `[${page.url().split(":", 1)[0]} URL omitted]`, title: redactText(targetInfo.title, 200) } });
|
|
15419
|
+
} catch (error51) {
|
|
15420
|
+
if (!page.isClosed()) throw error51;
|
|
15421
|
+
} finally {
|
|
15422
|
+
await session.detach().catch(() => void 0);
|
|
15423
|
+
}
|
|
15424
|
+
}
|
|
15425
|
+
return result;
|
|
15426
|
+
}
|
|
15427
|
+
async function listBrowserTabs(cdpUrl) {
|
|
15428
|
+
const browser = await chromium.connectOverCDP(cdpUrl, { noDefaults: true });
|
|
15429
|
+
try {
|
|
15430
|
+
return (await tabs(browser)).map((entry) => entry.tab);
|
|
15431
|
+
} finally {
|
|
15432
|
+
await browser.close();
|
|
15433
|
+
}
|
|
15434
|
+
}
|
|
15435
|
+
async function connectRecordingTab(cdpUrl, outputDirectory, tabId) {
|
|
15436
|
+
const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
|
|
15437
|
+
try {
|
|
15438
|
+
const available = await tabs(browser);
|
|
15439
|
+
const selected = tabId ? available.find((entry) => entry.tab.tabId === tabId) : available.length === 1 ? available[0] : void 0;
|
|
15440
|
+
if (!selected) {
|
|
15441
|
+
if (tabId) throw new Error("browser_unavailable: The selected tab is no longer available. Run record tabs --cdp-url <endpoint> and choose an open tab.");
|
|
15442
|
+
if (!available.length) throw new Error("browser_unavailable: The attached browser has no page target to record.");
|
|
15443
|
+
throw new Error("validation_failed: Several tabs are open. Run record tabs --cdp-url <endpoint>, then record start --cdp-url <endpoint> --tab-id <tab-id>.");
|
|
15444
|
+
}
|
|
15445
|
+
return { cdpUrl, browser, context: selected.context, page: selected.page, tabId: selected.tab.tabId };
|
|
15446
|
+
} catch (error51) {
|
|
15447
|
+
await browser.close().catch(() => void 0);
|
|
15448
|
+
throw error51;
|
|
15449
|
+
}
|
|
15450
|
+
}
|
|
15113
15451
|
|
|
15114
15452
|
// ../recorder/src/client.ts
|
|
15115
|
-
async function ensureDaemon(cliEntryPath) {
|
|
15453
|
+
async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
|
|
15454
|
+
const check2 = (health2) => {
|
|
15455
|
+
if (requireTabSelection && !health2.capabilities?.includes("tab-selection-v1")) throw new Error("browser_unavailable: The running Rill recorder predates tab selection. Stop active recordings, then restart the recorder service before attaching.");
|
|
15456
|
+
};
|
|
15457
|
+
let health;
|
|
15116
15458
|
try {
|
|
15117
|
-
await callDaemon("GET", "/health");
|
|
15118
|
-
return;
|
|
15459
|
+
health = await callDaemon("GET", "/health");
|
|
15119
15460
|
} catch {
|
|
15120
15461
|
}
|
|
15121
|
-
|
|
15462
|
+
if (health) {
|
|
15463
|
+
check2(health);
|
|
15464
|
+
return;
|
|
15465
|
+
}
|
|
15466
|
+
const child = spawn4(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env, windowsHide: true });
|
|
15467
|
+
let startupError;
|
|
15468
|
+
child.once("error", () => {
|
|
15469
|
+
startupError = new Error("The local recorder process could not be launched.");
|
|
15470
|
+
});
|
|
15122
15471
|
child.unref();
|
|
15123
15472
|
const deadline = Date.now() + 1e4;
|
|
15124
15473
|
while (Date.now() < deadline) {
|
|
15474
|
+
if (startupError) throw startupError;
|
|
15125
15475
|
try {
|
|
15126
|
-
await callDaemon("GET", "/health");
|
|
15127
|
-
return;
|
|
15476
|
+
health = await callDaemon("GET", "/health");
|
|
15128
15477
|
} catch {
|
|
15129
15478
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15479
|
+
continue;
|
|
15130
15480
|
}
|
|
15481
|
+
check2(health);
|
|
15482
|
+
return;
|
|
15131
15483
|
}
|
|
15132
15484
|
throw new Error("The local recorder service did not start.");
|
|
15133
15485
|
}
|
|
@@ -15140,10 +15492,16 @@ function stopRecording(recordingId) {
|
|
|
15140
15492
|
function recordingStatus(recordingId) {
|
|
15141
15493
|
return callDaemon("GET", `/sessions/${encodeURIComponent(recordingId)}`);
|
|
15142
15494
|
}
|
|
15143
|
-
|
|
15495
|
+
var windowsIdentityPromise;
|
|
15496
|
+
async function callDaemon(method, path, body) {
|
|
15497
|
+
if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC. Unset it before recording.");
|
|
15498
|
+
const identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
|
|
15499
|
+
windowsIdentityPromise = void 0;
|
|
15500
|
+
throw error51;
|
|
15501
|
+
})) : void 0;
|
|
15144
15502
|
return new Promise((resolve, reject) => {
|
|
15145
15503
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
15146
|
-
const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } :
|
|
15504
|
+
const outgoing = request2({ socketPath: identity?.pipe ?? daemonSocketPath(), method, path, headers: { ...payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}, ...identity ? { Authorization: "Bearer " + identity.token } : {} } }, (incoming) => {
|
|
15147
15505
|
const chunks = [];
|
|
15148
15506
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
15149
15507
|
incoming.on("end", () => {
|
|
@@ -15156,6 +15514,7 @@ function callDaemon(method, path, body) {
|
|
|
15156
15514
|
}
|
|
15157
15515
|
});
|
|
15158
15516
|
});
|
|
15517
|
+
outgoing.setTimeout(path === "/health" ? 2e3 : 12e4, () => outgoing.destroy(new Error("Local recorder request timed out.")));
|
|
15159
15518
|
outgoing.once("error", reject);
|
|
15160
15519
|
if (payload) outgoing.write(payload);
|
|
15161
15520
|
outgoing.end();
|
|
@@ -15163,17 +15522,17 @@ function callDaemon(method, path, body) {
|
|
|
15163
15522
|
}
|
|
15164
15523
|
|
|
15165
15524
|
// ../recorder/src/retention.ts
|
|
15166
|
-
import { readdir, readFile as
|
|
15167
|
-
import { join as
|
|
15525
|
+
import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
|
|
15526
|
+
import { join as join6 } from "path";
|
|
15168
15527
|
async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
|
|
15169
15528
|
const removed = [];
|
|
15170
15529
|
for (const id of await readdir(root).catch(() => [])) {
|
|
15171
15530
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
15172
|
-
const directory =
|
|
15531
|
+
const directory = join6(root, id);
|
|
15173
15532
|
try {
|
|
15174
|
-
const marker = JSON.parse(await
|
|
15533
|
+
const marker = JSON.parse(await readFile5(join6(directory, "remote-capture.json"), "utf8"));
|
|
15175
15534
|
if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
|
|
15176
|
-
await
|
|
15535
|
+
await rm4(directory, { recursive: true, force: true });
|
|
15177
15536
|
removed.push(id);
|
|
15178
15537
|
} catch {
|
|
15179
15538
|
}
|
|
@@ -15184,7 +15543,7 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
15184
15543
|
for (const id of await readdir(root).catch(() => [])) {
|
|
15185
15544
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
15186
15545
|
try {
|
|
15187
|
-
const marker = JSON.parse(await
|
|
15546
|
+
const marker = JSON.parse(await readFile5(join6(root, id, "remote-capture.json"), "utf8"));
|
|
15188
15547
|
if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
|
|
15189
15548
|
} catch {
|
|
15190
15549
|
}
|
|
@@ -15194,8 +15553,238 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
15194
15553
|
|
|
15195
15554
|
// ../recorder/src/daemon.ts
|
|
15196
15555
|
import { createServer as createServer2 } from "http";
|
|
15197
|
-
import { mkdir as
|
|
15198
|
-
import { dirname, join as
|
|
15556
|
+
import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
|
|
15557
|
+
import { dirname, join as join10 } from "path";
|
|
15558
|
+
|
|
15559
|
+
// ../recorder/src/click-capture.ts
|
|
15560
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
15561
|
+
async function captureClicks(page, session, onClick) {
|
|
15562
|
+
const binding = `__rillClick_${randomUUID2().replaceAll("-", "")}`;
|
|
15563
|
+
const source = `(() => {
|
|
15564
|
+
const key = ${JSON.stringify(binding)};
|
|
15565
|
+
if (window[key + '_listener']) return;
|
|
15566
|
+
const listener = event => {
|
|
15567
|
+
const target = event.target;
|
|
15568
|
+
const label = target?.innerText?.trim().slice(0, 80) || target?.getAttribute?.('aria-label') || target?.tagName || 'Element';
|
|
15569
|
+
try { window[key](JSON.stringify({ x: event.clientX, y: event.clientY, label })); } catch {}
|
|
15570
|
+
};
|
|
15571
|
+
window[key + '_listener'] = listener;
|
|
15572
|
+
document.addEventListener('click', listener, { capture: true });
|
|
15573
|
+
})();`;
|
|
15574
|
+
const receive = (event) => {
|
|
15575
|
+
if (event.name !== binding) return;
|
|
15576
|
+
try {
|
|
15577
|
+
const value = JSON.parse(event.payload);
|
|
15578
|
+
if (typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y) && typeof value.label === "string") onClick({ x: value.x, y: value.y, label: value.label.slice(0, 80) });
|
|
15579
|
+
} catch {
|
|
15580
|
+
}
|
|
15581
|
+
};
|
|
15582
|
+
let identifier;
|
|
15583
|
+
session.on("Runtime.bindingCalled", receive);
|
|
15584
|
+
const cleanup = async () => {
|
|
15585
|
+
session.off("Runtime.bindingCalled", receive);
|
|
15586
|
+
if (identifier) await session.send("Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => void 0);
|
|
15587
|
+
await Promise.all(page.frames().map((frame) => frame.evaluate((key2) => {
|
|
15588
|
+
const target = window;
|
|
15589
|
+
const listener = target[key2 + "_listener"];
|
|
15590
|
+
if (typeof listener === "function") document.removeEventListener("click", listener, { capture: true });
|
|
15591
|
+
delete target[key2 + "_listener"];
|
|
15592
|
+
delete target[key2];
|
|
15593
|
+
}, binding).catch(() => void 0)));
|
|
15594
|
+
await session.send("Runtime.removeBinding", { name: binding }).catch(() => void 0);
|
|
15595
|
+
};
|
|
15596
|
+
try {
|
|
15597
|
+
await session.send("Runtime.enable");
|
|
15598
|
+
await session.send("Runtime.addBinding", { name: binding });
|
|
15599
|
+
({ identifier } = await session.send("Page.addScriptToEvaluateOnNewDocument", { source }));
|
|
15600
|
+
await Promise.all(page.frames().map((frame) => frame.evaluate(source)));
|
|
15601
|
+
return cleanup;
|
|
15602
|
+
} catch (error51) {
|
|
15603
|
+
await cleanup();
|
|
15604
|
+
throw error51;
|
|
15605
|
+
}
|
|
15606
|
+
}
|
|
15607
|
+
|
|
15608
|
+
// ../contracts/src/cloud-blocklist.ts
|
|
15609
|
+
var blockedCloudDomains = [
|
|
15610
|
+
// Torrent indexes and unofficial software-download destinations.
|
|
15611
|
+
// Explicit domains only: do not block legitimate torrent clients or all downloads.
|
|
15612
|
+
"yasdl.com",
|
|
15613
|
+
"soft98.ir",
|
|
15614
|
+
"downloadha.com",
|
|
15615
|
+
"patoghu.com",
|
|
15616
|
+
"thepiratebay.org",
|
|
15617
|
+
"1337x.to",
|
|
15618
|
+
"yts.mx",
|
|
15619
|
+
"eztvx.to",
|
|
15620
|
+
"fitgirl-repacks.site",
|
|
15621
|
+
"dodi-repacks.site",
|
|
15622
|
+
// Search and reference
|
|
15623
|
+
"google.com",
|
|
15624
|
+
"google.co.uk",
|
|
15625
|
+
"google.co.in",
|
|
15626
|
+
"google.com.ng",
|
|
15627
|
+
"google.ca",
|
|
15628
|
+
"google.com.au",
|
|
15629
|
+
"google.de",
|
|
15630
|
+
"google.fr",
|
|
15631
|
+
"google.co.jp",
|
|
15632
|
+
"google.com.br",
|
|
15633
|
+
"google.es",
|
|
15634
|
+
"bing.com",
|
|
15635
|
+
"yahoo.com",
|
|
15636
|
+
"duckduckgo.com",
|
|
15637
|
+
"baidu.com",
|
|
15638
|
+
"yandex.com",
|
|
15639
|
+
"yandex.ru",
|
|
15640
|
+
"wikipedia.org",
|
|
15641
|
+
"quora.com",
|
|
15642
|
+
// Social networks and media
|
|
15643
|
+
"reddit.com",
|
|
15644
|
+
"redd.it",
|
|
15645
|
+
"facebook.com",
|
|
15646
|
+
"fb.com",
|
|
15647
|
+
"instagram.com",
|
|
15648
|
+
"threads.net",
|
|
15649
|
+
"threads.com",
|
|
15650
|
+
"twitter.com",
|
|
15651
|
+
"x.com",
|
|
15652
|
+
"t.co",
|
|
15653
|
+
"linkedin.com",
|
|
15654
|
+
"tiktok.com",
|
|
15655
|
+
"pinterest.com",
|
|
15656
|
+
"snapchat.com",
|
|
15657
|
+
"youtube.com",
|
|
15658
|
+
"youtu.be",
|
|
15659
|
+
"twitch.tv",
|
|
15660
|
+
"discord.com",
|
|
15661
|
+
"discord.gg",
|
|
15662
|
+
"telegram.org",
|
|
15663
|
+
"t.me",
|
|
15664
|
+
"whatsapp.com",
|
|
15665
|
+
// Shopping and marketplaces
|
|
15666
|
+
"amazon.com",
|
|
15667
|
+
"amazon.co.uk",
|
|
15668
|
+
"amazon.ca",
|
|
15669
|
+
"amazon.de",
|
|
15670
|
+
"amazon.fr",
|
|
15671
|
+
"amazon.in",
|
|
15672
|
+
"amazon.co.jp",
|
|
15673
|
+
"amazon.com.au",
|
|
15674
|
+
"amazon.it",
|
|
15675
|
+
"amazon.es",
|
|
15676
|
+
"ebay.com",
|
|
15677
|
+
"ebay.co.uk",
|
|
15678
|
+
"etsy.com",
|
|
15679
|
+
"walmart.com",
|
|
15680
|
+
"target.com",
|
|
15681
|
+
"aliexpress.com",
|
|
15682
|
+
"alibaba.com",
|
|
15683
|
+
"temu.com",
|
|
15684
|
+
"shein.com",
|
|
15685
|
+
"craigslist.org",
|
|
15686
|
+
"facebookmarketplace.com",
|
|
15687
|
+
"bestbuy.com",
|
|
15688
|
+
"costco.com",
|
|
15689
|
+
"homedepot.com",
|
|
15690
|
+
"wayfair.com",
|
|
15691
|
+
"mercadolibre.com",
|
|
15692
|
+
"shopee.com",
|
|
15693
|
+
"lazada.com",
|
|
15694
|
+
"jiji.ng",
|
|
15695
|
+
"jumia.com.ng",
|
|
15696
|
+
"konga.com",
|
|
15697
|
+
"propertypro.ng",
|
|
15698
|
+
"property24.com",
|
|
15699
|
+
// Travel, property, jobs and business directories
|
|
15700
|
+
"airbnb.com",
|
|
15701
|
+
"airbnb.co.uk",
|
|
15702
|
+
"airbnb.ca",
|
|
15703
|
+
"airbnb.com.au",
|
|
15704
|
+
"airbnb.fr",
|
|
15705
|
+
"airbnb.de",
|
|
15706
|
+
"booking.com",
|
|
15707
|
+
"expedia.com",
|
|
15708
|
+
"tripadvisor.com",
|
|
15709
|
+
"agoda.com",
|
|
15710
|
+
"hotels.com",
|
|
15711
|
+
"kayak.com",
|
|
15712
|
+
"skyscanner.net",
|
|
15713
|
+
"zillow.com",
|
|
15714
|
+
"realtor.com",
|
|
15715
|
+
"rightmove.co.uk",
|
|
15716
|
+
"zoopla.co.uk",
|
|
15717
|
+
"indeed.com",
|
|
15718
|
+
"glassdoor.com",
|
|
15719
|
+
"yelp.com",
|
|
15720
|
+
"yellowpages.com",
|
|
15721
|
+
"trustpilot.com",
|
|
15722
|
+
"crunchbase.com",
|
|
15723
|
+
"g2.com",
|
|
15724
|
+
"capterra.com",
|
|
15725
|
+
"clutch.co",
|
|
15726
|
+
"zoominfo.com",
|
|
15727
|
+
"apollo.io",
|
|
15728
|
+
"monster.com",
|
|
15729
|
+
"ziprecruiter.com",
|
|
15730
|
+
"seek.com.au",
|
|
15731
|
+
"naukri.com",
|
|
15732
|
+
"redfin.com",
|
|
15733
|
+
"trulia.com",
|
|
15734
|
+
"apartments.com",
|
|
15735
|
+
"realtor.ca",
|
|
15736
|
+
"realestate.com.au",
|
|
15737
|
+
"vrbo.com",
|
|
15738
|
+
"trip.com",
|
|
15739
|
+
"trivago.com",
|
|
15740
|
+
"hostelworld.com"
|
|
15741
|
+
];
|
|
15742
|
+
var blockedCloudTargetMessage = "This website is not supported for cloud runs. Use your own website or app instead.";
|
|
15743
|
+
function isBlockedCloudTarget(value) {
|
|
15744
|
+
try {
|
|
15745
|
+
const hostname5 = new URL(value).hostname.toLowerCase().replace(/\.+$/, "");
|
|
15746
|
+
return blockedCloudDomains.some((domain2) => hostname5 === domain2 || hostname5.endsWith(`.${domain2}`));
|
|
15747
|
+
} catch {
|
|
15748
|
+
return false;
|
|
15749
|
+
}
|
|
15750
|
+
}
|
|
15751
|
+
|
|
15752
|
+
// ../contracts/src/cloud-run.ts
|
|
15753
|
+
var publicTarget = external_exports.string().url().max(2048).refine((value) => {
|
|
15754
|
+
let u;
|
|
15755
|
+
try {
|
|
15756
|
+
u = new URL(value);
|
|
15757
|
+
} catch {
|
|
15758
|
+
return false;
|
|
15759
|
+
}
|
|
15760
|
+
return ["http:", "https:"].includes(u.protocol) && !u.username && !u.password && !u.port && !u.hash && u.hostname.includes(".") && !/^[0-9.]+$/.test(u.hostname) && !u.hostname.endsWith(".exe.xyz") && !u.hostname.endsWith(".local") && !u.hostname.endsWith(".internal") && !u.hostname.endsWith(".localhost");
|
|
15761
|
+
}, "Use a public HTTP(S) URL without credentials, fragments, or a custom port.");
|
|
15762
|
+
var cloudRunTarget = publicTarget.refine((value) => !isBlockedCloudTarget(value), blockedCloudTargetMessage);
|
|
15763
|
+
var interactionFields = {
|
|
15764
|
+
mode: external_exports.enum(["browse", "interact"]).optional(),
|
|
15765
|
+
interactionConfirmed: external_exports.boolean().optional(),
|
|
15766
|
+
// Legacy payload compatibility only; Interact automatically permits API destinations.
|
|
15767
|
+
allowedWriteOrigins: external_exports.array(cloudRunTarget.refine((v) => new URL(v).origin === v, "Use an exact origin.")).max(5).optional()
|
|
15768
|
+
};
|
|
15769
|
+
var sideEffectSchema = external_exports.object({
|
|
15770
|
+
action: external_exports.enum(["fill", "check", "select", "press", "click"]),
|
|
15771
|
+
page: external_exports.string().max(2048),
|
|
15772
|
+
target: external_exports.string().max(160),
|
|
15773
|
+
status: external_exports.enum(["performed", "unknown"])
|
|
15774
|
+
}).strict();
|
|
15775
|
+
var interactionReportFields = {
|
|
15776
|
+
mode: external_exports.enum(["browse", "interact"]).optional(),
|
|
15777
|
+
sideEffects: external_exports.array(sideEffectSchema).max(30).optional(),
|
|
15778
|
+
cleanup: external_exports.literal("not_attempted").optional()
|
|
15779
|
+
};
|
|
15780
|
+
var cloudSubmission = external_exports.object({ ...interactionFields, id: external_exports.string().uuid(), url: cloudRunTarget, prompt: external_exports.string().trim().min(1).max(2e3), expectation: external_exports.string().trim().min(1).max(1e3).optional(), account: external_exports.string().regex(/^[a-z0-9-]{1,60}$/).optional() }).strict();
|
|
15781
|
+
var siteAccountSchema = external_exports.object({
|
|
15782
|
+
name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
|
|
15783
|
+
origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
|
|
15784
|
+
loginUrl: publicTarget.refine((v) => new URL(v).protocol === "https:").optional(),
|
|
15785
|
+
username: external_exports.string().min(1).max(320),
|
|
15786
|
+
password: external_exports.string().min(1).max(1e3)
|
|
15787
|
+
}).strict().refine((v) => !v.loginUrl || new URL(v.loginUrl).origin === v.origin, "Login must use the saved website origin.");
|
|
15199
15788
|
|
|
15200
15789
|
// ../contracts/src/recording.ts
|
|
15201
15790
|
var cursorAppearanceSchema = external_exports.object({
|
|
@@ -15236,6 +15825,7 @@ var recordingSchema = external_exports.object({
|
|
|
15236
15825
|
})
|
|
15237
15826
|
});
|
|
15238
15827
|
var agentOutcomeSchema = external_exports.object({
|
|
15828
|
+
...interactionReportFields,
|
|
15239
15829
|
outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
|
|
15240
15830
|
summary: external_exports.string().min(1).max(1e3),
|
|
15241
15831
|
evidence: external_exports.string().max(1e3)
|
|
@@ -15323,15 +15913,15 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15323
15913
|
const appearance = cursorAppearanceSchema.parse(input ?? {});
|
|
15324
15914
|
if (!appearance.enabled) return async () => {
|
|
15325
15915
|
};
|
|
15326
|
-
const
|
|
15327
|
-
const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key })})`;
|
|
15916
|
+
const key2 = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
15917
|
+
const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key: key2 })})`;
|
|
15328
15918
|
await cdp.send("Page.enable");
|
|
15329
15919
|
const { identifier } = await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
|
|
15330
15920
|
let stopped = false;
|
|
15331
|
-
const removeFromFrame = (frame) => frame.evaluate((
|
|
15332
|
-
const cleanup2 = window[
|
|
15921
|
+
const removeFromFrame = (frame) => frame.evaluate((key3) => {
|
|
15922
|
+
const cleanup2 = window[key3];
|
|
15333
15923
|
if (typeof cleanup2 === "function") cleanup2();
|
|
15334
|
-
},
|
|
15924
|
+
}, key2).catch(() => void 0);
|
|
15335
15925
|
const onNavigation = (frame) => {
|
|
15336
15926
|
if (stopped) return;
|
|
15337
15927
|
void frame.evaluate(source).then(() => {
|
|
@@ -15355,11 +15945,11 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15355
15945
|
}
|
|
15356
15946
|
|
|
15357
15947
|
// ../recorder/src/session.ts
|
|
15358
|
-
import { chromium } from "playwright-core";
|
|
15359
|
-
import { spawn as
|
|
15948
|
+
import { chromium as chromium2 } from "playwright-core";
|
|
15949
|
+
import { spawn as spawn6 } from "child_process";
|
|
15360
15950
|
import { createServer } from "net";
|
|
15361
|
-
import { mkdir as
|
|
15362
|
-
import { join as
|
|
15951
|
+
import { mkdir as mkdir6, mkdtemp, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
15952
|
+
import { join as join9 } from "path";
|
|
15363
15953
|
import { tmpdir as tmpdir2 } from "os";
|
|
15364
15954
|
|
|
15365
15955
|
// ../recorder/src/bundle.ts
|
|
@@ -15385,63 +15975,22 @@ async function writeBundle(bundle, outputPath) {
|
|
|
15385
15975
|
|
|
15386
15976
|
// ../recorder/src/environment.ts
|
|
15387
15977
|
import { existsSync } from "fs";
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
if (
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
// ../recorder/src/redaction.ts
|
|
15396
|
-
var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
|
|
15397
|
-
var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
|
|
15398
|
-
function sanitizeUrl(input) {
|
|
15399
|
-
try {
|
|
15400
|
-
const url2 = new URL(input);
|
|
15401
|
-
url2.username = "";
|
|
15402
|
-
url2.password = "";
|
|
15403
|
-
url2.hash = "";
|
|
15404
|
-
for (const key of [...url2.searchParams.keys()]) url2.searchParams.set(key, "[REDACTED]");
|
|
15405
|
-
return url2.toString();
|
|
15406
|
-
} catch {
|
|
15407
|
-
return "[invalid-url]";
|
|
15978
|
+
import { win32 as win323 } from "path";
|
|
15979
|
+
function findChromeExecutable(platform = process.platform, env = process.env, exists = existsSync) {
|
|
15980
|
+
if (env.RILL_CHROME_PATH) return env.RILL_CHROME_PATH;
|
|
15981
|
+
if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
15982
|
+
if (platform === "win32") {
|
|
15983
|
+
const candidates2 = [env.PROGRAMFILES ?? env.ProgramFiles, env["PROGRAMFILES(X86)"] ?? env["ProgramFiles(x86)"], env.LOCALAPPDATA].filter((root) => Boolean(root)).map((root) => win323.join(root, "Google", "Chrome", "Application", "chrome.exe"));
|
|
15984
|
+
return candidates2.find(exists) ?? win323.join(env.PROGRAMFILES ?? env.ProgramFiles ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe");
|
|
15408
15985
|
}
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
|
|
15412
|
-
}
|
|
15413
|
-
function redactBody(value, contentType, maxBytes = 65536) {
|
|
15414
|
-
if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
|
|
15415
|
-
if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
|
|
15416
|
-
if (contentType?.includes("json")) {
|
|
15417
|
-
try {
|
|
15418
|
-
const parsed = JSON.parse(value);
|
|
15419
|
-
return { body: JSON.stringify(redactJson(parsed)) };
|
|
15420
|
-
} catch {
|
|
15421
|
-
return { body: redactText(value) };
|
|
15422
|
-
}
|
|
15423
|
-
}
|
|
15424
|
-
return { body: redactText(value) };
|
|
15425
|
-
}
|
|
15426
|
-
function isAllowedBodyType(contentType) {
|
|
15427
|
-
if (!contentType) return false;
|
|
15428
|
-
const normalized = contentType.toLowerCase();
|
|
15429
|
-
return normalized.includes("application/json") || normalized.includes("application/problem+json") || normalized.includes("application/graphql-response+json") || normalized.includes("text/plain") || normalized.includes("application/xml") || normalized.includes("text/xml");
|
|
15430
|
-
}
|
|
15431
|
-
function redactJson(value, depth = 0) {
|
|
15432
|
-
if (depth > 12) return "[TRUNCATED]";
|
|
15433
|
-
if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
|
|
15434
|
-
if (value && typeof value === "object") {
|
|
15435
|
-
return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key, child]) => [key, secretKeyPattern.test(key) ? "[REDACTED]" : redactJson(child, depth + 1)]));
|
|
15436
|
-
}
|
|
15437
|
-
if (typeof value === "string") return redactText(value, 8e3);
|
|
15438
|
-
return value;
|
|
15986
|
+
const candidates = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
|
|
15987
|
+
return candidates.find(exists) ?? "google-chrome";
|
|
15439
15988
|
}
|
|
15440
15989
|
|
|
15441
15990
|
// ../recorder/src/encoding.ts
|
|
15442
|
-
import { open as open2, writeFile, mkdir as
|
|
15443
|
-
import { spawn as
|
|
15444
|
-
import { join as
|
|
15991
|
+
import { open as open2, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
15992
|
+
import { spawn as spawn5 } from "child_process";
|
|
15993
|
+
import { join as join7 } from "path";
|
|
15445
15994
|
var active = 0;
|
|
15446
15995
|
var waiting = [];
|
|
15447
15996
|
function encodingConcurrency() {
|
|
@@ -15449,8 +15998,22 @@ function encodingConcurrency() {
|
|
|
15449
15998
|
if (!Number.isInteger(n) || n < 1 || n > 8) throw new Error("Invalid RILL_ENCODING_CONCURRENCY");
|
|
15450
15999
|
return n;
|
|
15451
16000
|
}
|
|
15452
|
-
async function withEncodingSlot(work) {
|
|
15453
|
-
|
|
16001
|
+
async function withEncodingSlot(work, signal) {
|
|
16002
|
+
signal?.throwIfAborted();
|
|
16003
|
+
if (active >= encodingConcurrency()) await new Promise((resolve, reject) => {
|
|
16004
|
+
const granted = () => {
|
|
16005
|
+
signal?.removeEventListener("abort", abort);
|
|
16006
|
+
resolve();
|
|
16007
|
+
};
|
|
16008
|
+
const abort = () => {
|
|
16009
|
+
const i = waiting.indexOf(granted);
|
|
16010
|
+
if (i >= 0) waiting.splice(i, 1);
|
|
16011
|
+
reject(new Error("encoding_aborted"));
|
|
16012
|
+
};
|
|
16013
|
+
waiting.push(granted);
|
|
16014
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
16015
|
+
if (signal?.aborted) abort();
|
|
16016
|
+
});
|
|
15454
16017
|
else active++;
|
|
15455
16018
|
try {
|
|
15456
16019
|
return await work();
|
|
@@ -15471,11 +16034,15 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15471
16034
|
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
15472
16035
|
}
|
|
15473
16036
|
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
15474
|
-
await
|
|
16037
|
+
await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
15475
16038
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
15476
16039
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
16040
|
+
signal?.throwIfAborted();
|
|
16041
|
+
const dimensions = `scale=${manifest.width}:${manifest.height}:force_original_aspect_ratio=decrease,pad=${manifest.width}:${manifest.height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
16042
|
+
const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
|
|
16043
|
+
const codec2 = output.endsWith(".mp4") ? ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-movflags", "+faststart", "-fps_mode", "cfr", "-t", String(Math.min(60, manifest.durationSeconds))] : ["-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-b:v", "0", "-crf", "32"];
|
|
15477
16044
|
await new Promise((resolve, reject) => {
|
|
15478
|
-
const child =
|
|
16045
|
+
const child = spawn5(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-vf", filters, ...codec2, "-threads", String(threads), "-y", output], { cwd: directory, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
|
|
15479
16046
|
let stderr = "";
|
|
15480
16047
|
child.stderr.on("data", (chunk) => {
|
|
15481
16048
|
stderr = (stderr + String(chunk)).slice(-2e3);
|
|
@@ -15483,6 +16050,7 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15483
16050
|
const abort = () => child.kill("SIGKILL");
|
|
15484
16051
|
const timer = setTimeout(abort, 20 * 6e4);
|
|
15485
16052
|
signal?.addEventListener("abort", abort, { once: true });
|
|
16053
|
+
if (signal?.aborted) abort();
|
|
15486
16054
|
const clean = () => {
|
|
15487
16055
|
clearTimeout(timer);
|
|
15488
16056
|
signal?.removeEventListener("abort", abort);
|
|
@@ -15496,12 +16064,12 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15496
16064
|
code === 0 ? resolve() : reject(new Error(`ffmpeg failed (${code}): ${stderr}`));
|
|
15497
16065
|
});
|
|
15498
16066
|
});
|
|
15499
|
-
});
|
|
16067
|
+
}, signal);
|
|
15500
16068
|
}
|
|
15501
16069
|
|
|
15502
16070
|
// ../recorder/src/screencast.ts
|
|
15503
|
-
import { mkdir as
|
|
15504
|
-
import { join as
|
|
16071
|
+
import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile6, open as open3 } from "fs/promises";
|
|
16072
|
+
import { join as join8 } from "path";
|
|
15505
16073
|
var ScreencastRecorder = class {
|
|
15506
16074
|
constructor(session, directory, width, height) {
|
|
15507
16075
|
this.session = session;
|
|
@@ -15509,6 +16077,10 @@ var ScreencastRecorder = class {
|
|
|
15509
16077
|
this.width = width;
|
|
15510
16078
|
this.height = height;
|
|
15511
16079
|
}
|
|
16080
|
+
session;
|
|
16081
|
+
directory;
|
|
16082
|
+
width;
|
|
16083
|
+
height;
|
|
15512
16084
|
frames = [];
|
|
15513
16085
|
pendingWrites = /* @__PURE__ */ new Set();
|
|
15514
16086
|
started = false;
|
|
@@ -15516,7 +16088,7 @@ var ScreencastRecorder = class {
|
|
|
15516
16088
|
frameBytes = 0;
|
|
15517
16089
|
captureError;
|
|
15518
16090
|
async start() {
|
|
15519
|
-
await
|
|
16091
|
+
await mkdir5(this.directory, { recursive: true, mode: 448 });
|
|
15520
16092
|
this.session.on("Page.screencastFrame", (payload) => {
|
|
15521
16093
|
const bytes = Buffer.from(payload.data, "base64");
|
|
15522
16094
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
@@ -15525,9 +16097,9 @@ var ScreencastRecorder = class {
|
|
|
15525
16097
|
return;
|
|
15526
16098
|
}
|
|
15527
16099
|
this.frameBytes += bytes.length;
|
|
15528
|
-
const path =
|
|
16100
|
+
const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
15529
16101
|
this.frameCounter += 1;
|
|
15530
|
-
const write =
|
|
16102
|
+
const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
|
|
15531
16103
|
this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
|
|
15532
16104
|
}).catch((error51) => {
|
|
15533
16105
|
this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
|
|
@@ -15541,7 +16113,7 @@ var ScreencastRecorder = class {
|
|
|
15541
16113
|
await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
|
|
15542
16114
|
this.started = true;
|
|
15543
16115
|
}
|
|
15544
|
-
async stop(outputPath, expectedDurationSeconds, captureOnly = false) {
|
|
16116
|
+
async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
|
|
15545
16117
|
if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
|
|
15546
16118
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
15547
16119
|
await Promise.all(this.pendingWrites);
|
|
@@ -15549,10 +16121,10 @@ var ScreencastRecorder = class {
|
|
|
15549
16121
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
15550
16122
|
const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
|
|
15551
16123
|
const frames = [];
|
|
15552
|
-
const bundle = await open3(
|
|
16124
|
+
const bundle = await open3(join8(this.directory, "frames.bin"), "w", 384);
|
|
15553
16125
|
try {
|
|
15554
16126
|
for (const frame of ordered) {
|
|
15555
|
-
const bytes = await
|
|
16127
|
+
const bytes = await readFile6(frame.path);
|
|
15556
16128
|
await bundle.writeFile(bytes);
|
|
15557
16129
|
frames.push({ timestamp: frame.timestamp, size: bytes.length });
|
|
15558
16130
|
}
|
|
@@ -15560,8 +16132,8 @@ var ScreencastRecorder = class {
|
|
|
15560
16132
|
await bundle.close();
|
|
15561
16133
|
}
|
|
15562
16134
|
const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
|
|
15563
|
-
await
|
|
15564
|
-
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
|
|
16135
|
+
await writeFile4(join8(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
|
|
16136
|
+
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath, signal);
|
|
15565
16137
|
return { frameCount: this.frames.length, outputPath };
|
|
15566
16138
|
}
|
|
15567
16139
|
};
|
|
@@ -15572,6 +16144,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15572
16144
|
recordingId;
|
|
15573
16145
|
startedAt = /* @__PURE__ */ new Date();
|
|
15574
16146
|
cdpUrl;
|
|
16147
|
+
tabId;
|
|
15575
16148
|
browser;
|
|
15576
16149
|
context;
|
|
15577
16150
|
page;
|
|
@@ -15585,6 +16158,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15585
16158
|
requestEvents = /* @__PURE__ */ new Map();
|
|
15586
16159
|
stopPromise;
|
|
15587
16160
|
maxTimer;
|
|
16161
|
+
maxDurationSeconds;
|
|
16162
|
+
removeClickCapture;
|
|
15588
16163
|
removeCursorOverlay;
|
|
15589
16164
|
diagnosticsPaused = false;
|
|
15590
16165
|
secretValues = [];
|
|
@@ -15595,9 +16170,13 @@ var RecordingSession = class _RecordingSession {
|
|
|
15595
16170
|
bodyBytes = 0;
|
|
15596
16171
|
truncated = false;
|
|
15597
16172
|
constructor(options, runtime) {
|
|
16173
|
+
this.maxDurationSeconds = options.maxDurationSeconds ?? 600;
|
|
15598
16174
|
this.encoding = options.encoding ?? "local";
|
|
16175
|
+
this.videoFormat = options.videoFormat ?? "webm";
|
|
16176
|
+
this.captureSignal = options.signal;
|
|
15599
16177
|
this.recordingId = options.recordingId;
|
|
15600
16178
|
this.cdpUrl = runtime.cdpUrl;
|
|
16179
|
+
this.tabId = runtime.tabId;
|
|
15601
16180
|
this.browser = runtime.browser;
|
|
15602
16181
|
this.context = runtime.context;
|
|
15603
16182
|
this.page = runtime.page;
|
|
@@ -15607,17 +16186,28 @@ var RecordingSession = class _RecordingSession {
|
|
|
15607
16186
|
this.outputDirectory = options.outputDirectory;
|
|
15608
16187
|
}
|
|
15609
16188
|
static async start(options) {
|
|
16189
|
+
validateTabSelection(options);
|
|
15610
16190
|
const width = options.width ?? 1280;
|
|
15611
16191
|
const height = options.height ?? 720;
|
|
15612
|
-
await
|
|
15613
|
-
if (options.encoding === "remote") await
|
|
15614
|
-
const runtime = options.cdpUrl ? await
|
|
15615
|
-
|
|
15616
|
-
|
|
16192
|
+
await mkdir6(options.outputDirectory, { recursive: true, mode: 448 });
|
|
16193
|
+
if (options.encoding === "remote") await writeFile5(join9(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
16194
|
+
const runtime = options.cdpUrl ? await connectRecordingTab(options.cdpUrl, options.outputDirectory, options.tabId) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer, blockServiceWorkers: options.blockServiceWorkers });
|
|
16195
|
+
let cdpSession;
|
|
16196
|
+
try {
|
|
16197
|
+
cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
16198
|
+
} catch (error51) {
|
|
16199
|
+
await runtime.browser.close().catch(() => void 0);
|
|
16200
|
+
if ("browserProcess" in runtime && runtime.browserProcess) await terminateBrowser(runtime.browserProcess);
|
|
16201
|
+
if ("profileDirectory" in runtime && runtime.profileDirectory) await rm5(runtime.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
16202
|
+
throw error51;
|
|
16203
|
+
}
|
|
16204
|
+
const screencast = new ScreencastRecorder(cdpSession, join9(options.outputDirectory, "frames"), width, height);
|
|
15617
16205
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
15618
16206
|
try {
|
|
15619
|
-
await session.attachDiagnostics(options.captureNetworkBodies ?? false);
|
|
15620
|
-
session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
|
|
16207
|
+
await session.attachDiagnostics(options.captureNetworkBodies ?? false, cdpSession);
|
|
16208
|
+
if (!options.hideCursor) session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
|
|
16209
|
+
await options.beforeCapture?.(runtime.page);
|
|
16210
|
+
session.startedAt = /* @__PURE__ */ new Date();
|
|
15621
16211
|
await screencast.start();
|
|
15622
16212
|
if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
15623
16213
|
} catch (error51) {
|
|
@@ -15626,7 +16216,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15626
16216
|
}
|
|
15627
16217
|
session.maxTimer = setTimeout(() => {
|
|
15628
16218
|
void session.stop("max_duration").catch(() => void 0);
|
|
15629
|
-
}, (
|
|
16219
|
+
}, Math.max(0, session.startedAt.getTime() + session.maxDurationSeconds * 1e3 - Date.now()));
|
|
15630
16220
|
return session;
|
|
15631
16221
|
}
|
|
15632
16222
|
// In-process agents share this connection so exposed diagnostic bindings are
|
|
@@ -15635,7 +16225,18 @@ var RecordingSession = class _RecordingSession {
|
|
|
15635
16225
|
return this.page;
|
|
15636
16226
|
}
|
|
15637
16227
|
status() {
|
|
15638
|
-
|
|
16228
|
+
const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
16229
|
+
return {
|
|
16230
|
+
recordingId: this.recordingId,
|
|
16231
|
+
cdpUrl: this.cdpUrl,
|
|
16232
|
+
...this.tabId ? { tabId: this.tabId } : {},
|
|
16233
|
+
startedAt: this.startedAt.toISOString(),
|
|
16234
|
+
maxDurationSeconds: this.maxDurationSeconds,
|
|
16235
|
+
elapsedSeconds,
|
|
16236
|
+
remainingSeconds: this.stopPromise ? 0 : Math.max(0, this.maxDurationSeconds - elapsedSeconds),
|
|
16237
|
+
stopsAt: new Date(this.startedAt.getTime() + this.maxDurationSeconds * 1e3).toISOString(),
|
|
16238
|
+
status: this.stopPromise ? "finalizing" : "recording"
|
|
16239
|
+
};
|
|
15639
16240
|
}
|
|
15640
16241
|
async stop(reason = "agent_request") {
|
|
15641
16242
|
this.stopPromise ??= this.finalize(reason);
|
|
@@ -15645,8 +16246,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15645
16246
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
15646
16247
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
15647
16248
|
const outputDirectory = this.outputDirectory;
|
|
15648
|
-
const videoPath =
|
|
15649
|
-
const diagnosticsPath =
|
|
16249
|
+
const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
|
|
16250
|
+
const diagnosticsPath = join9(outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
15650
16251
|
const bundle = {
|
|
15651
16252
|
schemaVersion: 1,
|
|
15652
16253
|
capturePolicyVersion: "safe-default-v1",
|
|
@@ -15663,24 +16264,27 @@ var RecordingSession = class _RecordingSession {
|
|
|
15663
16264
|
try {
|
|
15664
16265
|
const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
|
|
15665
16266
|
await writeBundle(safeBundle, diagnosticsPath);
|
|
15666
|
-
await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote");
|
|
16267
|
+
await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
|
|
15667
16268
|
} finally {
|
|
15668
|
-
await this.
|
|
16269
|
+
await this.removeClickCapture?.().catch(() => void 0);
|
|
16270
|
+
await this.removeCursorOverlay?.().catch(() => void 0);
|
|
16271
|
+
await this.browser.close().catch(() => void 0);
|
|
15669
16272
|
if (this.browserProcess) {
|
|
15670
|
-
await this.browser.close().catch(() => void 0);
|
|
15671
16273
|
await terminateBrowser(this.browserProcess);
|
|
15672
|
-
if (this.profileDirectory) await
|
|
16274
|
+
if (this.profileDirectory) await rm5(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15673
16275
|
}
|
|
15674
16276
|
}
|
|
15675
16277
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
15676
16278
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
15677
16279
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
15678
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory:
|
|
16280
|
+
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join9(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
15679
16281
|
}
|
|
15680
16282
|
captureNetworkBodies = false;
|
|
16283
|
+
captureSignal;
|
|
16284
|
+
videoFormat = "webm";
|
|
15681
16285
|
encoding = "local";
|
|
15682
16286
|
outputDirectory = "";
|
|
15683
|
-
async attachDiagnostics(captureNetworkBodies) {
|
|
16287
|
+
async attachDiagnostics(captureNetworkBodies, cdpSession) {
|
|
15684
16288
|
this.captureNetworkBodies = captureNetworkBodies;
|
|
15685
16289
|
const offset = () => Date.now() - this.startedAt.getTime();
|
|
15686
16290
|
const pushTimeline = (event) => {
|
|
@@ -15691,14 +16295,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15691
16295
|
}
|
|
15692
16296
|
this.timelineEvents.push({ id: `time_${this.timelineEvents.length + 1}`, ...event });
|
|
15693
16297
|
};
|
|
15694
|
-
await this.page
|
|
15695
|
-
await this.page.addInitScript(() => {
|
|
15696
|
-
document.addEventListener("click", (event) => {
|
|
15697
|
-
const target = event.target;
|
|
15698
|
-
const label = target?.innerText?.trim().slice(0, 80) || target?.getAttribute("aria-label") || target?.tagName || "Element";
|
|
15699
|
-
void window.__rillClick?.({ x: event.clientX, y: event.clientY, label });
|
|
15700
|
-
}, { capture: true });
|
|
15701
|
-
});
|
|
16298
|
+
this.removeClickCapture = await captureClicks(this.page, cdpSession, (payload) => pushTimeline({ offsetMs: offset(), type: "click", label: `${payload.label || "Element"} at ${Math.round(payload.x)},${Math.round(payload.y)}` }));
|
|
15702
16299
|
this.page.on("framenavigated", (frame) => {
|
|
15703
16300
|
if (frame === this.page.mainFrame()) pushTimeline({ offsetMs: offset(), type: "navigation", label: sanitizeUrl(frame.url()) });
|
|
15704
16301
|
});
|
|
@@ -15787,7 +16384,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15787
16384
|
};
|
|
15788
16385
|
async function launchManaged(options) {
|
|
15789
16386
|
const port = await getAvailablePort();
|
|
15790
|
-
const profileDirectory = await mkdtemp(
|
|
16387
|
+
const profileDirectory = await mkdtemp(join9(tmpdir2(), "rill-chrome-"));
|
|
15791
16388
|
const executable = findChromeExecutable();
|
|
15792
16389
|
const args = [
|
|
15793
16390
|
`--remote-debugging-port=${port}`,
|
|
@@ -15804,29 +16401,22 @@ async function launchManaged(options) {
|
|
|
15804
16401
|
...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
|
|
15805
16402
|
"about:blank"
|
|
15806
16403
|
];
|
|
15807
|
-
const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([
|
|
15808
|
-
const browserProcess =
|
|
16404
|
+
const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key2]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key2))) : process.env;
|
|
16405
|
+
const browserProcess = spawn6(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, windowsHide: true, env });
|
|
15809
16406
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15810
16407
|
try {
|
|
15811
16408
|
await waitForCdp(cdpUrl, browserProcess);
|
|
15812
|
-
const browser = await
|
|
15813
|
-
const context = browser.contexts()[0];
|
|
16409
|
+
const browser = await chromium2.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
|
|
16410
|
+
const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
|
|
15814
16411
|
const page = context.pages()[0] ?? await context.newPage();
|
|
15815
16412
|
await page.setViewportSize({ width: options.width, height: options.height });
|
|
15816
16413
|
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15817
16414
|
} catch (error51) {
|
|
15818
16415
|
await terminateBrowser(browserProcess);
|
|
15819
|
-
await
|
|
16416
|
+
await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15820
16417
|
throw error51;
|
|
15821
16418
|
}
|
|
15822
16419
|
}
|
|
15823
|
-
async function connectAttached(cdpUrl, outputDirectory) {
|
|
15824
|
-
const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
|
|
15825
|
-
const context = browser.contexts()[0];
|
|
15826
|
-
const page = context.pages()[0];
|
|
15827
|
-
if (!page) throw new Error("The attached browser has no page target to record.");
|
|
15828
|
-
return { cdpUrl, browser, context, page };
|
|
15829
|
-
}
|
|
15830
16420
|
async function getAvailablePort() {
|
|
15831
16421
|
const server = createServer();
|
|
15832
16422
|
await new Promise((resolve, reject) => server.listen(0, "127.0.0.1", resolve).once("error", reject));
|
|
@@ -15866,16 +16456,31 @@ async function waitForCdp(cdpUrl, browserProcess) {
|
|
|
15866
16456
|
var sessions = /* @__PURE__ */ new Map();
|
|
15867
16457
|
var lastActivity = Date.now();
|
|
15868
16458
|
async function runDaemon() {
|
|
15869
|
-
|
|
15870
|
-
await
|
|
15871
|
-
|
|
16459
|
+
if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
|
|
16460
|
+
const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
|
|
16461
|
+
const socketPath = identity?.pipe ?? daemonSocketPath();
|
|
16462
|
+
await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
|
|
16463
|
+
await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
15872
16464
|
await restoreRecoverableSessions();
|
|
15873
16465
|
await purgeExpiredCaptures();
|
|
15874
|
-
await unlink(socketPath).catch((error51) => {
|
|
16466
|
+
if (!identity) await unlink(socketPath).catch((error51) => {
|
|
15875
16467
|
if (error51.code !== "ENOENT") throw error51;
|
|
15876
16468
|
});
|
|
15877
|
-
const server = createServer2((request3, response) =>
|
|
15878
|
-
|
|
16469
|
+
const server = createServer2((request3, response) => {
|
|
16470
|
+
if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
|
|
16471
|
+
request3.resume();
|
|
16472
|
+
json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
|
|
16473
|
+
return;
|
|
16474
|
+
}
|
|
16475
|
+
void route(request3, response);
|
|
16476
|
+
});
|
|
16477
|
+
await new Promise((resolve, reject) => {
|
|
16478
|
+
server.once("error", reject);
|
|
16479
|
+
server.listen(socketPath, () => {
|
|
16480
|
+
if (identity) resolve();
|
|
16481
|
+
else void chmod(socketPath, 384).then(resolve, reject);
|
|
16482
|
+
});
|
|
16483
|
+
});
|
|
15879
16484
|
const idleTimer = setInterval(() => {
|
|
15880
16485
|
void (async () => {
|
|
15881
16486
|
await purgeExpiredCaptures();
|
|
@@ -15888,7 +16493,7 @@ async function runDaemon() {
|
|
|
15888
16493
|
const close = () => {
|
|
15889
16494
|
clearInterval(idleTimer);
|
|
15890
16495
|
server.close();
|
|
15891
|
-
void unlink(socketPath).catch(() => void 0);
|
|
16496
|
+
if (!identity) void unlink(socketPath).catch(() => void 0);
|
|
15892
16497
|
};
|
|
15893
16498
|
process.once("SIGTERM", close);
|
|
15894
16499
|
process.once("SIGINT", close);
|
|
@@ -15896,18 +16501,18 @@ async function runDaemon() {
|
|
|
15896
16501
|
async function route(request3, response) {
|
|
15897
16502
|
lastActivity = Date.now();
|
|
15898
16503
|
const url2 = new URL(request3.url ?? "/", "http://daemon.local");
|
|
15899
|
-
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
|
|
16504
|
+
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1"] });
|
|
15900
16505
|
if (request3.method === "POST" && url2.pathname === "/sessions") {
|
|
15901
16506
|
if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
|
|
15902
16507
|
try {
|
|
15903
16508
|
const input = await readJson(request3);
|
|
15904
16509
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15905
|
-
const outputDirectory =
|
|
16510
|
+
const outputDirectory = join10(recordingTempRoot(), input.recordingId);
|
|
15906
16511
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
15907
16512
|
const status = session.status();
|
|
15908
|
-
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
|
|
16513
|
+
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, tabId: status.tabId, startedAt: status.startedAt }, state: "recording" };
|
|
15909
16514
|
sessions.set(input.recordingId, entry);
|
|
15910
|
-
const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), (
|
|
16515
|
+
const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), Math.max(0, Date.parse(status.stopsAt) - Date.now()) + 250);
|
|
15911
16516
|
maxTimer.unref();
|
|
15912
16517
|
await persistState();
|
|
15913
16518
|
return json2(response, 201, session.status());
|
|
@@ -15942,14 +16547,21 @@ async function finalizeEntry(entry, reason) {
|
|
|
15942
16547
|
await persistState();
|
|
15943
16548
|
}
|
|
15944
16549
|
function serializeEntry(entry) {
|
|
15945
|
-
return {
|
|
16550
|
+
return {
|
|
16551
|
+
...entry.base,
|
|
16552
|
+
...entry.session?.status(),
|
|
16553
|
+
status: entry.state,
|
|
16554
|
+
...entry.state !== "recording" ? { remainingSeconds: 0, ...entry.result ? { elapsedSeconds: entry.result.durationSeconds } : {} } : {},
|
|
16555
|
+
result: entry.result,
|
|
16556
|
+
error: entry.error
|
|
16557
|
+
};
|
|
15946
16558
|
}
|
|
15947
16559
|
async function restoreRecoverableSessions() {
|
|
15948
16560
|
try {
|
|
15949
|
-
const value = JSON.parse(await
|
|
16561
|
+
const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
|
|
15950
16562
|
for (const [id, entry] of Object.entries(value)) {
|
|
15951
16563
|
if (entry.status !== "ready" && entry.status !== "failed") continue;
|
|
15952
|
-
sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
|
|
16564
|
+
sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, tabId: entry.tabId, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
|
|
15953
16565
|
}
|
|
15954
16566
|
} catch {
|
|
15955
16567
|
}
|
|
@@ -15957,9 +16569,9 @@ async function restoreRecoverableSessions() {
|
|
|
15957
16569
|
async function persistState() {
|
|
15958
16570
|
const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
|
|
15959
16571
|
const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
|
|
15960
|
-
await
|
|
16572
|
+
await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
|
|
15961
16573
|
`, { mode: 384 });
|
|
15962
|
-
await
|
|
16574
|
+
await rename2(temporary, daemonStatePath());
|
|
15963
16575
|
}
|
|
15964
16576
|
async function readJson(request3) {
|
|
15965
16577
|
const chunks = [];
|
|
@@ -15980,7 +16592,7 @@ async function purgeExpiredCaptures() {
|
|
|
15980
16592
|
const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
|
|
15981
16593
|
for (const id of removed) sessions.delete(id);
|
|
15982
16594
|
let changed = removed.length > 0;
|
|
15983
|
-
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await
|
|
16595
|
+
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat4(entry.result.frameDirectory).catch(() => null)) {
|
|
15984
16596
|
sessions.delete(id);
|
|
15985
16597
|
changed = true;
|
|
15986
16598
|
}
|
|
@@ -16025,7 +16637,7 @@ function emit(value, pretty = false) {
|
|
|
16025
16637
|
function failureValue(error51) {
|
|
16026
16638
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
16027
16639
|
const [candidate] = message.split(":", 1);
|
|
16028
|
-
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
|
|
16640
|
+
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "local_capture_failed", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
|
|
16029
16641
|
const structured = error51 && typeof error51 === "object" ? error51 : {};
|
|
16030
16642
|
const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
|
|
16031
16643
|
return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
|
|
@@ -16092,7 +16704,7 @@ async function withRunFeedback(session, start) {
|
|
|
16092
16704
|
}
|
|
16093
16705
|
|
|
16094
16706
|
// src/doctor.ts
|
|
16095
|
-
import { spawn as
|
|
16707
|
+
import { spawn as spawn7 } from "child_process";
|
|
16096
16708
|
|
|
16097
16709
|
// src/version.ts
|
|
16098
16710
|
import { createRequire } from "module";
|
|
@@ -16153,7 +16765,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16153
16765
|
credentialError = error51;
|
|
16154
16766
|
}
|
|
16155
16767
|
if (credentialError) {
|
|
16156
|
-
checks.push({ name: "authentication", status: "fail", message: errorMessage(credentialError), recovery: "Unlock the OS credential store, or use RILL_TOKEN. Windows
|
|
16768
|
+
checks.push({ name: "authentication", status: "fail", message: errorMessage(credentialError), recovery: "Unlock the OS credential store, or use RILL_TOKEN. On Windows, use the same Windows account that saved the credential." });
|
|
16157
16769
|
} else if (!token) {
|
|
16158
16770
|
checks.push({ name: "authentication", status: health.anonymousAccess ? "warn" : "fail", message: health.anonymousAccess ? "The first recording will create an anonymous identity in your OS credential store. Anonymous recordings expire after seven days." : "No Rill agent credential is available.", recovery: `Run \`rill --api-url ${apiUrl} login\` or set RILL_TOKEN.` });
|
|
16159
16771
|
} else {
|
|
@@ -16168,7 +16780,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16168
16780
|
}
|
|
16169
16781
|
checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
|
|
16170
16782
|
if (dependencies.encoding === "local") {
|
|
16171
|
-
checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion,
|
|
16783
|
+
checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion, dependencies.platform === "darwin" ? "Install ffmpeg with `brew install ffmpeg`, or set RILL_FFMPEG." : "Install ffmpeg using the system package manager, or set RILL_FFMPEG."));
|
|
16172
16784
|
} else {
|
|
16173
16785
|
checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
|
|
16174
16786
|
try {
|
|
@@ -16176,7 +16788,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16176
16788
|
if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
|
|
16177
16789
|
checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
|
|
16178
16790
|
} catch (error51) {
|
|
16179
|
-
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "
|
|
16791
|
+
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Run doctor --encoding local to check local FFmpeg, then record start --encoding local, or ask the workspace owner to enable remote encoding." });
|
|
16180
16792
|
}
|
|
16181
16793
|
}
|
|
16182
16794
|
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
@@ -16191,7 +16803,7 @@ async function executableCheck(name, path, args, run2, recovery) {
|
|
|
16191
16803
|
}
|
|
16192
16804
|
function commandVersion(command, args) {
|
|
16193
16805
|
return new Promise((resolve, reject) => {
|
|
16194
|
-
const child =
|
|
16806
|
+
const child = spawn7(command, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
16195
16807
|
const stdout = [];
|
|
16196
16808
|
const stderr = [];
|
|
16197
16809
|
const timeout = setTimeout(() => {
|
|
@@ -16252,16 +16864,24 @@ function formatCloudRun(run2) {
|
|
|
16252
16864
|
}
|
|
16253
16865
|
|
|
16254
16866
|
// src/recording-lifecycle.ts
|
|
16255
|
-
async function startRecordingWithCompensation(remote, dependencies) {
|
|
16867
|
+
async function startRecordingWithCompensation(remote, dependencies, apiUrl = "https://userill.dev") {
|
|
16256
16868
|
try {
|
|
16257
16869
|
return await dependencies.startLocal();
|
|
16258
16870
|
} catch (startupError) {
|
|
16259
16871
|
try {
|
|
16260
16872
|
await dependencies.cancelRemote(remote.recordingId, "local_start_failed");
|
|
16261
16873
|
} catch (cleanupError) {
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16874
|
+
const nextCommand = cliCommand(apiUrl, "record", "cancel", remote.recordingId);
|
|
16875
|
+
throw Object.assign(
|
|
16876
|
+
new AggregateError(
|
|
16877
|
+
[startupError, cleanupError],
|
|
16878
|
+
"browser_unavailable: Local startup failed and its remote reservation could not be cancelled."
|
|
16879
|
+
),
|
|
16880
|
+
{
|
|
16881
|
+
code: "browser_unavailable",
|
|
16882
|
+
recovery: `Run ${nextCommand} to release this failed startup reservation.`,
|
|
16883
|
+
details: { recordingId: remote.recordingId, nextCommand }
|
|
16884
|
+
}
|
|
16265
16885
|
);
|
|
16266
16886
|
}
|
|
16267
16887
|
throw startupError;
|
|
@@ -16279,11 +16899,62 @@ async function remoteAwareRecordingStatus(recordingId, dependencies, apiUrl = "h
|
|
|
16279
16899
|
try {
|
|
16280
16900
|
const local = await dependencies.localStatus(recordingId);
|
|
16281
16901
|
const status = local.status === "ready" ? "captured" : local.status;
|
|
16282
|
-
return {
|
|
16902
|
+
return {
|
|
16903
|
+
...local,
|
|
16904
|
+
status,
|
|
16905
|
+
source: "local",
|
|
16906
|
+
...recordingNextStep(apiUrl, recordingId, String(status)),
|
|
16907
|
+
...status === "failed" ? {
|
|
16908
|
+
nextCommand: cliCommand(apiUrl, "record", "cancel", recordingId),
|
|
16909
|
+
message: "Local capture failed; the remote reservation may still hold a slot. Preserve any local draft you need, then cancel this failed attempt to release its slot. Cancellation does not delete local drafts."
|
|
16910
|
+
} : {}
|
|
16911
|
+
};
|
|
16283
16912
|
} catch {
|
|
16284
16913
|
return remoteView;
|
|
16285
16914
|
}
|
|
16286
16915
|
}
|
|
16916
|
+
async function releaseFailedLocalCapture(id, dependencies) {
|
|
16917
|
+
const local = await dependencies.localStatus(id).catch(() => null);
|
|
16918
|
+
if (!local || local.recordingId !== id || local.status !== "failed" || local.result) return false;
|
|
16919
|
+
try {
|
|
16920
|
+
await dependencies.failRemote(id);
|
|
16921
|
+
return true;
|
|
16922
|
+
} catch {
|
|
16923
|
+
return false;
|
|
16924
|
+
}
|
|
16925
|
+
}
|
|
16926
|
+
async function createRecordingWithRecovery(dependencies) {
|
|
16927
|
+
try {
|
|
16928
|
+
return await dependencies.createRemote();
|
|
16929
|
+
} catch (error51) {
|
|
16930
|
+
if (!(error51 instanceof ControlPlaneError) || error51.code !== "quota_exceeded") throw error51;
|
|
16931
|
+
const quota = error51.details?.quota;
|
|
16932
|
+
if (!quota || !["credential_concurrency", "workspace_concurrency"].includes(String(quota.type)) || typeof quota.blockingRecordingId !== "string" || !/^rec_[a-zA-Z0-9_-]+$/.test(quota.blockingRecordingId)) throw error51;
|
|
16933
|
+
if (!await releaseFailedLocalCapture(quota.blockingRecordingId, dependencies)) throw error51;
|
|
16934
|
+
dependencies.onRecovered?.(quota.blockingRecordingId);
|
|
16935
|
+
return dependencies.createRemote();
|
|
16936
|
+
}
|
|
16937
|
+
}
|
|
16938
|
+
async function stopRecordingWithCompensation(id, dependencies, apiUrl) {
|
|
16939
|
+
let local;
|
|
16940
|
+
try {
|
|
16941
|
+
local = await dependencies.stopLocal(id);
|
|
16942
|
+
} catch (error51) {
|
|
16943
|
+
const known = await dependencies.localStatus(id).catch(() => null);
|
|
16944
|
+
if (!known || known.recordingId !== id || known.status !== "failed" || known.result) throw error51;
|
|
16945
|
+
local = known;
|
|
16946
|
+
}
|
|
16947
|
+
if (local.recordingId === id && local.status === "failed" && !local.result) {
|
|
16948
|
+
const released = await releaseFailedLocalCapture(id, dependencies);
|
|
16949
|
+
throw new ControlPlaneError(
|
|
16950
|
+
"local_capture_failed",
|
|
16951
|
+
local.error ?? "The local capture could not be finalized.",
|
|
16952
|
+
released ? "Its unused recording slot was released. Start a new recording." : `The recording slot could not be released automatically. Check ${cliCommand(apiUrl, "record", "status", id)} before cancelling it.`,
|
|
16953
|
+
{ recordingId: id, reservationReleased: released }
|
|
16954
|
+
);
|
|
16955
|
+
}
|
|
16956
|
+
return local;
|
|
16957
|
+
}
|
|
16287
16958
|
|
|
16288
16959
|
// src/config.ts
|
|
16289
16960
|
var PRODUCTION_API_URL = "https://userill.dev";
|
|
@@ -16295,10 +16966,10 @@ function controlPlaneUrl(environmentUrl) {
|
|
|
16295
16966
|
async function removeLocalCapture(id) {
|
|
16296
16967
|
await ensureDaemon(process.argv[1]);
|
|
16297
16968
|
const local = await recordingStatus(id).catch(() => null);
|
|
16298
|
-
if (local?.result?.frameDirectory) await
|
|
16969
|
+
if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16299
16970
|
}
|
|
16300
16971
|
function watchEncoding(apiUrl, id) {
|
|
16301
|
-
const child =
|
|
16972
|
+
const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
|
|
16302
16973
|
child.on("error", () => void 0);
|
|
16303
16974
|
child.unref();
|
|
16304
16975
|
}
|
|
@@ -16320,7 +16991,7 @@ async function authorize(quiet = false) {
|
|
|
16320
16991
|
process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
|
|
16321
16992
|
`);
|
|
16322
16993
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
16323
|
-
const child =
|
|
16994
|
+
const child = spawn8(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
|
|
16324
16995
|
child.on("error", () => void 0);
|
|
16325
16996
|
child.unref();
|
|
16326
16997
|
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
@@ -16387,9 +17058,14 @@ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<r
|
|
|
16387
17058
|
if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
|
|
16388
17059
|
else process.stdout.write(formatCloudRun(run2));
|
|
16389
17060
|
});
|
|
16390
|
-
record2.command("
|
|
17061
|
+
record2.command("tabs").requiredOption("--cdp-url <url>", "Existing Chromium CDP endpoint").action(async (commandOptions) => {
|
|
17062
|
+
const options = program.opts();
|
|
17063
|
+
emit({ schemaVersion: 1, tabs: await listBrowserTabs(commandOptions.cdpUrl) }, options.pretty);
|
|
17064
|
+
});
|
|
17065
|
+
record2.command("start").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
|
|
16391
17066
|
const options = program.opts();
|
|
16392
17067
|
assertRecorderTransport();
|
|
17068
|
+
validateTabSelection(commandOptions);
|
|
16393
17069
|
await ensureAnonymousCredential(options.apiUrl);
|
|
16394
17070
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16395
17071
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
@@ -16398,15 +17074,24 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16398
17074
|
if (!Number.isFinite(Number(commandOptions.maxDuration)) || Number(commandOptions.maxDuration) < 1 || Number(commandOptions.maxDuration) > 600) throw new Error("Recording duration must be between 1 and 600 seconds.");
|
|
16399
17075
|
const width = Number(commandOptions.width);
|
|
16400
17076
|
const height = Number(commandOptions.height);
|
|
16401
|
-
const
|
|
17077
|
+
const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height };
|
|
17078
|
+
const remote = await createRecordingWithRecovery({
|
|
17079
|
+
createRemote: () => client.createRecording(metadata),
|
|
17080
|
+
localStatus: recordingStatus,
|
|
17081
|
+
failRemote: (id) => client.failLocalCapture(id),
|
|
17082
|
+
onRecovered: (id) => {
|
|
17083
|
+
process.stderr.write(`Released the unused slot for failed local capture ${id}.
|
|
17084
|
+
`);
|
|
17085
|
+
}
|
|
17086
|
+
});
|
|
16402
17087
|
rememberRecording(remote.recordingId);
|
|
16403
17088
|
const local = await startRecordingWithCompensation(remote, {
|
|
16404
17089
|
startLocal: async () => {
|
|
16405
|
-
await ensureDaemon(process.argv[1]);
|
|
16406
|
-
return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
|
|
17090
|
+
await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl));
|
|
17091
|
+
return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
|
|
16407
17092
|
},
|
|
16408
17093
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
16409
|
-
});
|
|
17094
|
+
}, options.apiUrl);
|
|
16410
17095
|
await client.markRecordingStarted(remote.recordingId, {
|
|
16411
17096
|
cliVersion: CLI_VERSION,
|
|
16412
17097
|
os: process.platform,
|
|
@@ -16414,16 +17099,20 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16414
17099
|
headed: Boolean(commandOptions.headed),
|
|
16415
17100
|
attached: Boolean(commandOptions.cdpUrl)
|
|
16416
17101
|
}).catch(() => void 0);
|
|
16417
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl), bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
17102
|
+
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
16418
17103
|
});
|
|
16419
17104
|
emit(result, options.pretty);
|
|
16420
17105
|
});
|
|
16421
17106
|
record2.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback", "Disable feedback only when the user explicitly requests it").action(async (recordingId, commandOptions) => {
|
|
16422
17107
|
const options = program.opts();
|
|
16423
17108
|
await ensureDaemon(process.argv[1]);
|
|
16424
|
-
const local = await stopRecording(recordingId);
|
|
16425
|
-
if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
|
|
16426
17109
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
17110
|
+
const local = await stopRecordingWithCompensation(recordingId, {
|
|
17111
|
+
stopLocal: stopRecording,
|
|
17112
|
+
localStatus: recordingStatus,
|
|
17113
|
+
failRemote: (id) => client.failLocalCapture(id)
|
|
17114
|
+
}, options.apiUrl);
|
|
17115
|
+
if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
|
|
16427
17116
|
const stopped = await client.markRecordingStopped(recordingId, {
|
|
16428
17117
|
durationSeconds: local.result.durationSeconds,
|
|
16429
17118
|
stoppedReason: local.result.stoppedReason,
|
|
@@ -16435,11 +17124,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16435
17124
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
16436
17125
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16437
17126
|
emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16438
|
-
if (result2.status === "ready") await
|
|
17127
|
+
if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16439
17128
|
else watchEncoding(options.apiUrl, recordingId);
|
|
16440
17129
|
return;
|
|
16441
17130
|
}
|
|
16442
|
-
const video = await
|
|
17131
|
+
const video = await stat5(local.result.videoPath);
|
|
16443
17132
|
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
|
|
16444
17133
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
16445
17134
|
const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
@@ -16457,7 +17146,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16457
17146
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
16458
17147
|
};
|
|
16459
17148
|
emit(result, options.pretty);
|
|
16460
|
-
if (remote) await
|
|
17149
|
+
if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16461
17150
|
});
|
|
16462
17151
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
16463
17152
|
const options = program.opts();
|
|
@@ -16489,7 +17178,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
|
|
|
16489
17178
|
if (result.status === "ready") {
|
|
16490
17179
|
await ensureDaemon(process.argv[1]);
|
|
16491
17180
|
const local = await recordingStatus(id).catch(() => null);
|
|
16492
|
-
if (local?.result?.frameDirectory) await
|
|
17181
|
+
if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16493
17182
|
}
|
|
16494
17183
|
});
|
|
16495
17184
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
@@ -16544,7 +17233,7 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
|
|
|
16544
17233
|
const options = program.opts();
|
|
16545
17234
|
await ensureAnonymousCredential(options.apiUrl);
|
|
16546
17235
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16547
|
-
const file2 = await
|
|
17236
|
+
const file2 = await stat5(videoFile);
|
|
16548
17237
|
const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
|
|
16549
17238
|
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
|
|
16550
17239
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
@@ -16573,20 +17262,20 @@ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>"
|
|
|
16573
17262
|
var drafts = program.command("drafts");
|
|
16574
17263
|
drafts.command("list").action(async () => {
|
|
16575
17264
|
const options = program.opts();
|
|
16576
|
-
const root = process.env.RILL_TEMP ??
|
|
17265
|
+
const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
|
|
16577
17266
|
const names = await readdir2(root).catch(() => []);
|
|
16578
17267
|
emit({ schemaVersion: 1, drafts: names }, options.pretty);
|
|
16579
17268
|
});
|
|
16580
17269
|
drafts.command("purge").action(async () => {
|
|
16581
17270
|
const options = program.opts();
|
|
16582
|
-
const root = process.env.RILL_TEMP ??
|
|
17271
|
+
const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
|
|
16583
17272
|
const names = await readdir2(root).catch(() => []);
|
|
16584
17273
|
let purged = 0;
|
|
16585
17274
|
for (const name of names) {
|
|
16586
|
-
const path =
|
|
16587
|
-
const metadata = await
|
|
17275
|
+
const path = join11(root, name);
|
|
17276
|
+
const metadata = await stat5(path);
|
|
16588
17277
|
if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
|
|
16589
|
-
await
|
|
17278
|
+
await rm7(path, { recursive: true, force: true });
|
|
16590
17279
|
purged += 1;
|
|
16591
17280
|
}
|
|
16592
17281
|
}
|