@runalabs/rill-cli 0.1.1 → 0.1.2
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 +3 -4
- package/dist/cli.js +109 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,20 +5,19 @@ Record, inspect, and share agent-controlled browser runs with Rill.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npm install --global @runalabs/rill-cli@0.1.
|
|
8
|
+
npm install --global @runalabs/rill-cli@0.1.2
|
|
9
9
|
rill doctor
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Rill requires Node.js 22 or newer, Google Chrome or Chromium, and `ffmpeg`.
|
|
13
|
-
The CLI
|
|
14
|
-
pass `--api-url` to target another environment.
|
|
13
|
+
The CLI connects to `https://userill.dev` by default.
|
|
15
14
|
|
|
16
15
|
## Record
|
|
17
16
|
|
|
18
17
|
```sh
|
|
19
18
|
rill login
|
|
20
19
|
rill doctor
|
|
21
|
-
rill record start --url https://
|
|
20
|
+
rill record start --url https://example.com --title "Agent reproduction"
|
|
22
21
|
# Connect browser automation to the returned cdpUrl.
|
|
23
22
|
rill record stop <recording-id>
|
|
24
23
|
```
|
package/dist/cli.js
CHANGED
|
@@ -603,9 +603,24 @@ var ControlPlaneClient = class {
|
|
|
603
603
|
agentSession() {
|
|
604
604
|
return this.request("/api/agent/session");
|
|
605
605
|
}
|
|
606
|
+
reportCliCommand(input) {
|
|
607
|
+
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
608
|
+
}
|
|
606
609
|
createRecording(input) {
|
|
607
610
|
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
608
611
|
}
|
|
612
|
+
markRecordingStarted(recordingId, input) {
|
|
613
|
+
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
614
|
+
method: "POST",
|
|
615
|
+
body: JSON.stringify(input)
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
markRecordingStopped(recordingId, input) {
|
|
619
|
+
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
620
|
+
method: "POST",
|
|
621
|
+
body: JSON.stringify(input)
|
|
622
|
+
});
|
|
623
|
+
}
|
|
609
624
|
provisionVideo(recordingId, input) {
|
|
610
625
|
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
611
626
|
}
|
|
@@ -690,6 +705,33 @@ var ControlPlaneClient = class {
|
|
|
690
705
|
}
|
|
691
706
|
};
|
|
692
707
|
|
|
708
|
+
// src/analytics.ts
|
|
709
|
+
var commandCategories = /* @__PURE__ */ new Map([
|
|
710
|
+
["doctor", "doctor"],
|
|
711
|
+
["record start", "record_start"],
|
|
712
|
+
["record stop", "record_stop"],
|
|
713
|
+
["record status", "record_status"],
|
|
714
|
+
["record cancel", "record_cancel"],
|
|
715
|
+
["upload", "upload"],
|
|
716
|
+
["drafts list", "drafts_list"],
|
|
717
|
+
["drafts purge", "drafts_purge"]
|
|
718
|
+
]);
|
|
719
|
+
function cliCommandCategory(commandPath2) {
|
|
720
|
+
return commandCategories.get(commandPath2.join(" ")) ?? null;
|
|
721
|
+
}
|
|
722
|
+
function cliDurationBucket(milliseconds) {
|
|
723
|
+
if (milliseconds < 6e4) return "under_1m";
|
|
724
|
+
if (milliseconds < 5 * 6e4) return "1m_to_5m";
|
|
725
|
+
if (milliseconds < 15 * 6e4) return "5m_to_15m";
|
|
726
|
+
return "15m_plus";
|
|
727
|
+
}
|
|
728
|
+
function cliFailureCode(error) {
|
|
729
|
+
const directCode = typeof error === "object" && error !== null && "code" in error ? error.code : null;
|
|
730
|
+
if (typeof directCode === "string" && /^[a-z0-9_]{1,80}$/.test(directCode)) return directCode;
|
|
731
|
+
const message = error instanceof Error ? error.message : "";
|
|
732
|
+
return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
|
|
733
|
+
}
|
|
734
|
+
|
|
693
735
|
// src/keychain.ts
|
|
694
736
|
import { spawn as spawn4 } from "child_process";
|
|
695
737
|
var service = "rill";
|
|
@@ -785,8 +827,10 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
785
827
|
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." });
|
|
786
828
|
} else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
|
|
787
829
|
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}\`.` });
|
|
788
|
-
} else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion)
|
|
830
|
+
} else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) < 0) {
|
|
789
831
|
checks.push({ name: "cli_version", status: "warn", message: `CLI ${dependencies.cliVersion} is supported; ${health.recommendedCliVersion} is recommended.`, version: dependencies.cliVersion, recovery: `Run \`npm install --global @runalabs/rill-cli@${health.recommendedCliVersion}\`.` });
|
|
832
|
+
} else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) > 0) {
|
|
833
|
+
checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is supported.`, version: dependencies.cliVersion });
|
|
790
834
|
} else {
|
|
791
835
|
checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is the recommended version.`, version: dependencies.cliVersion });
|
|
792
836
|
}
|
|
@@ -889,9 +933,16 @@ async function remoteAwareRecordingStatus(recordingId, dependencies) {
|
|
|
889
933
|
}
|
|
890
934
|
}
|
|
891
935
|
|
|
936
|
+
// src/config.ts
|
|
937
|
+
var PRODUCTION_API_URL = "https://userill.dev";
|
|
938
|
+
function controlPlaneUrl(environmentUrl) {
|
|
939
|
+
return environmentUrl ?? PRODUCTION_API_URL;
|
|
940
|
+
}
|
|
941
|
+
|
|
892
942
|
// src/cli.ts
|
|
893
943
|
var program = new Command();
|
|
894
|
-
program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", process.env.RILL_API_URL
|
|
944
|
+
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);
|
|
945
|
+
var activeTelemetry = null;
|
|
895
946
|
program.command("daemon", { hidden: true }).action(() => runDaemon());
|
|
896
947
|
program.command("doctor").description("Validate Rill prerequisites before recording.").action(async () => {
|
|
897
948
|
const options = program.opts();
|
|
@@ -932,6 +983,13 @@ record.command("start").option("--url <url>").option("--title <title>").option("
|
|
|
932
983
|
},
|
|
933
984
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
934
985
|
});
|
|
986
|
+
await client.markRecordingStarted(remote.recordingId, {
|
|
987
|
+
cliVersion: CLI_VERSION,
|
|
988
|
+
os: process.platform,
|
|
989
|
+
arch: process.arch,
|
|
990
|
+
headed: Boolean(commandOptions.headed),
|
|
991
|
+
attached: Boolean(commandOptions.cdpUrl)
|
|
992
|
+
}).catch(() => void 0);
|
|
935
993
|
emit({ schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) }, options.pretty);
|
|
936
994
|
});
|
|
937
995
|
record.command("stop").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
@@ -941,8 +999,13 @@ record.command("stop").argument("<recording-id>").option("--no-wait").action(asy
|
|
|
941
999
|
if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
|
|
942
1000
|
const token = await loadCredential(options.apiUrl);
|
|
943
1001
|
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
1002
|
+
await client.markRecordingStopped(recordingId, {
|
|
1003
|
+
durationSeconds: local.result.durationSeconds,
|
|
1004
|
+
stoppedReason: local.result.stoppedReason,
|
|
1005
|
+
diagnosticsAvailable: true
|
|
1006
|
+
}).catch(() => void 0);
|
|
944
1007
|
const video = await stat2(local.result.videoPath);
|
|
945
|
-
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath) });
|
|
1008
|
+
const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
|
|
946
1009
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
947
1010
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
948
1011
|
if (remote) process.stderr.write("\n");
|
|
@@ -975,7 +1038,7 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
|
|
|
975
1038
|
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
976
1039
|
const file = await stat2(videoFile);
|
|
977
1040
|
const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
|
|
978
|
-
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file.size, filename: basename(videoFile) });
|
|
1041
|
+
const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file.size, filename: basename(videoFile), uploadKind: "existing_video" });
|
|
979
1042
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
980
1043
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
|
|
981
1044
|
if (remote) process.stderr.write("\n");
|
|
@@ -1007,4 +1070,45 @@ drafts.command("purge").action(async () => {
|
|
|
1007
1070
|
}
|
|
1008
1071
|
emit({ schemaVersion: 1, purged }, options.pretty);
|
|
1009
1072
|
});
|
|
1010
|
-
program.
|
|
1073
|
+
program.hook("preAction", async (_command, actionCommand) => {
|
|
1074
|
+
const command = cliCommandCategory(commandPath(actionCommand));
|
|
1075
|
+
if (!command) return;
|
|
1076
|
+
const options = program.opts();
|
|
1077
|
+
const token = await loadCredential(options.apiUrl).catch(() => null);
|
|
1078
|
+
if (!token) return;
|
|
1079
|
+
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
1080
|
+
const session = await client.agentSession().catch(() => null);
|
|
1081
|
+
if (!session?.analyticsEnabled) return;
|
|
1082
|
+
activeTelemetry = { client, command, startedAt: Date.now() };
|
|
1083
|
+
await client.reportCliCommand({ phase: "started", command, cliVersion: CLI_VERSION, os: process.platform, arch: process.arch }).catch(() => void 0);
|
|
1084
|
+
});
|
|
1085
|
+
program.hook("postAction", async () => {
|
|
1086
|
+
await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
|
|
1087
|
+
});
|
|
1088
|
+
program.parseAsync().catch(async (error) => {
|
|
1089
|
+
await finishCliTelemetry(error);
|
|
1090
|
+
fail(error);
|
|
1091
|
+
});
|
|
1092
|
+
function commandPath(actionCommand) {
|
|
1093
|
+
const path = [];
|
|
1094
|
+
let command = actionCommand;
|
|
1095
|
+
while (command && command !== program) {
|
|
1096
|
+
path.unshift(command.name());
|
|
1097
|
+
command = command.parent;
|
|
1098
|
+
}
|
|
1099
|
+
return path;
|
|
1100
|
+
}
|
|
1101
|
+
async function finishCliTelemetry(error) {
|
|
1102
|
+
const telemetry = activeTelemetry;
|
|
1103
|
+
activeTelemetry = null;
|
|
1104
|
+
if (!telemetry) return;
|
|
1105
|
+
await telemetry.client.reportCliCommand({
|
|
1106
|
+
phase: error ? "failed" : "completed",
|
|
1107
|
+
command: telemetry.command,
|
|
1108
|
+
cliVersion: CLI_VERSION,
|
|
1109
|
+
os: process.platform,
|
|
1110
|
+
arch: process.arch,
|
|
1111
|
+
durationBucket: cliDurationBucket(Date.now() - telemetry.startedAt),
|
|
1112
|
+
...error ? { failureCode: cliFailureCode(error) } : {}
|
|
1113
|
+
}).catch(() => void 0);
|
|
1114
|
+
}
|