@runalabs/rill-cli 0.1.10 → 0.1.12
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 +30 -7
- package/dist/cli.js +508 -318
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,389 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// 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
|
+
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
14
|
+
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15
|
+
var AgentAuthError = class extends Error {
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(`${code}: ${message}`);
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function parseAuthorization(value) {
|
|
22
|
+
if (!value?.startsWith("{")) return null;
|
|
23
|
+
const saved = JSON.parse(value);
|
|
24
|
+
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
25
|
+
return saved;
|
|
26
|
+
}
|
|
27
|
+
async function request(apiUrl, path, body, form = false) {
|
|
28
|
+
const url2 = new URL(apiUrl);
|
|
29
|
+
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
30
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
redirect: "error",
|
|
33
|
+
signal: AbortSignal.timeout(15e3),
|
|
34
|
+
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
35
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
36
|
+
});
|
|
37
|
+
const value = await response.json();
|
|
38
|
+
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function withTokens(saved, tokens) {
|
|
42
|
+
return {
|
|
43
|
+
...saved,
|
|
44
|
+
accessToken: tokens.access_token,
|
|
45
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
46
|
+
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
50
|
+
var pending = /* @__PURE__ */ new Map();
|
|
51
|
+
async function loadAgentCredential(apiUrl) {
|
|
52
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
53
|
+
const inFlight = pending.get(apiUrl);
|
|
54
|
+
if (inFlight) return inFlight;
|
|
55
|
+
const promise2 = (async () => {
|
|
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
|
+
})();
|
|
66
|
+
pending.set(apiUrl, promise2);
|
|
67
|
+
try {
|
|
68
|
+
return await promise2;
|
|
69
|
+
} finally {
|
|
70
|
+
pending.delete(apiUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function ensureAnonymousCredential(apiUrl) {
|
|
74
|
+
const root = join(homedir(), ".rill", "auth-locks");
|
|
75
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
76
|
+
const lock = join(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
77
|
+
const deadline = Date.now() + 3e4;
|
|
78
|
+
while (true) {
|
|
79
|
+
try {
|
|
80
|
+
await mkdir(lock, { mode: 448 });
|
|
81
|
+
break;
|
|
82
|
+
} catch (error51) {
|
|
83
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
84
|
+
const info = await stat(lock).catch(() => null);
|
|
85
|
+
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
86
|
+
await rm(lock, { recursive: true, force: true });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return await createAnonymousCredential(apiUrl);
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(lock, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function createAnonymousCredential(apiUrl) {
|
|
100
|
+
const existing = await loadAgentCredential(apiUrl);
|
|
101
|
+
if (existing) return existing;
|
|
102
|
+
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
103
|
+
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
104
|
+
await save(apiUrl, initial);
|
|
105
|
+
const registered = await request(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname()} \xB7 ${process.platform}` });
|
|
106
|
+
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
107
|
+
await save(apiUrl, saved);
|
|
108
|
+
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 loadAgentCredential(apiUrl);
|
|
110
|
+
}
|
|
111
|
+
async function connectAgent(apiUrl, email3) {
|
|
112
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
113
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
114
|
+
let claim;
|
|
115
|
+
if (saved?.claimToken && saved.anonymous) {
|
|
116
|
+
try {
|
|
117
|
+
const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
118
|
+
claim = result.claim_attempt;
|
|
119
|
+
} catch (error51) {
|
|
120
|
+
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!claim) {
|
|
124
|
+
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
125
|
+
const result = await request(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname()} \xB7 ${process.platform}` });
|
|
126
|
+
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
127
|
+
await save(apiUrl, saved);
|
|
128
|
+
claim = result.claim;
|
|
129
|
+
}
|
|
130
|
+
process.stderr.write(`Open ${claim.verification_uri}
|
|
131
|
+
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
132
|
+
`);
|
|
133
|
+
const deadline = Date.now() + claim.expires_in * 1e3;
|
|
134
|
+
let interval = claim.interval;
|
|
135
|
+
while (Date.now() < deadline) {
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
137
|
+
try {
|
|
138
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
139
|
+
const connected = withTokens({ version: 1, anonymous: false }, tokens);
|
|
140
|
+
await save(apiUrl, connected);
|
|
141
|
+
return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
|
|
142
|
+
} catch (error51) {
|
|
143
|
+
if (error51 instanceof AgentAuthError && error51.code === "expired_token") throw new AgentAuthError("expired_token", "The approval attempt expired. Run rill connect again to request a fresh code using the same saved claim token.");
|
|
144
|
+
if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
|
|
145
|
+
if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
|
|
146
|
+
interval += 5;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
throw error51;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/keychain.ts
|
|
156
|
+
import { spawn } from "child_process";
|
|
157
|
+
var service = "rill";
|
|
158
|
+
async function saveCredential(apiUrl, token) {
|
|
159
|
+
if (process.platform === "darwin") {
|
|
160
|
+
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (process.platform === "linux") {
|
|
164
|
+
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
168
|
+
}
|
|
169
|
+
function loadCredential(apiUrl) {
|
|
170
|
+
return loadAgentCredential(apiUrl);
|
|
171
|
+
}
|
|
172
|
+
async function loadStoredCredential(apiUrl) {
|
|
173
|
+
try {
|
|
174
|
+
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
175
|
+
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
176
|
+
} catch (error51) {
|
|
177
|
+
const code = error51.exitCode;
|
|
178
|
+
if (process.platform === "darwin" && code === 44) return null;
|
|
179
|
+
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
180
|
+
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
181
|
+
}
|
|
182
|
+
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
183
|
+
}
|
|
184
|
+
function run(command, args, input) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
187
|
+
const stdout = [];
|
|
188
|
+
const stderr = [];
|
|
189
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
190
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
191
|
+
child.once("error", reject);
|
|
192
|
+
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
|
|
193
|
+
if (input) child.stdin.end(input);
|
|
194
|
+
else child.stdin.end();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/control-plane.ts
|
|
199
|
+
import { createReadStream } from "fs";
|
|
200
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
201
|
+
import { Upload } from "tus-js-client";
|
|
202
|
+
var ControlPlaneError = class extends Error {
|
|
203
|
+
constructor(code, message, recovery, details, httpStatus) {
|
|
204
|
+
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
205
|
+
this.code = code;
|
|
206
|
+
this.recovery = recovery;
|
|
207
|
+
this.details = details;
|
|
208
|
+
this.httpStatus = httpStatus;
|
|
209
|
+
this.name = "ControlPlaneError";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var ControlPlaneHttpError = class extends Error {
|
|
213
|
+
constructor(httpStatus) {
|
|
214
|
+
super(`Request failed (${httpStatus}).`);
|
|
215
|
+
this.httpStatus = httpStatus;
|
|
216
|
+
this.name = "ControlPlaneHttpError";
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var ControlPlaneClient = class {
|
|
220
|
+
constructor(apiUrl, token) {
|
|
221
|
+
this.apiUrl = apiUrl;
|
|
222
|
+
this.token = token;
|
|
223
|
+
}
|
|
224
|
+
encodingCapabilities() {
|
|
225
|
+
return this.request("/api/encoding/capabilities");
|
|
226
|
+
}
|
|
227
|
+
beginEncoding(id, manifest) {
|
|
228
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
229
|
+
}
|
|
230
|
+
encodingStatus(id) {
|
|
231
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
232
|
+
}
|
|
233
|
+
uploadEncodingPart(id, part, bytes) {
|
|
234
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
235
|
+
}
|
|
236
|
+
completeEncoding(id) {
|
|
237
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
238
|
+
}
|
|
239
|
+
retryEncoding(id) {
|
|
240
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
241
|
+
}
|
|
242
|
+
createCloudRun(input) {
|
|
243
|
+
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
244
|
+
}
|
|
245
|
+
cloudRun(id) {
|
|
246
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
247
|
+
}
|
|
248
|
+
cancelCloudRun(id) {
|
|
249
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
250
|
+
}
|
|
251
|
+
health() {
|
|
252
|
+
return this.request("/api/health", {}, false);
|
|
253
|
+
}
|
|
254
|
+
agentSession() {
|
|
255
|
+
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
256
|
+
}
|
|
257
|
+
reportCliCommand(input) {
|
|
258
|
+
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
259
|
+
}
|
|
260
|
+
createRecording(input) {
|
|
261
|
+
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
262
|
+
}
|
|
263
|
+
markRecordingStarted(recordingId, input) {
|
|
264
|
+
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: JSON.stringify(input)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
markRecordingStopped(recordingId, input) {
|
|
270
|
+
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
body: JSON.stringify(input)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
submitRecordingFeedback(recordingId, input) {
|
|
276
|
+
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
body: JSON.stringify(input)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
provisionVideo(recordingId, input) {
|
|
282
|
+
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
283
|
+
}
|
|
284
|
+
recordingStatus(recordingId) {
|
|
285
|
+
return this.request(`/api/recordings/${recordingId}`);
|
|
286
|
+
}
|
|
287
|
+
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
288
|
+
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify({ reason })
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
294
|
+
const deadline = Date.now() + timeoutMs;
|
|
295
|
+
let intervalMs = 1e3;
|
|
296
|
+
while (Date.now() < deadline) {
|
|
297
|
+
const recording = await this.recordingStatus(recordingId);
|
|
298
|
+
if (recording.status === "ready" && recording.shareUrl) {
|
|
299
|
+
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
300
|
+
}
|
|
301
|
+
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
302
|
+
process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
304
|
+
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
305
|
+
}
|
|
306
|
+
throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
|
|
307
|
+
}
|
|
308
|
+
async uploadVideo(uploadUrl, filePath) {
|
|
309
|
+
const file2 = await stat2(filePath);
|
|
310
|
+
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
311
|
+
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
312
|
+
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
await new Promise((resolve, reject) => {
|
|
316
|
+
const upload = new Upload(createReadStream(filePath), {
|
|
317
|
+
uploadUrl,
|
|
318
|
+
uploadSize: file2.size,
|
|
319
|
+
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
320
|
+
removeFingerprintOnSuccess: true,
|
|
321
|
+
onError: reject,
|
|
322
|
+
onSuccess: () => resolve(),
|
|
323
|
+
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
324
|
+
});
|
|
325
|
+
upload.start();
|
|
326
|
+
});
|
|
327
|
+
process.stderr.write("\n");
|
|
328
|
+
}
|
|
329
|
+
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
330
|
+
const body = await readFile(filePath);
|
|
331
|
+
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
|
+
}
|
|
333
|
+
listRecordings() {
|
|
334
|
+
return this.request("/api/recordings");
|
|
335
|
+
}
|
|
336
|
+
createDeviceAuthorization(input) {
|
|
337
|
+
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
338
|
+
}
|
|
339
|
+
pollDeviceAuthorization(deviceCode) {
|
|
340
|
+
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
341
|
+
}
|
|
342
|
+
inspect(shareUrl) {
|
|
343
|
+
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
344
|
+
const url2 = new URL(shareUrl);
|
|
345
|
+
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
346
|
+
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
347
|
+
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
348
|
+
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
349
|
+
return response.json();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async request(path, init = {}, authenticated = true) {
|
|
353
|
+
const headers = new Headers(init.headers);
|
|
354
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
355
|
+
if (authenticated) {
|
|
356
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
357
|
+
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
358
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
359
|
+
}
|
|
360
|
+
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
361
|
+
const value = await response.json().catch(() => null);
|
|
362
|
+
if (!response.ok && value?.error?.code && value.error.message) {
|
|
363
|
+
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
|
|
364
|
+
}
|
|
365
|
+
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/whoami.ts
|
|
371
|
+
async function whoami(apiUrl) {
|
|
372
|
+
const token = await loadCredential(apiUrl);
|
|
373
|
+
if (!token) return { schemaVersion: 1, status: "not_connected", authenticated: false, apiUrl };
|
|
374
|
+
const session = await new ControlPlaneClient(apiUrl, token).agentSession();
|
|
375
|
+
return {
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
status: session.anonymous ? "anonymous" : "connected",
|
|
378
|
+
authenticated: true,
|
|
379
|
+
apiUrl,
|
|
380
|
+
anonymous: Boolean(session.anonymous),
|
|
381
|
+
email: session.email ?? null,
|
|
382
|
+
workspaceId: session.workspaceId ?? null,
|
|
383
|
+
scopes: session.scopes,
|
|
384
|
+
expiresAt: session.expiresAt
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
8
388
|
// src/remote-encoding.ts
|
|
9
389
|
import { open, readFile as readFile2 } from "fs/promises";
|
|
10
|
-
import { join } from "path";
|
|
390
|
+
import { join as join2 } from "path";
|
|
11
391
|
|
|
12
392
|
// ../../node_modules/zod/v4/classic/external.js
|
|
13
393
|
var external_exports = {};
|
|
@@ -142,7 +522,7 @@ __export(external_exports, {
|
|
|
142
522
|
guid: () => guid2,
|
|
143
523
|
hash: () => hash,
|
|
144
524
|
hex: () => hex2,
|
|
145
|
-
hostname: () =>
|
|
525
|
+
hostname: () => hostname3,
|
|
146
526
|
httpUrl: () => httpUrl,
|
|
147
527
|
includes: () => _includes,
|
|
148
528
|
instanceof: () => _instanceof,
|
|
@@ -1553,7 +1933,7 @@ __export(regexes_exports, {
|
|
|
1553
1933
|
extendedDuration: () => extendedDuration,
|
|
1554
1934
|
guid: () => guid,
|
|
1555
1935
|
hex: () => hex,
|
|
1556
|
-
hostname: () =>
|
|
1936
|
+
hostname: () => hostname2,
|
|
1557
1937
|
html5Email: () => html5Email,
|
|
1558
1938
|
httpProtocol: () => httpProtocol,
|
|
1559
1939
|
idnEmail: () => idnEmail,
|
|
@@ -1631,7 +2011,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
|
|
|
1631
2011
|
var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
|
|
1632
2012
|
var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
|
|
1633
2013
|
var base64url = /^[A-Za-z0-9_-]*$/;
|
|
1634
|
-
var
|
|
2014
|
+
var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
|
|
1635
2015
|
var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
1636
2016
|
var httpProtocol = /^https?$/;
|
|
1637
2017
|
var e164 = /^\+[1-9]\d{6,14}$/;
|
|
@@ -12503,7 +12883,7 @@ __export(schemas_exports2, {
|
|
|
12503
12883
|
guid: () => guid2,
|
|
12504
12884
|
hash: () => hash,
|
|
12505
12885
|
hex: () => hex2,
|
|
12506
|
-
hostname: () =>
|
|
12886
|
+
hostname: () => hostname3,
|
|
12507
12887
|
httpUrl: () => httpUrl,
|
|
12508
12888
|
instanceof: () => _instanceof,
|
|
12509
12889
|
int: () => int,
|
|
@@ -13123,7 +13503,7 @@ var ZodCustomStringFormat = /* @__PURE__ */ $constructor("ZodCustomStringFormat"
|
|
|
13123
13503
|
function stringFormat(format, fnOrRegex, _params = {}) {
|
|
13124
13504
|
return _stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);
|
|
13125
13505
|
}
|
|
13126
|
-
function
|
|
13506
|
+
function hostname3(_params) {
|
|
13127
13507
|
return _stringFormat(ZodCustomStringFormat, "hostname", regexes_exports.hostname, _params);
|
|
13128
13508
|
}
|
|
13129
13509
|
function hex2(_params) {
|
|
@@ -14538,173 +14918,6 @@ var frameManifestSchema = external_exports.object({
|
|
|
14538
14918
|
if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
|
|
14539
14919
|
});
|
|
14540
14920
|
|
|
14541
|
-
// src/control-plane.ts
|
|
14542
|
-
import { createReadStream } from "fs";
|
|
14543
|
-
import { readFile, stat } from "fs/promises";
|
|
14544
|
-
import { Upload } from "tus-js-client";
|
|
14545
|
-
var ControlPlaneError = class extends Error {
|
|
14546
|
-
constructor(code, message, recovery, details, httpStatus) {
|
|
14547
|
-
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
14548
|
-
this.code = code;
|
|
14549
|
-
this.recovery = recovery;
|
|
14550
|
-
this.details = details;
|
|
14551
|
-
this.httpStatus = httpStatus;
|
|
14552
|
-
this.name = "ControlPlaneError";
|
|
14553
|
-
}
|
|
14554
|
-
};
|
|
14555
|
-
var ControlPlaneHttpError = class extends Error {
|
|
14556
|
-
constructor(httpStatus) {
|
|
14557
|
-
super(`Request failed (${httpStatus}).`);
|
|
14558
|
-
this.httpStatus = httpStatus;
|
|
14559
|
-
this.name = "ControlPlaneHttpError";
|
|
14560
|
-
}
|
|
14561
|
-
};
|
|
14562
|
-
var ControlPlaneClient = class {
|
|
14563
|
-
constructor(apiUrl, token) {
|
|
14564
|
-
this.apiUrl = apiUrl;
|
|
14565
|
-
this.token = token;
|
|
14566
|
-
}
|
|
14567
|
-
encodingCapabilities() {
|
|
14568
|
-
return this.request("/api/encoding/capabilities");
|
|
14569
|
-
}
|
|
14570
|
-
beginEncoding(id, manifest) {
|
|
14571
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
14572
|
-
}
|
|
14573
|
-
encodingStatus(id) {
|
|
14574
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
14575
|
-
}
|
|
14576
|
-
uploadEncodingPart(id, part, bytes) {
|
|
14577
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
14578
|
-
}
|
|
14579
|
-
completeEncoding(id) {
|
|
14580
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
14581
|
-
}
|
|
14582
|
-
retryEncoding(id) {
|
|
14583
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
14584
|
-
}
|
|
14585
|
-
createCloudRun(input) {
|
|
14586
|
-
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
14587
|
-
}
|
|
14588
|
-
cloudRun(id) {
|
|
14589
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
14590
|
-
}
|
|
14591
|
-
cancelCloudRun(id) {
|
|
14592
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
14593
|
-
}
|
|
14594
|
-
health() {
|
|
14595
|
-
return this.request("/api/health", {}, false);
|
|
14596
|
-
}
|
|
14597
|
-
agentSession() {
|
|
14598
|
-
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
14599
|
-
}
|
|
14600
|
-
reportCliCommand(input) {
|
|
14601
|
-
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
14602
|
-
}
|
|
14603
|
-
createRecording(input) {
|
|
14604
|
-
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
14605
|
-
}
|
|
14606
|
-
markRecordingStarted(recordingId, input) {
|
|
14607
|
-
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
14608
|
-
method: "POST",
|
|
14609
|
-
body: JSON.stringify(input)
|
|
14610
|
-
});
|
|
14611
|
-
}
|
|
14612
|
-
markRecordingStopped(recordingId, input) {
|
|
14613
|
-
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
14614
|
-
method: "POST",
|
|
14615
|
-
body: JSON.stringify(input)
|
|
14616
|
-
});
|
|
14617
|
-
}
|
|
14618
|
-
submitRecordingFeedback(recordingId, input) {
|
|
14619
|
-
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
14620
|
-
method: "POST",
|
|
14621
|
-
body: JSON.stringify(input)
|
|
14622
|
-
});
|
|
14623
|
-
}
|
|
14624
|
-
provisionVideo(recordingId, input) {
|
|
14625
|
-
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
14626
|
-
}
|
|
14627
|
-
recordingStatus(recordingId) {
|
|
14628
|
-
return this.request(`/api/recordings/${recordingId}`);
|
|
14629
|
-
}
|
|
14630
|
-
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
14631
|
-
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
14632
|
-
method: "POST",
|
|
14633
|
-
body: JSON.stringify({ reason })
|
|
14634
|
-
});
|
|
14635
|
-
}
|
|
14636
|
-
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
14637
|
-
const deadline = Date.now() + timeoutMs;
|
|
14638
|
-
let intervalMs = 1e3;
|
|
14639
|
-
while (Date.now() < deadline) {
|
|
14640
|
-
const recording = await this.recordingStatus(recordingId);
|
|
14641
|
-
if (recording.status === "ready" && recording.shareUrl) {
|
|
14642
|
-
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
14643
|
-
}
|
|
14644
|
-
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
14645
|
-
process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
|
|
14646
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
14647
|
-
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
14648
|
-
}
|
|
14649
|
-
throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
|
|
14650
|
-
}
|
|
14651
|
-
async uploadVideo(uploadUrl, filePath) {
|
|
14652
|
-
const file2 = await stat(filePath);
|
|
14653
|
-
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
14654
|
-
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
14655
|
-
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
14656
|
-
return;
|
|
14657
|
-
}
|
|
14658
|
-
await new Promise((resolve, reject) => {
|
|
14659
|
-
const upload = new Upload(createReadStream(filePath), {
|
|
14660
|
-
uploadUrl,
|
|
14661
|
-
uploadSize: file2.size,
|
|
14662
|
-
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
14663
|
-
removeFingerprintOnSuccess: true,
|
|
14664
|
-
onError: reject,
|
|
14665
|
-
onSuccess: () => resolve(),
|
|
14666
|
-
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
14667
|
-
});
|
|
14668
|
-
upload.start();
|
|
14669
|
-
});
|
|
14670
|
-
process.stderr.write("\n");
|
|
14671
|
-
}
|
|
14672
|
-
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
14673
|
-
const body = await readFile(filePath);
|
|
14674
|
-
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 });
|
|
14675
|
-
}
|
|
14676
|
-
createDeviceAuthorization(input) {
|
|
14677
|
-
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
14678
|
-
}
|
|
14679
|
-
pollDeviceAuthorization(deviceCode) {
|
|
14680
|
-
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
14681
|
-
}
|
|
14682
|
-
inspect(shareUrl) {
|
|
14683
|
-
const url2 = new URL(shareUrl);
|
|
14684
|
-
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
14685
|
-
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
14686
|
-
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
14687
|
-
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
14688
|
-
return response.json();
|
|
14689
|
-
});
|
|
14690
|
-
}
|
|
14691
|
-
async request(path, init = {}, authenticated = true) {
|
|
14692
|
-
const headers = new Headers(init.headers);
|
|
14693
|
-
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
14694
|
-
if (authenticated) {
|
|
14695
|
-
if (!this.token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
14696
|
-
headers.set("Authorization", `Bearer ${this.token}`);
|
|
14697
|
-
}
|
|
14698
|
-
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
14699
|
-
const value = await response.json().catch(() => null);
|
|
14700
|
-
if (!response.ok && value?.error?.code && value.error.message) {
|
|
14701
|
-
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
|
|
14702
|
-
}
|
|
14703
|
-
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
14704
|
-
return value;
|
|
14705
|
-
}
|
|
14706
|
-
};
|
|
14707
|
-
|
|
14708
14921
|
// src/remote-encoding.ts
|
|
14709
14922
|
async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
14710
14923
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -14723,14 +14936,14 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
|
|
|
14723
14936
|
}
|
|
14724
14937
|
}
|
|
14725
14938
|
async function submitFrameCapture(client, id, directory) {
|
|
14726
|
-
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(
|
|
14939
|
+
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
|
|
14727
14940
|
let status = await client.beginEncoding(id, manifest);
|
|
14728
14941
|
if (status.status === "failed") status = await client.retryEncoding(id);
|
|
14729
14942
|
if (status.status !== "uploading") {
|
|
14730
14943
|
if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
|
|
14731
14944
|
return status;
|
|
14732
14945
|
}
|
|
14733
|
-
const file2 = await open(
|
|
14946
|
+
const file2 = await open(join2(directory, "frames.bin"), "r");
|
|
14734
14947
|
try {
|
|
14735
14948
|
const total = manifest.frames.reduce((n, f) => n + f.size, 0);
|
|
14736
14949
|
if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
|
|
@@ -14766,22 +14979,22 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
14766
14979
|
|
|
14767
14980
|
// src/cli.ts
|
|
14768
14981
|
import { spawn as spawn6 } from "child_process";
|
|
14982
|
+
import { hostname as hostname4 } from "os";
|
|
14769
14983
|
import { Command } from "commander";
|
|
14770
|
-
import { hostname as hostname3 } from "os";
|
|
14771
14984
|
import { basename, dirname as dirname2 } from "path";
|
|
14772
|
-
import { readdir as readdir2, rm as
|
|
14985
|
+
import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
|
|
14773
14986
|
|
|
14774
14987
|
// ../recorder/src/client.ts
|
|
14775
|
-
import { request } from "http";
|
|
14776
|
-
import { spawn } from "child_process";
|
|
14988
|
+
import { request as request2 } from "http";
|
|
14989
|
+
import { spawn as spawn2 } from "child_process";
|
|
14777
14990
|
|
|
14778
14991
|
// ../recorder/src/paths.ts
|
|
14779
|
-
import { homedir, tmpdir } from "os";
|
|
14780
|
-
import { join as
|
|
14781
|
-
var rillDirectory = () => process.env.RILL_HOME ??
|
|
14782
|
-
var daemonSocketPath = () => process.env.RILL_SOCKET ??
|
|
14783
|
-
var daemonStatePath = () =>
|
|
14784
|
-
var recordingTempRoot = () => process.env.RILL_TEMP ??
|
|
14992
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
14993
|
+
import { join as join3 } from "path";
|
|
14994
|
+
var rillDirectory = () => process.env.RILL_HOME ?? join3(homedir2(), ".rill");
|
|
14995
|
+
var daemonSocketPath = () => process.env.RILL_SOCKET ?? join3(rillDirectory(), "recorder-v2.sock");
|
|
14996
|
+
var daemonStatePath = () => join3(rillDirectory(), "sessions-v2.json");
|
|
14997
|
+
var recordingTempRoot = () => process.env.RILL_TEMP ?? join3(tmpdir(), "rill-recordings");
|
|
14785
14998
|
|
|
14786
14999
|
// ../recorder/src/client.ts
|
|
14787
15000
|
async function ensureDaemon(cliEntryPath) {
|
|
@@ -14790,7 +15003,7 @@ async function ensureDaemon(cliEntryPath) {
|
|
|
14790
15003
|
return;
|
|
14791
15004
|
} catch {
|
|
14792
15005
|
}
|
|
14793
|
-
const child =
|
|
15006
|
+
const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
|
|
14794
15007
|
child.unref();
|
|
14795
15008
|
const deadline = Date.now() + 1e4;
|
|
14796
15009
|
while (Date.now() < deadline) {
|
|
@@ -14815,7 +15028,7 @@ function recordingStatus(recordingId) {
|
|
|
14815
15028
|
function callDaemon(method, path, body) {
|
|
14816
15029
|
return new Promise((resolve, reject) => {
|
|
14817
15030
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
14818
|
-
const outgoing =
|
|
15031
|
+
const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
|
|
14819
15032
|
const chunks = [];
|
|
14820
15033
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
14821
15034
|
incoming.on("end", () => {
|
|
@@ -14835,17 +15048,17 @@ function callDaemon(method, path, body) {
|
|
|
14835
15048
|
}
|
|
14836
15049
|
|
|
14837
15050
|
// ../recorder/src/retention.ts
|
|
14838
|
-
import { readdir, readFile as readFile3, rm } from "fs/promises";
|
|
14839
|
-
import { join as
|
|
15051
|
+
import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
15052
|
+
import { join as join4 } from "path";
|
|
14840
15053
|
async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
|
|
14841
15054
|
const removed = [];
|
|
14842
15055
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14843
15056
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14844
|
-
const directory =
|
|
15057
|
+
const directory = join4(root, id);
|
|
14845
15058
|
try {
|
|
14846
|
-
const marker = JSON.parse(await readFile3(
|
|
15059
|
+
const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
|
|
14847
15060
|
if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
|
|
14848
|
-
await
|
|
15061
|
+
await rm2(directory, { recursive: true, force: true });
|
|
14849
15062
|
removed.push(id);
|
|
14850
15063
|
} catch {
|
|
14851
15064
|
}
|
|
@@ -14856,7 +15069,7 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14856
15069
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14857
15070
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14858
15071
|
try {
|
|
14859
|
-
const marker = JSON.parse(await readFile3(
|
|
15072
|
+
const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
|
|
14860
15073
|
if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
|
|
14861
15074
|
} catch {
|
|
14862
15075
|
}
|
|
@@ -14866,8 +15079,8 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14866
15079
|
|
|
14867
15080
|
// ../recorder/src/daemon.ts
|
|
14868
15081
|
import { createServer as createServer2 } from "http";
|
|
14869
|
-
import { mkdir as
|
|
14870
|
-
import { dirname, join as
|
|
15082
|
+
import { mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat3 } from "fs/promises";
|
|
15083
|
+
import { dirname, join as join8 } from "path";
|
|
14871
15084
|
|
|
14872
15085
|
// ../contracts/src/recording.ts
|
|
14873
15086
|
var cursorAppearanceSchema = external_exports.object({
|
|
@@ -15028,10 +15241,10 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15028
15241
|
|
|
15029
15242
|
// ../recorder/src/session.ts
|
|
15030
15243
|
import { chromium } from "playwright-core";
|
|
15031
|
-
import { spawn as
|
|
15244
|
+
import { spawn as spawn4 } from "child_process";
|
|
15032
15245
|
import { createServer } from "net";
|
|
15033
|
-
import { mkdir as
|
|
15034
|
-
import { join as
|
|
15246
|
+
import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
15247
|
+
import { join as join7 } from "path";
|
|
15035
15248
|
import { tmpdir as tmpdir2 } from "os";
|
|
15036
15249
|
|
|
15037
15250
|
// ../recorder/src/bundle.ts
|
|
@@ -15111,9 +15324,9 @@ function redactJson(value, depth = 0) {
|
|
|
15111
15324
|
}
|
|
15112
15325
|
|
|
15113
15326
|
// ../recorder/src/encoding.ts
|
|
15114
|
-
import { open as open2, writeFile, mkdir } from "fs/promises";
|
|
15115
|
-
import { spawn as
|
|
15116
|
-
import { join as
|
|
15327
|
+
import { open as open2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
15328
|
+
import { spawn as spawn3 } from "child_process";
|
|
15329
|
+
import { join as join5 } from "path";
|
|
15117
15330
|
var active = 0;
|
|
15118
15331
|
var waiting = [];
|
|
15119
15332
|
function encodingConcurrency() {
|
|
@@ -15143,11 +15356,11 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15143
15356
|
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
15144
15357
|
}
|
|
15145
15358
|
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
15146
|
-
await writeFile(
|
|
15359
|
+
await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
15147
15360
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
15148
15361
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
15149
15362
|
await new Promise((resolve, reject) => {
|
|
15150
|
-
const child =
|
|
15363
|
+
const child = spawn3(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-threads", String(threads), "-b:v", "0", "-crf", "32", "-y", output], { cwd: directory, stdio: ["ignore", "ignore", "pipe"] });
|
|
15151
15364
|
let stderr = "";
|
|
15152
15365
|
child.stderr.on("data", (chunk) => {
|
|
15153
15366
|
stderr = (stderr + String(chunk)).slice(-2e3);
|
|
@@ -15172,8 +15385,8 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15172
15385
|
}
|
|
15173
15386
|
|
|
15174
15387
|
// ../recorder/src/screencast.ts
|
|
15175
|
-
import { mkdir as
|
|
15176
|
-
import { join as
|
|
15388
|
+
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
|
|
15389
|
+
import { join as join6 } from "path";
|
|
15177
15390
|
var ScreencastRecorder = class {
|
|
15178
15391
|
constructor(session, directory, width, height) {
|
|
15179
15392
|
this.session = session;
|
|
@@ -15188,7 +15401,7 @@ var ScreencastRecorder = class {
|
|
|
15188
15401
|
frameBytes = 0;
|
|
15189
15402
|
captureError;
|
|
15190
15403
|
async start() {
|
|
15191
|
-
await
|
|
15404
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
15192
15405
|
this.session.on("Page.screencastFrame", (payload) => {
|
|
15193
15406
|
const bytes = Buffer.from(payload.data, "base64");
|
|
15194
15407
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
@@ -15197,7 +15410,7 @@ var ScreencastRecorder = class {
|
|
|
15197
15410
|
return;
|
|
15198
15411
|
}
|
|
15199
15412
|
this.frameBytes += bytes.length;
|
|
15200
|
-
const path =
|
|
15413
|
+
const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
15201
15414
|
this.frameCounter += 1;
|
|
15202
15415
|
const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
|
|
15203
15416
|
this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
|
|
@@ -15221,7 +15434,7 @@ var ScreencastRecorder = class {
|
|
|
15221
15434
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
15222
15435
|
const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
|
|
15223
15436
|
const frames = [];
|
|
15224
|
-
const bundle = await open3(
|
|
15437
|
+
const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
|
|
15225
15438
|
try {
|
|
15226
15439
|
for (const frame of ordered) {
|
|
15227
15440
|
const bytes = await readFile4(frame.path);
|
|
@@ -15232,7 +15445,7 @@ var ScreencastRecorder = class {
|
|
|
15232
15445
|
await bundle.close();
|
|
15233
15446
|
}
|
|
15234
15447
|
const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
|
|
15235
|
-
await writeFile2(
|
|
15448
|
+
await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
|
|
15236
15449
|
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
|
|
15237
15450
|
return { frameCount: this.frames.length, outputPath };
|
|
15238
15451
|
}
|
|
@@ -15281,11 +15494,11 @@ var RecordingSession = class _RecordingSession {
|
|
|
15281
15494
|
static async start(options) {
|
|
15282
15495
|
const width = options.width ?? 1280;
|
|
15283
15496
|
const height = options.height ?? 720;
|
|
15284
|
-
await
|
|
15285
|
-
if (options.encoding === "remote") await writeFile3(
|
|
15497
|
+
await mkdir4(options.outputDirectory, { recursive: true, mode: 448 });
|
|
15498
|
+
if (options.encoding === "remote") await writeFile3(join7(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
15286
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 });
|
|
15287
15500
|
const cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
15288
|
-
const screencast = new ScreencastRecorder(cdpSession,
|
|
15501
|
+
const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
|
|
15289
15502
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
15290
15503
|
try {
|
|
15291
15504
|
await session.attachDiagnostics(options.captureNetworkBodies ?? false);
|
|
@@ -15317,8 +15530,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15317
15530
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
15318
15531
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
15319
15532
|
const outputDirectory = this.outputDirectory;
|
|
15320
|
-
const videoPath =
|
|
15321
|
-
const diagnosticsPath =
|
|
15533
|
+
const videoPath = join7(outputDirectory, "recording.webm");
|
|
15534
|
+
const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
15322
15535
|
const bundle = {
|
|
15323
15536
|
schemaVersion: 1,
|
|
15324
15537
|
capturePolicyVersion: "safe-default-v1",
|
|
@@ -15341,13 +15554,13 @@ var RecordingSession = class _RecordingSession {
|
|
|
15341
15554
|
if (this.browserProcess) {
|
|
15342
15555
|
await this.browser.close().catch(() => void 0);
|
|
15343
15556
|
await terminateBrowser(this.browserProcess);
|
|
15344
|
-
if (this.profileDirectory) await
|
|
15557
|
+
if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15345
15558
|
}
|
|
15346
15559
|
}
|
|
15347
15560
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
15348
15561
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
15349
15562
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
15350
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory:
|
|
15563
|
+
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join7(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
15351
15564
|
}
|
|
15352
15565
|
captureNetworkBodies = false;
|
|
15353
15566
|
encoding = "local";
|
|
@@ -15399,34 +15612,34 @@ var RecordingSession = class _RecordingSession {
|
|
|
15399
15612
|
this.consoleEvents.push(event);
|
|
15400
15613
|
pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
|
|
15401
15614
|
});
|
|
15402
|
-
this.page.on("request", (
|
|
15615
|
+
this.page.on("request", (request3) => {
|
|
15403
15616
|
if (this.diagnosticsPaused) return;
|
|
15404
15617
|
if (this.networkEvents.length >= 5e3) {
|
|
15405
15618
|
this.truncated = true;
|
|
15406
15619
|
return;
|
|
15407
15620
|
}
|
|
15408
15621
|
const started = offset();
|
|
15409
|
-
this.requestStart.set(
|
|
15410
|
-
const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method:
|
|
15411
|
-
if (captureNetworkBodies &&
|
|
15412
|
-
const redacted = redactBody(
|
|
15622
|
+
this.requestStart.set(request3, started);
|
|
15623
|
+
const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
|
|
15624
|
+
if (captureNetworkBodies && request3.postData()) {
|
|
15625
|
+
const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
|
|
15413
15626
|
event.requestBody = redacted.body;
|
|
15414
15627
|
event.bodyOmittedReason = redacted.omittedReason;
|
|
15415
15628
|
if (redacted.body) this.bodyBytes += Buffer.byteLength(redacted.body);
|
|
15416
15629
|
}
|
|
15417
15630
|
this.networkEvents.push(event);
|
|
15418
|
-
this.requestEvents.set(
|
|
15631
|
+
this.requestEvents.set(request3, event);
|
|
15419
15632
|
});
|
|
15420
15633
|
this.page.on("response", (response) => this.updateResponse(response));
|
|
15421
|
-
this.page.on("requestfailed", (
|
|
15422
|
-
const event = this.requestEvents.get(
|
|
15634
|
+
this.page.on("requestfailed", (request3) => {
|
|
15635
|
+
const event = this.requestEvents.get(request3);
|
|
15423
15636
|
if (!event) return;
|
|
15424
|
-
event.failureReason =
|
|
15425
|
-
event.durationMs = Math.max(0, offset() - (this.requestStart.get(
|
|
15637
|
+
event.failureReason = request3.failure()?.errorText ?? "request_failed";
|
|
15638
|
+
event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
|
|
15426
15639
|
pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
|
|
15427
15640
|
});
|
|
15428
|
-
this.page.on("requestfinished", async (
|
|
15429
|
-
const response = await
|
|
15641
|
+
this.page.on("requestfinished", async (request3) => {
|
|
15642
|
+
const response = await request3.response();
|
|
15430
15643
|
if (response) await this.captureResponseBody(response);
|
|
15431
15644
|
});
|
|
15432
15645
|
}
|
|
@@ -15459,7 +15672,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15459
15672
|
};
|
|
15460
15673
|
async function launchManaged(options) {
|
|
15461
15674
|
const port = await getAvailablePort();
|
|
15462
|
-
const profileDirectory = await mkdtemp(
|
|
15675
|
+
const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
|
|
15463
15676
|
const executable = findChromeExecutable();
|
|
15464
15677
|
const args = [
|
|
15465
15678
|
`--remote-debugging-port=${port}`,
|
|
@@ -15477,7 +15690,7 @@ async function launchManaged(options) {
|
|
|
15477
15690
|
"about:blank"
|
|
15478
15691
|
];
|
|
15479
15692
|
const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key))) : process.env;
|
|
15480
|
-
const browserProcess =
|
|
15693
|
+
const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
|
|
15481
15694
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15482
15695
|
try {
|
|
15483
15696
|
await waitForCdp(cdpUrl, browserProcess);
|
|
@@ -15488,7 +15701,7 @@ async function launchManaged(options) {
|
|
|
15488
15701
|
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15489
15702
|
} catch (error51) {
|
|
15490
15703
|
await terminateBrowser(browserProcess);
|
|
15491
|
-
await
|
|
15704
|
+
await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15492
15705
|
throw error51;
|
|
15493
15706
|
}
|
|
15494
15707
|
}
|
|
@@ -15539,14 +15752,14 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
15539
15752
|
var lastActivity = Date.now();
|
|
15540
15753
|
async function runDaemon() {
|
|
15541
15754
|
const socketPath = daemonSocketPath();
|
|
15542
|
-
await
|
|
15543
|
-
await
|
|
15755
|
+
await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
|
|
15756
|
+
await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
15544
15757
|
await restoreRecoverableSessions();
|
|
15545
15758
|
await purgeExpiredCaptures();
|
|
15546
15759
|
await unlink(socketPath).catch((error51) => {
|
|
15547
15760
|
if (error51.code !== "ENOENT") throw error51;
|
|
15548
15761
|
});
|
|
15549
|
-
const server = createServer2((
|
|
15762
|
+
const server = createServer2((request3, response) => void route(request3, response));
|
|
15550
15763
|
server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
|
|
15551
15764
|
const idleTimer = setInterval(() => {
|
|
15552
15765
|
void (async () => {
|
|
@@ -15565,16 +15778,16 @@ async function runDaemon() {
|
|
|
15565
15778
|
process.once("SIGTERM", close);
|
|
15566
15779
|
process.once("SIGINT", close);
|
|
15567
15780
|
}
|
|
15568
|
-
async function route(
|
|
15781
|
+
async function route(request3, response) {
|
|
15569
15782
|
lastActivity = Date.now();
|
|
15570
|
-
const url2 = new URL(
|
|
15571
|
-
if (
|
|
15572
|
-
if (
|
|
15783
|
+
const url2 = new URL(request3.url ?? "/", "http://daemon.local");
|
|
15784
|
+
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
|
|
15785
|
+
if (request3.method === "POST" && url2.pathname === "/sessions") {
|
|
15573
15786
|
if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
|
|
15574
15787
|
try {
|
|
15575
|
-
const input = await readJson(
|
|
15788
|
+
const input = await readJson(request3);
|
|
15576
15789
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15577
|
-
const outputDirectory =
|
|
15790
|
+
const outputDirectory = join8(recordingTempRoot(), input.recordingId);
|
|
15578
15791
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
15579
15792
|
const status = session.status();
|
|
15580
15793
|
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
|
|
@@ -15591,8 +15804,8 @@ async function route(request2, response) {
|
|
|
15591
15804
|
if (match) {
|
|
15592
15805
|
const entry = sessions.get(decodeURIComponent(match[1]));
|
|
15593
15806
|
if (!entry) return json2(response, 404, { error: { code: "recording_not_found", message: "The local recording session was not found." } });
|
|
15594
|
-
if (
|
|
15595
|
-
if (
|
|
15807
|
+
if (request3.method === "GET" && !match[2]) return json2(response, 200, serializeEntry(entry));
|
|
15808
|
+
if (request3.method === "POST" && match[2] === "stop") {
|
|
15596
15809
|
if (entry.state === "ready") return json2(response, 200, serializeEntry(entry));
|
|
15597
15810
|
await finalizeEntry(entry, "agent_request");
|
|
15598
15811
|
return json2(response, entry.result ? 200 : 500, serializeEntry(entry));
|
|
@@ -15633,10 +15846,10 @@ async function persistState() {
|
|
|
15633
15846
|
`, { mode: 384 });
|
|
15634
15847
|
await rename(temporary, daemonStatePath());
|
|
15635
15848
|
}
|
|
15636
|
-
async function readJson(
|
|
15849
|
+
async function readJson(request3) {
|
|
15637
15850
|
const chunks = [];
|
|
15638
15851
|
let size = 0;
|
|
15639
|
-
for await (const chunk of
|
|
15852
|
+
for await (const chunk of request3) {
|
|
15640
15853
|
const buffer = Buffer.from(chunk);
|
|
15641
15854
|
size += buffer.length;
|
|
15642
15855
|
if (size > 1e6) throw new Error("Request body exceeds 1 MB.");
|
|
@@ -15652,7 +15865,7 @@ async function purgeExpiredCaptures() {
|
|
|
15652
15865
|
const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
|
|
15653
15866
|
for (const id of removed) sessions.delete(id);
|
|
15654
15867
|
let changed = removed.length > 0;
|
|
15655
|
-
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await
|
|
15868
|
+
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
|
|
15656
15869
|
sessions.delete(id);
|
|
15657
15870
|
changed = true;
|
|
15658
15871
|
}
|
|
@@ -15689,44 +15902,6 @@ function cliFailureCode(error51) {
|
|
|
15689
15902
|
return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
|
|
15690
15903
|
}
|
|
15691
15904
|
|
|
15692
|
-
// src/keychain.ts
|
|
15693
|
-
import { spawn as spawn4 } from "child_process";
|
|
15694
|
-
var service = "rill";
|
|
15695
|
-
async function saveCredential(apiUrl, token) {
|
|
15696
|
-
if (process.platform === "darwin") {
|
|
15697
|
-
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
15698
|
-
return;
|
|
15699
|
-
}
|
|
15700
|
-
if (process.platform === "linux") {
|
|
15701
|
-
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
15702
|
-
return;
|
|
15703
|
-
}
|
|
15704
|
-
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
15705
|
-
}
|
|
15706
|
-
async function loadCredential(apiUrl) {
|
|
15707
|
-
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
15708
|
-
try {
|
|
15709
|
-
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim();
|
|
15710
|
-
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim();
|
|
15711
|
-
} catch {
|
|
15712
|
-
return null;
|
|
15713
|
-
}
|
|
15714
|
-
return null;
|
|
15715
|
-
}
|
|
15716
|
-
function run(command, args, input) {
|
|
15717
|
-
return new Promise((resolve, reject) => {
|
|
15718
|
-
const child = spawn4(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
15719
|
-
const stdout = [];
|
|
15720
|
-
const stderr = [];
|
|
15721
|
-
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
15722
|
-
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
15723
|
-
child.once("error", reject);
|
|
15724
|
-
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(new Error(Buffer.concat(stderr).toString("utf8").trim() || `${command} exited with ${code}.`)));
|
|
15725
|
-
if (input) child.stdin.end(input);
|
|
15726
|
-
else child.stdin.end();
|
|
15727
|
-
});
|
|
15728
|
-
}
|
|
15729
|
-
|
|
15730
15905
|
// src/output.ts
|
|
15731
15906
|
function emit(value, pretty = false) {
|
|
15732
15907
|
process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : void 0)}
|
|
@@ -15855,7 +16030,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
15855
16030
|
} else {
|
|
15856
16031
|
const token = await dependencies.loadCredential(apiUrl);
|
|
15857
16032
|
if (!token) {
|
|
15858
|
-
checks.push({ name: "authentication", status: "fail", message: "No Rill agent credential is available.", recovery: `Run \`rill --api-url ${apiUrl} login\` or set RILL_TOKEN.` });
|
|
16033
|
+
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.` });
|
|
15859
16034
|
} else {
|
|
15860
16035
|
try {
|
|
15861
16036
|
const session = await dependencies.agentSession(apiUrl, token);
|
|
@@ -15873,10 +16048,10 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
15873
16048
|
checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
|
|
15874
16049
|
try {
|
|
15875
16050
|
const token = await dependencies.loadCredential(apiUrl);
|
|
15876
|
-
if (!token
|
|
15877
|
-
checks.push({ name: "encoding", status: "pass", message: "Remote encoding is enabled." });
|
|
16051
|
+
if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
|
|
16052
|
+
checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
|
|
15878
16053
|
} catch (error51) {
|
|
15879
|
-
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "
|
|
16054
|
+
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Remote encoding must be enabled on the server before using the default recording workflow." });
|
|
15880
16055
|
}
|
|
15881
16056
|
}
|
|
15882
16057
|
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
@@ -15990,10 +16165,10 @@ function controlPlaneUrl(environmentUrl) {
|
|
|
15990
16165
|
async function removeLocalCapture(id) {
|
|
15991
16166
|
await ensureDaemon(process.argv[1]);
|
|
15992
16167
|
const local = await recordingStatus(id).catch(() => null);
|
|
15993
|
-
if (local?.result?.frameDirectory) await
|
|
16168
|
+
if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
15994
16169
|
}
|
|
15995
|
-
function watchEncoding(apiUrl, id
|
|
15996
|
-
const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env
|
|
16170
|
+
function watchEncoding(apiUrl, id) {
|
|
16171
|
+
const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env } });
|
|
15997
16172
|
child.on("error", () => void 0);
|
|
15998
16173
|
child.unref();
|
|
15999
16174
|
}
|
|
@@ -16009,8 +16184,9 @@ program.command("doctor").description("Validate Rill prerequisites before record
|
|
|
16009
16184
|
});
|
|
16010
16185
|
async function authorize(quiet = false) {
|
|
16011
16186
|
const options = program.opts();
|
|
16187
|
+
if (parseAuthorization(await loadStoredCredential(options.apiUrl))?.anonymous) throw new Error("Use rill connect --email <your-email> to keep and claim this anonymous identity before using account access.");
|
|
16012
16188
|
const client = new ControlPlaneClient(options.apiUrl, null);
|
|
16013
|
-
const authorization = await client.createDeviceAuthorization({ deviceName: `${
|
|
16189
|
+
const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname4()} \xB7 ${process.platform}` });
|
|
16014
16190
|
process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
|
|
16015
16191
|
`);
|
|
16016
16192
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
@@ -16029,7 +16205,17 @@ async function authorize(quiet = false) {
|
|
|
16029
16205
|
}
|
|
16030
16206
|
throw new Error("Device authorization expired.");
|
|
16031
16207
|
}
|
|
16032
|
-
program.command("
|
|
16208
|
+
program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
|
|
16209
|
+
const options = program.opts();
|
|
16210
|
+
const identity = await whoami(options.apiUrl);
|
|
16211
|
+
emit(identity, options.pretty);
|
|
16212
|
+
if (!identity.authenticated) process.exitCode = 1;
|
|
16213
|
+
});
|
|
16214
|
+
program.command("connect").description("Connect this agent once, with renewable account access.").requiredOption("--email <email>", "Email of the Rill account approving access").action(async (input) => {
|
|
16215
|
+
const options = program.opts();
|
|
16216
|
+
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
|
|
16217
|
+
});
|
|
16218
|
+
program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
|
|
16033
16219
|
await authorize();
|
|
16034
16220
|
});
|
|
16035
16221
|
var record2 = program.command("record").description("Run a cloud browser from a URL and prompt, or manage a local recording.").argument("[url]").option("-p, --prompt <prompt>").option("--expectation <text>").option("--account <name>", "Saved website test account").option("--id <uuid>", "Retry a submission with the same run ID").option("--no-wait").action(async (url2, commandOptions) => {
|
|
@@ -16059,15 +16245,15 @@ var record2 = program.command("record").description("Run a cloud browser from a
|
|
|
16059
16245
|
var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
|
|
16060
16246
|
for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
|
|
16061
16247
|
const options = program.opts();
|
|
16062
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16248
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16063
16249
|
const run2 = await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id));
|
|
16064
16250
|
if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
|
|
16065
16251
|
else process.stdout.write(formatCloudRun(run2));
|
|
16066
16252
|
});
|
|
16067
16253
|
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) => {
|
|
16068
16254
|
const options = program.opts();
|
|
16069
|
-
|
|
16070
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16255
|
+
await ensureAnonymousCredential(options.apiUrl);
|
|
16256
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16071
16257
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
16072
16258
|
if (!["local", "remote"].includes(commandOptions.encoding)) throw new Error("Encoding must be remote or local.");
|
|
16073
16259
|
if (commandOptions.encoding === "remote" && !(await client.encodingCapabilities()).enabled) throw new Error("Remote encoding is unavailable for this workspace. Use --encoding local.");
|
|
@@ -16079,7 +16265,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16079
16265
|
const local = await startRecordingWithCompensation(remote, {
|
|
16080
16266
|
startLocal: async () => {
|
|
16081
16267
|
await ensureDaemon(process.argv[1]);
|
|
16082
|
-
return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Number(commandOptions.maxDuration), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
|
|
16268
|
+
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 });
|
|
16083
16269
|
},
|
|
16084
16270
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
16085
16271
|
});
|
|
@@ -16090,7 +16276,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16090
16276
|
headed: Boolean(commandOptions.headed),
|
|
16091
16277
|
attached: Boolean(commandOptions.cdpUrl)
|
|
16092
16278
|
}).catch(() => void 0);
|
|
16093
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) };
|
|
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) };
|
|
16094
16280
|
});
|
|
16095
16281
|
emit(result, options.pretty);
|
|
16096
16282
|
});
|
|
@@ -16099,8 +16285,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16099
16285
|
await ensureDaemon(process.argv[1]);
|
|
16100
16286
|
const local = await stopRecording(recordingId);
|
|
16101
16287
|
if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
|
|
16102
|
-
const
|
|
16103
|
-
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
16288
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16104
16289
|
const stopped = await client.markRecordingStopped(recordingId, {
|
|
16105
16290
|
durationSeconds: local.result.durationSeconds,
|
|
16106
16291
|
stoppedReason: local.result.stoppedReason,
|
|
@@ -16112,11 +16297,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16112
16297
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
16113
16298
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16114
16299
|
emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16115
|
-
if (result2.status === "ready") await
|
|
16116
|
-
else watchEncoding(options.apiUrl, recordingId
|
|
16300
|
+
if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16301
|
+
else watchEncoding(options.apiUrl, recordingId);
|
|
16117
16302
|
return;
|
|
16118
16303
|
}
|
|
16119
|
-
const video = await
|
|
16304
|
+
const video = await stat4(local.result.videoPath);
|
|
16120
16305
|
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
|
|
16121
16306
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
16122
16307
|
const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
@@ -16126,17 +16311,18 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16126
16311
|
recordingId,
|
|
16127
16312
|
status: remote ? "ready" : "processing",
|
|
16128
16313
|
shareUrl: remote?.shareUrl ?? null,
|
|
16314
|
+
expiresAt: remote?.expires_at ?? null,
|
|
16129
16315
|
durationSeconds: local.result.durationSeconds,
|
|
16130
16316
|
stoppedReason: local.result.stoppedReason,
|
|
16131
16317
|
diagnostics: local.result.diagnostics,
|
|
16132
16318
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
16133
16319
|
};
|
|
16134
16320
|
emit(result, options.pretty);
|
|
16135
|
-
if (remote) await
|
|
16321
|
+
if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16136
16322
|
});
|
|
16137
16323
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
16138
16324
|
const options = program.opts();
|
|
16139
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16325
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16140
16326
|
const current = await client.encodingStatus(recordingId).catch((error51) => {
|
|
16141
16327
|
if (error51 instanceof ControlPlaneError && error51.code === "recording_not_found") return null;
|
|
16142
16328
|
throw error51;
|
|
@@ -16154,17 +16340,17 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
|
|
|
16154
16340
|
const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16155
16341
|
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16156
16342
|
if (result.status === "ready") await removeLocalCapture(recordingId);
|
|
16157
|
-
else watchEncoding(options.apiUrl, recordingId
|
|
16343
|
+
else watchEncoding(options.apiUrl, recordingId);
|
|
16158
16344
|
});
|
|
16159
16345
|
program.command("encoding-watch", { hidden: true }).argument("<recording-id>").action(async (id) => {
|
|
16160
16346
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid recording ID");
|
|
16161
16347
|
const options = program.opts();
|
|
16162
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16348
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16163
16349
|
const result = await waitForEncoding(client, id, 864e5);
|
|
16164
16350
|
if (result.status === "ready") {
|
|
16165
16351
|
await ensureDaemon(process.argv[1]);
|
|
16166
16352
|
const local = await recordingStatus(id).catch(() => null);
|
|
16167
|
-
if (local?.result?.frameDirectory) await
|
|
16353
|
+
if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16168
16354
|
}
|
|
16169
16355
|
});
|
|
16170
16356
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
@@ -16174,7 +16360,7 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
|
|
|
16174
16360
|
if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
|
|
16175
16361
|
const options = program.opts();
|
|
16176
16362
|
const token = await loadCredential(options.apiUrl);
|
|
16177
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16363
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16178
16364
|
await client.submitRecordingFeedback(recordingId ?? null, {
|
|
16179
16365
|
outcome: commandOptions.outcome,
|
|
16180
16366
|
whatHelped: commandOptions.helped,
|
|
@@ -16183,10 +16369,14 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
|
|
|
16183
16369
|
});
|
|
16184
16370
|
emit(feedbackSubmissionReceipt(recordingId ?? null), options.pretty);
|
|
16185
16371
|
});
|
|
16372
|
+
record2.command("list").description("List recordings visible to this connection.").action(async () => {
|
|
16373
|
+
const options = program.opts();
|
|
16374
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).listRecordings(), options.pretty);
|
|
16375
|
+
});
|
|
16186
16376
|
record2.command("status").argument("<recording-id>").action(async (recordingId) => {
|
|
16187
16377
|
const options = program.opts();
|
|
16188
16378
|
const token = await loadCredential(options.apiUrl);
|
|
16189
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16379
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16190
16380
|
try {
|
|
16191
16381
|
const encoding = await client.encodingStatus(recordingId);
|
|
16192
16382
|
const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
|
|
@@ -16208,24 +16398,24 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16208
16398
|
record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
16209
16399
|
const options = program.opts();
|
|
16210
16400
|
const token = await loadCredential(options.apiUrl);
|
|
16211
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16401
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16212
16402
|
emit(await client.cancelRecording(recordingId), options.pretty);
|
|
16213
16403
|
});
|
|
16214
16404
|
program.command("upload").argument("<video-file>").option("--title <title>").option("--no-wait").action(async (videoFile, commandOptions) => {
|
|
16215
16405
|
const options = program.opts();
|
|
16216
|
-
|
|
16217
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16218
|
-
const file2 = await
|
|
16406
|
+
await ensureAnonymousCredential(options.apiUrl);
|
|
16407
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16408
|
+
const file2 = await stat4(videoFile);
|
|
16219
16409
|
const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
|
|
16220
16410
|
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
|
|
16221
16411
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
16222
16412
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
|
|
16223
16413
|
if (remote) process.stderr.write("\n");
|
|
16224
|
-
emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, diagnosticsAvailable: false }, options.pretty);
|
|
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);
|
|
16225
16415
|
});
|
|
16226
16416
|
program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
|
|
16227
16417
|
const options = program.opts();
|
|
16228
|
-
emit(await new ControlPlaneClient(options.apiUrl,
|
|
16418
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
|
|
16229
16419
|
});
|
|
16230
16420
|
var drafts = program.command("drafts");
|
|
16231
16421
|
drafts.command("list").action(async () => {
|
|
@@ -16241,9 +16431,9 @@ drafts.command("purge").action(async () => {
|
|
|
16241
16431
|
let purged = 0;
|
|
16242
16432
|
for (const name of names) {
|
|
16243
16433
|
const path = `${root}/${name}`;
|
|
16244
|
-
const metadata = await
|
|
16434
|
+
const metadata = await stat4(path);
|
|
16245
16435
|
if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
|
|
16246
|
-
await
|
|
16436
|
+
await rm5(path, { recursive: true, force: true });
|
|
16247
16437
|
purged += 1;
|
|
16248
16438
|
}
|
|
16249
16439
|
}
|
|
@@ -16255,7 +16445,7 @@ program.hook("preAction", async (_command, actionCommand) => {
|
|
|
16255
16445
|
const options = program.opts();
|
|
16256
16446
|
const token = await loadCredential(options.apiUrl).catch(() => null);
|
|
16257
16447
|
if (!token) return;
|
|
16258
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16448
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16259
16449
|
await startCliTelemetry(client, command);
|
|
16260
16450
|
});
|
|
16261
16451
|
async function startCliTelemetry(client, command) {
|