@runalabs/rill-cli 0.1.15 → 0.1.17
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 +22 -3
- package/dist/cli.js +150 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ The following local-recording instructions remain available through `record star
|
|
|
25
25
|
|
|
26
26
|
## Install and verify
|
|
27
27
|
|
|
28
|
+
Check `node --version`, `node -p "process.platform"`, and `rill --version` before installing. Reuse an installed compatible CLI. Commands execute in the agent's environment, which may be a separate sandbox from the user's desktop. Chrome must be accessible there, and the agent must be able to attach over CDP; doctor does not verify browser-control capability.
|
|
29
|
+
|
|
30
|
+
If the CLI is missing, check npm with `npm ping --fetch-timeout=5000 --fetch-retries=0` before installation. See the [canonical skill](https://userill.dev/skill.md) for the bounded Rill connectivity check and Windows Terminal fallback. Report blocked network access instead of repeating installation attempts.
|
|
31
|
+
|
|
28
32
|
Browser recording runs on macOS, Linux, and Windows. Install Node.js 22+ and Google Chrome or Chromium. Rill encodes recordings remotely by default; local FFmpeg is optional. Chrome stays on your machine and can access localhost. Raw frames upload after recording stops.
|
|
29
33
|
|
|
30
34
|
Then install the scoped package:
|
|
@@ -42,7 +46,7 @@ Do not use `npx rill`: the unscoped package belongs to a different product. Doct
|
|
|
42
46
|
|
|
43
47
|
### Connect with pollable approval
|
|
44
48
|
|
|
45
|
-
`rill whoami`
|
|
49
|
+
`rill whoami` shows a short account summary without starting sign-in. Agents should use `rill whoami --json` for verified identity fields. `rill connect --email you@example.com` waits for human approval by default. For agents that cannot hold a foreground command open:
|
|
46
50
|
|
|
47
51
|
```sh
|
|
48
52
|
rill connect --email you@example.com --no-wait
|
|
@@ -221,7 +225,20 @@ Use `rill --help` or `<command> --help` for supported flags.
|
|
|
221
225
|
|
|
222
226
|
## Output and errors
|
|
223
227
|
|
|
224
|
-
|
|
228
|
+
`whoami` shows a concise human summary by default. Use `rill whoami --json` for the existing schema-versioned identity response, including email, workspace, scopes and expiry. Its exit codes are unchanged: missing credentials exit 1; connected and anonymous access exit 0.
|
|
229
|
+
|
|
230
|
+
Doctor and local recording commands retain JSON on stdout by default. Use `--human` for readable doctor, login, recording start/stop/status/retry summaries and errors. Doctor shows the command execution OS, named prerequisite checks, recovery guidance, and a pass/fail/warning count. Recording summaries show the ID, status, duration when available, share URL when returned, and the exact next command. Pending processing is distinct from a ready recording; a private recording may have no share URL.
|
|
231
|
+
|
|
232
|
+
Explicit `--json` or `--pretty` takes precedence over `--human`. Use `--pretty` for indented JSON. Human output uses restrained terminal color only on a TTY, respects `NO_COLOR` and `TERM=dumb`, and retains status symbols and text without color. Human errors go to stderr; JSON errors remain on stdout. Help and version commands print plain text. Feedback and attachment metadata remain available in JSON; selecting a human view does not disable workspace feedback.
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
rill whoami
|
|
236
|
+
rill whoami --json
|
|
237
|
+
rill --human doctor
|
|
238
|
+
rill --human record start --url https://example.com --title "Smoke test" --headed
|
|
239
|
+
rill --human record stop <recording-id>
|
|
240
|
+
rill --human record status <recording-id>
|
|
241
|
+
```
|
|
225
242
|
|
|
226
243
|
```sh
|
|
227
244
|
rill --pretty record status <recording-id>
|
|
@@ -353,11 +370,13 @@ Progress runs through `uploading` → `queued` → `encoding` → `processing`
|
|
|
353
370
|
|
|
354
371
|
Run `rill connect --email you@example.com` to authorize account access or keep your anonymous recordings. Open the displayed link, sign in, and enter the code yourself. Approval allows creation and reading of your existing and new recordings for 90 days, with automatic renewal of one-hour access tokens. Revoke the connection in Settings → Agent access to stop access and renewal. Deletion and changes to existing sharing settings are not included.
|
|
355
372
|
|
|
373
|
+
Browser approval does not finish the handshake: the CLI must collect and save credentials. With `--no-wait`, keep polling the returned `connect status <authorizationId>` command until `authenticated`. Starting with 0.1.17, the CLI checks the server even after its local approval deadline. Approval codes expire after ten minutes; successful approval starts a separate 30-minute collection window, with a 60-second lost-response retry window after first collection. If collection expires, run `connect --email` again with the same account to request fresh human approval while preserving the registration and recordings. Do not delete saved credentials to recover. Auth requests include the CLI version for consent-aware diagnostics.
|
|
374
|
+
|
|
356
375
|
Use `rill record list` to list accessible recordings and `rill inspect <recording-id>` for private recording details. `rill inspect <share-url>` continues to work without account access. The existing `rill login` and `RILL_TOKEN` flows remain supported. Local credential storage requires macOS Keychain, Linux Secret Service, or Windows current-user DPAPI.
|
|
357
376
|
|
|
358
377
|
### Check the connected account
|
|
359
378
|
|
|
360
|
-
Run `rill whoami`
|
|
379
|
+
Run `rill whoami` for a short human account summary, or `rill whoami --json` (also `rill --pretty whoami`) for structured identity fields. JSON includes status, account email when available, workspace, scopes, and access expiry. It uses RILL_TOKEN when set, otherwise the saved OS credential. Existing access can renew automatically; it never creates an identity or opens an approval flow. Missing credentials return `not_connected` in JSON and exit 1. Invalid or revoked credentials return an error.
|
|
361
380
|
|
|
362
381
|
### Capture time and upload progress (0.1.14)
|
|
363
382
|
|
package/dist/cli.js
CHANGED
|
@@ -127,6 +127,11 @@ function browserAttachment(cdpUrl, tabId) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// src/version.ts
|
|
131
|
+
import { createRequire } from "module";
|
|
132
|
+
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
133
|
+
var CLI_VERSION = packageJson.version;
|
|
134
|
+
|
|
130
135
|
// src/agent-session.ts
|
|
131
136
|
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
132
137
|
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
@@ -150,7 +155,7 @@ async function request(apiUrl, path, body, form = false) {
|
|
|
150
155
|
method: "POST",
|
|
151
156
|
redirect: "error",
|
|
152
157
|
signal: AbortSignal.timeout(15e3),
|
|
153
|
-
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
158
|
+
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json", "X-Rill-CLI-Version": CLI_VERSION },
|
|
154
159
|
body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
|
|
155
160
|
});
|
|
156
161
|
const value = await response.json();
|
|
@@ -246,6 +251,12 @@ async function connectAgent(apiUrl, email3, options = {}) {
|
|
|
246
251
|
async function beginConnection(apiUrl, email3) {
|
|
247
252
|
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
248
253
|
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
254
|
+
if (saved?.connection?.status === "pending" && saved.connection.expiresAt <= Date.now()) {
|
|
255
|
+
if (saved.connection.email !== email3) throw new Error("Use the original account to finish or recover this connection.");
|
|
256
|
+
const result = await pollSavedConnection(apiUrl, saved.connection.id);
|
|
257
|
+
if (result.status !== "expired") return result;
|
|
258
|
+
saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
259
|
+
}
|
|
249
260
|
if (saved?.connection?.status === "pending" && saved.connection.expiresAt > Date.now()) {
|
|
250
261
|
if (saved.connection.email !== email3) throw new Error("A connection for another email is pending. Let it expire before starting a different connection.");
|
|
251
262
|
process.stderr.write(`Open ${saved.connection.verificationUri}
|
|
@@ -259,6 +270,22 @@ Sign in as ${email3} and enter ${saved.connection.userCode}. Approve only the re
|
|
|
259
270
|
const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
260
271
|
claim = result.claim_attempt;
|
|
261
272
|
} catch (error51) {
|
|
273
|
+
if (error51 instanceof AgentAuthError && error51.code === "claim_already_approved") {
|
|
274
|
+
saved.connection ??= {
|
|
275
|
+
id: randomBytes(16).toString("hex"),
|
|
276
|
+
email: email3,
|
|
277
|
+
verificationUri: "",
|
|
278
|
+
userCode: "",
|
|
279
|
+
expiresAt: Date.now(),
|
|
280
|
+
interval: 5,
|
|
281
|
+
nextPollAt: 0,
|
|
282
|
+
status: "pending"
|
|
283
|
+
};
|
|
284
|
+
saved.connection.status = "pending";
|
|
285
|
+
saved.connection.nextPollAt = 0;
|
|
286
|
+
await save(apiUrl, saved);
|
|
287
|
+
return pollSavedConnection(apiUrl, saved.connection.id);
|
|
288
|
+
}
|
|
262
289
|
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
263
290
|
}
|
|
264
291
|
}
|
|
@@ -309,11 +336,6 @@ async function pollSavedConnection(apiUrl, authorizationId) {
|
|
|
309
336
|
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.");
|
|
310
337
|
const connection = saved.connection;
|
|
311
338
|
if (connection.status !== "pending") return connectionView(apiUrl, saved);
|
|
312
|
-
if (Date.now() >= connection.expiresAt) {
|
|
313
|
-
connection.status = "expired";
|
|
314
|
-
await save(apiUrl, saved);
|
|
315
|
-
return connectionView(apiUrl, saved);
|
|
316
|
-
}
|
|
317
339
|
if (Date.now() < connection.nextPollAt) return connectionView(apiUrl, saved);
|
|
318
340
|
connection.nextPollAt = Date.now() + connection.interval * 1e3;
|
|
319
341
|
await save(apiUrl, saved);
|
|
@@ -322,7 +344,7 @@ async function pollSavedConnection(apiUrl, authorizationId) {
|
|
|
322
344
|
saved = withTokens({ version: 1, anonymous: false, connection: { ...connection, status: "authenticated", userCode: "" } }, tokens);
|
|
323
345
|
} catch (error51) {
|
|
324
346
|
if (!(error51 instanceof AgentAuthError)) throw error51;
|
|
325
|
-
if (error51.code === "expired_token" || error51.code === "claim_expired") connection.status = "expired";
|
|
347
|
+
if (error51.code === "expired_token" || error51.code === "claim_expired" || error51.code === "claim_delivery_expired" || error51.code === "claimed_or_in_flight" && Date.now() >= connection.expiresAt) connection.status = "expired";
|
|
326
348
|
else if (error51.code === "access_denied" || error51.code === "claim_revoked") connection.status = "denied";
|
|
327
349
|
else if (error51.code === "slow_down") {
|
|
328
350
|
connection.interval += 5;
|
|
@@ -442,7 +464,7 @@ var ControlPlaneClient = class {
|
|
|
442
464
|
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
443
465
|
}
|
|
444
466
|
health() {
|
|
445
|
-
return this.request("/api/health", {}, false);
|
|
467
|
+
return this.request("/api/health", { signal: AbortSignal.timeout(5e3) }, false);
|
|
446
468
|
}
|
|
447
469
|
agentSession() {
|
|
448
470
|
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
@@ -15244,7 +15266,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
15244
15266
|
// src/cli.ts
|
|
15245
15267
|
import { spawn as spawn8 } from "child_process";
|
|
15246
15268
|
import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
|
|
15247
|
-
import { Command } from "commander";
|
|
15269
|
+
import { Command, CommanderError } from "commander";
|
|
15248
15270
|
import { basename, dirname as dirname2, join as join11 } from "path";
|
|
15249
15271
|
import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
|
|
15250
15272
|
|
|
@@ -16647,6 +16669,72 @@ function fail(error51) {
|
|
|
16647
16669
|
process.exitCode = 1;
|
|
16648
16670
|
}
|
|
16649
16671
|
|
|
16672
|
+
// src/human-output.ts
|
|
16673
|
+
function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
|
|
16674
|
+
return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
16675
|
+
}
|
|
16676
|
+
function formatIdentity(identity, color = terminalColor()) {
|
|
16677
|
+
if (identity.status === "not_connected") return `${color("! Not connected to Rill", 33)}
|
|
16678
|
+
|
|
16679
|
+
Sign in: ${cliCommand(identity.apiUrl, "login")}
|
|
16680
|
+
`;
|
|
16681
|
+
if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
|
|
16682
|
+
|
|
16683
|
+
Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
|
|
16684
|
+
`;
|
|
16685
|
+
return `${color("\u2713 Connected to Rill", 32)}
|
|
16686
|
+
Account: ${identity.email ?? "Email unavailable"}
|
|
16687
|
+
`;
|
|
16688
|
+
}
|
|
16689
|
+
function formatDoctor(result, color = terminalColor()) {
|
|
16690
|
+
const labels = { node: "Node.js", api: "API connectivity", cli_version: "CLI version", authentication: "Authentication", chrome: "Browser", ffmpeg: "FFmpeg", encoding: "Encoding", recorder_transport: "Recorder transport" };
|
|
16691
|
+
const symbols = { pass: "\u2713", fail: "\xD7", warn: "!", skip: "\u2013" };
|
|
16692
|
+
const colors = { pass: 32, fail: 31, warn: 33, skip: 90 };
|
|
16693
|
+
const lines = [color("Rill CLI Doctor", 34), `Execution environment: ${result.environment.platform}`, ""];
|
|
16694
|
+
for (const check2 of result.checks) {
|
|
16695
|
+
lines.push(color(`${symbols[check2.status]} ${labels[check2.name]}: ${check2.message}`, colors[check2.status]));
|
|
16696
|
+
if (check2.recovery) lines.push(` \u2192 ${check2.recovery}`);
|
|
16697
|
+
}
|
|
16698
|
+
const warnings = result.checks.filter((c) => c.status === "warn").length;
|
|
16699
|
+
lines.push(
|
|
16700
|
+
"",
|
|
16701
|
+
"Browser control: not verified. Your agent must attach to the returned CDP endpoint.",
|
|
16702
|
+
"",
|
|
16703
|
+
`${result.checks.filter((c) => c.status === "pass").length} passed \xB7 ${result.checks.filter((c) => c.status === "fail").length} failed \xB7 ${warnings} ${warnings === 1 ? "warning" : "warnings"}`
|
|
16704
|
+
);
|
|
16705
|
+
return lines.join("\n") + "\n";
|
|
16706
|
+
}
|
|
16707
|
+
function formatRecording(value, apiUrl, color = terminalColor()) {
|
|
16708
|
+
const artifact = value.result && typeof value.result === "object" ? value.result : {};
|
|
16709
|
+
const id = String(value.recordingId ?? value.id ?? "");
|
|
16710
|
+
const status = String(value.status ?? "unknown");
|
|
16711
|
+
const titles = { recording: "Recording started", ready: "Recording ready", captured: "Captured locally \xB7 upload required", uploading: "Uploading recording", queued: "Recording queued for encoding", encoding: "Encoding recording", processing: "Processing recording", finalizing: "Finalizing recording", failed: "Recording failed", cancelled: "Recording cancelled", expired: "Recording expired" };
|
|
16712
|
+
const lines = [color(`${["recording", "ready"].includes(status) ? "\u2713" : ["failed", "expired"].includes(status) ? "\xD7" : "\u2013"} ${titles[status] ?? `Recording status: ${status}`}`, ["failed", "expired"].includes(status) ? 31 : status === "ready" || status === "recording" ? 32 : 33)];
|
|
16713
|
+
if (id) lines.push(`ID: ${id}`);
|
|
16714
|
+
const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
|
|
16715
|
+
if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
|
|
16716
|
+
if (typeof value.remainingSeconds === "number") lines.push(`Time remaining: ${Math.max(0, Math.ceil(value.remainingSeconds))}s`);
|
|
16717
|
+
if (typeof value.cdpUrl === "string") lines.push(`Browser connection: ${value.cdpUrl}`);
|
|
16718
|
+
if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
|
|
16719
|
+
if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
|
|
16720
|
+
else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
|
|
16721
|
+
const message = value.message ?? value.error;
|
|
16722
|
+
if (typeof message === "string" && message) lines.push("", message);
|
|
16723
|
+
const next = typeof value.nextCommand === "string" ? value.nextCommand : status === "recording" && id ? cliCommand(apiUrl, "record", "stop", id) : void 0;
|
|
16724
|
+
if (next) lines.push("", status === "recording" ? "Perform your test, then stop and upload:" : "Next:", ` ${next}`);
|
|
16725
|
+
return lines.join("\n") + "\n";
|
|
16726
|
+
}
|
|
16727
|
+
function formatFailure(error51, color = terminalColor()) {
|
|
16728
|
+
const { error: failure } = failureValue(error51);
|
|
16729
|
+
let message = failure.message.startsWith(`${failure.code}: `) ? failure.message.slice(failure.code.length + 2) : failure.message;
|
|
16730
|
+
if (failure.recovery && message.endsWith(`: ${failure.recovery}`)) message = message.slice(0, -failure.recovery.length - 2);
|
|
16731
|
+
const lines = [color(`\xD7 ${message}`, 31)];
|
|
16732
|
+
if (typeof failure.details?.recordingId === "string") lines.push(`ID: ${failure.details.recordingId}`);
|
|
16733
|
+
if (failure.recovery) lines.push("", `Next: ${failure.recovery}`);
|
|
16734
|
+
else if (typeof failure.details?.nextCommand === "string") lines.push("", `Next: ${failure.details.nextCommand}`);
|
|
16735
|
+
return lines.join("\n") + "\n";
|
|
16736
|
+
}
|
|
16737
|
+
|
|
16650
16738
|
// src/feedback-protocol.ts
|
|
16651
16739
|
function suggestedFeedbackAction(recordingId) {
|
|
16652
16740
|
return {
|
|
@@ -16705,13 +16793,6 @@ async function withRunFeedback(session, start) {
|
|
|
16705
16793
|
|
|
16706
16794
|
// src/doctor.ts
|
|
16707
16795
|
import { spawn as spawn7 } from "child_process";
|
|
16708
|
-
|
|
16709
|
-
// src/version.ts
|
|
16710
|
-
import { createRequire } from "module";
|
|
16711
|
-
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
16712
|
-
var CLI_VERSION = packageJson.version;
|
|
16713
|
-
|
|
16714
|
-
// src/doctor.ts
|
|
16715
16796
|
var defaultDependencies = {
|
|
16716
16797
|
platform: process.platform,
|
|
16717
16798
|
encoding: "remote",
|
|
@@ -16741,9 +16822,11 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16741
16822
|
if (!health.ok || health.service !== "rill") throw new Error("The endpoint did not identify itself as Rill.");
|
|
16742
16823
|
checks.push({ name: "api", status: "pass", message: `Rill API is reachable at ${apiUrl}.` });
|
|
16743
16824
|
} catch (error51) {
|
|
16744
|
-
checks.push({ name: "api", status: "fail", message: `Rill API is unreachable: ${errorMessage(error51)}`, recovery: `Check
|
|
16825
|
+
checks.push({ name: "api", status: "fail", message: `Rill API is unreachable: ${errorMessage(error51)}`, recovery: `Check network access in this ${dependencies.platform} execution environment and the API URL, then rerun \`rill --api-url ${apiUrl} doctor\`. An agent sandbox may have different network access from your desktop.` });
|
|
16745
16826
|
}
|
|
16746
|
-
if (!health
|
|
16827
|
+
if (!health) {
|
|
16828
|
+
checks.push({ name: "cli_version", status: "skip", message: `CLI ${dependencies.cliVersion} compatibility was not checked because the API is unreachable.`, version: dependencies.cliVersion });
|
|
16829
|
+
} else if (!health.minimumCliVersion || !health.recommendedCliVersion) {
|
|
16747
16830
|
checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} could not be checked against the API compatibility policy.`, version: dependencies.cliVersion, recovery: "Deploy a Rill API that advertises minimumCliVersion and recommendedCliVersion." });
|
|
16748
16831
|
} else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
|
|
16749
16832
|
checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} is older than the minimum supported version ${health.minimumCliVersion}.`, version: dependencies.cliVersion, recovery: `Run \`npm install --global @runalabs/rill-cli@${health.recommendedCliVersion}\`.` });
|
|
@@ -16778,20 +16861,23 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16778
16861
|
}
|
|
16779
16862
|
}
|
|
16780
16863
|
}
|
|
16781
|
-
checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion,
|
|
16864
|
+
checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, `Chrome must be available in this ${dependencies.platform} execution environment. Install Chrome/Chromium here or set RILL_CHROME_PATH. A browser on a separate desktop is not automatically accessible; use that desktop's terminal to record locally.`));
|
|
16782
16865
|
if (dependencies.encoding === "local") {
|
|
16783
16866
|
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."));
|
|
16784
16867
|
} else {
|
|
16785
16868
|
checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16869
|
+
if (!health) checks.push({ name: "encoding", status: "skip", message: "Remote encoding was not checked because the API is unreachable." });
|
|
16870
|
+
else {
|
|
16871
|
+
try {
|
|
16872
|
+
const token = await dependencies.loadCredential(apiUrl);
|
|
16873
|
+
if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
|
|
16874
|
+
checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
|
|
16875
|
+
} catch (error51) {
|
|
16876
|
+
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." });
|
|
16877
|
+
}
|
|
16792
16878
|
}
|
|
16793
16879
|
}
|
|
16794
|
-
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
16880
|
+
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, environment: { platform: dependencies.platform }, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
16795
16881
|
}
|
|
16796
16882
|
async function executableCheck(name, path, args, run2, recovery) {
|
|
16797
16883
|
try {
|
|
@@ -16974,13 +17060,23 @@ function watchEncoding(apiUrl, id) {
|
|
|
16974
17060
|
child.unref();
|
|
16975
17061
|
}
|
|
16976
17062
|
var program = new Command();
|
|
16977
|
-
program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output
|
|
17063
|
+
program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output machine-readable JSON (including whoami)", false).option("--human", "Readable doctor, login, whoami and recording start/stop/status/retry output", false);
|
|
17064
|
+
program.configureOutput({ outputError: () => void 0 }).exitOverride();
|
|
17065
|
+
function humanOutput(identity = false) {
|
|
17066
|
+
const options = program.opts();
|
|
17067
|
+
return !options.json && !options.pretty && (identity || options.human);
|
|
17068
|
+
}
|
|
17069
|
+
function emitRecording(value, pretty) {
|
|
17070
|
+
if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
|
|
17071
|
+
else emit(value, pretty);
|
|
17072
|
+
}
|
|
16978
17073
|
var activeTelemetry = null;
|
|
16979
17074
|
program.command("daemon", { hidden: true }).action(() => runDaemon());
|
|
16980
17075
|
program.command("doctor").description("Validate Rill prerequisites before recording.").option("--encoding <mode>", "remote or local", "remote").action(async (commandOptions) => {
|
|
16981
17076
|
const options = program.opts();
|
|
16982
17077
|
const result = await runDoctor(options.apiUrl, { encoding: commandOptions.encoding });
|
|
16983
|
-
|
|
17078
|
+
if (humanOutput()) process.stdout.write(formatDoctor(result));
|
|
17079
|
+
else emit(result, options.pretty);
|
|
16984
17080
|
if (result.status === "error") process.exitCode = 1;
|
|
16985
17081
|
});
|
|
16986
17082
|
async function authorize(quiet = false) {
|
|
@@ -17001,15 +17097,19 @@ async function authorize(quiet = false) {
|
|
|
17001
17097
|
if (result.status === "pending") continue;
|
|
17002
17098
|
if (result.status !== "approved" || !result.token) throw new Error(`Device authorization ${result.status}.`);
|
|
17003
17099
|
await saveCredential(options.apiUrl, result.token);
|
|
17004
|
-
if (!quiet)
|
|
17100
|
+
if (!quiet) {
|
|
17101
|
+
if (humanOutput()) process.stdout.write("\u2713 Signed in to Rill\nCheck your account: rill whoami\n");
|
|
17102
|
+
else emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
|
|
17103
|
+
}
|
|
17005
17104
|
return result.token;
|
|
17006
17105
|
}
|
|
17007
17106
|
throw new Error("Device authorization expired.");
|
|
17008
17107
|
}
|
|
17009
|
-
program.command("whoami").description("Show the current verified
|
|
17108
|
+
program.command("whoami").description("Show the current verified identity without starting sign-in.").addHelpText("after", "\nShows a short account summary by default. Use rill whoami --json for agents, or rill --pretty whoami for indented JSON.").action(async () => {
|
|
17010
17109
|
const options = program.opts();
|
|
17011
17110
|
const identity = await whoami(options.apiUrl);
|
|
17012
|
-
|
|
17111
|
+
if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
|
|
17112
|
+
else emit(identity, options.pretty);
|
|
17013
17113
|
if (!identity.authenticated) process.exitCode = 1;
|
|
17014
17114
|
});
|
|
17015
17115
|
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) => {
|
|
@@ -17101,7 +17201,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
17101
17201
|
}).catch(() => void 0);
|
|
17102
17202
|
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
17103
17203
|
});
|
|
17104
|
-
|
|
17204
|
+
emitRecording(result, options.pretty);
|
|
17105
17205
|
});
|
|
17106
17206
|
record2.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback", "Disable feedback only when the user explicitly requests it").action(async (recordingId, commandOptions) => {
|
|
17107
17207
|
const options = program.opts();
|
|
@@ -17123,7 +17223,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
17123
17223
|
await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
|
|
17124
17224
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
17125
17225
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
17126
|
-
|
|
17226
|
+
emitRecording({ schemaVersion: 1, ...result2, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
17127
17227
|
if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
17128
17228
|
else watchEncoding(options.apiUrl, recordingId);
|
|
17129
17229
|
return;
|
|
@@ -17145,7 +17245,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
17145
17245
|
...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
|
|
17146
17246
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
17147
17247
|
};
|
|
17148
|
-
|
|
17248
|
+
emitRecording(result, options.pretty);
|
|
17149
17249
|
if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
17150
17250
|
});
|
|
17151
17251
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
@@ -17166,7 +17266,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
|
|
|
17166
17266
|
if (!accepted) throw new Error("Encoding job is unavailable.");
|
|
17167
17267
|
if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
|
|
17168
17268
|
const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
17169
|
-
|
|
17269
|
+
emitRecording({ schemaVersion: 1, ...result, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
|
|
17170
17270
|
if (result.status === "ready") await removeLocalCapture(recordingId);
|
|
17171
17271
|
else watchEncoding(options.apiUrl, recordingId);
|
|
17172
17272
|
});
|
|
@@ -17208,7 +17308,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
17208
17308
|
try {
|
|
17209
17309
|
const encoding = await client.encodingStatus(recordingId);
|
|
17210
17310
|
const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
|
|
17211
|
-
|
|
17311
|
+
emitRecording({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
|
|
17212
17312
|
if (encoding.status === "ready") await removeLocalCapture(recordingId);
|
|
17213
17313
|
return;
|
|
17214
17314
|
} catch (error51) {
|
|
@@ -17221,7 +17321,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
17221
17321
|
},
|
|
17222
17322
|
remoteStatus: (id) => client.recordingStatus(id)
|
|
17223
17323
|
}, options.apiUrl);
|
|
17224
|
-
|
|
17324
|
+
emitRecording({ schemaVersion: 1, ...status, recordingId }, options.pretty);
|
|
17225
17325
|
});
|
|
17226
17326
|
record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
17227
17327
|
const options = program.opts();
|
|
@@ -17301,8 +17401,20 @@ program.hook("postAction", async () => {
|
|
|
17301
17401
|
await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
|
|
17302
17402
|
});
|
|
17303
17403
|
program.parseAsync().catch(async (error51) => {
|
|
17404
|
+
if (error51 instanceof CommanderError && error51.exitCode === 0) return;
|
|
17405
|
+
if (error51 instanceof CommanderError) {
|
|
17406
|
+
const action = program.args[0] === "record" ? program.args[1] : void 0;
|
|
17407
|
+
const missingRecording = error51.code === "commander.missingArgument" && ["stop", "status", "retry", "cancel"].includes(action ?? "");
|
|
17408
|
+
error51 = Object.assign(new Error(`validation_failed: ${missingRecording ? "A recording ID is required." : error51.message.replace(/^error: /, "")}`), {
|
|
17409
|
+
code: "validation_failed",
|
|
17410
|
+
recovery: missingRecording ? `Run rill record ${action} <recording-id>. Use rill record list to find the ID.` : "Run rill --help for available commands and options."
|
|
17411
|
+
});
|
|
17412
|
+
}
|
|
17304
17413
|
await finishCliTelemetry(error51);
|
|
17305
|
-
|
|
17414
|
+
if (humanOutput(program.args[0] === "whoami")) {
|
|
17415
|
+
process.stderr.write(formatFailure(error51));
|
|
17416
|
+
process.exitCode = 1;
|
|
17417
|
+
} else fail(error51);
|
|
17306
17418
|
});
|
|
17307
17419
|
function commandPath(actionCommand) {
|
|
17308
17420
|
const path = [];
|