@runalabs/rill-cli 0.1.0 → 0.1.1
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 +6 -1
- package/dist/cli.js +78 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ 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.1
|
|
9
9
|
rill doctor
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -23,5 +23,10 @@ rill record start --url https://staging.example.com --title "Agent reproduction"
|
|
|
23
23
|
rill record stop <recording-id>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
If the local recorder is unavailable, `rill record status <recording-id>` falls
|
|
27
|
+
back to the control plane. Use `rill record cancel <recording-id>` to abandon an
|
|
28
|
+
active recording and release its quota reservation. A failed local startup
|
|
29
|
+
automatically cancels the reservation it just created.
|
|
30
|
+
|
|
26
31
|
Commands emit one schema-versioned JSON object to stdout. Human-readable
|
|
27
32
|
progress is written to stderr.
|
package/dist/cli.js
CHANGED
|
@@ -583,6 +583,15 @@ function json(response, status, value) {
|
|
|
583
583
|
import { createReadStream } from "fs";
|
|
584
584
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
585
585
|
import { Upload } from "tus-js-client";
|
|
586
|
+
var ControlPlaneError = class extends Error {
|
|
587
|
+
constructor(code, message, recovery, details) {
|
|
588
|
+
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
589
|
+
this.code = code;
|
|
590
|
+
this.recovery = recovery;
|
|
591
|
+
this.details = details;
|
|
592
|
+
this.name = "ControlPlaneError";
|
|
593
|
+
}
|
|
594
|
+
};
|
|
586
595
|
var ControlPlaneClient = class {
|
|
587
596
|
constructor(apiUrl, token) {
|
|
588
597
|
this.apiUrl = apiUrl;
|
|
@@ -603,6 +612,12 @@ var ControlPlaneClient = class {
|
|
|
603
612
|
recordingStatus(recordingId) {
|
|
604
613
|
return this.request(`/api/recordings/${recordingId}`);
|
|
605
614
|
}
|
|
615
|
+
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
616
|
+
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
617
|
+
method: "POST",
|
|
618
|
+
body: JSON.stringify({ reason })
|
|
619
|
+
});
|
|
620
|
+
}
|
|
606
621
|
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
607
622
|
const deadline = Date.now() + timeoutMs;
|
|
608
623
|
let intervalMs = 1e3;
|
|
@@ -667,7 +682,10 @@ var ControlPlaneClient = class {
|
|
|
667
682
|
}
|
|
668
683
|
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
669
684
|
const value = await response.json().catch(() => null);
|
|
670
|
-
if (!response.ok
|
|
685
|
+
if (!response.ok && value?.error?.code && value.error.message) {
|
|
686
|
+
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details);
|
|
687
|
+
}
|
|
688
|
+
if (!response.ok) throw new Error(`Request failed (${response.status}).`);
|
|
671
689
|
return value;
|
|
672
690
|
}
|
|
673
691
|
};
|
|
@@ -715,11 +733,16 @@ function emit(value, pretty = false) {
|
|
|
715
733
|
process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : void 0)}
|
|
716
734
|
`);
|
|
717
735
|
}
|
|
718
|
-
function
|
|
736
|
+
function failureValue(error) {
|
|
719
737
|
const message = error instanceof Error ? error.message : String(error);
|
|
720
738
|
const [candidate] = message.split(":", 1);
|
|
721
739
|
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_failed", "share_revoked", "validation_failed"]);
|
|
722
|
-
|
|
740
|
+
const structured = error && typeof error === "object" ? error : {};
|
|
741
|
+
const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
|
|
742
|
+
return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
|
|
743
|
+
}
|
|
744
|
+
function fail(error) {
|
|
745
|
+
emit(failureValue(error));
|
|
723
746
|
process.exitCode = 1;
|
|
724
747
|
}
|
|
725
748
|
|
|
@@ -837,6 +860,35 @@ function errorMessage(error) {
|
|
|
837
860
|
return error instanceof Error ? error.message : String(error);
|
|
838
861
|
}
|
|
839
862
|
|
|
863
|
+
// src/recording-lifecycle.ts
|
|
864
|
+
async function startRecordingWithCompensation(remote, dependencies) {
|
|
865
|
+
try {
|
|
866
|
+
return await dependencies.startLocal();
|
|
867
|
+
} catch (startupError) {
|
|
868
|
+
try {
|
|
869
|
+
await dependencies.cancelRemote(remote.recordingId, "local_start_failed");
|
|
870
|
+
} catch (cleanupError) {
|
|
871
|
+
throw new AggregateError(
|
|
872
|
+
[startupError, cleanupError],
|
|
873
|
+
"Local recording startup failed and the remote reservation could not be cancelled. Run `rill record cancel <recording-id>`."
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
throw startupError;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async function remoteAwareRecordingStatus(recordingId, dependencies) {
|
|
880
|
+
try {
|
|
881
|
+
return { ...await dependencies.localStatus(recordingId), source: "local" };
|
|
882
|
+
} catch {
|
|
883
|
+
const remote = await dependencies.remoteStatus(recordingId);
|
|
884
|
+
return {
|
|
885
|
+
...remote,
|
|
886
|
+
recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
|
|
887
|
+
source: "remote"
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
840
892
|
// src/cli.ts
|
|
841
893
|
var program = new Command();
|
|
842
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 ?? "https://rill-staging.tight-shadow-8e12.workers.dev").option("--pretty", "Pretty-print JSON output", false);
|
|
@@ -873,8 +925,13 @@ record.command("start").option("--url <url>").option("--title <title>").option("
|
|
|
873
925
|
const width = Number(commandOptions.width);
|
|
874
926
|
const height = Number(commandOptions.height);
|
|
875
927
|
const remote = await client.createRecording({ title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height });
|
|
876
|
-
await
|
|
877
|
-
|
|
928
|
+
const local = await startRecordingWithCompensation(remote, {
|
|
929
|
+
startLocal: async () => {
|
|
930
|
+
await ensureDaemon(process.argv[1]);
|
|
931
|
+
return startRecording({ recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Number(commandOptions.maxDuration), captureNetworkBodies: remote.bodyCaptureEnabled });
|
|
932
|
+
},
|
|
933
|
+
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
934
|
+
});
|
|
878
935
|
emit({ schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) }, options.pretty);
|
|
879
936
|
});
|
|
880
937
|
record.command("stop").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
@@ -895,8 +952,22 @@ record.command("stop").argument("<recording-id>").option("--no-wait").action(asy
|
|
|
895
952
|
});
|
|
896
953
|
record.command("status").argument("<recording-id>").action(async (recordingId) => {
|
|
897
954
|
const options = program.opts();
|
|
898
|
-
await
|
|
899
|
-
|
|
955
|
+
const token = await loadCredential(options.apiUrl);
|
|
956
|
+
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
957
|
+
const status = await remoteAwareRecordingStatus(recordingId, {
|
|
958
|
+
localStatus: async (id) => {
|
|
959
|
+
await ensureDaemon(process.argv[1]);
|
|
960
|
+
return recordingStatus(id);
|
|
961
|
+
},
|
|
962
|
+
remoteStatus: (id) => client.recordingStatus(id)
|
|
963
|
+
});
|
|
964
|
+
emit({ schemaVersion: 1, ...status }, options.pretty);
|
|
965
|
+
});
|
|
966
|
+
record.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
967
|
+
const options = program.opts();
|
|
968
|
+
const token = await loadCredential(options.apiUrl);
|
|
969
|
+
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
970
|
+
emit(await client.cancelRecording(recordingId), options.pretty);
|
|
900
971
|
});
|
|
901
972
|
program.command("upload").argument("<video-file>").option("--title <title>").option("--no-wait").action(async (videoFile, commandOptions) => {
|
|
902
973
|
const options = program.opts();
|