@runalabs/rill-cli 0.1.12 → 0.1.14
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 +61 -10
- package/dist/cli.js +994 -346
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,11 +5,128 @@ 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
|
+
|
|
90
|
+
// src/agent-session.ts
|
|
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";
|
|
95
|
+
|
|
96
|
+
// src/recording-guidance.ts
|
|
97
|
+
function cliCommandForPlatform(platform, apiUrl, ...args) {
|
|
98
|
+
const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
99
|
+
return ["rill", "--api-url", apiUrl, ...args].map(quote).join(" ");
|
|
100
|
+
}
|
|
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." };
|
|
108
|
+
}
|
|
109
|
+
function assertRecorderTransport(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
|
|
110
|
+
const check2 = recorderTransportCheck(platform, socketOverride);
|
|
111
|
+
if (check2.status === "fail") throw Object.assign(new Error(`unsupported_platform: ${check2.message}`), { code: "unsupported_platform", recovery: check2.recovery });
|
|
112
|
+
}
|
|
113
|
+
function recordingNextStep(apiUrl, recordingId, status, options = {}) {
|
|
114
|
+
if (status === "uploading" && options.resumableFrames) return { nextCommand: cliCommand(apiUrl, "record", "retry", recordingId, "--no-wait"), message: "Frame upload is incomplete. If the uploader is still running, let it finish. If it exited or was interrupted, run nextCommand to resume missing parts and finalize the upload. Do not start a second uploader while the first is active." };
|
|
115
|
+
if (status === "captured") return { nextCommand: cliCommand(apiUrl, "record", "stop", recordingId, "--no-wait"), message: "Captured locally; upload required. This is not a playable share yet." };
|
|
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." };
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
function browserAttachment(cdpUrl) {
|
|
120
|
+
return {
|
|
121
|
+
protocol: "cdp",
|
|
122
|
+
cdpUrl,
|
|
123
|
+
playwrightMethod: "chromium.connectOverCDP",
|
|
124
|
+
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
|
+
shellFallback: "See the shell-only Playwright example in the Rill CLI README; install playwright-core in a temporary directory outside the project."
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
8
129
|
// 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";
|
|
13
130
|
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
14
131
|
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15
132
|
var AgentAuthError = class extends Error {
|
|
@@ -17,6 +134,7 @@ var AgentAuthError = class extends Error {
|
|
|
17
134
|
super(`${code}: ${message}`);
|
|
18
135
|
this.code = code;
|
|
19
136
|
}
|
|
137
|
+
code;
|
|
20
138
|
};
|
|
21
139
|
function parseAuthorization(value) {
|
|
22
140
|
if (!value?.startsWith("{")) return null;
|
|
@@ -32,7 +150,7 @@ async function request(apiUrl, path, body, form = false) {
|
|
|
32
150
|
redirect: "error",
|
|
33
151
|
signal: AbortSignal.timeout(15e3),
|
|
34
152
|
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
35
|
-
body: form ? new URLSearchParams(Object.entries(body).map(([
|
|
153
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
|
|
36
154
|
});
|
|
37
155
|
const value = await response.json();
|
|
38
156
|
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
@@ -52,17 +170,7 @@ async function loadAgentCredential(apiUrl) {
|
|
|
52
170
|
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
53
171
|
const inFlight = pending.get(apiUrl);
|
|
54
172
|
if (inFlight) return inFlight;
|
|
55
|
-
const promise2 = (
|
|
56
|
-
const raw = await loadStoredCredential(apiUrl);
|
|
57
|
-
const saved = parseAuthorization(raw);
|
|
58
|
-
if (!saved) return raw;
|
|
59
|
-
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
60
|
-
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
61
|
-
if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
62
|
-
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
63
|
-
await save(apiUrl, withTokens(saved, tokens));
|
|
64
|
-
return tokens.access_token;
|
|
65
|
-
})();
|
|
173
|
+
const promise2 = withAuthorizationLock(apiUrl, () => readAgentCredential(apiUrl));
|
|
66
174
|
pending.set(apiUrl, promise2);
|
|
67
175
|
try {
|
|
68
176
|
return await promise2;
|
|
@@ -70,20 +178,35 @@ async function loadAgentCredential(apiUrl) {
|
|
|
70
178
|
pending.delete(apiUrl);
|
|
71
179
|
}
|
|
72
180
|
}
|
|
181
|
+
async function readAgentCredential(apiUrl) {
|
|
182
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
183
|
+
const raw = await loadStoredCredential(apiUrl);
|
|
184
|
+
const saved = parseAuthorization(raw);
|
|
185
|
+
if (!saved) return raw;
|
|
186
|
+
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
187
|
+
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
188
|
+
if (!saved.identityAssertion) throw new Error(saved.connection ? `Connection is ${saved.connection.status}. Run ${cliCommand(apiUrl, "connect", "status", saved.connection.id)}.` : "Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
189
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
190
|
+
await save(apiUrl, withTokens(saved, tokens));
|
|
191
|
+
return tokens.access_token;
|
|
192
|
+
}
|
|
73
193
|
async function ensureAnonymousCredential(apiUrl) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
194
|
+
return withAuthorizationLock(apiUrl, () => createAnonymousCredential(apiUrl));
|
|
195
|
+
}
|
|
196
|
+
async function withAuthorizationLock(apiUrl, action) {
|
|
197
|
+
const root = join2(homedir2(), ".rill", "auth-locks");
|
|
198
|
+
await mkdir2(root, { recursive: true, mode: 448 });
|
|
199
|
+
const lock = join2(root, createHash2("sha256").update(apiUrl).digest("hex"));
|
|
77
200
|
const deadline = Date.now() + 3e4;
|
|
78
201
|
while (true) {
|
|
79
202
|
try {
|
|
80
|
-
await
|
|
203
|
+
await mkdir2(lock, { mode: 448 });
|
|
81
204
|
break;
|
|
82
205
|
} catch (error51) {
|
|
83
206
|
if (error51.code !== "EEXIST") throw error51;
|
|
84
|
-
const info = await
|
|
207
|
+
const info = await stat2(lock).catch(() => null);
|
|
85
208
|
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
86
|
-
await
|
|
209
|
+
await rm2(lock, { recursive: true, force: true });
|
|
87
210
|
continue;
|
|
88
211
|
}
|
|
89
212
|
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
@@ -91,13 +214,13 @@ async function ensureAnonymousCredential(apiUrl) {
|
|
|
91
214
|
}
|
|
92
215
|
}
|
|
93
216
|
try {
|
|
94
|
-
return await
|
|
217
|
+
return await action();
|
|
95
218
|
} finally {
|
|
96
|
-
await
|
|
219
|
+
await rm2(lock, { recursive: true, force: true });
|
|
97
220
|
}
|
|
98
221
|
}
|
|
99
222
|
async function createAnonymousCredential(apiUrl) {
|
|
100
|
-
const existing = await
|
|
223
|
+
const existing = await readAgentCredential(apiUrl);
|
|
101
224
|
if (existing) return existing;
|
|
102
225
|
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
103
226
|
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
@@ -106,11 +229,29 @@ async function createAnonymousCredential(apiUrl) {
|
|
|
106
229
|
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
107
230
|
await save(apiUrl, saved);
|
|
108
231
|
process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
|
|
109
|
-
return await
|
|
232
|
+
return await readAgentCredential(apiUrl);
|
|
110
233
|
}
|
|
111
|
-
async function connectAgent(apiUrl, email3) {
|
|
234
|
+
async function connectAgent(apiUrl, email3, options = {}) {
|
|
235
|
+
let result = await withAuthorizationLock(apiUrl, () => beginConnection(apiUrl, email3));
|
|
236
|
+
if (options.wait === false) return result;
|
|
237
|
+
while (result.status === "pending") {
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, result.retryAfterSeconds * 1e3));
|
|
239
|
+
result = await pollConnection(apiUrl, result.authorizationId);
|
|
240
|
+
}
|
|
241
|
+
if (result.status === "expired") throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again using the same saved identity.");
|
|
242
|
+
if (result.status === "denied") throw new AgentAuthError("access_denied", "Connection request was denied or revoked. Run rill connect again only when ready to request approval.");
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
async function beginConnection(apiUrl, email3) {
|
|
112
246
|
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
113
247
|
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
248
|
+
if (saved?.connection?.status === "pending" && saved.connection.expiresAt > Date.now()) {
|
|
249
|
+
if (saved.connection.email !== email3) throw new Error("A connection for another email is pending. Let it expire before starting a different connection.");
|
|
250
|
+
process.stderr.write(`Open ${saved.connection.verificationUri}
|
|
251
|
+
Sign in as ${email3} and enter ${saved.connection.userCode}. Approve only the request you started.
|
|
252
|
+
`);
|
|
253
|
+
return connectionView(apiUrl, saved);
|
|
254
|
+
}
|
|
114
255
|
let claim;
|
|
115
256
|
if (saved?.claimToken && saved.anonymous) {
|
|
116
257
|
try {
|
|
@@ -130,32 +271,72 @@ async function connectAgent(apiUrl, email3) {
|
|
|
130
271
|
process.stderr.write(`Open ${claim.verification_uri}
|
|
131
272
|
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
132
273
|
`);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
274
|
+
saved = { ...saved, connection: {
|
|
275
|
+
id: randomBytes(16).toString("hex"),
|
|
276
|
+
email: email3,
|
|
277
|
+
verificationUri: claim.verification_uri,
|
|
278
|
+
userCode: claim.user_code,
|
|
279
|
+
expiresAt: Date.now() + claim.expires_in * 1e3,
|
|
280
|
+
interval: claim.interval,
|
|
281
|
+
nextPollAt: Date.now() + claim.interval * 1e3,
|
|
282
|
+
status: "pending"
|
|
283
|
+
} };
|
|
284
|
+
await save(apiUrl, saved);
|
|
285
|
+
return connectionView(apiUrl, saved);
|
|
286
|
+
}
|
|
287
|
+
function connectionView(apiUrl, saved) {
|
|
288
|
+
const connection = saved.connection;
|
|
289
|
+
return {
|
|
290
|
+
status: connection.status,
|
|
291
|
+
authorizationId: connection.id,
|
|
292
|
+
apiUrl,
|
|
293
|
+
expiresAt: connection.status === "authenticated" ? saved.assertionExpires : new Date(connection.expiresAt).toISOString(),
|
|
294
|
+
retryAfterSeconds: connection.status === "pending" ? Math.max(0, Math.ceil((connection.nextPollAt - Date.now()) / 1e3)) : 0,
|
|
295
|
+
...connection.status === "pending" ? {
|
|
296
|
+
verificationUri: connection.verificationUri,
|
|
297
|
+
userCode: connection.userCode,
|
|
298
|
+
nextCommand: cliCommand(apiUrl, "connect", "status", connection.id)
|
|
299
|
+
} : connection.status === "authenticated" ? {} : { nextCommand: cliCommand(apiUrl, "connect", "--email", connection.email) }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function pollConnection(apiUrl, authorizationId) {
|
|
303
|
+
return withAuthorizationLock(apiUrl, () => pollSavedConnection(apiUrl, authorizationId));
|
|
304
|
+
}
|
|
305
|
+
async function pollSavedConnection(apiUrl, authorizationId) {
|
|
306
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before polling a local connection.");
|
|
307
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
308
|
+
if (!saved?.connection || saved.connection.id !== authorizationId) throw new AgentAuthError("authorization_not_found", "No matching saved connection for this API URL. Use the latest connect response.");
|
|
309
|
+
const connection = saved.connection;
|
|
310
|
+
if (connection.status !== "pending") return connectionView(apiUrl, saved);
|
|
311
|
+
if (Date.now() >= connection.expiresAt) {
|
|
312
|
+
connection.status = "expired";
|
|
313
|
+
await save(apiUrl, saved);
|
|
314
|
+
return connectionView(apiUrl, saved);
|
|
315
|
+
}
|
|
316
|
+
if (Date.now() < connection.nextPollAt) return connectionView(apiUrl, saved);
|
|
317
|
+
connection.nextPollAt = Date.now() + connection.interval * 1e3;
|
|
318
|
+
await save(apiUrl, saved);
|
|
319
|
+
try {
|
|
320
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
321
|
+
saved = withTokens({ version: 1, anonymous: false, connection: { ...connection, status: "authenticated", userCode: "" } }, tokens);
|
|
322
|
+
} catch (error51) {
|
|
323
|
+
if (!(error51 instanceof AgentAuthError)) throw error51;
|
|
324
|
+
if (error51.code === "expired_token" || error51.code === "claim_expired") connection.status = "expired";
|
|
325
|
+
else if (error51.code === "access_denied" || error51.code === "claim_revoked") connection.status = "denied";
|
|
326
|
+
else if (error51.code === "slow_down") {
|
|
327
|
+
connection.interval += 5;
|
|
328
|
+
connection.nextPollAt = Date.now() + connection.interval * 1e3;
|
|
329
|
+
} else if (error51.code !== "authorization_pending") throw error51;
|
|
151
330
|
}
|
|
152
|
-
|
|
331
|
+
await save(apiUrl, saved);
|
|
332
|
+
return connectionView(apiUrl, saved);
|
|
153
333
|
}
|
|
154
334
|
|
|
155
335
|
// src/keychain.ts
|
|
156
|
-
import { spawn } from "child_process";
|
|
336
|
+
import { spawn as spawn2 } from "child_process";
|
|
157
337
|
var service = "rill";
|
|
158
338
|
async function saveCredential(apiUrl, token) {
|
|
339
|
+
if (process.platform === "win32") return saveWindowsCredential(apiUrl, token);
|
|
159
340
|
if (process.platform === "darwin") {
|
|
160
341
|
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
161
342
|
return;
|
|
@@ -170,6 +351,7 @@ function loadCredential(apiUrl) {
|
|
|
170
351
|
return loadAgentCredential(apiUrl);
|
|
171
352
|
}
|
|
172
353
|
async function loadStoredCredential(apiUrl) {
|
|
354
|
+
if (process.platform === "win32") return loadWindowsCredential(apiUrl);
|
|
173
355
|
try {
|
|
174
356
|
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
175
357
|
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
@@ -183,7 +365,7 @@ async function loadStoredCredential(apiUrl) {
|
|
|
183
365
|
}
|
|
184
366
|
function run(command, args, input) {
|
|
185
367
|
return new Promise((resolve, reject) => {
|
|
186
|
-
const child =
|
|
368
|
+
const child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
187
369
|
const stdout = [];
|
|
188
370
|
const stderr = [];
|
|
189
371
|
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
@@ -197,7 +379,7 @@ function run(command, args, input) {
|
|
|
197
379
|
|
|
198
380
|
// src/control-plane.ts
|
|
199
381
|
import { createReadStream } from "fs";
|
|
200
|
-
import { readFile, stat as
|
|
382
|
+
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
201
383
|
import { Upload } from "tus-js-client";
|
|
202
384
|
var ControlPlaneError = class extends Error {
|
|
203
385
|
constructor(code, message, recovery, details, httpStatus) {
|
|
@@ -208,6 +390,10 @@ var ControlPlaneError = class extends Error {
|
|
|
208
390
|
this.httpStatus = httpStatus;
|
|
209
391
|
this.name = "ControlPlaneError";
|
|
210
392
|
}
|
|
393
|
+
code;
|
|
394
|
+
recovery;
|
|
395
|
+
details;
|
|
396
|
+
httpStatus;
|
|
211
397
|
};
|
|
212
398
|
var ControlPlaneHttpError = class extends Error {
|
|
213
399
|
constructor(httpStatus) {
|
|
@@ -215,12 +401,18 @@ var ControlPlaneHttpError = class extends Error {
|
|
|
215
401
|
this.httpStatus = httpStatus;
|
|
216
402
|
this.name = "ControlPlaneHttpError";
|
|
217
403
|
}
|
|
404
|
+
httpStatus;
|
|
218
405
|
};
|
|
219
406
|
var ControlPlaneClient = class {
|
|
220
407
|
constructor(apiUrl, token) {
|
|
221
408
|
this.apiUrl = apiUrl;
|
|
222
409
|
this.token = token;
|
|
223
410
|
}
|
|
411
|
+
apiUrl;
|
|
412
|
+
token;
|
|
413
|
+
recordingStatusCommand(id) {
|
|
414
|
+
return cliCommand(this.apiUrl, "record", "status", id);
|
|
415
|
+
}
|
|
224
416
|
encodingCapabilities() {
|
|
225
417
|
return this.request("/api/encoding/capabilities");
|
|
226
418
|
}
|
|
@@ -299,17 +491,18 @@ var ControlPlaneClient = class {
|
|
|
299
491
|
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
300
492
|
}
|
|
301
493
|
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
302
|
-
process.stderr.write(
|
|
494
|
+
process.stderr.write("\rUpload complete; waiting for playable video. ");
|
|
303
495
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
304
496
|
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
305
497
|
}
|
|
306
|
-
throw new
|
|
498
|
+
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) });
|
|
307
499
|
}
|
|
308
500
|
async uploadVideo(uploadUrl, filePath) {
|
|
309
|
-
const file2 = await
|
|
501
|
+
const file2 = await stat3(filePath);
|
|
310
502
|
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
311
503
|
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
312
504
|
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
505
|
+
process.stderr.write("Upload complete; playback processing may continue.\n");
|
|
313
506
|
return;
|
|
314
507
|
}
|
|
315
508
|
await new Promise((resolve, reject) => {
|
|
@@ -324,10 +517,10 @@ var ControlPlaneClient = class {
|
|
|
324
517
|
});
|
|
325
518
|
upload.start();
|
|
326
519
|
});
|
|
327
|
-
process.stderr.write("\n");
|
|
520
|
+
process.stderr.write("\nUpload complete; playback processing may continue.\n");
|
|
328
521
|
}
|
|
329
522
|
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
330
|
-
const body = await
|
|
523
|
+
const body = await readFile2(filePath);
|
|
331
524
|
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 });
|
|
332
525
|
}
|
|
333
526
|
listRecordings() {
|
|
@@ -339,18 +532,49 @@ var ControlPlaneClient = class {
|
|
|
339
532
|
pollDeviceAuthorization(deviceCode) {
|
|
340
533
|
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
341
534
|
}
|
|
342
|
-
inspect(shareUrl) {
|
|
535
|
+
async inspect(shareUrl, cursor) {
|
|
343
536
|
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
344
537
|
const url2 = new URL(shareUrl);
|
|
345
538
|
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
346
539
|
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
347
|
-
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context
|
|
540
|
+
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: await this.reviewHeaders(url2.origin) }).then(async (response) => {
|
|
348
541
|
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
349
542
|
return response.json();
|
|
350
543
|
});
|
|
351
544
|
}
|
|
545
|
+
async reviewHeaders(origin) {
|
|
546
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
547
|
+
if (origin !== new URL(this.apiUrl).origin) return headers;
|
|
548
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
549
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
550
|
+
this.identifyAgent(headers);
|
|
551
|
+
const id = process.env.RILL_AGENT_ID;
|
|
552
|
+
if (id && /^[a-zA-Z0-9_-]{8,80}$/.test(id)) headers.set("x-rill-agent-id", id);
|
|
553
|
+
if (id && process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1") headers.set("x-rill-analytics", "1");
|
|
554
|
+
return headers;
|
|
555
|
+
}
|
|
556
|
+
identifyAgent(headers) {
|
|
557
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.RILL_ANONYMOUS_ANALYTICS === "0") headers.set("x-rill-analytics", "0");
|
|
558
|
+
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);
|
|
559
|
+
if (client) headers.set("x-rill-agent-client", client.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 80));
|
|
560
|
+
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));
|
|
561
|
+
}
|
|
562
|
+
reviewPath(shareUrl) {
|
|
563
|
+
const url2 = new URL(shareUrl);
|
|
564
|
+
if (url2.origin !== new URL(this.apiUrl).origin) throw new Error("The share URL must match --api-url before sending agent credentials.");
|
|
565
|
+
const match = /^\/s\/([^/]+)\/?$/.exec(url2.pathname);
|
|
566
|
+
if (!match) throw new Error("Expected a Rill /s/<slug> review link.");
|
|
567
|
+
return `/api/review/shares/${encodeURIComponent(decodeURIComponent(match[1]))}`;
|
|
568
|
+
}
|
|
569
|
+
reply(shareUrl, discussionId, body, requestId) {
|
|
570
|
+
return this.request(`${this.reviewPath(shareUrl)}/discussions/${encodeURIComponent(discussionId)}/replies`, { method: "POST", body: JSON.stringify({ body, requestId }) });
|
|
571
|
+
}
|
|
572
|
+
editReply(shareUrl, commentId, body) {
|
|
573
|
+
return this.request(`${this.reviewPath(shareUrl)}/comments/${encodeURIComponent(commentId)}`, { method: "PATCH", body: JSON.stringify({ body }) });
|
|
574
|
+
}
|
|
352
575
|
async request(path, init = {}, authenticated = true) {
|
|
353
576
|
const headers = new Headers(init.headers);
|
|
577
|
+
if (authenticated) this.identifyAgent(headers);
|
|
354
578
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
355
579
|
if (authenticated) {
|
|
356
580
|
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
@@ -360,7 +584,11 @@ var ControlPlaneClient = class {
|
|
|
360
584
|
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
361
585
|
const value = await response.json().catch(() => null);
|
|
362
586
|
if (!response.ok && value?.error?.code && value.error.message) {
|
|
363
|
-
|
|
587
|
+
const quota = value.error.details?.quota;
|
|
588
|
+
const blockingId = quota?.blockingRecordingId;
|
|
589
|
+
const concurrency = quota?.type === "credential_concurrency" || quota?.type === "workspace_concurrency";
|
|
590
|
+
const recovery = value.error.code === "quota_exceeded" && concurrency && blockingId ? `Run ${cliCommand(this.apiUrl, "record", "status", blockingId)}. Uploading and processing recordings hold a concurrency slot: wait for ready. If captured locally, run record stop to upload. Cancel only an abandoned recording.` : value.error.recovery;
|
|
591
|
+
throw new ControlPlaneError(value.error.code, value.error.message, recovery, value.error.details, response.status);
|
|
364
592
|
}
|
|
365
593
|
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
366
594
|
return value;
|
|
@@ -386,8 +614,8 @@ async function whoami(apiUrl) {
|
|
|
386
614
|
}
|
|
387
615
|
|
|
388
616
|
// src/remote-encoding.ts
|
|
389
|
-
import { open, readFile as
|
|
390
|
-
import { join as
|
|
617
|
+
import { open, readFile as readFile3 } from "fs/promises";
|
|
618
|
+
import { join as join3 } from "path";
|
|
391
619
|
|
|
392
620
|
// ../../node_modules/zod/v4/classic/external.js
|
|
393
621
|
var external_exports = {};
|
|
@@ -1111,9 +1339,9 @@ function floatSafeRemainder(val, step) {
|
|
|
1111
1339
|
return ratio - roundedRatio;
|
|
1112
1340
|
}
|
|
1113
1341
|
var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
|
|
1114
|
-
function defineLazy(object2,
|
|
1342
|
+
function defineLazy(object2, key2, getter) {
|
|
1115
1343
|
let value = void 0;
|
|
1116
|
-
Object.defineProperty(object2,
|
|
1344
|
+
Object.defineProperty(object2, key2, {
|
|
1117
1345
|
get() {
|
|
1118
1346
|
if (value === EVALUATING) {
|
|
1119
1347
|
return void 0;
|
|
@@ -1125,7 +1353,7 @@ function defineLazy(object2, key, getter) {
|
|
|
1125
1353
|
return value;
|
|
1126
1354
|
},
|
|
1127
1355
|
set(v) {
|
|
1128
|
-
Object.defineProperty(object2,
|
|
1356
|
+
Object.defineProperty(object2, key2, {
|
|
1129
1357
|
value: v
|
|
1130
1358
|
// configurable: true,
|
|
1131
1359
|
});
|
|
@@ -1158,11 +1386,11 @@ function cloneDef(schema) {
|
|
|
1158
1386
|
function getElementAtPath(obj, path) {
|
|
1159
1387
|
if (!path)
|
|
1160
1388
|
return obj;
|
|
1161
|
-
return path.reduce((acc,
|
|
1389
|
+
return path.reduce((acc, key2) => acc?.[key2], obj);
|
|
1162
1390
|
}
|
|
1163
1391
|
function promiseAllObject(promisesObj) {
|
|
1164
1392
|
const keys = Object.keys(promisesObj);
|
|
1165
|
-
const promises = keys.map((
|
|
1393
|
+
const promises = keys.map((key2) => promisesObj[key2]);
|
|
1166
1394
|
return Promise.all(promises).then((results) => {
|
|
1167
1395
|
const resolvedObj = {};
|
|
1168
1396
|
for (let i = 0; i < keys.length; i++) {
|
|
@@ -1234,8 +1462,8 @@ function shallowClone(o) {
|
|
|
1234
1462
|
}
|
|
1235
1463
|
function numKeys(data) {
|
|
1236
1464
|
let keyCount = 0;
|
|
1237
|
-
for (const
|
|
1238
|
-
if (Object.prototype.hasOwnProperty.call(data,
|
|
1465
|
+
for (const key2 in data) {
|
|
1466
|
+
if (Object.prototype.hasOwnProperty.call(data, key2)) {
|
|
1239
1467
|
keyCount++;
|
|
1240
1468
|
}
|
|
1241
1469
|
}
|
|
@@ -1385,13 +1613,13 @@ function pick(schema, mask) {
|
|
|
1385
1613
|
const def = mergeDefs(schema._zod.def, {
|
|
1386
1614
|
get shape() {
|
|
1387
1615
|
const newShape = {};
|
|
1388
|
-
for (const
|
|
1389
|
-
if (!(
|
|
1390
|
-
throw new Error(`Unrecognized key: "${
|
|
1616
|
+
for (const key2 in mask) {
|
|
1617
|
+
if (!(key2 in currDef.shape)) {
|
|
1618
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1391
1619
|
}
|
|
1392
|
-
if (!mask[
|
|
1620
|
+
if (!mask[key2])
|
|
1393
1621
|
continue;
|
|
1394
|
-
newShape[
|
|
1622
|
+
newShape[key2] = currDef.shape[key2];
|
|
1395
1623
|
}
|
|
1396
1624
|
assignProp(this, "shape", newShape);
|
|
1397
1625
|
return newShape;
|
|
@@ -1410,13 +1638,13 @@ function omit(schema, mask) {
|
|
|
1410
1638
|
const def = mergeDefs(schema._zod.def, {
|
|
1411
1639
|
get shape() {
|
|
1412
1640
|
const newShape = { ...schema._zod.def.shape };
|
|
1413
|
-
for (const
|
|
1414
|
-
if (!(
|
|
1415
|
-
throw new Error(`Unrecognized key: "${
|
|
1641
|
+
for (const key2 in mask) {
|
|
1642
|
+
if (!(key2 in currDef.shape)) {
|
|
1643
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1416
1644
|
}
|
|
1417
|
-
if (!mask[
|
|
1645
|
+
if (!mask[key2])
|
|
1418
1646
|
continue;
|
|
1419
|
-
delete newShape[
|
|
1647
|
+
delete newShape[key2];
|
|
1420
1648
|
}
|
|
1421
1649
|
assignProp(this, "shape", newShape);
|
|
1422
1650
|
return newShape;
|
|
@@ -1433,8 +1661,8 @@ function extend(schema, shape) {
|
|
|
1433
1661
|
const hasChecks = checks && checks.length > 0;
|
|
1434
1662
|
if (hasChecks) {
|
|
1435
1663
|
const existingShape = schema._zod.def.shape;
|
|
1436
|
-
for (const
|
|
1437
|
-
if (Object.getOwnPropertyDescriptor(existingShape,
|
|
1664
|
+
for (const key2 in shape) {
|
|
1665
|
+
if (Object.getOwnPropertyDescriptor(existingShape, key2) !== void 0) {
|
|
1438
1666
|
throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
|
|
1439
1667
|
}
|
|
1440
1668
|
}
|
|
@@ -1490,23 +1718,23 @@ function partial(Class2, schema, mask) {
|
|
|
1490
1718
|
const oldShape = schema._zod.def.shape;
|
|
1491
1719
|
const shape = { ...oldShape };
|
|
1492
1720
|
if (mask) {
|
|
1493
|
-
for (const
|
|
1494
|
-
if (!(
|
|
1495
|
-
throw new Error(`Unrecognized key: "${
|
|
1721
|
+
for (const key2 in mask) {
|
|
1722
|
+
if (!(key2 in oldShape)) {
|
|
1723
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1496
1724
|
}
|
|
1497
|
-
if (!mask[
|
|
1725
|
+
if (!mask[key2])
|
|
1498
1726
|
continue;
|
|
1499
|
-
shape[
|
|
1727
|
+
shape[key2] = Class2 ? new Class2({
|
|
1500
1728
|
type: "optional",
|
|
1501
|
-
innerType: oldShape[
|
|
1502
|
-
}) : oldShape[
|
|
1729
|
+
innerType: oldShape[key2]
|
|
1730
|
+
}) : oldShape[key2];
|
|
1503
1731
|
}
|
|
1504
1732
|
} else {
|
|
1505
|
-
for (const
|
|
1506
|
-
shape[
|
|
1733
|
+
for (const key2 in oldShape) {
|
|
1734
|
+
shape[key2] = Class2 ? new Class2({
|
|
1507
1735
|
type: "optional",
|
|
1508
|
-
innerType: oldShape[
|
|
1509
|
-
}) : oldShape[
|
|
1736
|
+
innerType: oldShape[key2]
|
|
1737
|
+
}) : oldShape[key2];
|
|
1510
1738
|
}
|
|
1511
1739
|
}
|
|
1512
1740
|
assignProp(this, "shape", shape);
|
|
@@ -1522,22 +1750,22 @@ function required(Class2, schema, mask) {
|
|
|
1522
1750
|
const oldShape = schema._zod.def.shape;
|
|
1523
1751
|
const shape = { ...oldShape };
|
|
1524
1752
|
if (mask) {
|
|
1525
|
-
for (const
|
|
1526
|
-
if (!(
|
|
1527
|
-
throw new Error(`Unrecognized key: "${
|
|
1753
|
+
for (const key2 in mask) {
|
|
1754
|
+
if (!(key2 in shape)) {
|
|
1755
|
+
throw new Error(`Unrecognized key: "${key2}"`);
|
|
1528
1756
|
}
|
|
1529
|
-
if (!mask[
|
|
1757
|
+
if (!mask[key2])
|
|
1530
1758
|
continue;
|
|
1531
|
-
shape[
|
|
1759
|
+
shape[key2] = new Class2({
|
|
1532
1760
|
type: "nonoptional",
|
|
1533
|
-
innerType: oldShape[
|
|
1761
|
+
innerType: oldShape[key2]
|
|
1534
1762
|
});
|
|
1535
1763
|
}
|
|
1536
1764
|
} else {
|
|
1537
|
-
for (const
|
|
1538
|
-
shape[
|
|
1765
|
+
for (const key2 in oldShape) {
|
|
1766
|
+
shape[key2] = new Class2({
|
|
1539
1767
|
type: "nonoptional",
|
|
1540
|
-
innerType: oldShape[
|
|
1768
|
+
innerType: oldShape[key2]
|
|
1541
1769
|
});
|
|
1542
1770
|
}
|
|
1543
1771
|
}
|
|
@@ -3318,13 +3546,13 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
|
|
|
3318
3546
|
return payload;
|
|
3319
3547
|
};
|
|
3320
3548
|
});
|
|
3321
|
-
function handlePropertyResult(result, final,
|
|
3322
|
-
const isPresent =
|
|
3549
|
+
function handlePropertyResult(result, final, key2, input, isOptionalIn, isOptionalOut) {
|
|
3550
|
+
const isPresent = key2 in input;
|
|
3323
3551
|
if (result.issues.length) {
|
|
3324
3552
|
if (isOptionalIn && isOptionalOut && !isPresent) {
|
|
3325
3553
|
return;
|
|
3326
3554
|
}
|
|
3327
|
-
final.issues.push(...prefixIssues(
|
|
3555
|
+
final.issues.push(...prefixIssues(key2, result.issues));
|
|
3328
3556
|
}
|
|
3329
3557
|
if (!isPresent && !isOptionalIn) {
|
|
3330
3558
|
if (!result.issues.length) {
|
|
@@ -3332,17 +3560,17 @@ function handlePropertyResult(result, final, key, input, isOptionalIn, isOptiona
|
|
|
3332
3560
|
code: "invalid_type",
|
|
3333
3561
|
expected: "nonoptional",
|
|
3334
3562
|
input: void 0,
|
|
3335
|
-
path: [
|
|
3563
|
+
path: [key2]
|
|
3336
3564
|
});
|
|
3337
3565
|
}
|
|
3338
3566
|
return;
|
|
3339
3567
|
}
|
|
3340
3568
|
if (result.value === void 0) {
|
|
3341
3569
|
if (isPresent) {
|
|
3342
|
-
final.value[
|
|
3570
|
+
final.value[key2] = void 0;
|
|
3343
3571
|
}
|
|
3344
3572
|
} else {
|
|
3345
|
-
final.value[
|
|
3573
|
+
final.value[key2] = result.value;
|
|
3346
3574
|
}
|
|
3347
3575
|
}
|
|
3348
3576
|
function normalizeDef(def) {
|
|
@@ -3368,20 +3596,20 @@ function handleCatchall(proms, input, payload, ctx, def, inst) {
|
|
|
3368
3596
|
const t = _catchall.def.type;
|
|
3369
3597
|
const isOptionalIn = _catchall.optin === "optional";
|
|
3370
3598
|
const isOptionalOut = _catchall.optout === "optional";
|
|
3371
|
-
for (const
|
|
3372
|
-
if (
|
|
3599
|
+
for (const key2 in input) {
|
|
3600
|
+
if (key2 === "__proto__")
|
|
3373
3601
|
continue;
|
|
3374
|
-
if (keySet.has(
|
|
3602
|
+
if (keySet.has(key2))
|
|
3375
3603
|
continue;
|
|
3376
3604
|
if (t === "never") {
|
|
3377
|
-
unrecognized.push(
|
|
3605
|
+
unrecognized.push(key2);
|
|
3378
3606
|
continue;
|
|
3379
3607
|
}
|
|
3380
|
-
const r = _catchall.run({ value: input[
|
|
3608
|
+
const r = _catchall.run({ value: input[key2], issues: [] }, ctx);
|
|
3381
3609
|
if (r instanceof Promise) {
|
|
3382
|
-
proms.push(r.then((r2) => handlePropertyResult(r2, payload,
|
|
3610
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
|
|
3383
3611
|
} else {
|
|
3384
|
-
handlePropertyResult(r, payload,
|
|
3612
|
+
handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
|
|
3385
3613
|
}
|
|
3386
3614
|
}
|
|
3387
3615
|
if (unrecognized.length) {
|
|
@@ -3417,12 +3645,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3417
3645
|
defineLazy(inst._zod, "propValues", () => {
|
|
3418
3646
|
const shape = def.shape;
|
|
3419
3647
|
const propValues = {};
|
|
3420
|
-
for (const
|
|
3421
|
-
const field = shape[
|
|
3648
|
+
for (const key2 in shape) {
|
|
3649
|
+
const field = shape[key2]._zod;
|
|
3422
3650
|
if (field.values) {
|
|
3423
|
-
propValues[
|
|
3651
|
+
propValues[key2] ?? (propValues[key2] = /* @__PURE__ */ new Set());
|
|
3424
3652
|
for (const v of field.values)
|
|
3425
|
-
propValues[
|
|
3653
|
+
propValues[key2].add(v);
|
|
3426
3654
|
}
|
|
3427
3655
|
}
|
|
3428
3656
|
return propValues;
|
|
@@ -3445,15 +3673,15 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3445
3673
|
payload.value = {};
|
|
3446
3674
|
const proms = [];
|
|
3447
3675
|
const shape = value.shape;
|
|
3448
|
-
for (const
|
|
3449
|
-
const el = shape[
|
|
3676
|
+
for (const key2 of value.keys) {
|
|
3677
|
+
const el = shape[key2];
|
|
3450
3678
|
const isOptionalIn = el._zod.optin === "optional";
|
|
3451
3679
|
const isOptionalOut = el._zod.optout === "optional";
|
|
3452
|
-
const r = el._zod.run({ value: input[
|
|
3680
|
+
const r = el._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
3453
3681
|
if (r instanceof Promise) {
|
|
3454
|
-
proms.push(r.then((r2) => handlePropertyResult(r2, payload,
|
|
3682
|
+
proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
|
|
3455
3683
|
} else {
|
|
3456
|
-
handlePropertyResult(r, payload,
|
|
3684
|
+
handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
|
|
3457
3685
|
}
|
|
3458
3686
|
}
|
|
3459
3687
|
if (!catchall) {
|
|
@@ -3469,24 +3697,24 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
3469
3697
|
const generateFastpass = (shape) => {
|
|
3470
3698
|
const doc = new Doc(["shape", "payload", "ctx"]);
|
|
3471
3699
|
const normalized = _normalized.value;
|
|
3472
|
-
const parseStr = (
|
|
3473
|
-
const k = esc(
|
|
3700
|
+
const parseStr = (key2) => {
|
|
3701
|
+
const k = esc(key2);
|
|
3474
3702
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
3475
3703
|
};
|
|
3476
3704
|
doc.write(`const input = payload.value;`);
|
|
3477
3705
|
const ids = /* @__PURE__ */ Object.create(null);
|
|
3478
3706
|
let counter = 0;
|
|
3479
|
-
for (const
|
|
3480
|
-
ids[
|
|
3707
|
+
for (const key2 of normalized.keys) {
|
|
3708
|
+
ids[key2] = `key_${counter++}`;
|
|
3481
3709
|
}
|
|
3482
3710
|
doc.write(`const newResult = {};`);
|
|
3483
|
-
for (const
|
|
3484
|
-
const id = ids[
|
|
3485
|
-
const k = esc(
|
|
3486
|
-
const schema = shape[
|
|
3711
|
+
for (const key2 of normalized.keys) {
|
|
3712
|
+
const id = ids[key2];
|
|
3713
|
+
const k = esc(key2);
|
|
3714
|
+
const schema = shape[key2];
|
|
3487
3715
|
const isOptionalIn = schema?._zod?.optin === "optional";
|
|
3488
3716
|
const isOptionalOut = schema?._zod?.optout === "optional";
|
|
3489
|
-
doc.write(`const ${id} = ${parseStr(
|
|
3717
|
+
doc.write(`const ${id} = ${parseStr(key2)};`);
|
|
3490
3718
|
if (isOptionalIn && isOptionalOut) {
|
|
3491
3719
|
doc.write(`
|
|
3492
3720
|
if (${id}.issues.length) {
|
|
@@ -3798,17 +4026,17 @@ function mergeValues(a, b) {
|
|
|
3798
4026
|
}
|
|
3799
4027
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
3800
4028
|
const bKeys = Object.keys(b);
|
|
3801
|
-
const sharedKeys = Object.keys(a).filter((
|
|
4029
|
+
const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
|
|
3802
4030
|
const newObj = { ...a, ...b };
|
|
3803
|
-
for (const
|
|
3804
|
-
const sharedValue = mergeValues(a[
|
|
4031
|
+
for (const key2 of sharedKeys) {
|
|
4032
|
+
const sharedValue = mergeValues(a[key2], b[key2]);
|
|
3805
4033
|
if (!sharedValue.valid) {
|
|
3806
4034
|
return {
|
|
3807
4035
|
valid: false,
|
|
3808
|
-
mergeErrorPath: [
|
|
4036
|
+
mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
|
|
3809
4037
|
};
|
|
3810
4038
|
}
|
|
3811
|
-
newObj[
|
|
4039
|
+
newObj[key2] = sharedValue.data;
|
|
3812
4040
|
}
|
|
3813
4041
|
return { valid: true, data: newObj };
|
|
3814
4042
|
}
|
|
@@ -3943,9 +4171,9 @@ var $ZodTuple = /* @__PURE__ */ $constructor("$ZodTuple", (inst, def) => {
|
|
|
3943
4171
|
return handleTupleResults(itemResults, payload, items, input, optoutStart);
|
|
3944
4172
|
};
|
|
3945
4173
|
});
|
|
3946
|
-
function getTupleOptStart(items,
|
|
4174
|
+
function getTupleOptStart(items, key2) {
|
|
3947
4175
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
3948
|
-
if (items[i]._zod[
|
|
4176
|
+
if (items[i]._zod[key2] !== "optional")
|
|
3949
4177
|
return i + 1;
|
|
3950
4178
|
}
|
|
3951
4179
|
return 0;
|
|
@@ -3996,10 +4224,10 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
3996
4224
|
if (values) {
|
|
3997
4225
|
payload.value = {};
|
|
3998
4226
|
const recordKeys = /* @__PURE__ */ new Set();
|
|
3999
|
-
for (const
|
|
4000
|
-
if (typeof
|
|
4001
|
-
recordKeys.add(typeof
|
|
4002
|
-
const keyResult = def.keyType._zod.run({ value:
|
|
4227
|
+
for (const key2 of values) {
|
|
4228
|
+
if (typeof key2 === "string" || typeof key2 === "number" || typeof key2 === "symbol") {
|
|
4229
|
+
recordKeys.add(typeof key2 === "number" ? key2.toString() : key2);
|
|
4230
|
+
const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4003
4231
|
if (keyResult instanceof Promise) {
|
|
4004
4232
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4005
4233
|
}
|
|
@@ -4008,34 +4236,34 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4008
4236
|
code: "invalid_key",
|
|
4009
4237
|
origin: "record",
|
|
4010
4238
|
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4011
|
-
input:
|
|
4012
|
-
path: [
|
|
4239
|
+
input: key2,
|
|
4240
|
+
path: [key2],
|
|
4013
4241
|
inst
|
|
4014
4242
|
});
|
|
4015
4243
|
continue;
|
|
4016
4244
|
}
|
|
4017
4245
|
const outKey = keyResult.value;
|
|
4018
|
-
const result = def.valueType._zod.run({ value: input[
|
|
4246
|
+
const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
4019
4247
|
if (result instanceof Promise) {
|
|
4020
4248
|
proms.push(result.then((result2) => {
|
|
4021
4249
|
if (result2.issues.length) {
|
|
4022
|
-
payload.issues.push(...prefixIssues(
|
|
4250
|
+
payload.issues.push(...prefixIssues(key2, result2.issues));
|
|
4023
4251
|
}
|
|
4024
4252
|
payload.value[outKey] = result2.value;
|
|
4025
4253
|
}));
|
|
4026
4254
|
} else {
|
|
4027
4255
|
if (result.issues.length) {
|
|
4028
|
-
payload.issues.push(...prefixIssues(
|
|
4256
|
+
payload.issues.push(...prefixIssues(key2, result.issues));
|
|
4029
4257
|
}
|
|
4030
4258
|
payload.value[outKey] = result.value;
|
|
4031
4259
|
}
|
|
4032
4260
|
}
|
|
4033
4261
|
}
|
|
4034
4262
|
let unrecognized;
|
|
4035
|
-
for (const
|
|
4036
|
-
if (!recordKeys.has(
|
|
4263
|
+
for (const key2 in input) {
|
|
4264
|
+
if (!recordKeys.has(key2)) {
|
|
4037
4265
|
unrecognized = unrecognized ?? [];
|
|
4038
|
-
unrecognized.push(
|
|
4266
|
+
unrecognized.push(key2);
|
|
4039
4267
|
}
|
|
4040
4268
|
}
|
|
4041
4269
|
if (unrecognized && unrecognized.length > 0) {
|
|
@@ -4048,18 +4276,18 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4048
4276
|
}
|
|
4049
4277
|
} else {
|
|
4050
4278
|
payload.value = {};
|
|
4051
|
-
for (const
|
|
4052
|
-
if (
|
|
4279
|
+
for (const key2 of Reflect.ownKeys(input)) {
|
|
4280
|
+
if (key2 === "__proto__")
|
|
4053
4281
|
continue;
|
|
4054
|
-
if (!Object.prototype.propertyIsEnumerable.call(input,
|
|
4282
|
+
if (!Object.prototype.propertyIsEnumerable.call(input, key2))
|
|
4055
4283
|
continue;
|
|
4056
|
-
let keyResult = def.keyType._zod.run({ value:
|
|
4284
|
+
let keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4057
4285
|
if (keyResult instanceof Promise) {
|
|
4058
4286
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4059
4287
|
}
|
|
4060
|
-
const checkNumericKey = typeof
|
|
4288
|
+
const checkNumericKey = typeof key2 === "string" && number.test(key2) && keyResult.issues.length;
|
|
4061
4289
|
if (checkNumericKey) {
|
|
4062
|
-
const retryResult = def.keyType._zod.run({ value: Number(
|
|
4290
|
+
const retryResult = def.keyType._zod.run({ value: Number(key2), issues: [] }, ctx);
|
|
4063
4291
|
if (retryResult instanceof Promise) {
|
|
4064
4292
|
throw new Error("Async schemas not supported in object keys currently");
|
|
4065
4293
|
}
|
|
@@ -4069,30 +4297,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4069
4297
|
}
|
|
4070
4298
|
if (keyResult.issues.length) {
|
|
4071
4299
|
if (def.mode === "loose") {
|
|
4072
|
-
payload.value[
|
|
4300
|
+
payload.value[key2] = input[key2];
|
|
4073
4301
|
} else {
|
|
4074
4302
|
payload.issues.push({
|
|
4075
4303
|
code: "invalid_key",
|
|
4076
4304
|
origin: "record",
|
|
4077
4305
|
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4078
|
-
input:
|
|
4079
|
-
path: [
|
|
4306
|
+
input: key2,
|
|
4307
|
+
path: [key2],
|
|
4080
4308
|
inst
|
|
4081
4309
|
});
|
|
4082
4310
|
}
|
|
4083
4311
|
continue;
|
|
4084
4312
|
}
|
|
4085
|
-
const result = def.valueType._zod.run({ value: input[
|
|
4313
|
+
const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
|
|
4086
4314
|
if (result instanceof Promise) {
|
|
4087
4315
|
proms.push(result.then((result2) => {
|
|
4088
4316
|
if (result2.issues.length) {
|
|
4089
|
-
payload.issues.push(...prefixIssues(
|
|
4317
|
+
payload.issues.push(...prefixIssues(key2, result2.issues));
|
|
4090
4318
|
}
|
|
4091
4319
|
payload.value[keyResult.value] = result2.value;
|
|
4092
4320
|
}));
|
|
4093
4321
|
} else {
|
|
4094
4322
|
if (result.issues.length) {
|
|
4095
|
-
payload.issues.push(...prefixIssues(
|
|
4323
|
+
payload.issues.push(...prefixIssues(key2, result.issues));
|
|
4096
4324
|
}
|
|
4097
4325
|
payload.value[keyResult.value] = result.value;
|
|
4098
4326
|
}
|
|
@@ -4119,15 +4347,15 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
|
|
|
4119
4347
|
}
|
|
4120
4348
|
const proms = [];
|
|
4121
4349
|
payload.value = /* @__PURE__ */ new Map();
|
|
4122
|
-
for (const [
|
|
4123
|
-
const keyResult = def.keyType._zod.run({ value:
|
|
4350
|
+
for (const [key2, value] of input) {
|
|
4351
|
+
const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
|
|
4124
4352
|
const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
|
|
4125
4353
|
if (keyResult instanceof Promise || valueResult instanceof Promise) {
|
|
4126
4354
|
proms.push(Promise.all([keyResult, valueResult]).then(([keyResult2, valueResult2]) => {
|
|
4127
|
-
handleMapResult(keyResult2, valueResult2, payload,
|
|
4355
|
+
handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
|
|
4128
4356
|
}));
|
|
4129
4357
|
} else {
|
|
4130
|
-
handleMapResult(keyResult, valueResult, payload,
|
|
4358
|
+
handleMapResult(keyResult, valueResult, payload, key2, input, inst, ctx);
|
|
4131
4359
|
}
|
|
4132
4360
|
}
|
|
4133
4361
|
if (proms.length)
|
|
@@ -4135,10 +4363,10 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
|
|
|
4135
4363
|
return payload;
|
|
4136
4364
|
};
|
|
4137
4365
|
});
|
|
4138
|
-
function handleMapResult(keyResult, valueResult, final,
|
|
4366
|
+
function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
|
|
4139
4367
|
if (keyResult.issues.length) {
|
|
4140
|
-
if (propertyKeyTypes.has(typeof
|
|
4141
|
-
final.issues.push(...prefixIssues(
|
|
4368
|
+
if (propertyKeyTypes.has(typeof key2)) {
|
|
4369
|
+
final.issues.push(...prefixIssues(key2, keyResult.issues));
|
|
4142
4370
|
} else {
|
|
4143
4371
|
final.issues.push({
|
|
4144
4372
|
code: "invalid_key",
|
|
@@ -4150,15 +4378,15 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
|
|
|
4150
4378
|
}
|
|
4151
4379
|
}
|
|
4152
4380
|
if (valueResult.issues.length) {
|
|
4153
|
-
if (propertyKeyTypes.has(typeof
|
|
4154
|
-
final.issues.push(...prefixIssues(
|
|
4381
|
+
if (propertyKeyTypes.has(typeof key2)) {
|
|
4382
|
+
final.issues.push(...prefixIssues(key2, valueResult.issues));
|
|
4155
4383
|
} else {
|
|
4156
4384
|
final.issues.push({
|
|
4157
4385
|
origin: "map",
|
|
4158
4386
|
code: "invalid_element",
|
|
4159
4387
|
input,
|
|
4160
4388
|
inst,
|
|
4161
|
-
key,
|
|
4389
|
+
key: key2,
|
|
4162
4390
|
issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config()))
|
|
4163
4391
|
});
|
|
4164
4392
|
}
|
|
@@ -11919,8 +12147,8 @@ function extractDefs(ctx, schema) {
|
|
|
11919
12147
|
if (defId)
|
|
11920
12148
|
seen.defId = defId;
|
|
11921
12149
|
const schema2 = seen.schema;
|
|
11922
|
-
for (const
|
|
11923
|
-
delete schema2[
|
|
12150
|
+
for (const key2 in schema2) {
|
|
12151
|
+
delete schema2[key2];
|
|
11924
12152
|
}
|
|
11925
12153
|
schema2.$ref = ref;
|
|
11926
12154
|
};
|
|
@@ -11989,20 +12217,20 @@ function finalize(ctx, schema) {
|
|
|
11989
12217
|
Object.assign(schema2, _cached);
|
|
11990
12218
|
const isParentRef = zodSchema._zod.parent === ref;
|
|
11991
12219
|
if (isParentRef) {
|
|
11992
|
-
for (const
|
|
11993
|
-
if (
|
|
12220
|
+
for (const key2 in schema2) {
|
|
12221
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
11994
12222
|
continue;
|
|
11995
|
-
if (!(
|
|
11996
|
-
delete schema2[
|
|
12223
|
+
if (!(key2 in _cached)) {
|
|
12224
|
+
delete schema2[key2];
|
|
11997
12225
|
}
|
|
11998
12226
|
}
|
|
11999
12227
|
}
|
|
12000
12228
|
if (refSchema.$ref && refSeen.def) {
|
|
12001
|
-
for (const
|
|
12002
|
-
if (
|
|
12229
|
+
for (const key2 in schema2) {
|
|
12230
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
12003
12231
|
continue;
|
|
12004
|
-
if (
|
|
12005
|
-
delete schema2[
|
|
12232
|
+
if (key2 in refSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(refSeen.def[key2])) {
|
|
12233
|
+
delete schema2[key2];
|
|
12006
12234
|
}
|
|
12007
12235
|
}
|
|
12008
12236
|
}
|
|
@@ -12014,11 +12242,11 @@ function finalize(ctx, schema) {
|
|
|
12014
12242
|
if (parentSeen?.schema.$ref) {
|
|
12015
12243
|
schema2.$ref = parentSeen.schema.$ref;
|
|
12016
12244
|
if (parentSeen.def) {
|
|
12017
|
-
for (const
|
|
12018
|
-
if (
|
|
12245
|
+
for (const key2 in schema2) {
|
|
12246
|
+
if (key2 === "$ref" || key2 === "allOf")
|
|
12019
12247
|
continue;
|
|
12020
|
-
if (
|
|
12021
|
-
delete schema2[
|
|
12248
|
+
if (key2 in parentSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(parentSeen.def[key2])) {
|
|
12249
|
+
delete schema2[key2];
|
|
12022
12250
|
}
|
|
12023
12251
|
}
|
|
12024
12252
|
}
|
|
@@ -12119,8 +12347,8 @@ function isTransforming(_schema, _ctx) {
|
|
|
12119
12347
|
return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
|
|
12120
12348
|
}
|
|
12121
12349
|
if (def.type === "object") {
|
|
12122
|
-
for (const
|
|
12123
|
-
if (isTransforming(def.shape[
|
|
12350
|
+
for (const key2 in def.shape) {
|
|
12351
|
+
if (isTransforming(def.shape[key2], ctx))
|
|
12124
12352
|
return true;
|
|
12125
12353
|
}
|
|
12126
12354
|
return false;
|
|
@@ -12409,15 +12637,15 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
12409
12637
|
json3.type = "object";
|
|
12410
12638
|
json3.properties = {};
|
|
12411
12639
|
const shape = def.shape;
|
|
12412
|
-
for (const
|
|
12413
|
-
json3.properties[
|
|
12640
|
+
for (const key2 in shape) {
|
|
12641
|
+
json3.properties[key2] = process2(shape[key2], ctx, {
|
|
12414
12642
|
...params,
|
|
12415
|
-
path: [...params.path, "properties",
|
|
12643
|
+
path: [...params.path, "properties", key2]
|
|
12416
12644
|
});
|
|
12417
12645
|
}
|
|
12418
12646
|
const allKeys = new Set(Object.keys(shape));
|
|
12419
|
-
const requiredKeys = new Set([...allKeys].filter((
|
|
12420
|
-
const v = def.shape[
|
|
12647
|
+
const requiredKeys = new Set([...allKeys].filter((key2) => {
|
|
12648
|
+
const v = def.shape[key2]._zod;
|
|
12421
12649
|
if (ctx.io === "input") {
|
|
12422
12650
|
return v.optin === void 0;
|
|
12423
12651
|
} else {
|
|
@@ -12683,9 +12911,9 @@ function toJSONSchema(input, params) {
|
|
|
12683
12911
|
};
|
|
12684
12912
|
ctx2.external = external;
|
|
12685
12913
|
for (const entry of registry2._idmap.entries()) {
|
|
12686
|
-
const [
|
|
12914
|
+
const [key2, schema] = entry;
|
|
12687
12915
|
extractDefs(ctx2, schema);
|
|
12688
|
-
schemas[
|
|
12916
|
+
schemas[key2] = finalize(ctx2, schema);
|
|
12689
12917
|
}
|
|
12690
12918
|
if (Object.keys(defs).length > 0) {
|
|
12691
12919
|
const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
|
|
@@ -13091,14 +13319,14 @@ function _installLazyMethods(inst, group, methods) {
|
|
|
13091
13319
|
if (installed.has(group))
|
|
13092
13320
|
return;
|
|
13093
13321
|
installed.add(group);
|
|
13094
|
-
for (const
|
|
13095
|
-
const fn = methods[
|
|
13096
|
-
Object.defineProperty(proto,
|
|
13322
|
+
for (const key2 in methods) {
|
|
13323
|
+
const fn = methods[key2];
|
|
13324
|
+
Object.defineProperty(proto, key2, {
|
|
13097
13325
|
configurable: true,
|
|
13098
13326
|
enumerable: false,
|
|
13099
13327
|
get() {
|
|
13100
13328
|
const bound = fn.bind(this);
|
|
13101
|
-
Object.defineProperty(this,
|
|
13329
|
+
Object.defineProperty(this, key2, {
|
|
13102
13330
|
configurable: true,
|
|
13103
13331
|
writable: true,
|
|
13104
13332
|
enumerable: true,
|
|
@@ -13107,7 +13335,7 @@ function _installLazyMethods(inst, group, methods) {
|
|
|
13107
13335
|
return bound;
|
|
13108
13336
|
},
|
|
13109
13337
|
set(v) {
|
|
13110
|
-
Object.defineProperty(this,
|
|
13338
|
+
Object.defineProperty(this, key2, {
|
|
13111
13339
|
configurable: true,
|
|
13112
13340
|
writable: true,
|
|
13113
13341
|
enumerable: true,
|
|
@@ -14495,11 +14723,11 @@ function resolveRef(ref, ctx) {
|
|
|
14495
14723
|
}
|
|
14496
14724
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
14497
14725
|
if (path[0] === defsKey) {
|
|
14498
|
-
const
|
|
14499
|
-
if (!
|
|
14726
|
+
const key2 = path[1];
|
|
14727
|
+
if (!key2 || !ctx.defs[key2]) {
|
|
14500
14728
|
throw new Error(`Reference not found: ${ref}`);
|
|
14501
14729
|
}
|
|
14502
|
-
return ctx.defs[
|
|
14730
|
+
return ctx.defs[key2];
|
|
14503
14731
|
}
|
|
14504
14732
|
throw new Error(`Reference not found: ${ref}`);
|
|
14505
14733
|
}
|
|
@@ -14685,9 +14913,9 @@ function convertBaseSchema(schema, ctx) {
|
|
|
14685
14913
|
const shape = {};
|
|
14686
14914
|
const properties = schema.properties || {};
|
|
14687
14915
|
const requiredSet = new Set(schema.required || []);
|
|
14688
|
-
for (const [
|
|
14916
|
+
for (const [key2, propSchema] of Object.entries(properties)) {
|
|
14689
14917
|
const propZodSchema = convertSchema(propSchema, ctx);
|
|
14690
|
-
shape[
|
|
14918
|
+
shape[key2] = requiredSet.has(key2) ? propZodSchema : propZodSchema.optional();
|
|
14691
14919
|
}
|
|
14692
14920
|
if (schema.propertyNames) {
|
|
14693
14921
|
const keySchema = convertSchema(schema.propertyNames, ctx);
|
|
@@ -14828,20 +15056,20 @@ function convertSchema(schema, ctx) {
|
|
|
14828
15056
|
}
|
|
14829
15057
|
const extraMeta = {};
|
|
14830
15058
|
const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
|
|
14831
|
-
for (const
|
|
14832
|
-
if (
|
|
14833
|
-
extraMeta[
|
|
15059
|
+
for (const key2 of coreMetadataKeys) {
|
|
15060
|
+
if (key2 in schema) {
|
|
15061
|
+
extraMeta[key2] = schema[key2];
|
|
14834
15062
|
}
|
|
14835
15063
|
}
|
|
14836
15064
|
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
14837
|
-
for (const
|
|
14838
|
-
if (
|
|
14839
|
-
extraMeta[
|
|
15065
|
+
for (const key2 of contentMetadataKeys) {
|
|
15066
|
+
if (key2 in schema) {
|
|
15067
|
+
extraMeta[key2] = schema[key2];
|
|
14840
15068
|
}
|
|
14841
15069
|
}
|
|
14842
|
-
for (const
|
|
14843
|
-
if (!RECOGNIZED_KEYS.has(
|
|
14844
|
-
extraMeta[
|
|
15070
|
+
for (const key2 of Object.keys(schema)) {
|
|
15071
|
+
if (!RECOGNIZED_KEYS.has(key2)) {
|
|
15072
|
+
extraMeta[key2] = schema[key2];
|
|
14845
15073
|
}
|
|
14846
15074
|
}
|
|
14847
15075
|
if (Object.keys(extraMeta).length > 0) {
|
|
@@ -14936,18 +15164,28 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
|
|
|
14936
15164
|
}
|
|
14937
15165
|
}
|
|
14938
15166
|
async function submitFrameCapture(client, id, directory) {
|
|
14939
|
-
const manifest = frameManifestSchema.parse(JSON.parse(await
|
|
15167
|
+
const manifest = frameManifestSchema.parse(JSON.parse(await readFile3(join3(directory, "capture-manifest.json"), "utf8")));
|
|
14940
15168
|
let status = await client.beginEncoding(id, manifest);
|
|
14941
15169
|
if (status.status === "failed") status = await client.retryEncoding(id);
|
|
14942
15170
|
if (status.status !== "uploading") {
|
|
14943
15171
|
if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
|
|
14944
15172
|
return status;
|
|
14945
15173
|
}
|
|
14946
|
-
const file2 = await open(
|
|
15174
|
+
const file2 = await open(join3(directory, "frames.bin"), "r");
|
|
14947
15175
|
try {
|
|
14948
15176
|
const total = manifest.frames.reduce((n, f) => n + f.size, 0);
|
|
14949
15177
|
if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
|
|
14950
15178
|
const uploaded = new Set(status.uploadedParts);
|
|
15179
|
+
let acceptedBytes = [...uploaded].reduce((sum, part) => sum + (Number.isInteger(part) && part >= 0 && part < Math.ceil(total / ENCODING_PART_BYTES) ? Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES) : 0), 0);
|
|
15180
|
+
let transferredBytes = 0;
|
|
15181
|
+
const startedAt = Date.now();
|
|
15182
|
+
const progress = () => {
|
|
15183
|
+
const seconds = (Date.now() - startedAt) / 1e3;
|
|
15184
|
+
const rate = seconds > 0 ? transferredBytes / seconds : 0;
|
|
15185
|
+
const remaining = rate > 0 ? ` \xB7 about ${Math.ceil((total - acceptedBytes) / rate)}s left` : "";
|
|
15186
|
+
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}`);
|
|
15187
|
+
};
|
|
15188
|
+
progress();
|
|
14951
15189
|
for (let part = 0; part < Math.ceil(total / ENCODING_PART_BYTES); part++) {
|
|
14952
15190
|
if (uploaded.has(part)) continue;
|
|
14953
15191
|
const bytes = Buffer.alloc(Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES));
|
|
@@ -14958,11 +15196,14 @@ async function submitFrameCapture(client, id, directory) {
|
|
|
14958
15196
|
offset += r.bytesRead;
|
|
14959
15197
|
}
|
|
14960
15198
|
await withFrameUploadRetry(() => client.uploadEncodingPart(id, part, bytes));
|
|
14961
|
-
|
|
15199
|
+
acceptedBytes += bytes.length;
|
|
15200
|
+
transferredBytes += bytes.length;
|
|
15201
|
+
progress();
|
|
14962
15202
|
}
|
|
14963
15203
|
} finally {
|
|
14964
15204
|
await file2.close();
|
|
14965
15205
|
}
|
|
15206
|
+
process.stderr.write("\nFrame upload complete; submitting for encoding.\n");
|
|
14966
15207
|
return client.completeEncoding(id);
|
|
14967
15208
|
}
|
|
14968
15209
|
async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
@@ -14974,39 +15215,133 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
14974
15215
|
process.stderr.write(`\r${status.status.padEnd(20)}`);
|
|
14975
15216
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
14976
15217
|
}
|
|
14977
|
-
throw new
|
|
15218
|
+
throw new ControlPlaneError("processing_pending", "Encoding continues remotely.", `Run ${client.recordingStatusCommand(id)}.`, { recordingId: id, nextCommand: client.recordingStatusCommand(id) });
|
|
14978
15219
|
}
|
|
14979
15220
|
|
|
14980
15221
|
// src/cli.ts
|
|
14981
|
-
import { spawn as
|
|
14982
|
-
import { hostname as hostname4 } from "os";
|
|
15222
|
+
import { spawn as spawn8 } from "child_process";
|
|
15223
|
+
import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
|
|
14983
15224
|
import { Command } from "commander";
|
|
14984
|
-
import { basename, dirname as dirname2 } from "path";
|
|
14985
|
-
import { readdir as readdir2, rm as
|
|
15225
|
+
import { basename, dirname as dirname2, join as join11 } from "path";
|
|
15226
|
+
import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
|
|
14986
15227
|
|
|
14987
|
-
// ../recorder/src/
|
|
14988
|
-
import {
|
|
14989
|
-
import {
|
|
15228
|
+
// ../recorder/src/windows-ipc.ts
|
|
15229
|
+
import { spawn as spawn3 } from "child_process";
|
|
15230
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
15231
|
+
import { link, lstat, mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
15232
|
+
import { join as join5, win32 as win322 } from "path";
|
|
14990
15233
|
|
|
14991
15234
|
// ../recorder/src/paths.ts
|
|
14992
|
-
import { homedir as
|
|
14993
|
-
import { join as
|
|
14994
|
-
var rillDirectory = () => process.env.RILL_HOME ??
|
|
14995
|
-
var daemonSocketPath = () => process.env.RILL_SOCKET ??
|
|
14996
|
-
var daemonStatePath = () =>
|
|
14997
|
-
var recordingTempRoot = () => process.env.RILL_TEMP ??
|
|
15235
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
15236
|
+
import { join as join4 } from "path";
|
|
15237
|
+
var rillDirectory = () => process.env.RILL_HOME ?? join4(homedir3(), ".rill");
|
|
15238
|
+
var daemonSocketPath = () => process.env.RILL_SOCKET ?? join4(rillDirectory(), "recorder-v2.sock");
|
|
15239
|
+
var daemonStatePath = () => join4(rillDirectory(), "sessions-v2.json");
|
|
15240
|
+
var recordingTempRoot = () => process.env.RILL_TEMP ?? join4(tmpdir(), "rill-recordings");
|
|
15241
|
+
|
|
15242
|
+
// ../recorder/src/windows-ipc.ts
|
|
15243
|
+
var aclScript = String.raw`$ErrorActionPreference='Stop';try {
|
|
15244
|
+
$path=([Console]::In.ReadToEnd() | ConvertFrom-Json).path
|
|
15245
|
+
$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
15246
|
+
$acl=[System.Security.AccessControl.DirectorySecurity]::new()
|
|
15247
|
+
$acl.SetOwner($sid)
|
|
15248
|
+
$acl.SetAccessRuleProtection($true,$false)
|
|
15249
|
+
$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)
|
|
15250
|
+
$acl.AddAccessRule($rule)
|
|
15251
|
+
([System.IO.DirectoryInfo]::new($path)).SetAccessControl($acl)
|
|
15252
|
+
[Console]::Out.Write('secured')
|
|
15253
|
+
}catch{[Console]::Error.Write('Recorder directory protection failed.');exit 1}`;
|
|
15254
|
+
async function secureWindowsIpcDirectory(path) {
|
|
15255
|
+
if (process.platform !== "win32") throw new Error("Windows IPC directory protection requires Windows.");
|
|
15256
|
+
await new Promise((resolve, reject) => {
|
|
15257
|
+
const executable = win322.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
15258
|
+
const child = spawn3(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", aclScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
15259
|
+
let output = "", failed = false;
|
|
15260
|
+
const fail2 = () => {
|
|
15261
|
+
if (failed) return;
|
|
15262
|
+
failed = true;
|
|
15263
|
+
child.kill();
|
|
15264
|
+
reject(new Error("Cannot secure the Windows recorder directory. Recording has not started."));
|
|
15265
|
+
};
|
|
15266
|
+
const timer = setTimeout(fail2, 15e3);
|
|
15267
|
+
child.once("error", fail2);
|
|
15268
|
+
child.stdin.once("error", fail2);
|
|
15269
|
+
child.stderr.resume();
|
|
15270
|
+
child.stdout.on("data", (chunk) => {
|
|
15271
|
+
output += chunk.toString("ascii");
|
|
15272
|
+
if (output.length > 1024) fail2();
|
|
15273
|
+
});
|
|
15274
|
+
child.once("close", (code) => {
|
|
15275
|
+
clearTimeout(timer);
|
|
15276
|
+
if (failed) return;
|
|
15277
|
+
if (code !== 0 || output !== "secured") {
|
|
15278
|
+
fail2();
|
|
15279
|
+
return;
|
|
15280
|
+
}
|
|
15281
|
+
resolve();
|
|
15282
|
+
});
|
|
15283
|
+
child.stdin.end(JSON.stringify({ path }));
|
|
15284
|
+
});
|
|
15285
|
+
}
|
|
15286
|
+
function validWindowsIpcIdentity(value) {
|
|
15287
|
+
const v = value;
|
|
15288
|
+
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);
|
|
15289
|
+
}
|
|
15290
|
+
function acceptsWindowsIpcToken(expected, header) {
|
|
15291
|
+
if (typeof header !== "string" || !/^Bearer [a-f0-9]{64}$/.test(header)) return false;
|
|
15292
|
+
const supplied = header.slice(7);
|
|
15293
|
+
return supplied.length === expected.length && timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
|
|
15294
|
+
}
|
|
15295
|
+
async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ipc-v3"), secure = secureWindowsIpcDirectory) {
|
|
15296
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
15297
|
+
if ((await lstat(directory)).isSymbolicLink()) throw new Error("Recorder directory cannot be a symbolic link.");
|
|
15298
|
+
await secure(directory);
|
|
15299
|
+
const path = join5(directory, "identity.json");
|
|
15300
|
+
const load = async () => {
|
|
15301
|
+
const info = await lstat(path);
|
|
15302
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 4096) throw new Error("Invalid recorder identity.");
|
|
15303
|
+
const value = JSON.parse(await readFile4(path, "utf8"));
|
|
15304
|
+
if (!validWindowsIpcIdentity(value)) throw new Error("Invalid recorder identity.");
|
|
15305
|
+
return value;
|
|
15306
|
+
};
|
|
15307
|
+
try {
|
|
15308
|
+
return await load();
|
|
15309
|
+
} catch (e) {
|
|
15310
|
+
if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
|
|
15311
|
+
}
|
|
15312
|
+
const identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
|
|
15313
|
+
const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
|
|
15314
|
+
try {
|
|
15315
|
+
await writeFile2(temporary, JSON.stringify(identity), { mode: 384, flag: "wx" });
|
|
15316
|
+
try {
|
|
15317
|
+
await link(temporary, path);
|
|
15318
|
+
} catch (e) {
|
|
15319
|
+
if (e.code !== "EEXIST") throw e;
|
|
15320
|
+
}
|
|
15321
|
+
return await load();
|
|
15322
|
+
} finally {
|
|
15323
|
+
await rm3(temporary, { force: true });
|
|
15324
|
+
}
|
|
15325
|
+
}
|
|
14998
15326
|
|
|
14999
15327
|
// ../recorder/src/client.ts
|
|
15328
|
+
import { request as request2 } from "http";
|
|
15329
|
+
import { spawn as spawn4 } from "child_process";
|
|
15000
15330
|
async function ensureDaemon(cliEntryPath) {
|
|
15001
15331
|
try {
|
|
15002
15332
|
await callDaemon("GET", "/health");
|
|
15003
15333
|
return;
|
|
15004
15334
|
} catch {
|
|
15005
15335
|
}
|
|
15006
|
-
const child =
|
|
15336
|
+
const child = spawn4(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env, windowsHide: true });
|
|
15337
|
+
let startupError;
|
|
15338
|
+
child.once("error", () => {
|
|
15339
|
+
startupError = new Error("The local recorder process could not be launched.");
|
|
15340
|
+
});
|
|
15007
15341
|
child.unref();
|
|
15008
15342
|
const deadline = Date.now() + 1e4;
|
|
15009
15343
|
while (Date.now() < deadline) {
|
|
15344
|
+
if (startupError) throw startupError;
|
|
15010
15345
|
try {
|
|
15011
15346
|
await callDaemon("GET", "/health");
|
|
15012
15347
|
return;
|
|
@@ -15025,10 +15360,16 @@ function stopRecording(recordingId) {
|
|
|
15025
15360
|
function recordingStatus(recordingId) {
|
|
15026
15361
|
return callDaemon("GET", `/sessions/${encodeURIComponent(recordingId)}`);
|
|
15027
15362
|
}
|
|
15028
|
-
|
|
15363
|
+
var windowsIdentityPromise;
|
|
15364
|
+
async function callDaemon(method, path, body) {
|
|
15365
|
+
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.");
|
|
15366
|
+
const identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
|
|
15367
|
+
windowsIdentityPromise = void 0;
|
|
15368
|
+
throw error51;
|
|
15369
|
+
})) : void 0;
|
|
15029
15370
|
return new Promise((resolve, reject) => {
|
|
15030
15371
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
15031
|
-
const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } :
|
|
15372
|
+
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) => {
|
|
15032
15373
|
const chunks = [];
|
|
15033
15374
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
15034
15375
|
incoming.on("end", () => {
|
|
@@ -15041,6 +15382,7 @@ function callDaemon(method, path, body) {
|
|
|
15041
15382
|
}
|
|
15042
15383
|
});
|
|
15043
15384
|
});
|
|
15385
|
+
outgoing.setTimeout(path === "/health" ? 2e3 : 12e4, () => outgoing.destroy(new Error("Local recorder request timed out.")));
|
|
15044
15386
|
outgoing.once("error", reject);
|
|
15045
15387
|
if (payload) outgoing.write(payload);
|
|
15046
15388
|
outgoing.end();
|
|
@@ -15048,17 +15390,17 @@ function callDaemon(method, path, body) {
|
|
|
15048
15390
|
}
|
|
15049
15391
|
|
|
15050
15392
|
// ../recorder/src/retention.ts
|
|
15051
|
-
import { readdir, readFile as
|
|
15052
|
-
import { join as
|
|
15393
|
+
import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
|
|
15394
|
+
import { join as join6 } from "path";
|
|
15053
15395
|
async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
|
|
15054
15396
|
const removed = [];
|
|
15055
15397
|
for (const id of await readdir(root).catch(() => [])) {
|
|
15056
15398
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
15057
|
-
const directory =
|
|
15399
|
+
const directory = join6(root, id);
|
|
15058
15400
|
try {
|
|
15059
|
-
const marker = JSON.parse(await
|
|
15401
|
+
const marker = JSON.parse(await readFile5(join6(directory, "remote-capture.json"), "utf8"));
|
|
15060
15402
|
if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
|
|
15061
|
-
await
|
|
15403
|
+
await rm4(directory, { recursive: true, force: true });
|
|
15062
15404
|
removed.push(id);
|
|
15063
15405
|
} catch {
|
|
15064
15406
|
}
|
|
@@ -15069,7 +15411,7 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
15069
15411
|
for (const id of await readdir(root).catch(() => [])) {
|
|
15070
15412
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
15071
15413
|
try {
|
|
15072
|
-
const marker = JSON.parse(await
|
|
15414
|
+
const marker = JSON.parse(await readFile5(join6(root, id, "remote-capture.json"), "utf8"));
|
|
15073
15415
|
if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
|
|
15074
15416
|
} catch {
|
|
15075
15417
|
}
|
|
@@ -15079,8 +15421,189 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
15079
15421
|
|
|
15080
15422
|
// ../recorder/src/daemon.ts
|
|
15081
15423
|
import { createServer as createServer2 } from "http";
|
|
15082
|
-
import { mkdir as
|
|
15083
|
-
import { dirname, join as
|
|
15424
|
+
import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
|
|
15425
|
+
import { dirname, join as join10 } from "path";
|
|
15426
|
+
|
|
15427
|
+
// ../contracts/src/cloud-blocklist.ts
|
|
15428
|
+
var blockedCloudDomains = [
|
|
15429
|
+
// Torrent indexes and unofficial software-download destinations.
|
|
15430
|
+
// Explicit domains only: do not block legitimate torrent clients or all downloads.
|
|
15431
|
+
"yasdl.com",
|
|
15432
|
+
"soft98.ir",
|
|
15433
|
+
"downloadha.com",
|
|
15434
|
+
"patoghu.com",
|
|
15435
|
+
"thepiratebay.org",
|
|
15436
|
+
"1337x.to",
|
|
15437
|
+
"yts.mx",
|
|
15438
|
+
"eztvx.to",
|
|
15439
|
+
"fitgirl-repacks.site",
|
|
15440
|
+
"dodi-repacks.site",
|
|
15441
|
+
// Search and reference
|
|
15442
|
+
"google.com",
|
|
15443
|
+
"google.co.uk",
|
|
15444
|
+
"google.co.in",
|
|
15445
|
+
"google.com.ng",
|
|
15446
|
+
"google.ca",
|
|
15447
|
+
"google.com.au",
|
|
15448
|
+
"google.de",
|
|
15449
|
+
"google.fr",
|
|
15450
|
+
"google.co.jp",
|
|
15451
|
+
"google.com.br",
|
|
15452
|
+
"google.es",
|
|
15453
|
+
"bing.com",
|
|
15454
|
+
"yahoo.com",
|
|
15455
|
+
"duckduckgo.com",
|
|
15456
|
+
"baidu.com",
|
|
15457
|
+
"yandex.com",
|
|
15458
|
+
"yandex.ru",
|
|
15459
|
+
"wikipedia.org",
|
|
15460
|
+
"quora.com",
|
|
15461
|
+
// Social networks and media
|
|
15462
|
+
"reddit.com",
|
|
15463
|
+
"redd.it",
|
|
15464
|
+
"facebook.com",
|
|
15465
|
+
"fb.com",
|
|
15466
|
+
"instagram.com",
|
|
15467
|
+
"threads.net",
|
|
15468
|
+
"threads.com",
|
|
15469
|
+
"twitter.com",
|
|
15470
|
+
"x.com",
|
|
15471
|
+
"t.co",
|
|
15472
|
+
"linkedin.com",
|
|
15473
|
+
"tiktok.com",
|
|
15474
|
+
"pinterest.com",
|
|
15475
|
+
"snapchat.com",
|
|
15476
|
+
"youtube.com",
|
|
15477
|
+
"youtu.be",
|
|
15478
|
+
"twitch.tv",
|
|
15479
|
+
"discord.com",
|
|
15480
|
+
"discord.gg",
|
|
15481
|
+
"telegram.org",
|
|
15482
|
+
"t.me",
|
|
15483
|
+
"whatsapp.com",
|
|
15484
|
+
// Shopping and marketplaces
|
|
15485
|
+
"amazon.com",
|
|
15486
|
+
"amazon.co.uk",
|
|
15487
|
+
"amazon.ca",
|
|
15488
|
+
"amazon.de",
|
|
15489
|
+
"amazon.fr",
|
|
15490
|
+
"amazon.in",
|
|
15491
|
+
"amazon.co.jp",
|
|
15492
|
+
"amazon.com.au",
|
|
15493
|
+
"amazon.it",
|
|
15494
|
+
"amazon.es",
|
|
15495
|
+
"ebay.com",
|
|
15496
|
+
"ebay.co.uk",
|
|
15497
|
+
"etsy.com",
|
|
15498
|
+
"walmart.com",
|
|
15499
|
+
"target.com",
|
|
15500
|
+
"aliexpress.com",
|
|
15501
|
+
"alibaba.com",
|
|
15502
|
+
"temu.com",
|
|
15503
|
+
"shein.com",
|
|
15504
|
+
"craigslist.org",
|
|
15505
|
+
"facebookmarketplace.com",
|
|
15506
|
+
"bestbuy.com",
|
|
15507
|
+
"costco.com",
|
|
15508
|
+
"homedepot.com",
|
|
15509
|
+
"wayfair.com",
|
|
15510
|
+
"mercadolibre.com",
|
|
15511
|
+
"shopee.com",
|
|
15512
|
+
"lazada.com",
|
|
15513
|
+
"jiji.ng",
|
|
15514
|
+
"jumia.com.ng",
|
|
15515
|
+
"konga.com",
|
|
15516
|
+
"propertypro.ng",
|
|
15517
|
+
"property24.com",
|
|
15518
|
+
// Travel, property, jobs and business directories
|
|
15519
|
+
"airbnb.com",
|
|
15520
|
+
"airbnb.co.uk",
|
|
15521
|
+
"airbnb.ca",
|
|
15522
|
+
"airbnb.com.au",
|
|
15523
|
+
"airbnb.fr",
|
|
15524
|
+
"airbnb.de",
|
|
15525
|
+
"booking.com",
|
|
15526
|
+
"expedia.com",
|
|
15527
|
+
"tripadvisor.com",
|
|
15528
|
+
"agoda.com",
|
|
15529
|
+
"hotels.com",
|
|
15530
|
+
"kayak.com",
|
|
15531
|
+
"skyscanner.net",
|
|
15532
|
+
"zillow.com",
|
|
15533
|
+
"realtor.com",
|
|
15534
|
+
"rightmove.co.uk",
|
|
15535
|
+
"zoopla.co.uk",
|
|
15536
|
+
"indeed.com",
|
|
15537
|
+
"glassdoor.com",
|
|
15538
|
+
"yelp.com",
|
|
15539
|
+
"yellowpages.com",
|
|
15540
|
+
"trustpilot.com",
|
|
15541
|
+
"crunchbase.com",
|
|
15542
|
+
"g2.com",
|
|
15543
|
+
"capterra.com",
|
|
15544
|
+
"clutch.co",
|
|
15545
|
+
"zoominfo.com",
|
|
15546
|
+
"apollo.io",
|
|
15547
|
+
"monster.com",
|
|
15548
|
+
"ziprecruiter.com",
|
|
15549
|
+
"seek.com.au",
|
|
15550
|
+
"naukri.com",
|
|
15551
|
+
"redfin.com",
|
|
15552
|
+
"trulia.com",
|
|
15553
|
+
"apartments.com",
|
|
15554
|
+
"realtor.ca",
|
|
15555
|
+
"realestate.com.au",
|
|
15556
|
+
"vrbo.com",
|
|
15557
|
+
"trip.com",
|
|
15558
|
+
"trivago.com",
|
|
15559
|
+
"hostelworld.com"
|
|
15560
|
+
];
|
|
15561
|
+
var blockedCloudTargetMessage = "This website is not supported for cloud runs. Use your own website or app instead.";
|
|
15562
|
+
function isBlockedCloudTarget(value) {
|
|
15563
|
+
try {
|
|
15564
|
+
const hostname5 = new URL(value).hostname.toLowerCase().replace(/\.+$/, "");
|
|
15565
|
+
return blockedCloudDomains.some((domain2) => hostname5 === domain2 || hostname5.endsWith(`.${domain2}`));
|
|
15566
|
+
} catch {
|
|
15567
|
+
return false;
|
|
15568
|
+
}
|
|
15569
|
+
}
|
|
15570
|
+
|
|
15571
|
+
// ../contracts/src/cloud-run.ts
|
|
15572
|
+
var publicTarget = external_exports.string().url().max(2048).refine((value) => {
|
|
15573
|
+
let u;
|
|
15574
|
+
try {
|
|
15575
|
+
u = new URL(value);
|
|
15576
|
+
} catch {
|
|
15577
|
+
return false;
|
|
15578
|
+
}
|
|
15579
|
+
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");
|
|
15580
|
+
}, "Use a public HTTP(S) URL without credentials, fragments, or a custom port.");
|
|
15581
|
+
var cloudRunTarget = publicTarget.refine((value) => !isBlockedCloudTarget(value), blockedCloudTargetMessage);
|
|
15582
|
+
var interactionFields = {
|
|
15583
|
+
mode: external_exports.enum(["browse", "interact"]).optional(),
|
|
15584
|
+
interactionConfirmed: external_exports.boolean().optional(),
|
|
15585
|
+
// Legacy payload compatibility only; Interact automatically permits API destinations.
|
|
15586
|
+
allowedWriteOrigins: external_exports.array(cloudRunTarget.refine((v) => new URL(v).origin === v, "Use an exact origin.")).max(5).optional()
|
|
15587
|
+
};
|
|
15588
|
+
var sideEffectSchema = external_exports.object({
|
|
15589
|
+
action: external_exports.enum(["fill", "check", "select", "press", "click"]),
|
|
15590
|
+
page: external_exports.string().max(2048),
|
|
15591
|
+
target: external_exports.string().max(160),
|
|
15592
|
+
status: external_exports.enum(["performed", "unknown"])
|
|
15593
|
+
}).strict();
|
|
15594
|
+
var interactionReportFields = {
|
|
15595
|
+
mode: external_exports.enum(["browse", "interact"]).optional(),
|
|
15596
|
+
sideEffects: external_exports.array(sideEffectSchema).max(30).optional(),
|
|
15597
|
+
cleanup: external_exports.literal("not_attempted").optional()
|
|
15598
|
+
};
|
|
15599
|
+
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();
|
|
15600
|
+
var siteAccountSchema = external_exports.object({
|
|
15601
|
+
name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
|
|
15602
|
+
origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
|
|
15603
|
+
loginUrl: publicTarget.refine((v) => new URL(v).protocol === "https:").optional(),
|
|
15604
|
+
username: external_exports.string().min(1).max(320),
|
|
15605
|
+
password: external_exports.string().min(1).max(1e3)
|
|
15606
|
+
}).strict().refine((v) => !v.loginUrl || new URL(v.loginUrl).origin === v.origin, "Login must use the saved website origin.");
|
|
15084
15607
|
|
|
15085
15608
|
// ../contracts/src/recording.ts
|
|
15086
15609
|
var cursorAppearanceSchema = external_exports.object({
|
|
@@ -15121,6 +15644,7 @@ var recordingSchema = external_exports.object({
|
|
|
15121
15644
|
})
|
|
15122
15645
|
});
|
|
15123
15646
|
var agentOutcomeSchema = external_exports.object({
|
|
15647
|
+
...interactionReportFields,
|
|
15124
15648
|
outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
|
|
15125
15649
|
summary: external_exports.string().min(1).max(1e3),
|
|
15126
15650
|
evidence: external_exports.string().max(1e3)
|
|
@@ -15208,15 +15732,15 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15208
15732
|
const appearance = cursorAppearanceSchema.parse(input ?? {});
|
|
15209
15733
|
if (!appearance.enabled) return async () => {
|
|
15210
15734
|
};
|
|
15211
|
-
const
|
|
15212
|
-
const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key })})`;
|
|
15735
|
+
const key2 = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
15736
|
+
const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key: key2 })})`;
|
|
15213
15737
|
await cdp.send("Page.enable");
|
|
15214
15738
|
const { identifier } = await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
|
|
15215
15739
|
let stopped = false;
|
|
15216
|
-
const removeFromFrame = (frame) => frame.evaluate((
|
|
15217
|
-
const cleanup2 = window[
|
|
15740
|
+
const removeFromFrame = (frame) => frame.evaluate((key3) => {
|
|
15741
|
+
const cleanup2 = window[key3];
|
|
15218
15742
|
if (typeof cleanup2 === "function") cleanup2();
|
|
15219
|
-
},
|
|
15743
|
+
}, key2).catch(() => void 0);
|
|
15220
15744
|
const onNavigation = (frame) => {
|
|
15221
15745
|
if (stopped) return;
|
|
15222
15746
|
void frame.evaluate(source).then(() => {
|
|
@@ -15241,10 +15765,10 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15241
15765
|
|
|
15242
15766
|
// ../recorder/src/session.ts
|
|
15243
15767
|
import { chromium } from "playwright-core";
|
|
15244
|
-
import { spawn as
|
|
15768
|
+
import { spawn as spawn6 } from "child_process";
|
|
15245
15769
|
import { createServer } from "net";
|
|
15246
|
-
import { mkdir as
|
|
15247
|
-
import { join as
|
|
15770
|
+
import { mkdir as mkdir6, mkdtemp, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
15771
|
+
import { join as join9 } from "path";
|
|
15248
15772
|
import { tmpdir as tmpdir2 } from "os";
|
|
15249
15773
|
|
|
15250
15774
|
// ../recorder/src/bundle.ts
|
|
@@ -15270,11 +15794,16 @@ async function writeBundle(bundle, outputPath) {
|
|
|
15270
15794
|
|
|
15271
15795
|
// ../recorder/src/environment.ts
|
|
15272
15796
|
import { existsSync } from "fs";
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
if (
|
|
15797
|
+
import { win32 as win323 } from "path";
|
|
15798
|
+
function findChromeExecutable(platform = process.platform, env = process.env, exists = existsSync) {
|
|
15799
|
+
if (env.RILL_CHROME_PATH) return env.RILL_CHROME_PATH;
|
|
15800
|
+
if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
15801
|
+
if (platform === "win32") {
|
|
15802
|
+
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"));
|
|
15803
|
+
return candidates2.find(exists) ?? win323.join(env.PROGRAMFILES ?? env.ProgramFiles ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe");
|
|
15804
|
+
}
|
|
15276
15805
|
const candidates = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
|
|
15277
|
-
return candidates.find(
|
|
15806
|
+
return candidates.find(exists) ?? "google-chrome";
|
|
15278
15807
|
}
|
|
15279
15808
|
|
|
15280
15809
|
// ../recorder/src/redaction.ts
|
|
@@ -15286,7 +15815,7 @@ function sanitizeUrl(input) {
|
|
|
15286
15815
|
url2.username = "";
|
|
15287
15816
|
url2.password = "";
|
|
15288
15817
|
url2.hash = "";
|
|
15289
|
-
for (const
|
|
15818
|
+
for (const key2 of [...url2.searchParams.keys()]) url2.searchParams.set(key2, "[REDACTED]");
|
|
15290
15819
|
return url2.toString();
|
|
15291
15820
|
} catch {
|
|
15292
15821
|
return "[invalid-url]";
|
|
@@ -15317,16 +15846,16 @@ function redactJson(value, depth = 0) {
|
|
|
15317
15846
|
if (depth > 12) return "[TRUNCATED]";
|
|
15318
15847
|
if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
|
|
15319
15848
|
if (value && typeof value === "object") {
|
|
15320
|
-
return Object.fromEntries(Object.entries(value).slice(0, 200).map(([
|
|
15849
|
+
return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key2, child]) => [key2, secretKeyPattern.test(key2) ? "[REDACTED]" : redactJson(child, depth + 1)]));
|
|
15321
15850
|
}
|
|
15322
15851
|
if (typeof value === "string") return redactText(value, 8e3);
|
|
15323
15852
|
return value;
|
|
15324
15853
|
}
|
|
15325
15854
|
|
|
15326
15855
|
// ../recorder/src/encoding.ts
|
|
15327
|
-
import { open as open2, writeFile, mkdir as
|
|
15328
|
-
import { spawn as
|
|
15329
|
-
import { join as
|
|
15856
|
+
import { open as open2, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
15857
|
+
import { spawn as spawn5 } from "child_process";
|
|
15858
|
+
import { join as join7 } from "path";
|
|
15330
15859
|
var active = 0;
|
|
15331
15860
|
var waiting = [];
|
|
15332
15861
|
function encodingConcurrency() {
|
|
@@ -15334,8 +15863,22 @@ function encodingConcurrency() {
|
|
|
15334
15863
|
if (!Number.isInteger(n) || n < 1 || n > 8) throw new Error("Invalid RILL_ENCODING_CONCURRENCY");
|
|
15335
15864
|
return n;
|
|
15336
15865
|
}
|
|
15337
|
-
async function withEncodingSlot(work) {
|
|
15338
|
-
|
|
15866
|
+
async function withEncodingSlot(work, signal) {
|
|
15867
|
+
signal?.throwIfAborted();
|
|
15868
|
+
if (active >= encodingConcurrency()) await new Promise((resolve, reject) => {
|
|
15869
|
+
const granted = () => {
|
|
15870
|
+
signal?.removeEventListener("abort", abort);
|
|
15871
|
+
resolve();
|
|
15872
|
+
};
|
|
15873
|
+
const abort = () => {
|
|
15874
|
+
const i = waiting.indexOf(granted);
|
|
15875
|
+
if (i >= 0) waiting.splice(i, 1);
|
|
15876
|
+
reject(new Error("encoding_aborted"));
|
|
15877
|
+
};
|
|
15878
|
+
waiting.push(granted);
|
|
15879
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
15880
|
+
if (signal?.aborted) abort();
|
|
15881
|
+
});
|
|
15339
15882
|
else active++;
|
|
15340
15883
|
try {
|
|
15341
15884
|
return await work();
|
|
@@ -15356,11 +15899,15 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15356
15899
|
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
15357
15900
|
}
|
|
15358
15901
|
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
15359
|
-
await
|
|
15902
|
+
await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
15360
15903
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
15361
15904
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
15905
|
+
signal?.throwIfAborted();
|
|
15906
|
+
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`;
|
|
15907
|
+
const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
|
|
15908
|
+
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"];
|
|
15362
15909
|
await new Promise((resolve, reject) => {
|
|
15363
|
-
const child =
|
|
15910
|
+
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"] });
|
|
15364
15911
|
let stderr = "";
|
|
15365
15912
|
child.stderr.on("data", (chunk) => {
|
|
15366
15913
|
stderr = (stderr + String(chunk)).slice(-2e3);
|
|
@@ -15368,6 +15915,7 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15368
15915
|
const abort = () => child.kill("SIGKILL");
|
|
15369
15916
|
const timer = setTimeout(abort, 20 * 6e4);
|
|
15370
15917
|
signal?.addEventListener("abort", abort, { once: true });
|
|
15918
|
+
if (signal?.aborted) abort();
|
|
15371
15919
|
const clean = () => {
|
|
15372
15920
|
clearTimeout(timer);
|
|
15373
15921
|
signal?.removeEventListener("abort", abort);
|
|
@@ -15381,12 +15929,12 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15381
15929
|
code === 0 ? resolve() : reject(new Error(`ffmpeg failed (${code}): ${stderr}`));
|
|
15382
15930
|
});
|
|
15383
15931
|
});
|
|
15384
|
-
});
|
|
15932
|
+
}, signal);
|
|
15385
15933
|
}
|
|
15386
15934
|
|
|
15387
15935
|
// ../recorder/src/screencast.ts
|
|
15388
|
-
import { mkdir as
|
|
15389
|
-
import { join as
|
|
15936
|
+
import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile6, open as open3 } from "fs/promises";
|
|
15937
|
+
import { join as join8 } from "path";
|
|
15390
15938
|
var ScreencastRecorder = class {
|
|
15391
15939
|
constructor(session, directory, width, height) {
|
|
15392
15940
|
this.session = session;
|
|
@@ -15394,6 +15942,10 @@ var ScreencastRecorder = class {
|
|
|
15394
15942
|
this.width = width;
|
|
15395
15943
|
this.height = height;
|
|
15396
15944
|
}
|
|
15945
|
+
session;
|
|
15946
|
+
directory;
|
|
15947
|
+
width;
|
|
15948
|
+
height;
|
|
15397
15949
|
frames = [];
|
|
15398
15950
|
pendingWrites = /* @__PURE__ */ new Set();
|
|
15399
15951
|
started = false;
|
|
@@ -15401,7 +15953,7 @@ var ScreencastRecorder = class {
|
|
|
15401
15953
|
frameBytes = 0;
|
|
15402
15954
|
captureError;
|
|
15403
15955
|
async start() {
|
|
15404
|
-
await
|
|
15956
|
+
await mkdir5(this.directory, { recursive: true, mode: 448 });
|
|
15405
15957
|
this.session.on("Page.screencastFrame", (payload) => {
|
|
15406
15958
|
const bytes = Buffer.from(payload.data, "base64");
|
|
15407
15959
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
@@ -15410,9 +15962,9 @@ var ScreencastRecorder = class {
|
|
|
15410
15962
|
return;
|
|
15411
15963
|
}
|
|
15412
15964
|
this.frameBytes += bytes.length;
|
|
15413
|
-
const path =
|
|
15965
|
+
const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
15414
15966
|
this.frameCounter += 1;
|
|
15415
|
-
const write =
|
|
15967
|
+
const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
|
|
15416
15968
|
this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
|
|
15417
15969
|
}).catch((error51) => {
|
|
15418
15970
|
this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
|
|
@@ -15426,7 +15978,7 @@ var ScreencastRecorder = class {
|
|
|
15426
15978
|
await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
|
|
15427
15979
|
this.started = true;
|
|
15428
15980
|
}
|
|
15429
|
-
async stop(outputPath, expectedDurationSeconds, captureOnly = false) {
|
|
15981
|
+
async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
|
|
15430
15982
|
if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
|
|
15431
15983
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
15432
15984
|
await Promise.all(this.pendingWrites);
|
|
@@ -15434,10 +15986,10 @@ var ScreencastRecorder = class {
|
|
|
15434
15986
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
15435
15987
|
const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
|
|
15436
15988
|
const frames = [];
|
|
15437
|
-
const bundle = await open3(
|
|
15989
|
+
const bundle = await open3(join8(this.directory, "frames.bin"), "w", 384);
|
|
15438
15990
|
try {
|
|
15439
15991
|
for (const frame of ordered) {
|
|
15440
|
-
const bytes = await
|
|
15992
|
+
const bytes = await readFile6(frame.path);
|
|
15441
15993
|
await bundle.writeFile(bytes);
|
|
15442
15994
|
frames.push({ timestamp: frame.timestamp, size: bytes.length });
|
|
15443
15995
|
}
|
|
@@ -15445,8 +15997,8 @@ var ScreencastRecorder = class {
|
|
|
15445
15997
|
await bundle.close();
|
|
15446
15998
|
}
|
|
15447
15999
|
const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
|
|
15448
|
-
await
|
|
15449
|
-
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
|
|
16000
|
+
await writeFile4(join8(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
|
|
16001
|
+
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath, signal);
|
|
15450
16002
|
return { frameCount: this.frames.length, outputPath };
|
|
15451
16003
|
}
|
|
15452
16004
|
};
|
|
@@ -15470,6 +16022,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15470
16022
|
requestEvents = /* @__PURE__ */ new Map();
|
|
15471
16023
|
stopPromise;
|
|
15472
16024
|
maxTimer;
|
|
16025
|
+
maxDurationSeconds;
|
|
15473
16026
|
removeCursorOverlay;
|
|
15474
16027
|
diagnosticsPaused = false;
|
|
15475
16028
|
secretValues = [];
|
|
@@ -15480,7 +16033,10 @@ var RecordingSession = class _RecordingSession {
|
|
|
15480
16033
|
bodyBytes = 0;
|
|
15481
16034
|
truncated = false;
|
|
15482
16035
|
constructor(options, runtime) {
|
|
16036
|
+
this.maxDurationSeconds = options.maxDurationSeconds ?? 600;
|
|
15483
16037
|
this.encoding = options.encoding ?? "local";
|
|
16038
|
+
this.videoFormat = options.videoFormat ?? "webm";
|
|
16039
|
+
this.captureSignal = options.signal;
|
|
15484
16040
|
this.recordingId = options.recordingId;
|
|
15485
16041
|
this.cdpUrl = runtime.cdpUrl;
|
|
15486
16042
|
this.browser = runtime.browser;
|
|
@@ -15494,15 +16050,17 @@ var RecordingSession = class _RecordingSession {
|
|
|
15494
16050
|
static async start(options) {
|
|
15495
16051
|
const width = options.width ?? 1280;
|
|
15496
16052
|
const height = options.height ?? 720;
|
|
15497
|
-
await
|
|
15498
|
-
if (options.encoding === "remote") await
|
|
15499
|
-
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
|
|
16053
|
+
await mkdir6(options.outputDirectory, { recursive: true, mode: 448 });
|
|
16054
|
+
if (options.encoding === "remote") await writeFile5(join9(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
16055
|
+
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer, blockServiceWorkers: options.blockServiceWorkers });
|
|
15500
16056
|
const cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
15501
|
-
const screencast = new ScreencastRecorder(cdpSession,
|
|
16057
|
+
const screencast = new ScreencastRecorder(cdpSession, join9(options.outputDirectory, "frames"), width, height);
|
|
15502
16058
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
15503
16059
|
try {
|
|
15504
16060
|
await session.attachDiagnostics(options.captureNetworkBodies ?? false);
|
|
15505
|
-
session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
|
|
16061
|
+
if (!options.hideCursor) session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
|
|
16062
|
+
await options.beforeCapture?.(runtime.page);
|
|
16063
|
+
session.startedAt = /* @__PURE__ */ new Date();
|
|
15506
16064
|
await screencast.start();
|
|
15507
16065
|
if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
15508
16066
|
} catch (error51) {
|
|
@@ -15511,7 +16069,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15511
16069
|
}
|
|
15512
16070
|
session.maxTimer = setTimeout(() => {
|
|
15513
16071
|
void session.stop("max_duration").catch(() => void 0);
|
|
15514
|
-
}, (
|
|
16072
|
+
}, Math.max(0, session.startedAt.getTime() + session.maxDurationSeconds * 1e3 - Date.now()));
|
|
15515
16073
|
return session;
|
|
15516
16074
|
}
|
|
15517
16075
|
// In-process agents share this connection so exposed diagnostic bindings are
|
|
@@ -15520,7 +16078,17 @@ var RecordingSession = class _RecordingSession {
|
|
|
15520
16078
|
return this.page;
|
|
15521
16079
|
}
|
|
15522
16080
|
status() {
|
|
15523
|
-
|
|
16081
|
+
const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
16082
|
+
return {
|
|
16083
|
+
recordingId: this.recordingId,
|
|
16084
|
+
cdpUrl: this.cdpUrl,
|
|
16085
|
+
startedAt: this.startedAt.toISOString(),
|
|
16086
|
+
maxDurationSeconds: this.maxDurationSeconds,
|
|
16087
|
+
elapsedSeconds,
|
|
16088
|
+
remainingSeconds: this.stopPromise ? 0 : Math.max(0, this.maxDurationSeconds - elapsedSeconds),
|
|
16089
|
+
stopsAt: new Date(this.startedAt.getTime() + this.maxDurationSeconds * 1e3).toISOString(),
|
|
16090
|
+
status: this.stopPromise ? "finalizing" : "recording"
|
|
16091
|
+
};
|
|
15524
16092
|
}
|
|
15525
16093
|
async stop(reason = "agent_request") {
|
|
15526
16094
|
this.stopPromise ??= this.finalize(reason);
|
|
@@ -15530,8 +16098,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15530
16098
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
15531
16099
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
15532
16100
|
const outputDirectory = this.outputDirectory;
|
|
15533
|
-
const videoPath =
|
|
15534
|
-
const diagnosticsPath =
|
|
16101
|
+
const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
|
|
16102
|
+
const diagnosticsPath = join9(outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
15535
16103
|
const bundle = {
|
|
15536
16104
|
schemaVersion: 1,
|
|
15537
16105
|
capturePolicyVersion: "safe-default-v1",
|
|
@@ -15548,21 +16116,23 @@ var RecordingSession = class _RecordingSession {
|
|
|
15548
16116
|
try {
|
|
15549
16117
|
const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
|
|
15550
16118
|
await writeBundle(safeBundle, diagnosticsPath);
|
|
15551
|
-
await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote");
|
|
16119
|
+
await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
|
|
15552
16120
|
} finally {
|
|
15553
16121
|
await this.removeCursorOverlay?.();
|
|
15554
16122
|
if (this.browserProcess) {
|
|
15555
16123
|
await this.browser.close().catch(() => void 0);
|
|
15556
16124
|
await terminateBrowser(this.browserProcess);
|
|
15557
|
-
if (this.profileDirectory) await
|
|
16125
|
+
if (this.profileDirectory) await rm5(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15558
16126
|
}
|
|
15559
16127
|
}
|
|
15560
16128
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
15561
16129
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
15562
16130
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
15563
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory:
|
|
16131
|
+
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 } };
|
|
15564
16132
|
}
|
|
15565
16133
|
captureNetworkBodies = false;
|
|
16134
|
+
captureSignal;
|
|
16135
|
+
videoFormat = "webm";
|
|
15566
16136
|
encoding = "local";
|
|
15567
16137
|
outputDirectory = "";
|
|
15568
16138
|
async attachDiagnostics(captureNetworkBodies) {
|
|
@@ -15672,7 +16242,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15672
16242
|
};
|
|
15673
16243
|
async function launchManaged(options) {
|
|
15674
16244
|
const port = await getAvailablePort();
|
|
15675
|
-
const profileDirectory = await mkdtemp(
|
|
16245
|
+
const profileDirectory = await mkdtemp(join9(tmpdir2(), "rill-chrome-"));
|
|
15676
16246
|
const executable = findChromeExecutable();
|
|
15677
16247
|
const args = [
|
|
15678
16248
|
`--remote-debugging-port=${port}`,
|
|
@@ -15689,19 +16259,19 @@ async function launchManaged(options) {
|
|
|
15689
16259
|
...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
|
|
15690
16260
|
"about:blank"
|
|
15691
16261
|
];
|
|
15692
|
-
const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([
|
|
15693
|
-
const browserProcess =
|
|
16262
|
+
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;
|
|
16263
|
+
const browserProcess = spawn6(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, windowsHide: true, env });
|
|
15694
16264
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15695
16265
|
try {
|
|
15696
16266
|
await waitForCdp(cdpUrl, browserProcess);
|
|
15697
16267
|
const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
|
|
15698
|
-
const context = browser.contexts()[0];
|
|
16268
|
+
const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
|
|
15699
16269
|
const page = context.pages()[0] ?? await context.newPage();
|
|
15700
16270
|
await page.setViewportSize({ width: options.width, height: options.height });
|
|
15701
16271
|
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15702
16272
|
} catch (error51) {
|
|
15703
16273
|
await terminateBrowser(browserProcess);
|
|
15704
|
-
await
|
|
16274
|
+
await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15705
16275
|
throw error51;
|
|
15706
16276
|
}
|
|
15707
16277
|
}
|
|
@@ -15751,16 +16321,31 @@ async function waitForCdp(cdpUrl, browserProcess) {
|
|
|
15751
16321
|
var sessions = /* @__PURE__ */ new Map();
|
|
15752
16322
|
var lastActivity = Date.now();
|
|
15753
16323
|
async function runDaemon() {
|
|
15754
|
-
|
|
15755
|
-
await
|
|
15756
|
-
|
|
16324
|
+
if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
|
|
16325
|
+
const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
|
|
16326
|
+
const socketPath = identity?.pipe ?? daemonSocketPath();
|
|
16327
|
+
await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
|
|
16328
|
+
await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
15757
16329
|
await restoreRecoverableSessions();
|
|
15758
16330
|
await purgeExpiredCaptures();
|
|
15759
|
-
await unlink(socketPath).catch((error51) => {
|
|
16331
|
+
if (!identity) await unlink(socketPath).catch((error51) => {
|
|
15760
16332
|
if (error51.code !== "ENOENT") throw error51;
|
|
15761
16333
|
});
|
|
15762
|
-
const server = createServer2((request3, response) =>
|
|
15763
|
-
|
|
16334
|
+
const server = createServer2((request3, response) => {
|
|
16335
|
+
if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
|
|
16336
|
+
request3.resume();
|
|
16337
|
+
json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
|
|
16338
|
+
return;
|
|
16339
|
+
}
|
|
16340
|
+
void route(request3, response);
|
|
16341
|
+
});
|
|
16342
|
+
await new Promise((resolve, reject) => {
|
|
16343
|
+
server.once("error", reject);
|
|
16344
|
+
server.listen(socketPath, () => {
|
|
16345
|
+
if (identity) resolve();
|
|
16346
|
+
else void chmod(socketPath, 384).then(resolve, reject);
|
|
16347
|
+
});
|
|
16348
|
+
});
|
|
15764
16349
|
const idleTimer = setInterval(() => {
|
|
15765
16350
|
void (async () => {
|
|
15766
16351
|
await purgeExpiredCaptures();
|
|
@@ -15773,7 +16358,7 @@ async function runDaemon() {
|
|
|
15773
16358
|
const close = () => {
|
|
15774
16359
|
clearInterval(idleTimer);
|
|
15775
16360
|
server.close();
|
|
15776
|
-
void unlink(socketPath).catch(() => void 0);
|
|
16361
|
+
if (!identity) void unlink(socketPath).catch(() => void 0);
|
|
15777
16362
|
};
|
|
15778
16363
|
process.once("SIGTERM", close);
|
|
15779
16364
|
process.once("SIGINT", close);
|
|
@@ -15787,12 +16372,12 @@ async function route(request3, response) {
|
|
|
15787
16372
|
try {
|
|
15788
16373
|
const input = await readJson(request3);
|
|
15789
16374
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15790
|
-
const outputDirectory =
|
|
16375
|
+
const outputDirectory = join10(recordingTempRoot(), input.recordingId);
|
|
15791
16376
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
15792
16377
|
const status = session.status();
|
|
15793
16378
|
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
|
|
15794
16379
|
sessions.set(input.recordingId, entry);
|
|
15795
|
-
const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), (
|
|
16380
|
+
const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), Math.max(0, Date.parse(status.stopsAt) - Date.now()) + 250);
|
|
15796
16381
|
maxTimer.unref();
|
|
15797
16382
|
await persistState();
|
|
15798
16383
|
return json2(response, 201, session.status());
|
|
@@ -15827,11 +16412,18 @@ async function finalizeEntry(entry, reason) {
|
|
|
15827
16412
|
await persistState();
|
|
15828
16413
|
}
|
|
15829
16414
|
function serializeEntry(entry) {
|
|
15830
|
-
return {
|
|
16415
|
+
return {
|
|
16416
|
+
...entry.base,
|
|
16417
|
+
...entry.session?.status(),
|
|
16418
|
+
status: entry.state,
|
|
16419
|
+
...entry.state !== "recording" ? { remainingSeconds: 0, ...entry.result ? { elapsedSeconds: entry.result.durationSeconds } : {} } : {},
|
|
16420
|
+
result: entry.result,
|
|
16421
|
+
error: entry.error
|
|
16422
|
+
};
|
|
15831
16423
|
}
|
|
15832
16424
|
async function restoreRecoverableSessions() {
|
|
15833
16425
|
try {
|
|
15834
|
-
const value = JSON.parse(await
|
|
16426
|
+
const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
|
|
15835
16427
|
for (const [id, entry] of Object.entries(value)) {
|
|
15836
16428
|
if (entry.status !== "ready" && entry.status !== "failed") continue;
|
|
15837
16429
|
sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
|
|
@@ -15842,9 +16434,9 @@ async function restoreRecoverableSessions() {
|
|
|
15842
16434
|
async function persistState() {
|
|
15843
16435
|
const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
|
|
15844
16436
|
const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
|
|
15845
|
-
await
|
|
16437
|
+
await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
|
|
15846
16438
|
`, { mode: 384 });
|
|
15847
|
-
await
|
|
16439
|
+
await rename2(temporary, daemonStatePath());
|
|
15848
16440
|
}
|
|
15849
16441
|
async function readJson(request3) {
|
|
15850
16442
|
const chunks = [];
|
|
@@ -15865,7 +16457,7 @@ async function purgeExpiredCaptures() {
|
|
|
15865
16457
|
const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
|
|
15866
16458
|
for (const id of removed) sessions.delete(id);
|
|
15867
16459
|
let changed = removed.length > 0;
|
|
15868
|
-
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await
|
|
16460
|
+
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat4(entry.result.frameDirectory).catch(() => null)) {
|
|
15869
16461
|
sessions.delete(id);
|
|
15870
16462
|
changed = true;
|
|
15871
16463
|
}
|
|
@@ -15910,7 +16502,7 @@ function emit(value, pretty = false) {
|
|
|
15910
16502
|
function failureValue(error51) {
|
|
15911
16503
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
15912
16504
|
const [candidate] = message.split(":", 1);
|
|
15913
|
-
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_failed", "share_revoked", "validation_failed"]);
|
|
16505
|
+
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"]);
|
|
15914
16506
|
const structured = error51 && typeof error51 === "object" ? error51 : {};
|
|
15915
16507
|
const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
|
|
15916
16508
|
return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
|
|
@@ -15977,7 +16569,7 @@ async function withRunFeedback(session, start) {
|
|
|
15977
16569
|
}
|
|
15978
16570
|
|
|
15979
16571
|
// src/doctor.ts
|
|
15980
|
-
import { spawn as
|
|
16572
|
+
import { spawn as spawn7 } from "child_process";
|
|
15981
16573
|
|
|
15982
16574
|
// src/version.ts
|
|
15983
16575
|
import { createRequire } from "module";
|
|
@@ -15986,6 +16578,7 @@ var CLI_VERSION = packageJson.version;
|
|
|
15986
16578
|
|
|
15987
16579
|
// src/doctor.ts
|
|
15988
16580
|
var defaultDependencies = {
|
|
16581
|
+
platform: process.platform,
|
|
15989
16582
|
encoding: "remote",
|
|
15990
16583
|
encodingCapabilities: (apiUrl, token) => new ControlPlaneClient(apiUrl, token).encodingCapabilities(),
|
|
15991
16584
|
nodeVersion: process.version,
|
|
@@ -16001,6 +16594,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16001
16594
|
const dependencies = { ...defaultDependencies, ...overrides };
|
|
16002
16595
|
const checks = [];
|
|
16003
16596
|
let feedbackEnabled = false;
|
|
16597
|
+
checks.push(recorderTransportCheck(dependencies.platform));
|
|
16004
16598
|
if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
|
|
16005
16599
|
checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
|
|
16006
16600
|
} else {
|
|
@@ -16028,8 +16622,16 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16028
16622
|
if (!health) {
|
|
16029
16623
|
checks.push({ name: "authentication", status: "skip", message: "Authentication was not checked because the API is unreachable.", recovery: `Restore API reachability, then run \`rill --api-url ${apiUrl} doctor\` again.` });
|
|
16030
16624
|
} else {
|
|
16031
|
-
|
|
16032
|
-
|
|
16625
|
+
let token = null;
|
|
16626
|
+
let credentialError;
|
|
16627
|
+
try {
|
|
16628
|
+
token = await dependencies.loadCredential(apiUrl);
|
|
16629
|
+
} catch (error51) {
|
|
16630
|
+
credentialError = error51;
|
|
16631
|
+
}
|
|
16632
|
+
if (credentialError) {
|
|
16633
|
+
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." });
|
|
16634
|
+
} else if (!token) {
|
|
16033
16635
|
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.` });
|
|
16034
16636
|
} else {
|
|
16035
16637
|
try {
|
|
@@ -16043,7 +16645,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16043
16645
|
}
|
|
16044
16646
|
checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
|
|
16045
16647
|
if (dependencies.encoding === "local") {
|
|
16046
|
-
checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion,
|
|
16648
|
+
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."));
|
|
16047
16649
|
} else {
|
|
16048
16650
|
checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
|
|
16049
16651
|
try {
|
|
@@ -16051,7 +16653,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16051
16653
|
if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
|
|
16052
16654
|
checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
|
|
16053
16655
|
} catch (error51) {
|
|
16054
|
-
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "
|
|
16656
|
+
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." });
|
|
16055
16657
|
}
|
|
16056
16658
|
}
|
|
16057
16659
|
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
@@ -16066,7 +16668,7 @@ async function executableCheck(name, path, args, run2, recovery) {
|
|
|
16066
16668
|
}
|
|
16067
16669
|
function commandVersion(command, args) {
|
|
16068
16670
|
return new Promise((resolve, reject) => {
|
|
16069
|
-
const child =
|
|
16671
|
+
const child = spawn7(command, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
16070
16672
|
const stdout = [];
|
|
16071
16673
|
const stderr = [];
|
|
16072
16674
|
const timeout = setTimeout(() => {
|
|
@@ -16127,31 +16729,53 @@ function formatCloudRun(run2) {
|
|
|
16127
16729
|
}
|
|
16128
16730
|
|
|
16129
16731
|
// src/recording-lifecycle.ts
|
|
16130
|
-
async function startRecordingWithCompensation(remote, dependencies) {
|
|
16732
|
+
async function startRecordingWithCompensation(remote, dependencies, apiUrl = "https://userill.dev") {
|
|
16131
16733
|
try {
|
|
16132
16734
|
return await dependencies.startLocal();
|
|
16133
16735
|
} catch (startupError) {
|
|
16134
16736
|
try {
|
|
16135
16737
|
await dependencies.cancelRemote(remote.recordingId, "local_start_failed");
|
|
16136
16738
|
} catch (cleanupError) {
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
16739
|
+
const nextCommand = cliCommand(apiUrl, "record", "cancel", remote.recordingId);
|
|
16740
|
+
throw Object.assign(
|
|
16741
|
+
new AggregateError(
|
|
16742
|
+
[startupError, cleanupError],
|
|
16743
|
+
"browser_unavailable: Local startup failed and its remote reservation could not be cancelled."
|
|
16744
|
+
),
|
|
16745
|
+
{
|
|
16746
|
+
code: "browser_unavailable",
|
|
16747
|
+
recovery: `Run ${nextCommand} to release this failed startup reservation.`,
|
|
16748
|
+
details: { recordingId: remote.recordingId, nextCommand }
|
|
16749
|
+
}
|
|
16140
16750
|
);
|
|
16141
16751
|
}
|
|
16142
16752
|
throw startupError;
|
|
16143
16753
|
}
|
|
16144
16754
|
}
|
|
16145
|
-
async function remoteAwareRecordingStatus(recordingId, dependencies) {
|
|
16755
|
+
async function remoteAwareRecordingStatus(recordingId, dependencies, apiUrl = "https://userill.dev") {
|
|
16756
|
+
const remote = await dependencies.remoteStatus(recordingId);
|
|
16757
|
+
const remoteView = {
|
|
16758
|
+
...remote,
|
|
16759
|
+
recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
|
|
16760
|
+
source: "remote",
|
|
16761
|
+
...recordingNextStep(apiUrl, recordingId, String(remote.status))
|
|
16762
|
+
};
|
|
16763
|
+
if (["uploading", "processing", "ready", "failed", "expired", "cancelled"].includes(String(remote.status))) return remoteView;
|
|
16146
16764
|
try {
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
const remote = await dependencies.remoteStatus(recordingId);
|
|
16765
|
+
const local = await dependencies.localStatus(recordingId);
|
|
16766
|
+
const status = local.status === "ready" ? "captured" : local.status;
|
|
16150
16767
|
return {
|
|
16151
|
-
...
|
|
16152
|
-
|
|
16153
|
-
source: "
|
|
16768
|
+
...local,
|
|
16769
|
+
status,
|
|
16770
|
+
source: "local",
|
|
16771
|
+
...recordingNextStep(apiUrl, recordingId, String(status)),
|
|
16772
|
+
...status === "failed" ? {
|
|
16773
|
+
nextCommand: cliCommand(apiUrl, "record", "cancel", recordingId),
|
|
16774
|
+
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."
|
|
16775
|
+
} : {}
|
|
16154
16776
|
};
|
|
16777
|
+
} catch {
|
|
16778
|
+
return remoteView;
|
|
16155
16779
|
}
|
|
16156
16780
|
}
|
|
16157
16781
|
|
|
@@ -16165,10 +16789,10 @@ function controlPlaneUrl(environmentUrl) {
|
|
|
16165
16789
|
async function removeLocalCapture(id) {
|
|
16166
16790
|
await ensureDaemon(process.argv[1]);
|
|
16167
16791
|
const local = await recordingStatus(id).catch(() => null);
|
|
16168
|
-
if (local?.result?.frameDirectory) await
|
|
16792
|
+
if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16169
16793
|
}
|
|
16170
16794
|
function watchEncoding(apiUrl, id) {
|
|
16171
|
-
const child =
|
|
16795
|
+
const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
|
|
16172
16796
|
child.on("error", () => void 0);
|
|
16173
16797
|
child.unref();
|
|
16174
16798
|
}
|
|
@@ -16190,7 +16814,7 @@ async function authorize(quiet = false) {
|
|
|
16190
16814
|
process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
|
|
16191
16815
|
`);
|
|
16192
16816
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
16193
|
-
const child =
|
|
16817
|
+
const child = spawn8(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
|
|
16194
16818
|
child.on("error", () => void 0);
|
|
16195
16819
|
child.unref();
|
|
16196
16820
|
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
@@ -16211,9 +16835,16 @@ program.command("whoami").description("Show the current verified agent identity
|
|
|
16211
16835
|
emit(identity, options.pretty);
|
|
16212
16836
|
if (!identity.authenticated) process.exitCode = 1;
|
|
16213
16837
|
});
|
|
16214
|
-
program.command("connect").description("Connect this agent once, with renewable account access.").
|
|
16838
|
+
var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
|
|
16839
|
+
if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
|
|
16215
16840
|
const options = program.opts();
|
|
16216
|
-
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
|
|
16841
|
+
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email, { wait: input.wait }) }, options.pretty);
|
|
16842
|
+
});
|
|
16843
|
+
connect.command("status").argument("<authorization-id>").description("Poll a saved connection once without starting another approval.").action(async (id) => {
|
|
16844
|
+
const options = program.opts();
|
|
16845
|
+
const result = await pollConnection(options.apiUrl, id);
|
|
16846
|
+
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16847
|
+
if (result.status === "expired" || result.status === "denied") process.exitCode = 1;
|
|
16217
16848
|
});
|
|
16218
16849
|
program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
|
|
16219
16850
|
await authorize();
|
|
@@ -16252,6 +16883,7 @@ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<r
|
|
|
16252
16883
|
});
|
|
16253
16884
|
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("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
|
|
16254
16885
|
const options = program.opts();
|
|
16886
|
+
assertRecorderTransport();
|
|
16255
16887
|
await ensureAnonymousCredential(options.apiUrl);
|
|
16256
16888
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16257
16889
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
@@ -16268,7 +16900,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16268
16900
|
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 });
|
|
16269
16901
|
},
|
|
16270
16902
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
16271
|
-
});
|
|
16903
|
+
}, options.apiUrl);
|
|
16272
16904
|
await client.markRecordingStarted(remote.recordingId, {
|
|
16273
16905
|
cliVersion: CLI_VERSION,
|
|
16274
16906
|
os: process.platform,
|
|
@@ -16276,7 +16908,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16276
16908
|
headed: Boolean(commandOptions.headed),
|
|
16277
16909
|
attached: Boolean(commandOptions.cdpUrl)
|
|
16278
16910
|
}).catch(() => void 0);
|
|
16279
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
16911
|
+
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl), 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) };
|
|
16280
16912
|
});
|
|
16281
16913
|
emit(result, options.pretty);
|
|
16282
16914
|
});
|
|
@@ -16296,12 +16928,12 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16296
16928
|
await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
|
|
16297
16929
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
16298
16930
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16299
|
-
emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16300
|
-
if (result2.status === "ready") await
|
|
16931
|
+
emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16932
|
+
if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16301
16933
|
else watchEncoding(options.apiUrl, recordingId);
|
|
16302
16934
|
return;
|
|
16303
16935
|
}
|
|
16304
|
-
const video = await
|
|
16936
|
+
const video = await stat5(local.result.videoPath);
|
|
16305
16937
|
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
|
|
16306
16938
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
16307
16939
|
const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
@@ -16315,10 +16947,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16315
16947
|
durationSeconds: local.result.durationSeconds,
|
|
16316
16948
|
stoppedReason: local.result.stoppedReason,
|
|
16317
16949
|
diagnostics: local.result.diagnostics,
|
|
16950
|
+
...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
|
|
16318
16951
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
16319
16952
|
};
|
|
16320
16953
|
emit(result, options.pretty);
|
|
16321
|
-
if (remote) await
|
|
16954
|
+
if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16322
16955
|
});
|
|
16323
16956
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
16324
16957
|
const options = program.opts();
|
|
@@ -16338,7 +16971,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
|
|
|
16338
16971
|
if (!accepted) throw new Error("Encoding job is unavailable.");
|
|
16339
16972
|
if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
|
|
16340
16973
|
const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16341
|
-
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16974
|
+
emit({ schemaVersion: 1, ...result, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
|
|
16342
16975
|
if (result.status === "ready") await removeLocalCapture(recordingId);
|
|
16343
16976
|
else watchEncoding(options.apiUrl, recordingId);
|
|
16344
16977
|
});
|
|
@@ -16350,7 +16983,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
|
|
|
16350
16983
|
if (result.status === "ready") {
|
|
16351
16984
|
await ensureDaemon(process.argv[1]);
|
|
16352
16985
|
const local = await recordingStatus(id).catch(() => null);
|
|
16353
|
-
if (local?.result?.frameDirectory) await
|
|
16986
|
+
if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16354
16987
|
}
|
|
16355
16988
|
});
|
|
16356
16989
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
@@ -16380,11 +17013,11 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16380
17013
|
try {
|
|
16381
17014
|
const encoding = await client.encodingStatus(recordingId);
|
|
16382
17015
|
const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
|
|
16383
|
-
emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote" }, options.pretty);
|
|
17016
|
+
emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
|
|
16384
17017
|
if (encoding.status === "ready") await removeLocalCapture(recordingId);
|
|
16385
17018
|
return;
|
|
16386
17019
|
} catch (error51) {
|
|
16387
|
-
if (!(error51 instanceof
|
|
17020
|
+
if (!(error51 instanceof ControlPlaneError) || error51.code !== "recording_not_found") throw error51;
|
|
16388
17021
|
}
|
|
16389
17022
|
const status = await remoteAwareRecordingStatus(recordingId, {
|
|
16390
17023
|
localStatus: async (id) => {
|
|
@@ -16392,7 +17025,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16392
17025
|
return recordingStatus(id);
|
|
16393
17026
|
},
|
|
16394
17027
|
remoteStatus: (id) => client.recordingStatus(id)
|
|
16395
|
-
});
|
|
17028
|
+
}, options.apiUrl);
|
|
16396
17029
|
emit({ schemaVersion: 1, ...status }, options.pretty);
|
|
16397
17030
|
});
|
|
16398
17031
|
record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
@@ -16405,41 +17038,56 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
|
|
|
16405
17038
|
const options = program.opts();
|
|
16406
17039
|
await ensureAnonymousCredential(options.apiUrl);
|
|
16407
17040
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16408
|
-
const file2 = await
|
|
17041
|
+
const file2 = await stat5(videoFile);
|
|
16409
17042
|
const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
|
|
16410
17043
|
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
|
|
16411
17044
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
16412
17045
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
|
|
16413
17046
|
if (remote) process.stderr.write("\n");
|
|
16414
|
-
emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false }, options.pretty);
|
|
17047
|
+
emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false, ...recordingNextStep(options.apiUrl, recording.recordingId, remote ? "ready" : "processing") }, options.pretty);
|
|
16415
17048
|
});
|
|
16416
|
-
program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
|
|
17049
|
+
program.command("inspect").argument("<share-url>").option("--cursor <cursor>", "Read the next page of discussions").action(async (shareUrl, commandOptions) => {
|
|
16417
17050
|
const options = program.opts();
|
|
16418
|
-
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
|
|
17051
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl, commandOptions.cursor), options.pretty);
|
|
17052
|
+
});
|
|
17053
|
+
var comments = program.command("comments").description("Reply to pinned review discussions.");
|
|
17054
|
+
comments.command("reply").argument("<share-url>").requiredOption("--discussion <id>").requiredOption("--body <text>").option("--request-id <uuid>", "Reuse on retry to prevent duplicate replies").action(async (shareUrl, input) => {
|
|
17055
|
+
const options = program.opts();
|
|
17056
|
+
const requestId = input.requestId ?? crypto.randomUUID();
|
|
17057
|
+
process.stderr.write(`Reply request ID: ${requestId} (reuse with --request-id when retrying)
|
|
17058
|
+
`);
|
|
17059
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
17060
|
+
emit(await client.reply(shareUrl, input.discussion, input.body, requestId), options.pretty);
|
|
17061
|
+
});
|
|
17062
|
+
comments.command("edit").argument("<share-url>").requiredOption("--comment <id>").requiredOption("--body <text>").action(async (shareUrl, input) => {
|
|
17063
|
+
const options = program.opts();
|
|
17064
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
17065
|
+
emit(await client.editReply(shareUrl, input.comment, input.body), options.pretty);
|
|
16419
17066
|
});
|
|
16420
17067
|
var drafts = program.command("drafts");
|
|
16421
17068
|
drafts.command("list").action(async () => {
|
|
16422
17069
|
const options = program.opts();
|
|
16423
|
-
const root = process.env.RILL_TEMP ??
|
|
17070
|
+
const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
|
|
16424
17071
|
const names = await readdir2(root).catch(() => []);
|
|
16425
17072
|
emit({ schemaVersion: 1, drafts: names }, options.pretty);
|
|
16426
17073
|
});
|
|
16427
17074
|
drafts.command("purge").action(async () => {
|
|
16428
17075
|
const options = program.opts();
|
|
16429
|
-
const root = process.env.RILL_TEMP ??
|
|
17076
|
+
const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
|
|
16430
17077
|
const names = await readdir2(root).catch(() => []);
|
|
16431
17078
|
let purged = 0;
|
|
16432
17079
|
for (const name of names) {
|
|
16433
|
-
const path =
|
|
16434
|
-
const metadata = await
|
|
17080
|
+
const path = join11(root, name);
|
|
17081
|
+
const metadata = await stat5(path);
|
|
16435
17082
|
if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
|
|
16436
|
-
await
|
|
17083
|
+
await rm7(path, { recursive: true, force: true });
|
|
16437
17084
|
purged += 1;
|
|
16438
17085
|
}
|
|
16439
17086
|
}
|
|
16440
17087
|
emit({ schemaVersion: 1, purged }, options.pretty);
|
|
16441
17088
|
});
|
|
16442
17089
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
17090
|
+
if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
|
|
16443
17091
|
const command = cliCommandCategory(commandPath(actionCommand));
|
|
16444
17092
|
if (!command) return;
|
|
16445
17093
|
const options = program.opts();
|