@runalabs/rill-cli 0.1.6 → 0.1.7
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 +17 -0
- package/dist/cli.js +97 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,23 @@ Rill starts a local Chromium recorder. Your browser tool drives the returned CDP
|
|
|
6
6
|
|
|
7
7
|
[Getting started](https://userill.dev/docs) · [Website CLI reference](https://userill.dev/docs/cli) · [Agent instructions](https://userill.dev/docs/cli#agents)
|
|
8
8
|
|
|
9
|
+
## Cloud agent runs
|
|
10
|
+
|
|
11
|
+
Invited workspaces can supply a URL and prompt. Cloud runs need Node.js 22+ and a Rill login; Chrome and ffmpeg run on Rill's server.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
rill record https://example.com -p "Open pricing and describe the plans."
|
|
15
|
+
rill record https://example.com/dashboard -p "Describe the dashboard." --account staging
|
|
16
|
+
rill run status <run-id>
|
|
17
|
+
rill run cancel <run-id>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The first run opens Rill authorization if needed. Save optional website test accounts in Rill Settings using an account name, exact HTTPS origin, username, password, and optional login URL. Login appears in the private video, with masked password inputs; login diagnostics are omitted. MFA, CAPTCHA, external SSO, and unsupported login forms return a blocker.
|
|
21
|
+
|
|
22
|
+
Cloud recordings are private by default. Closing the terminal disconnects without cancelling. Use `--no-wait` to return after submission, and `--id <same-run-id>` with identical arguments to retry an uncertain submission. Cloud runs use run credits: Free includes 3 per calendar month and Pro includes 20. Buy additional credits from Billing.
|
|
23
|
+
|
|
24
|
+
The following local-recording instructions remain available through `record start` and `record stop`.
|
|
25
|
+
|
|
9
26
|
## Install and verify
|
|
10
27
|
|
|
11
28
|
Browser recording runs on macOS and Linux. Install Node.js 22+, Google Chrome or Chromium, and ffmpeg on `PATH`.
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
+
import { spawn as spawn6 } from "child_process";
|
|
9
10
|
import { Command } from "commander";
|
|
10
11
|
import { hostname as hostname3 } from "os";
|
|
11
12
|
import { basename, dirname as dirname2 } from "path";
|
|
@@ -14631,6 +14632,11 @@ var recordingSchema = external_exports.object({
|
|
|
14631
14632
|
truncated: external_exports.boolean()
|
|
14632
14633
|
})
|
|
14633
14634
|
});
|
|
14635
|
+
var agentOutcomeSchema = external_exports.object({
|
|
14636
|
+
outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
|
|
14637
|
+
summary: external_exports.string().min(1).max(1e3),
|
|
14638
|
+
evidence: external_exports.string().max(1e3)
|
|
14639
|
+
}).strict();
|
|
14634
14640
|
|
|
14635
14641
|
// ../recorder/src/cursor-overlay.ts
|
|
14636
14642
|
function installCursorOverlay(options) {
|
|
@@ -14917,6 +14923,12 @@ var RecordingSession = class _RecordingSession {
|
|
|
14917
14923
|
stopPromise;
|
|
14918
14924
|
maxTimer;
|
|
14919
14925
|
removeCursorOverlay;
|
|
14926
|
+
diagnosticsPaused = false;
|
|
14927
|
+
secretValues = [];
|
|
14928
|
+
setDiagnosticPrivacy(paused, secrets) {
|
|
14929
|
+
this.diagnosticsPaused = paused;
|
|
14930
|
+
if (secrets) this.secretValues = secrets.filter(Boolean);
|
|
14931
|
+
}
|
|
14920
14932
|
bodyBytes = 0;
|
|
14921
14933
|
truncated = false;
|
|
14922
14934
|
constructor(options, runtime) {
|
|
@@ -14934,7 +14946,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
14934
14946
|
const width = options.width ?? 1280;
|
|
14935
14947
|
const height = options.height ?? 720;
|
|
14936
14948
|
await mkdir2(options.outputDirectory, { recursive: true, mode: 448 });
|
|
14937
|
-
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory });
|
|
14949
|
+
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
|
|
14938
14950
|
const cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
14939
14951
|
const screencast = new ScreencastRecorder(cdpSession, join3(options.outputDirectory, "frames"), width, height);
|
|
14940
14952
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
@@ -14952,6 +14964,11 @@ var RecordingSession = class _RecordingSession {
|
|
|
14952
14964
|
}, (options.maxDurationSeconds ?? 600) * 1e3);
|
|
14953
14965
|
return session;
|
|
14954
14966
|
}
|
|
14967
|
+
// In-process agents share this connection so exposed diagnostic bindings are
|
|
14968
|
+
// not dispatched through a second Playwright client.
|
|
14969
|
+
get recordedPage() {
|
|
14970
|
+
return this.page;
|
|
14971
|
+
}
|
|
14955
14972
|
status() {
|
|
14956
14973
|
return { recordingId: this.recordingId, cdpUrl: this.cdpUrl, startedAt: this.startedAt.toISOString(), status: this.stopPromise ? "finalizing" : "recording" };
|
|
14957
14974
|
}
|
|
@@ -14979,13 +14996,14 @@ var RecordingSession = class _RecordingSession {
|
|
|
14979
14996
|
timeline: this.timelineEvents.toSorted((left, right) => left.offsetMs - right.offsetMs)
|
|
14980
14997
|
};
|
|
14981
14998
|
try {
|
|
14982
|
-
|
|
14999
|
+
const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
|
|
15000
|
+
await writeBundle(safeBundle, diagnosticsPath);
|
|
14983
15001
|
await this.screencast.stop(videoPath, durationSeconds);
|
|
14984
15002
|
} finally {
|
|
14985
15003
|
await this.removeCursorOverlay?.();
|
|
14986
15004
|
if (this.browserProcess) {
|
|
14987
15005
|
await this.browser.close().catch(() => void 0);
|
|
14988
|
-
this.browserProcess
|
|
15006
|
+
await terminateBrowser(this.browserProcess);
|
|
14989
15007
|
if (this.profileDirectory) await rm(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
14990
15008
|
}
|
|
14991
15009
|
}
|
|
@@ -15000,6 +15018,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15000
15018
|
this.captureNetworkBodies = captureNetworkBodies;
|
|
15001
15019
|
const offset = () => Date.now() - this.startedAt.getTime();
|
|
15002
15020
|
const pushTimeline = (event) => {
|
|
15021
|
+
if (this.diagnosticsPaused) return;
|
|
15003
15022
|
if (this.timelineEvents.length >= 1e4) {
|
|
15004
15023
|
this.truncated = true;
|
|
15005
15024
|
return;
|
|
@@ -15018,6 +15037,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15018
15037
|
if (frame === this.page.mainFrame()) pushTimeline({ offsetMs: offset(), type: "navigation", label: sanitizeUrl(frame.url()) });
|
|
15019
15038
|
});
|
|
15020
15039
|
this.page.on("console", async (message) => {
|
|
15040
|
+
if (this.diagnosticsPaused) return;
|
|
15021
15041
|
if (this.consoleEvents.length >= 1e4) {
|
|
15022
15042
|
this.truncated = true;
|
|
15023
15043
|
return;
|
|
@@ -15036,11 +15056,13 @@ var RecordingSession = class _RecordingSession {
|
|
|
15036
15056
|
if (severity === "error" || severity === "warning") pushTimeline({ offsetMs: event.offsetMs, type: "console", label: event.text.slice(0, 160), severity });
|
|
15037
15057
|
});
|
|
15038
15058
|
this.page.on("pageerror", (error51) => {
|
|
15059
|
+
if (this.diagnosticsPaused) return;
|
|
15039
15060
|
const event = { id: `con_${this.consoleEvents.length + 1}`, offsetMs: offset(), severity: "error", text: redactText(error51.message), stack: redactText(error51.stack ?? "").split("\n").slice(0, 20) };
|
|
15040
15061
|
this.consoleEvents.push(event);
|
|
15041
15062
|
pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
|
|
15042
15063
|
});
|
|
15043
15064
|
this.page.on("request", (request2) => {
|
|
15065
|
+
if (this.diagnosticsPaused) return;
|
|
15044
15066
|
if (this.networkEvents.length >= 5e3) {
|
|
15045
15067
|
this.truncated = true;
|
|
15046
15068
|
return;
|
|
@@ -15112,17 +15134,25 @@ async function launchManaged(options) {
|
|
|
15112
15134
|
"--disable-component-update",
|
|
15113
15135
|
"--disable-sync",
|
|
15114
15136
|
"--metrics-recording-only",
|
|
15137
|
+
...options.proxyServer ? [`--proxy-server=${options.proxyServer}`, "--proxy-bypass-list=<-loopback>", "--disable-quic", "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [],
|
|
15115
15138
|
...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
|
|
15116
15139
|
"about:blank"
|
|
15117
15140
|
];
|
|
15118
|
-
const
|
|
15141
|
+
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;
|
|
15142
|
+
const browserProcess = spawn3(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
|
|
15119
15143
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15144
|
+
try {
|
|
15145
|
+
await waitForCdp(cdpUrl, browserProcess);
|
|
15146
|
+
const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
|
|
15147
|
+
const context = browser.contexts()[0];
|
|
15148
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
15149
|
+
await page.setViewportSize({ width: options.width, height: options.height });
|
|
15150
|
+
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15151
|
+
} catch (error51) {
|
|
15152
|
+
await terminateBrowser(browserProcess);
|
|
15153
|
+
await rm(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15154
|
+
throw error51;
|
|
15155
|
+
}
|
|
15126
15156
|
}
|
|
15127
15157
|
async function connectAttached(cdpUrl, outputDirectory) {
|
|
15128
15158
|
const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
|
|
@@ -15140,6 +15170,17 @@ async function getAvailablePort() {
|
|
|
15140
15170
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
15141
15171
|
return port;
|
|
15142
15172
|
}
|
|
15173
|
+
async function terminateBrowser(child) {
|
|
15174
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
15175
|
+
await new Promise((resolve) => {
|
|
15176
|
+
const timeout = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
15177
|
+
child.once("exit", () => {
|
|
15178
|
+
clearTimeout(timeout);
|
|
15179
|
+
resolve();
|
|
15180
|
+
});
|
|
15181
|
+
child.kill("SIGTERM");
|
|
15182
|
+
});
|
|
15183
|
+
}
|
|
15143
15184
|
async function waitForCdp(cdpUrl, browserProcess) {
|
|
15144
15185
|
const deadline = Date.now() + 15e3;
|
|
15145
15186
|
while (Date.now() < deadline) {
|
|
@@ -15281,6 +15322,15 @@ var ControlPlaneClient = class {
|
|
|
15281
15322
|
this.apiUrl = apiUrl;
|
|
15282
15323
|
this.token = token;
|
|
15283
15324
|
}
|
|
15325
|
+
createCloudRun(input) {
|
|
15326
|
+
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
15327
|
+
}
|
|
15328
|
+
cloudRun(id) {
|
|
15329
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
15330
|
+
}
|
|
15331
|
+
cancelCloudRun(id) {
|
|
15332
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
15333
|
+
}
|
|
15284
15334
|
health() {
|
|
15285
15335
|
return this.request("/api/health", {}, false);
|
|
15286
15336
|
}
|
|
@@ -15698,12 +15748,16 @@ program.command("doctor").description("Validate Rill prerequisites before record
|
|
|
15698
15748
|
emit(result, options.pretty);
|
|
15699
15749
|
if (result.status === "error") process.exitCode = 1;
|
|
15700
15750
|
});
|
|
15701
|
-
|
|
15751
|
+
async function authorize() {
|
|
15702
15752
|
const options = program.opts();
|
|
15703
15753
|
const client = new ControlPlaneClient(options.apiUrl, null);
|
|
15704
15754
|
const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname3()} \xB7 ${process.platform}` });
|
|
15705
15755
|
process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
|
|
15706
15756
|
`);
|
|
15757
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
15758
|
+
const child = spawn6(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
|
|
15759
|
+
child.on("error", () => void 0);
|
|
15760
|
+
child.unref();
|
|
15707
15761
|
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
15708
15762
|
while (Date.now() < deadline) {
|
|
15709
15763
|
await new Promise((resolve) => setTimeout(resolve, authorization.interval * 1e3));
|
|
@@ -15712,11 +15766,41 @@ program.command("login").description("Authorize this agent using a human-approve
|
|
|
15712
15766
|
if (result.status !== "approved" || !result.token) throw new Error(`Device authorization ${result.status}.`);
|
|
15713
15767
|
await saveCredential(options.apiUrl, result.token);
|
|
15714
15768
|
emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
|
|
15715
|
-
return;
|
|
15769
|
+
return result.token;
|
|
15716
15770
|
}
|
|
15717
15771
|
throw new Error("Device authorization expired.");
|
|
15772
|
+
}
|
|
15773
|
+
program.command("login").description("Authorize Rill once.").action(async () => {
|
|
15774
|
+
await authorize();
|
|
15775
|
+
});
|
|
15776
|
+
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) => {
|
|
15777
|
+
if (!url2 || !commandOptions.prompt) throw new Error("Use rill record <url> -p <prompt>.");
|
|
15778
|
+
const options = program.opts();
|
|
15779
|
+
const token = await loadCredential(options.apiUrl) ?? await authorize();
|
|
15780
|
+
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
15781
|
+
const id = commandOptions.id ?? crypto.randomUUID();
|
|
15782
|
+
process.stderr.write(`Run ID: ${id}
|
|
15783
|
+
`);
|
|
15784
|
+
let run2 = await client.createCloudRun({ id, url: url2, prompt: commandOptions.prompt, expectation: commandOptions.expectation, account: commandOptions.account });
|
|
15785
|
+
let last = "";
|
|
15786
|
+
while (commandOptions.wait !== false && !["completed", "failed", "cancelled"].includes(run2.status)) {
|
|
15787
|
+
if (last !== run2.status) {
|
|
15788
|
+
last = run2.status;
|
|
15789
|
+
process.stderr.write(`${last}
|
|
15790
|
+
`);
|
|
15791
|
+
}
|
|
15792
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
15793
|
+
run2 = await client.cloudRun(id);
|
|
15794
|
+
}
|
|
15795
|
+
emit(run2, options.pretty);
|
|
15796
|
+
if (run2.status === "failed") process.exitCode = 1;
|
|
15797
|
+
});
|
|
15798
|
+
var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
|
|
15799
|
+
for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
|
|
15800
|
+
const options = program.opts();
|
|
15801
|
+
const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
|
|
15802
|
+
emit(await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id)), options.pretty);
|
|
15718
15803
|
});
|
|
15719
|
-
var record2 = program.command("record").description("Manage a browser recording session.");
|
|
15720
15804
|
record2.command("start").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) => {
|
|
15721
15805
|
const options = program.opts();
|
|
15722
15806
|
const token = await loadCredential(options.apiUrl);
|