@runalabs/rill-cli 0.1.10 → 0.1.11
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 +26 -7
- package/dist/cli.js +235 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,7 +148,8 @@ Use `rill --help` or `<command> --help` for supported flags.
|
|
|
148
148
|
| `rill doctor` | Check prerequisites, API compatibility, and authentication. |
|
|
149
149
|
| `rill record start [options]` | Start a managed or attached browser recording. |
|
|
150
150
|
| `rill record stop <recording-id>` | Finalize and upload; supports `--no-wait` and `--no-feedback`. |
|
|
151
|
-
| `rill record status <recording-id>` | Read
|
|
151
|
+
| `rill record status <recording-id>` | Read the remote encoding job first; otherwise check local capture state with a remote fallback. |
|
|
152
|
+
| `rill record retry <recording-id>` | Resume a retained remote capture or retry a failed encode; supports `--no-wait`. |
|
|
152
153
|
| `rill record cancel <recording-id>` | Abandon a run created by this credential and release its remote quota reservation. |
|
|
153
154
|
| `rill upload <video-file>` | Upload video-only evidence; supports `--title` and `--no-wait`. |
|
|
154
155
|
| `rill inspect <share-url>` | Read bounded context without login. |
|
|
@@ -185,14 +186,15 @@ Check the exit status first, then structured fields. Known codes include `authen
|
|
|
185
186
|
|
|
186
187
|
- **Doctor fails:** read the failed check's recovery field. Correct the credential, API connectivity, or executable path before recording.
|
|
187
188
|
- **Quota exceeded:** inspect `error.details.quota.type` and `blockingRecordingId` when present. Check the blocking recording; cancel only an abandoned run. Review storage and daily allowance in Billing.
|
|
188
|
-
- **
|
|
189
|
+
- **Remote upload interrupted or encoding failed:** keep the recording ID and local capture files. Run `rill record status <recording-id>`, then `rill record retry <recording-id>` with the original credential before capture expiry. Retry resumes accepted chunks or requeues the encode without repeating browser actions. A `ready` result needs a share URL before you return it as evidence.
|
|
189
190
|
- **Duration limit reached:** local capture finalizes automatically, but `record stop` is still needed to upload.
|
|
190
|
-
- **
|
|
191
|
+
- **Terminal closed:** after upload acceptance, the server finishes independently; retrieve the result with `record status`. Before acceptance, retry from the original machine while its captured frames remain available.
|
|
192
|
+
- **Local session missing:** `record status` checks for a remote encoding job before falling back to local or remote recording state. `drafts list` locates local directories, which may be incomplete. There is no `drafts resume` command.
|
|
191
193
|
- **Share cannot be inspected:** open the complete URL in a browser; ask the owner for a current link if needed.
|
|
192
194
|
|
|
193
195
|
A failed local startup automatically attempts to cancel the remote reservation it just created. `record cancel` changes remote state and does not stop the local browser; finish local capture before abandoning the remote reservation.
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
For `--encoding local`, when finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.ndjson.gz` in the recording's draft directory. `rill upload <path-to-recording.webm>` is a video-only fallback: it creates a new recording and does not restore the original diagnostics. Successful stop with readiness removes that run's local artifact directory.
|
|
196
198
|
|
|
197
199
|
`drafts purge` deletes local draft directories older than 24 hours by modification time, without a confirmation prompt. Recover needed evidence before purging.
|
|
198
200
|
|
|
@@ -203,7 +205,7 @@ When finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.nd
|
|
|
203
205
|
| `RILL_TOKEN` | Agent credential; overrides the keychain. |
|
|
204
206
|
| `RILL_API_URL` | API origin; defaults to `https://userill.dev`. Global `--api-url` takes precedence. |
|
|
205
207
|
| `RILL_CHROME_PATH` | Chrome or Chromium executable path. |
|
|
206
|
-
| `RILL_FFMPEG` |
|
|
208
|
+
| `RILL_FFMPEG` | FFmpeg executable for `--encoding local`; defaults to `ffmpeg` on `PATH`. |
|
|
207
209
|
| `RILL_HOME` | Recorder state directory; defaults to `~/.rill`. |
|
|
208
210
|
| `RILL_SOCKET` | Recorder socket; defaults to `recorder.sock` inside `RILL_HOME`. |
|
|
209
211
|
| `RILL_TEMP` | Artifact root; defaults to `rill-recordings` inside the system temporary directory. |
|
|
@@ -264,8 +266,25 @@ Use `--json` for a machine-readable result (`--pretty` also enables JSON formatt
|
|
|
264
266
|
Provider token usage is retained internally and omitted from customer output.
|
|
265
267
|
`rill run status <run-id>` and `rill run cancel <run-id>` support the same output options.
|
|
266
268
|
|
|
267
|
-
|
|
269
|
+
## Remote encoding
|
|
268
270
|
|
|
269
271
|
Since 0.1.10, `record start` uses remote encoding by default. Chrome stays local, including access to localhost; FFmpeg is required only for `record start --encoding local`. For local encoding, install FFmpeg with `brew install ffmpeg` on macOS or `sudo apt install ffmpeg` on Debian/Ubuntu, then run `doctor --encoding local`. Remote encoding adds no charge and consumes no cloud-run credits; existing recording allowances still apply. Captures are limited to ten minutes and 1 GiB of raw frames, with three pending jobs per workspace. Busy recordings can reach the byte limit sooner.
|
|
270
272
|
|
|
271
|
-
`record stop` uploads frames and waits for playback. `record stop ID --no-wait` returns after upload acceptance; processing continues if the terminal closes. Use `record status ID` to retrieve progress and the share link, or `record retry ID` to resume an interrupted upload or retry encoding without repeating browser actions.
|
|
273
|
+
`record stop` uploads frames and waits for playback. `record stop ID --no-wait` returns after upload acceptance; processing continues if the terminal closes. Use `record status ID` to retrieve progress and the share link, or `record retry ID` to resume an interrupted upload or retry encoding without repeating browser actions. Raw captures are removed after successful publication. Recoverable captures expire after 24 hours: local retention starts at capture start, server retention at upload initialization. Keep the original credential and local files until upload acceptance; `expiresAt` reports the server deadline once a job exists.
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
rill record stop <recording-id> --no-wait
|
|
277
|
+
rill record status <recording-id>
|
|
278
|
+
# If upload or encoding failed, retry the same recording:
|
|
279
|
+
rill record retry <recording-id>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Progress runs through `uploading` → `queued` → `encoding` → `processing` → `ready`. Waiting for capacity is normal; do not repeat the browser journey merely because a job is queued. Long or busy recordings can take several minutes to upload and encode.
|
|
283
|
+
|
|
284
|
+
### Anonymous local recordings and account connection
|
|
285
|
+
|
|
286
|
+
`rill record start` can begin without sign-in when the server supports anonymous access. The CLI stores the identity in the OS credential store and uses local encoding. Anonymous access includes three successful recordings, at most five minutes and 250 MB each; each recording and its unlisted share link expires seven days after creation. Failed uploads do not consume the successful-recording allowance. Source and service rate limits also apply.
|
|
287
|
+
|
|
288
|
+
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 Rill's Credentials page to stop access and renewal. Deletion and changes to existing sharing settings are not included.
|
|
289
|
+
|
|
290
|
+
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 or Linux Secret Service.
|
package/dist/cli.js
CHANGED
|
@@ -14673,6 +14673,9 @@ var ControlPlaneClient = class {
|
|
|
14673
14673
|
const body = await readFile(filePath);
|
|
14674
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
14675
|
}
|
|
14676
|
+
listRecordings() {
|
|
14677
|
+
return this.request("/api/recordings");
|
|
14678
|
+
}
|
|
14676
14679
|
createDeviceAuthorization(input) {
|
|
14677
14680
|
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
14678
14681
|
}
|
|
@@ -14680,6 +14683,7 @@ var ControlPlaneClient = class {
|
|
|
14680
14683
|
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
14681
14684
|
}
|
|
14682
14685
|
inspect(shareUrl) {
|
|
14686
|
+
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
14683
14687
|
const url2 = new URL(shareUrl);
|
|
14684
14688
|
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
14685
14689
|
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
@@ -14692,8 +14696,9 @@ var ControlPlaneClient = class {
|
|
|
14692
14696
|
const headers = new Headers(init.headers);
|
|
14693
14697
|
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
14694
14698
|
if (authenticated) {
|
|
14695
|
-
|
|
14696
|
-
|
|
14699
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
14700
|
+
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
14701
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
14697
14702
|
}
|
|
14698
14703
|
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
14699
14704
|
const value = await response.json().catch(() => null);
|
|
@@ -14766,10 +14771,10 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
14766
14771
|
|
|
14767
14772
|
// src/cli.ts
|
|
14768
14773
|
import { spawn as spawn6 } from "child_process";
|
|
14774
|
+
import { hostname as hostname4 } from "os";
|
|
14769
14775
|
import { Command } from "commander";
|
|
14770
|
-
import { hostname as hostname3 } from "os";
|
|
14771
14776
|
import { basename, dirname as dirname2 } from "path";
|
|
14772
|
-
import { readdir as readdir2, rm as
|
|
14777
|
+
import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
|
|
14773
14778
|
|
|
14774
14779
|
// ../recorder/src/client.ts
|
|
14775
14780
|
import { request } from "http";
|
|
@@ -15399,34 +15404,34 @@ var RecordingSession = class _RecordingSession {
|
|
|
15399
15404
|
this.consoleEvents.push(event);
|
|
15400
15405
|
pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
|
|
15401
15406
|
});
|
|
15402
|
-
this.page.on("request", (
|
|
15407
|
+
this.page.on("request", (request3) => {
|
|
15403
15408
|
if (this.diagnosticsPaused) return;
|
|
15404
15409
|
if (this.networkEvents.length >= 5e3) {
|
|
15405
15410
|
this.truncated = true;
|
|
15406
15411
|
return;
|
|
15407
15412
|
}
|
|
15408
15413
|
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(
|
|
15414
|
+
this.requestStart.set(request3, started);
|
|
15415
|
+
const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
|
|
15416
|
+
if (captureNetworkBodies && request3.postData()) {
|
|
15417
|
+
const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
|
|
15413
15418
|
event.requestBody = redacted.body;
|
|
15414
15419
|
event.bodyOmittedReason = redacted.omittedReason;
|
|
15415
15420
|
if (redacted.body) this.bodyBytes += Buffer.byteLength(redacted.body);
|
|
15416
15421
|
}
|
|
15417
15422
|
this.networkEvents.push(event);
|
|
15418
|
-
this.requestEvents.set(
|
|
15423
|
+
this.requestEvents.set(request3, event);
|
|
15419
15424
|
});
|
|
15420
15425
|
this.page.on("response", (response) => this.updateResponse(response));
|
|
15421
|
-
this.page.on("requestfailed", (
|
|
15422
|
-
const event = this.requestEvents.get(
|
|
15426
|
+
this.page.on("requestfailed", (request3) => {
|
|
15427
|
+
const event = this.requestEvents.get(request3);
|
|
15423
15428
|
if (!event) return;
|
|
15424
|
-
event.failureReason =
|
|
15425
|
-
event.durationMs = Math.max(0, offset() - (this.requestStart.get(
|
|
15429
|
+
event.failureReason = request3.failure()?.errorText ?? "request_failed";
|
|
15430
|
+
event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
|
|
15426
15431
|
pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
|
|
15427
15432
|
});
|
|
15428
|
-
this.page.on("requestfinished", async (
|
|
15429
|
-
const response = await
|
|
15433
|
+
this.page.on("requestfinished", async (request3) => {
|
|
15434
|
+
const response = await request3.response();
|
|
15430
15435
|
if (response) await this.captureResponseBody(response);
|
|
15431
15436
|
});
|
|
15432
15437
|
}
|
|
@@ -15546,7 +15551,7 @@ async function runDaemon() {
|
|
|
15546
15551
|
await unlink(socketPath).catch((error51) => {
|
|
15547
15552
|
if (error51.code !== "ENOENT") throw error51;
|
|
15548
15553
|
});
|
|
15549
|
-
const server = createServer2((
|
|
15554
|
+
const server = createServer2((request3, response) => void route(request3, response));
|
|
15550
15555
|
server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
|
|
15551
15556
|
const idleTimer = setInterval(() => {
|
|
15552
15557
|
void (async () => {
|
|
@@ -15565,14 +15570,14 @@ async function runDaemon() {
|
|
|
15565
15570
|
process.once("SIGTERM", close);
|
|
15566
15571
|
process.once("SIGINT", close);
|
|
15567
15572
|
}
|
|
15568
|
-
async function route(
|
|
15573
|
+
async function route(request3, response) {
|
|
15569
15574
|
lastActivity = Date.now();
|
|
15570
|
-
const url2 = new URL(
|
|
15571
|
-
if (
|
|
15572
|
-
if (
|
|
15575
|
+
const url2 = new URL(request3.url ?? "/", "http://daemon.local");
|
|
15576
|
+
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
|
|
15577
|
+
if (request3.method === "POST" && url2.pathname === "/sessions") {
|
|
15573
15578
|
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
15579
|
try {
|
|
15575
|
-
const input = await readJson(
|
|
15580
|
+
const input = await readJson(request3);
|
|
15576
15581
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15577
15582
|
const outputDirectory = join7(recordingTempRoot(), input.recordingId);
|
|
15578
15583
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
@@ -15591,8 +15596,8 @@ async function route(request2, response) {
|
|
|
15591
15596
|
if (match) {
|
|
15592
15597
|
const entry = sessions.get(decodeURIComponent(match[1]));
|
|
15593
15598
|
if (!entry) return json2(response, 404, { error: { code: "recording_not_found", message: "The local recording session was not found." } });
|
|
15594
|
-
if (
|
|
15595
|
-
if (
|
|
15599
|
+
if (request3.method === "GET" && !match[2]) return json2(response, 200, serializeEntry(entry));
|
|
15600
|
+
if (request3.method === "POST" && match[2] === "stop") {
|
|
15596
15601
|
if (entry.state === "ready") return json2(response, 200, serializeEntry(entry));
|
|
15597
15602
|
await finalizeEntry(entry, "agent_request");
|
|
15598
15603
|
return json2(response, entry.result ? 200 : 500, serializeEntry(entry));
|
|
@@ -15633,10 +15638,10 @@ async function persistState() {
|
|
|
15633
15638
|
`, { mode: 384 });
|
|
15634
15639
|
await rename(temporary, daemonStatePath());
|
|
15635
15640
|
}
|
|
15636
|
-
async function readJson(
|
|
15641
|
+
async function readJson(request3) {
|
|
15637
15642
|
const chunks = [];
|
|
15638
15643
|
let size = 0;
|
|
15639
|
-
for await (const chunk of
|
|
15644
|
+
for await (const chunk of request3) {
|
|
15640
15645
|
const buffer = Buffer.from(chunk);
|
|
15641
15646
|
size += buffer.length;
|
|
15642
15647
|
if (size > 1e6) throw new Error("Request body exceeds 1 MB.");
|
|
@@ -15689,6 +15694,153 @@ function cliFailureCode(error51) {
|
|
|
15689
15694
|
return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
|
|
15690
15695
|
}
|
|
15691
15696
|
|
|
15697
|
+
// src/agent-session.ts
|
|
15698
|
+
import { hostname as hostname3, homedir as homedir2 } from "os";
|
|
15699
|
+
import { mkdir as mkdir5, rm as rm4, stat as stat3 } from "fs/promises";
|
|
15700
|
+
import { join as join8 } from "path";
|
|
15701
|
+
import { createHash, randomBytes } from "crypto";
|
|
15702
|
+
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
15703
|
+
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15704
|
+
var AgentAuthError = class extends Error {
|
|
15705
|
+
constructor(code, message) {
|
|
15706
|
+
super(`${code}: ${message}`);
|
|
15707
|
+
this.code = code;
|
|
15708
|
+
}
|
|
15709
|
+
};
|
|
15710
|
+
function parseAuthorization(value) {
|
|
15711
|
+
if (!value?.startsWith("{")) return null;
|
|
15712
|
+
const saved = JSON.parse(value);
|
|
15713
|
+
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
15714
|
+
return saved;
|
|
15715
|
+
}
|
|
15716
|
+
async function request2(apiUrl, path, body, form = false) {
|
|
15717
|
+
const url2 = new URL(apiUrl);
|
|
15718
|
+
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
15719
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
15720
|
+
method: "POST",
|
|
15721
|
+
redirect: "error",
|
|
15722
|
+
signal: AbortSignal.timeout(15e3),
|
|
15723
|
+
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
15724
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
15725
|
+
});
|
|
15726
|
+
const value = await response.json();
|
|
15727
|
+
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
15728
|
+
return value;
|
|
15729
|
+
}
|
|
15730
|
+
function withTokens(saved, tokens) {
|
|
15731
|
+
return {
|
|
15732
|
+
...saved,
|
|
15733
|
+
accessToken: tokens.access_token,
|
|
15734
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
15735
|
+
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
15736
|
+
};
|
|
15737
|
+
}
|
|
15738
|
+
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
15739
|
+
var pending = /* @__PURE__ */ new Map();
|
|
15740
|
+
async function loadAgentCredential(apiUrl) {
|
|
15741
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
15742
|
+
const inFlight = pending.get(apiUrl);
|
|
15743
|
+
if (inFlight) return inFlight;
|
|
15744
|
+
const promise2 = (async () => {
|
|
15745
|
+
const raw = await loadStoredCredential(apiUrl);
|
|
15746
|
+
const saved = parseAuthorization(raw);
|
|
15747
|
+
if (!saved) return raw;
|
|
15748
|
+
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
15749
|
+
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
15750
|
+
if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
15751
|
+
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
15752
|
+
await save(apiUrl, withTokens(saved, tokens));
|
|
15753
|
+
return tokens.access_token;
|
|
15754
|
+
})();
|
|
15755
|
+
pending.set(apiUrl, promise2);
|
|
15756
|
+
try {
|
|
15757
|
+
return await promise2;
|
|
15758
|
+
} finally {
|
|
15759
|
+
pending.delete(apiUrl);
|
|
15760
|
+
}
|
|
15761
|
+
}
|
|
15762
|
+
async function ensureAnonymousCredential(apiUrl) {
|
|
15763
|
+
const root = join8(homedir2(), ".rill", "auth-locks");
|
|
15764
|
+
await mkdir5(root, { recursive: true, mode: 448 });
|
|
15765
|
+
const lock = join8(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
15766
|
+
const deadline = Date.now() + 3e4;
|
|
15767
|
+
while (true) {
|
|
15768
|
+
try {
|
|
15769
|
+
await mkdir5(lock, { mode: 448 });
|
|
15770
|
+
break;
|
|
15771
|
+
} catch (error51) {
|
|
15772
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
15773
|
+
const info = await stat3(lock).catch(() => null);
|
|
15774
|
+
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
15775
|
+
await rm4(lock, { recursive: true, force: true });
|
|
15776
|
+
continue;
|
|
15777
|
+
}
|
|
15778
|
+
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
15779
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
15780
|
+
}
|
|
15781
|
+
}
|
|
15782
|
+
try {
|
|
15783
|
+
return await createAnonymousCredential(apiUrl);
|
|
15784
|
+
} finally {
|
|
15785
|
+
await rm4(lock, { recursive: true, force: true });
|
|
15786
|
+
}
|
|
15787
|
+
}
|
|
15788
|
+
async function createAnonymousCredential(apiUrl) {
|
|
15789
|
+
const existing = await loadAgentCredential(apiUrl);
|
|
15790
|
+
if (existing) return existing;
|
|
15791
|
+
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15792
|
+
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
15793
|
+
await save(apiUrl, initial);
|
|
15794
|
+
const registered = await request2(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15795
|
+
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
15796
|
+
await save(apiUrl, saved);
|
|
15797
|
+
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");
|
|
15798
|
+
return await loadAgentCredential(apiUrl);
|
|
15799
|
+
}
|
|
15800
|
+
async function connectAgent(apiUrl, email3) {
|
|
15801
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
15802
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15803
|
+
let claim;
|
|
15804
|
+
if (saved?.claimToken && saved.anonymous) {
|
|
15805
|
+
try {
|
|
15806
|
+
const result = await request2(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
15807
|
+
claim = result.claim_attempt;
|
|
15808
|
+
} catch (error51) {
|
|
15809
|
+
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
15810
|
+
}
|
|
15811
|
+
}
|
|
15812
|
+
if (!claim) {
|
|
15813
|
+
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
15814
|
+
const result = await request2(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15815
|
+
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
15816
|
+
await save(apiUrl, saved);
|
|
15817
|
+
claim = result.claim;
|
|
15818
|
+
}
|
|
15819
|
+
process.stderr.write(`Open ${claim.verification_uri}
|
|
15820
|
+
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
15821
|
+
`);
|
|
15822
|
+
const deadline = Date.now() + claim.expires_in * 1e3;
|
|
15823
|
+
let interval = claim.interval;
|
|
15824
|
+
while (Date.now() < deadline) {
|
|
15825
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
15826
|
+
try {
|
|
15827
|
+
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
15828
|
+
const connected = withTokens({ version: 1, anonymous: false }, tokens);
|
|
15829
|
+
await save(apiUrl, connected);
|
|
15830
|
+
return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
|
|
15831
|
+
} catch (error51) {
|
|
15832
|
+
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.");
|
|
15833
|
+
if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
|
|
15834
|
+
if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
|
|
15835
|
+
interval += 5;
|
|
15836
|
+
continue;
|
|
15837
|
+
}
|
|
15838
|
+
throw error51;
|
|
15839
|
+
}
|
|
15840
|
+
}
|
|
15841
|
+
throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
|
|
15842
|
+
}
|
|
15843
|
+
|
|
15692
15844
|
// src/keychain.ts
|
|
15693
15845
|
import { spawn as spawn4 } from "child_process";
|
|
15694
15846
|
var service = "rill";
|
|
@@ -15703,15 +15855,20 @@ async function saveCredential(apiUrl, token) {
|
|
|
15703
15855
|
}
|
|
15704
15856
|
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
15705
15857
|
}
|
|
15706
|
-
|
|
15707
|
-
|
|
15858
|
+
function loadCredential(apiUrl) {
|
|
15859
|
+
return loadAgentCredential(apiUrl);
|
|
15860
|
+
}
|
|
15861
|
+
async function loadStoredCredential(apiUrl) {
|
|
15708
15862
|
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
|
-
|
|
15863
|
+
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
15864
|
+
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
15865
|
+
} catch (error51) {
|
|
15866
|
+
const code = error51.exitCode;
|
|
15867
|
+
if (process.platform === "darwin" && code === 44) return null;
|
|
15868
|
+
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
15869
|
+
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
15713
15870
|
}
|
|
15714
|
-
|
|
15871
|
+
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
15715
15872
|
}
|
|
15716
15873
|
function run(command, args, input) {
|
|
15717
15874
|
return new Promise((resolve, reject) => {
|
|
@@ -15721,7 +15878,7 @@ function run(command, args, input) {
|
|
|
15721
15878
|
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
15722
15879
|
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
15723
15880
|
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()
|
|
15881
|
+
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 })));
|
|
15725
15882
|
if (input) child.stdin.end(input);
|
|
15726
15883
|
else child.stdin.end();
|
|
15727
15884
|
});
|
|
@@ -15855,7 +16012,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
15855
16012
|
} else {
|
|
15856
16013
|
const token = await dependencies.loadCredential(apiUrl);
|
|
15857
16014
|
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.` });
|
|
16015
|
+
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
16016
|
} else {
|
|
15860
16017
|
try {
|
|
15861
16018
|
const session = await dependencies.agentSession(apiUrl, token);
|
|
@@ -15873,10 +16030,10 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
15873
16030
|
checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
|
|
15874
16031
|
try {
|
|
15875
16032
|
const token = await dependencies.loadCredential(apiUrl);
|
|
15876
|
-
if (!token
|
|
15877
|
-
checks.push({ name: "encoding", status: "pass", message: "Remote encoding is enabled." });
|
|
16033
|
+
if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
|
|
16034
|
+
checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
|
|
15878
16035
|
} catch (error51) {
|
|
15879
|
-
checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "
|
|
16036
|
+
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
16037
|
}
|
|
15881
16038
|
}
|
|
15882
16039
|
return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
@@ -15990,10 +16147,10 @@ function controlPlaneUrl(environmentUrl) {
|
|
|
15990
16147
|
async function removeLocalCapture(id) {
|
|
15991
16148
|
await ensureDaemon(process.argv[1]);
|
|
15992
16149
|
const local = await recordingStatus(id).catch(() => null);
|
|
15993
|
-
if (local?.result?.frameDirectory) await
|
|
16150
|
+
if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
15994
16151
|
}
|
|
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
|
|
16152
|
+
function watchEncoding(apiUrl, id) {
|
|
16153
|
+
const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env } });
|
|
15997
16154
|
child.on("error", () => void 0);
|
|
15998
16155
|
child.unref();
|
|
15999
16156
|
}
|
|
@@ -16009,8 +16166,9 @@ program.command("doctor").description("Validate Rill prerequisites before record
|
|
|
16009
16166
|
});
|
|
16010
16167
|
async function authorize(quiet = false) {
|
|
16011
16168
|
const options = program.opts();
|
|
16169
|
+
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
16170
|
const client = new ControlPlaneClient(options.apiUrl, null);
|
|
16013
|
-
const authorization = await client.createDeviceAuthorization({ deviceName: `${
|
|
16171
|
+
const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname4()} \xB7 ${process.platform}` });
|
|
16014
16172
|
process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
|
|
16015
16173
|
`);
|
|
16016
16174
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
@@ -16029,7 +16187,11 @@ async function authorize(quiet = false) {
|
|
|
16029
16187
|
}
|
|
16030
16188
|
throw new Error("Device authorization expired.");
|
|
16031
16189
|
}
|
|
16032
|
-
program.command("
|
|
16190
|
+
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) => {
|
|
16191
|
+
const options = program.opts();
|
|
16192
|
+
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
|
|
16193
|
+
});
|
|
16194
|
+
program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
|
|
16033
16195
|
await authorize();
|
|
16034
16196
|
});
|
|
16035
16197
|
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 +16221,15 @@ var record2 = program.command("record").description("Run a cloud browser from a
|
|
|
16059
16221
|
var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
|
|
16060
16222
|
for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
|
|
16061
16223
|
const options = program.opts();
|
|
16062
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16224
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16063
16225
|
const run2 = await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id));
|
|
16064
16226
|
if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
|
|
16065
16227
|
else process.stdout.write(formatCloudRun(run2));
|
|
16066
16228
|
});
|
|
16067
16229
|
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
16230
|
const options = program.opts();
|
|
16069
|
-
|
|
16070
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16231
|
+
await ensureAnonymousCredential(options.apiUrl);
|
|
16232
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16071
16233
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
16072
16234
|
if (!["local", "remote"].includes(commandOptions.encoding)) throw new Error("Encoding must be remote or local.");
|
|
16073
16235
|
if (commandOptions.encoding === "remote" && !(await client.encodingCapabilities()).enabled) throw new Error("Remote encoding is unavailable for this workspace. Use --encoding local.");
|
|
@@ -16079,7 +16241,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16079
16241
|
const local = await startRecordingWithCompensation(remote, {
|
|
16080
16242
|
startLocal: async () => {
|
|
16081
16243
|
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 });
|
|
16244
|
+
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
16245
|
},
|
|
16084
16246
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
16085
16247
|
});
|
|
@@ -16090,7 +16252,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16090
16252
|
headed: Boolean(commandOptions.headed),
|
|
16091
16253
|
attached: Boolean(commandOptions.cdpUrl)
|
|
16092
16254
|
}).catch(() => void 0);
|
|
16093
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) };
|
|
16255
|
+
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
16256
|
});
|
|
16095
16257
|
emit(result, options.pretty);
|
|
16096
16258
|
});
|
|
@@ -16099,8 +16261,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16099
16261
|
await ensureDaemon(process.argv[1]);
|
|
16100
16262
|
const local = await stopRecording(recordingId);
|
|
16101
16263
|
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);
|
|
16264
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16104
16265
|
const stopped = await client.markRecordingStopped(recordingId, {
|
|
16105
16266
|
durationSeconds: local.result.durationSeconds,
|
|
16106
16267
|
stoppedReason: local.result.stoppedReason,
|
|
@@ -16112,11 +16273,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16112
16273
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
16113
16274
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16114
16275
|
emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16115
|
-
if (result2.status === "ready") await
|
|
16116
|
-
else watchEncoding(options.apiUrl, recordingId
|
|
16276
|
+
if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16277
|
+
else watchEncoding(options.apiUrl, recordingId);
|
|
16117
16278
|
return;
|
|
16118
16279
|
}
|
|
16119
|
-
const video = await
|
|
16280
|
+
const video = await stat4(local.result.videoPath);
|
|
16120
16281
|
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
|
|
16121
16282
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
16122
16283
|
const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
@@ -16126,17 +16287,18 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16126
16287
|
recordingId,
|
|
16127
16288
|
status: remote ? "ready" : "processing",
|
|
16128
16289
|
shareUrl: remote?.shareUrl ?? null,
|
|
16290
|
+
expiresAt: remote?.expires_at ?? null,
|
|
16129
16291
|
durationSeconds: local.result.durationSeconds,
|
|
16130
16292
|
stoppedReason: local.result.stoppedReason,
|
|
16131
16293
|
diagnostics: local.result.diagnostics,
|
|
16132
16294
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
16133
16295
|
};
|
|
16134
16296
|
emit(result, options.pretty);
|
|
16135
|
-
if (remote) await
|
|
16297
|
+
if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16136
16298
|
});
|
|
16137
16299
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
16138
16300
|
const options = program.opts();
|
|
16139
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16301
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16140
16302
|
const current = await client.encodingStatus(recordingId).catch((error51) => {
|
|
16141
16303
|
if (error51 instanceof ControlPlaneError && error51.code === "recording_not_found") return null;
|
|
16142
16304
|
throw error51;
|
|
@@ -16154,17 +16316,17 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
|
|
|
16154
16316
|
const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16155
16317
|
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16156
16318
|
if (result.status === "ready") await removeLocalCapture(recordingId);
|
|
16157
|
-
else watchEncoding(options.apiUrl, recordingId
|
|
16319
|
+
else watchEncoding(options.apiUrl, recordingId);
|
|
16158
16320
|
});
|
|
16159
16321
|
program.command("encoding-watch", { hidden: true }).argument("<recording-id>").action(async (id) => {
|
|
16160
16322
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid recording ID");
|
|
16161
16323
|
const options = program.opts();
|
|
16162
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16324
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16163
16325
|
const result = await waitForEncoding(client, id, 864e5);
|
|
16164
16326
|
if (result.status === "ready") {
|
|
16165
16327
|
await ensureDaemon(process.argv[1]);
|
|
16166
16328
|
const local = await recordingStatus(id).catch(() => null);
|
|
16167
|
-
if (local?.result?.frameDirectory) await
|
|
16329
|
+
if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16168
16330
|
}
|
|
16169
16331
|
});
|
|
16170
16332
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
@@ -16174,7 +16336,7 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
|
|
|
16174
16336
|
if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
|
|
16175
16337
|
const options = program.opts();
|
|
16176
16338
|
const token = await loadCredential(options.apiUrl);
|
|
16177
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16339
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16178
16340
|
await client.submitRecordingFeedback(recordingId ?? null, {
|
|
16179
16341
|
outcome: commandOptions.outcome,
|
|
16180
16342
|
whatHelped: commandOptions.helped,
|
|
@@ -16183,10 +16345,14 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
|
|
|
16183
16345
|
});
|
|
16184
16346
|
emit(feedbackSubmissionReceipt(recordingId ?? null), options.pretty);
|
|
16185
16347
|
});
|
|
16348
|
+
record2.command("list").description("List recordings visible to this connection.").action(async () => {
|
|
16349
|
+
const options = program.opts();
|
|
16350
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).listRecordings(), options.pretty);
|
|
16351
|
+
});
|
|
16186
16352
|
record2.command("status").argument("<recording-id>").action(async (recordingId) => {
|
|
16187
16353
|
const options = program.opts();
|
|
16188
16354
|
const token = await loadCredential(options.apiUrl);
|
|
16189
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16355
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16190
16356
|
try {
|
|
16191
16357
|
const encoding = await client.encodingStatus(recordingId);
|
|
16192
16358
|
const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
|
|
@@ -16208,24 +16374,24 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16208
16374
|
record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
16209
16375
|
const options = program.opts();
|
|
16210
16376
|
const token = await loadCredential(options.apiUrl);
|
|
16211
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16377
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16212
16378
|
emit(await client.cancelRecording(recordingId), options.pretty);
|
|
16213
16379
|
});
|
|
16214
16380
|
program.command("upload").argument("<video-file>").option("--title <title>").option("--no-wait").action(async (videoFile, commandOptions) => {
|
|
16215
16381
|
const options = program.opts();
|
|
16216
|
-
|
|
16217
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16218
|
-
const file2 = await
|
|
16382
|
+
await ensureAnonymousCredential(options.apiUrl);
|
|
16383
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16384
|
+
const file2 = await stat4(videoFile);
|
|
16219
16385
|
const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
|
|
16220
16386
|
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
|
|
16221
16387
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
16222
16388
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
|
|
16223
16389
|
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);
|
|
16390
|
+
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
16391
|
});
|
|
16226
16392
|
program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
|
|
16227
16393
|
const options = program.opts();
|
|
16228
|
-
emit(await new ControlPlaneClient(options.apiUrl,
|
|
16394
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
|
|
16229
16395
|
});
|
|
16230
16396
|
var drafts = program.command("drafts");
|
|
16231
16397
|
drafts.command("list").action(async () => {
|
|
@@ -16241,9 +16407,9 @@ drafts.command("purge").action(async () => {
|
|
|
16241
16407
|
let purged = 0;
|
|
16242
16408
|
for (const name of names) {
|
|
16243
16409
|
const path = `${root}/${name}`;
|
|
16244
|
-
const metadata = await
|
|
16410
|
+
const metadata = await stat4(path);
|
|
16245
16411
|
if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
|
|
16246
|
-
await
|
|
16412
|
+
await rm5(path, { recursive: true, force: true });
|
|
16247
16413
|
purged += 1;
|
|
16248
16414
|
}
|
|
16249
16415
|
}
|
|
@@ -16255,7 +16421,7 @@ program.hook("preAction", async (_command, actionCommand) => {
|
|
|
16255
16421
|
const options = program.opts();
|
|
16256
16422
|
const token = await loadCredential(options.apiUrl).catch(() => null);
|
|
16257
16423
|
if (!token) return;
|
|
16258
|
-
const client = new ControlPlaneClient(options.apiUrl,
|
|
16424
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16259
16425
|
await startCliTelemetry(client, command);
|
|
16260
16426
|
});
|
|
16261
16427
|
async function startCliTelemetry(client, command) {
|