@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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +78 -7
  3. 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.0
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) throw new Error([value?.error?.code, value?.error?.message, value?.error?.recovery].filter(Boolean).join(": ") || `Request failed (${response.status}).`);
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 fail(error) {
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
- emit({ error: { code: knownCodes.has(candidate) ? candidate : "internal_error", message } });
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 ensureDaemon(process.argv[1]);
877
- const local = await startRecording({ recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Number(commandOptions.maxDuration), captureNetworkBodies: remote.bodyCaptureEnabled });
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 ensureDaemon(process.argv[1]);
899
- emit({ schemaVersion: 1, ...await recordingStatus(recordingId) }, options.pretty);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {