@ricsam/r5dctl 0.0.22 → 0.0.24

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 CHANGED
@@ -51,7 +51,7 @@ r5dctl -p <project> get branches
51
51
  r5dctl -p <project> describe branch <branch>
52
52
  r5dctl -p <project> -b <branch> get envs
53
53
  r5dctl -p <project> -b <branch> set envs --from-env-file ~/.env --only-missing
54
- r5dctl -p <project> -b <branch> set envs -e backend/API_KEY=value --description "API credential"
54
+ r5dctl -p <project> -b <branch> set envs -e API_KEY=value --description "API credential"
55
55
  r5dctl -p <project> get sessions --branch <branch>
56
56
  r5dctl -p <project> create session -b <branch> --name "Spec session"
57
57
  r5dctl describe session <session-id>
@@ -65,11 +65,22 @@ r5dctl -s <session-id> conversation --system # include system prompt m
65
65
  r5dctl -s <session-id> conversation --raw --tools --system # exact raw agent request transcript
66
66
  r5dctl -s <session-id> prompt --mode plan --model max "..."
67
67
  r5dctl -s <session-id> answer-questions -a1 "Recipe Collection" -a2 "Both Manual & AI"
68
- r5dctl -s <session-id> apply-required-envs -e backend/KEY=value -e frontend/KEY=value
68
+ r5dctl -s <session-id> apply-required-envs -e KEY=value
69
69
 
70
+ r5dctl -p <project> -b <branch> shell
71
+ r5dctl -p <project> -b <branch> shell -c "bun test"
72
+
73
+ r5dctl ps list
74
+ r5dctl -p <project> -b <branch> ps list --worker <label>
75
+ r5dctl -s <session-id> ps list --json
76
+ r5dctl ps stop <run-id>
70
77
  ```
71
78
 
72
- `get envs` reports names and whether values are set, but never prints the values. Prefer `--from-env-file` for bulk imports so secret values do not appear in shell arguments or history. Dotenv imports target backend envs by default; use `--target frontend` when needed. Empty dotenv values are skipped unless `--include-empty` is passed, and `--only-missing` preserves values that are already set.
79
+ `shell` connects to the same live worker PTY as the web Shell tab. Without `-c` it attaches the local terminal interactively. With `-c`/`--command`, it runs the supplied shell expression, streams combined PTY output, and exits with the remote command's status. Because command output uses a PTY, stdout and stderr are combined and programs may emit colors or other terminal control sequences.
80
+
81
+ `ps list` shows active long-running commands managed by r5d. It does not expose the worker host's general OS process table or interactive PTYs. Global project, branch, and session options filter the list, while `--worker` selects a worker label. `ps stop` sends `SIGTERM` to the selected run. Inside an r5d-provided worker shell, the temporary credential can only list and stop runs on that shell's worker.
82
+
83
+ `get envs` reports names and whether values are set, but never prints the values. Prefer `--from-env-file` for bulk imports so secret values do not appear in shell arguments or history. Empty dotenv values are skipped unless `--include-empty` is passed, and `--only-missing` preserves values that are already set.
73
84
 
74
85
  Conversation output is human-readable by default and excludes both the system prompt and tool definitions. `--raw` keeps the transcript layout but renders structured message and tool parts as JSON, `--tools` includes the provider-shaped tool definitions, and `--system` includes system prompt messages. The global `--json` flag remains separate and prints the complete API response envelope.
75
86
 
package/dist/cjs/cli.cjs CHANGED
@@ -28,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var cli_exports = {};
30
30
  __export(cli_exports, {
31
+ formatProcessAge: () => formatProcessAge,
31
32
  getCliHelpText: () => getCliHelpText,
32
33
  getR5dctlVersion: () => getR5dctlVersion,
33
34
  main: () => main,
@@ -36,9 +37,12 @@ __export(cli_exports, {
36
37
  parseEnvFlags: () => parseEnvFlags,
37
38
  parseGlobalArgs: () => parseGlobalArgs,
38
39
  parsePromptArgs: () => parsePromptArgs,
40
+ parsePsListArgs: () => parsePsListArgs,
39
41
  parseSetEnvArgs: () => parseSetEnvArgs,
42
+ parseShellArgs: () => parseShellArgs,
40
43
  readDotenvFile: () => readDotenvFile,
41
44
  renderConversationResponse: () => renderConversationResponse,
45
+ renderProcessList: () => renderProcessList,
42
46
  resolveCommandExecution: () => resolveCommandExecution,
43
47
  runR5dctlCli: () => runR5dctlCli,
44
48
  summarizeEnvData: () => summarizeEnvData
@@ -51,6 +55,7 @@ var import_node_child_process = require("node:child_process");
51
55
  var import_promises = require("node:timers/promises");
52
56
  var import_dotenv = require("dotenv");
53
57
  var import_r5d_api = require("@ricsam/r5d-api");
58
+ var import_shell = require("./shell.cjs");
54
59
  const CHAT_MODES = /* @__PURE__ */ new Set([
55
60
  "ask",
56
61
  "plan",
@@ -83,14 +88,32 @@ const CLI_ONLY_HELP_ENTRIES = [
83
88
  section: "auth",
84
89
  usage: "auth login [--no-open]",
85
90
  description: "Start the browser login flow."
91
+ },
92
+ {
93
+ section: "shell",
94
+ usage: "-p <project> -b <branch> shell [-c <command>]",
95
+ description: "Open an interactive worker shell or run one command through a PTY."
86
96
  }
87
97
  ];
88
98
  const CLI_ONLY_COMMAND_HELP = [
89
99
  {
90
100
  path: ["auth", "login"],
91
101
  usage: "auth login [--no-open]"
102
+ },
103
+ {
104
+ path: ["shell"],
105
+ usage: "-p <project> -b <branch> shell [-c <command>]"
92
106
  }
93
107
  ];
108
+ const PS_HELP_TEXT = [
109
+ "Usage:",
110
+ " r5dctl ps list [--worker <label>]",
111
+ " r5dctl ps stop <run-id>",
112
+ "",
113
+ "List or gracefully stop active r5d-managed worker processes.",
114
+ "Use -p/--project, -b/--branch, or -s/--session to filter ps list.",
115
+ ""
116
+ ].join("\n");
94
117
  const SHARED_HELP_ENTRIES = [
95
118
  { section: "auth", usage: "auth status", description: "Show the current authenticated user and credential source." },
96
119
  { section: "auth", usage: "auth logout", description: "Revoke the current credential and clear saved auth." },
@@ -114,12 +137,12 @@ const SHARED_HELP_ENTRIES = [
114
137
  },
115
138
  {
116
139
  section: "envs",
117
- usage: "-p <project> -b <branch> set envs --from-env-file <path> [--target <backend|frontend>] [--only-missing]",
140
+ usage: "-p <project> -b <branch> set envs --from-env-file <path> [--only-missing]",
118
141
  description: "Securely import branch envs from a dotenv file without putting values in command arguments."
119
142
  },
120
143
  {
121
144
  section: "envs",
122
- usage: "-p <project> -b <branch> set envs -e backend/KEY=value [--description <text>] [--only-missing]",
145
+ usage: "-p <project> -b <branch> set envs -e KEY=value [--description <text>] [--only-missing]",
123
146
  description: "Set one or more branch envs directly."
124
147
  },
125
148
  {
@@ -152,9 +175,15 @@ const SHARED_HELP_ENTRIES = [
152
175
  },
153
176
  {
154
177
  section: "sessions",
155
- usage: "-s <session-id> apply-required-envs -e backend/KEY=value -e frontend/KEY=value",
156
- description: "Apply required backend or frontend env values."
178
+ usage: "-s <session-id> apply-required-envs -e KEY=value",
179
+ description: "Apply required environment variable values."
180
+ },
181
+ {
182
+ section: "processes",
183
+ usage: "ps list [--worker <label>]",
184
+ description: "List active r5d-managed worker processes; global project, branch, and session filters apply."
157
185
  },
186
+ { section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
158
187
  {
159
188
  section: "agents",
160
189
  usage: '-p <project> start-agent <src-branch> <agent-type> "<prompt>"',
@@ -170,13 +199,25 @@ const SHARED_HELP_ENTRIES = [
170
199
  { section: "merges", usage: "-p <project> continue-merge <target-branch>", description: "Commit a resolved merge." },
171
200
  { section: "merges", usage: "-p <project> abort-merge <target-branch>", description: "Abort an in-progress merge." }
172
201
  ];
173
- const HELP_SECTION_ORDER = ["auth", "projects", "branches", "envs", "sessions", "agents", "merges"];
202
+ const HELP_SECTION_ORDER = [
203
+ "auth",
204
+ "projects",
205
+ "branches",
206
+ "envs",
207
+ "sessions",
208
+ "processes",
209
+ "shell",
210
+ "agents",
211
+ "merges"
212
+ ];
174
213
  const HELP_SECTION_TITLES = {
175
214
  auth: "Auth",
176
215
  projects: "Projects",
177
216
  branches: "Branches",
178
217
  envs: "Environment variables",
179
218
  sessions: "Sessions",
219
+ processes: "Processes",
220
+ shell: "Shell",
180
221
  agents: "Agents",
181
222
  merges: "Merges"
182
223
  };
@@ -276,7 +317,7 @@ function renderCliOnlyCommandHelp(entry) {
276
317
  }
277
318
  function renderSharedCommandHelp(pathSegments) {
278
319
  const entry = SHARED_HELP_ENTRIES.find((candidate) => {
279
- const usageSegments = candidate.usage.split(" ").filter((segment) => !segment.startsWith("-") && !segment.startsWith("<") && !segment.startsWith("["));
320
+ const usageSegments = candidate.usage.split(" ").map((segment) => segment.replace(/^\[/, "").replace(/\]$/, "")).filter((segment) => !segment.startsWith("-") && !segment.startsWith("<") && !segment.startsWith("["));
280
321
  return usageSegments.length <= pathSegments.length && usageSegments.every((segment, index) => segment === pathSegments[index]);
281
322
  });
282
323
  if (!entry) {
@@ -512,33 +553,24 @@ function parseAnswerFlags(args) {
512
553
  function validateEnvAssignment(value) {
513
554
  const equalsIndex = value.indexOf("=");
514
555
  if (equalsIndex <= 0) {
515
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
516
- }
517
- const assignment = value.slice(0, equalsIndex);
518
- const slashIndex = assignment.indexOf("/");
519
- if (slashIndex <= 0) {
520
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
556
+ throw new Error(`Invalid env assignment '${value}'. Expected KEY=value`);
521
557
  }
522
- const target = assignment.slice(0, slashIndex);
523
- const key = assignment.slice(slashIndex + 1);
524
- if (target !== "backend" && target !== "frontend" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
525
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
558
+ const key = value.slice(0, equalsIndex);
559
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
560
+ throw new Error(`Invalid env assignment '${value}'. Expected KEY=value`);
526
561
  }
527
562
  }
528
563
  function parseEnvAssignment(value) {
529
564
  validateEnvAssignment(value);
530
565
  const equalsIndex = value.indexOf("=");
531
- const slashIndex = value.indexOf("/");
532
566
  return {
533
- target: value.slice(0, slashIndex),
534
- name: value.slice(slashIndex + 1, equalsIndex),
567
+ name: value.slice(0, equalsIndex),
535
568
  value: value.slice(equalsIndex + 1)
536
569
  };
537
570
  }
538
571
  function parseSetEnvArgs(args) {
539
572
  const parsed = {
540
573
  assignments: [],
541
- target: "backend",
542
574
  onlyMissing: false,
543
575
  includeEmpty: false
544
576
  };
@@ -568,23 +600,6 @@ function parseSetEnvArgs(args) {
568
600
  parsed.fromEnvFile = requireValue(inlineFile, "Missing value for --from-env-file");
569
601
  continue;
570
602
  }
571
- if (arg === "--target") {
572
- const target = requireValue(args[index + 1], "Missing value for --target");
573
- if (target !== "backend" && target !== "frontend") {
574
- throw new Error("Invalid --target. Expected backend or frontend");
575
- }
576
- parsed.target = target;
577
- index += 1;
578
- continue;
579
- }
580
- const inlineTarget = parseLongOptionWithEquals(arg, "--target");
581
- if (inlineTarget !== void 0) {
582
- if (inlineTarget !== "backend" && inlineTarget !== "frontend") {
583
- throw new Error("Invalid --target. Expected backend or frontend");
584
- }
585
- parsed.target = inlineTarget;
586
- continue;
587
- }
588
603
  if (arg === "--description") {
589
604
  parsed.description = requireValue(args[index + 1], "Missing value for --description");
590
605
  index += 1;
@@ -658,7 +673,7 @@ function parseEnvFlags(args) {
658
673
  if (arg.startsWith("-")) {
659
674
  throw new Error(`Unknown env flag: ${arg}`);
660
675
  }
661
- throw new Error(`Unexpected value '${arg}'. Use -e backend/KEY=value.`);
676
+ throw new Error(`Unexpected value '${arg}'. Use -e KEY=value.`);
662
677
  }
663
678
  if (envs.length === 0) {
664
679
  throw new Error("At least one -e/--env value is required.");
@@ -725,6 +740,67 @@ function parsePromptArgs(args) {
725
740
  message
726
741
  };
727
742
  }
743
+ function parseShellArgs(args) {
744
+ let command;
745
+ for (let index = 0; index < args.length; index += 1) {
746
+ const arg = args[index];
747
+ if (!arg) {
748
+ continue;
749
+ }
750
+ if (arg === "-c" || arg === "--command") {
751
+ if (command !== void 0) {
752
+ throw new Error("Use only one -c/--command value");
753
+ }
754
+ command = requireValue(args[index + 1], `Missing value for ${arg}`);
755
+ index += 1;
756
+ continue;
757
+ }
758
+ const inlineCommand = parseLongOptionWithEquals(arg, "--command");
759
+ if (inlineCommand !== void 0) {
760
+ if (command !== void 0) {
761
+ throw new Error("Use only one -c/--command value");
762
+ }
763
+ command = requireValue(inlineCommand, "Missing value for --command");
764
+ continue;
765
+ }
766
+ throw new Error(arg.startsWith("-") ? `Unknown shell flag: ${arg}` : `Unexpected shell value: ${arg}`);
767
+ }
768
+ return command === void 0 ? {} : { command };
769
+ }
770
+ function parsePsListArgs(args) {
771
+ const input = {};
772
+ const definitions = [
773
+ { flag: "--project", key: "project" },
774
+ { flag: "--branch", key: "branch" },
775
+ { flag: "--session", key: "session" },
776
+ { flag: "--worker", key: "worker" }
777
+ ];
778
+ const assign = (key, value, flag) => {
779
+ if (input[key] !== void 0) {
780
+ throw new Error(`Use only one ${flag} value`);
781
+ }
782
+ input[key] = requireValue(value, `Missing value for ${flag}`);
783
+ };
784
+ for (let index = 0; index < args.length; index += 1) {
785
+ const arg = args[index];
786
+ if (!arg) continue;
787
+ const definition = definitions.find(({ flag }) => arg === flag || arg.startsWith(`${flag}=`));
788
+ if (!definition) {
789
+ throw new Error(arg.startsWith("-") ? `Unknown ps list flag: ${arg}` : `Unexpected ps list value: ${arg}`);
790
+ }
791
+ if (arg === definition.flag) {
792
+ assign(definition.key, requireValue(args[index + 1], `Missing value for ${definition.flag}`), definition.flag);
793
+ index += 1;
794
+ continue;
795
+ }
796
+ assign(
797
+ definition.key,
798
+ requireValue(parseLongOptionWithEquals(arg, definition.flag), `Missing value for ${definition.flag}`),
799
+ definition.flag
800
+ );
801
+ }
802
+ return input;
803
+ }
728
804
  async function openInBrowser(url) {
729
805
  if (process.platform === "darwin") {
730
806
  (0, import_node_child_process.spawn)("open", [url], { stdio: "ignore", detached: true }).unref();
@@ -866,34 +942,66 @@ function renderSessionList(sessions) {
866
942
  function renderSessionDescription(session) {
867
943
  return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
868
944
  }
945
+ function formatProcessAge(startedAt, nowMs = Date.now()) {
946
+ const startedAtMs = Date.parse(startedAt);
947
+ if (!Number.isFinite(startedAtMs)) {
948
+ return "unknown";
949
+ }
950
+ const totalSeconds = Math.max(0, Math.floor((nowMs - startedAtMs) / 1e3));
951
+ if (totalSeconds < 60) {
952
+ return `${totalSeconds}s`;
953
+ }
954
+ if (totalSeconds < 60 * 60) {
955
+ const minutes = Math.floor(totalSeconds / 60);
956
+ const seconds = totalSeconds % 60;
957
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
958
+ }
959
+ if (totalSeconds < 24 * 60 * 60) {
960
+ const hours2 = Math.floor(totalSeconds / (60 * 60));
961
+ const minutes = Math.floor(totalSeconds % (60 * 60) / 60);
962
+ return `${hours2}h ${String(minutes).padStart(2, "0")}m`;
963
+ }
964
+ const days = Math.floor(totalSeconds / (24 * 60 * 60));
965
+ const hours = Math.floor(totalSeconds % (24 * 60 * 60) / (60 * 60));
966
+ return `${days}d ${hours}h`;
967
+ }
968
+ function renderProcessList(processes, nowMs = Date.now()) {
969
+ if (processes.length === 0) {
970
+ return "No active processes.\n";
971
+ }
972
+ const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "PROJECT", "BRANCH", "SESSION", "COMMAND"];
973
+ const rows = processes.map((process2) => [
974
+ process2.runId,
975
+ process2.status,
976
+ formatProcessAge(process2.startedAt, nowMs),
977
+ process2.workerLabel,
978
+ process2.projectPath,
979
+ process2.branchName,
980
+ process2.sessionId,
981
+ process2.command
982
+ ]);
983
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
984
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
985
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
986
+ return `${[formatRow(headers), separator, ...rows.map(formatRow)].join("\n")}
987
+ `;
988
+ }
869
989
  function summarizeEnvData(data) {
870
- const summary = {};
871
- for (const target of ["backend", "frontend"]) {
872
- const entries = data[target];
873
- if (!entries) continue;
874
- summary[target] = Object.fromEntries(
875
- Object.entries(entries).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
876
- name,
877
- {
878
- optional: env.optional ?? false,
879
- description: env.description,
880
- hasValue: env.value !== null
881
- }
882
- ])
883
- );
884
- }
885
- return summary;
990
+ return Object.fromEntries(
991
+ Object.entries(data).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
992
+ name,
993
+ {
994
+ optional: env.optional ?? false,
995
+ description: env.description,
996
+ hasValue: env.value !== null
997
+ }
998
+ ])
999
+ );
886
1000
  }
887
1001
  function renderEnvSummary(data) {
888
1002
  const lines = [];
889
- for (const target of ["backend", "frontend"]) {
890
- const entries = Object.entries(data[target] ?? {});
891
- if (entries.length === 0) continue;
892
- if (lines.length > 0) lines.push("");
893
- lines.push(`${target}:`);
894
- for (const [name, env] of entries) {
895
- lines.push(` ${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
896
- }
1003
+ for (const [name, env] of Object.entries(data)) {
1004
+ lines.push(`${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
897
1005
  }
898
1006
  return lines.length > 0 ? `${lines.join("\n")}
899
1007
  ` : "No environment variables declared.\n";
@@ -905,7 +1013,7 @@ function renderEnvSetResult(result) {
905
1013
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
906
1014
  if (result.skippedEmpty.length > 0) lines.push(`Skipped ${result.skippedEmpty.length} empty value(s).`);
907
1015
  if (result.updated.length > 0) {
908
- lines.push(...result.updated.map(({ target, name }) => ` ${target}/${name}`));
1016
+ lines.push(...result.updated.map(({ name }) => ` ${name}`));
909
1017
  }
910
1018
  return `${lines.join("\n")}
911
1019
  `;
@@ -918,10 +1026,10 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
918
1026
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
919
1027
  throw new Error(`Invalid environment variable name in dotenv file: ${name}`);
920
1028
  }
921
- return { target: options.target, name, value };
1029
+ return { name, value };
922
1030
  }) : options.assignments.map(parseEnvAssignment);
923
1031
  const uniqueAssignments = /* @__PURE__ */ new Map();
924
- for (const assignment of assignments) uniqueAssignments.set(`${assignment.target}/${assignment.name}`, assignment);
1032
+ for (const assignment of assignments) uniqueAssignments.set(assignment.name, assignment);
925
1033
  const update = {};
926
1034
  const result = {
927
1035
  success: true,
@@ -934,12 +1042,12 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
934
1042
  const missingDescriptions = [];
935
1043
  const importedDescription = options.fromEnvFile ? `Imported from ${import_node_path.default.basename(options.fromEnvFile)}.` : void 0;
936
1044
  for (const assignment of uniqueAssignments.values()) {
937
- const identity = { target: assignment.target, name: assignment.name };
1045
+ const identity = { name: assignment.name };
938
1046
  if (!options.includeEmpty && assignment.value.length === 0) {
939
1047
  result.skippedEmpty.push(identity);
940
1048
  continue;
941
1049
  }
942
- const existing = current[assignment.target]?.[assignment.name];
1050
+ const existing = current[assignment.name];
943
1051
  if (options.onlyMissing && existing?.value !== null && existing?.value !== void 0) {
944
1052
  result.skippedExisting.push(identity);
945
1053
  continue;
@@ -949,9 +1057,8 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
949
1057
  missingDescriptions.push(identity);
950
1058
  continue;
951
1059
  }
952
- const targetUpdate = update[assignment.target] ??= {};
953
1060
  const optional = options.optional ?? existing?.optional;
954
- targetUpdate[assignment.name] = {
1061
+ update[assignment.name] = {
955
1062
  description,
956
1063
  value: assignment.value,
957
1064
  ...optional === void 0 ? {} : { optional }
@@ -959,9 +1066,7 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
959
1066
  result.updated.push(identity);
960
1067
  }
961
1068
  if (missingDescriptions.length > 0) {
962
- throw new Error(
963
- `--description is required when declaring new envs with -e: ${missingDescriptions.map(({ target, name }) => `${target}/${name}`).join(", ")}`
964
- );
1069
+ throw new Error(`--description is required when declaring new envs with -e: ${missingDescriptions.map(({ name }) => name).join(", ")}`);
965
1070
  }
966
1071
  if (result.updated.length > 0) {
967
1072
  await client.projects.env.update(project.id, branchName, update);
@@ -1126,6 +1231,17 @@ async function executeR5dctlCommand(client, json, args) {
1126
1231
  write(result, "API key revoked.\n");
1127
1232
  return;
1128
1233
  }
1234
+ if (first === "ps" && second === "list") {
1235
+ const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
1236
+ write(processes, renderProcessList(processes));
1237
+ return;
1238
+ }
1239
+ if (first === "ps" && second === "stop") {
1240
+ const process2 = await client.processes.stop(requireValue(args[2], "Missing process run id"));
1241
+ write(process2, `Stopped process ${process2.runId}: ${process2.command}
1242
+ `);
1243
+ return;
1244
+ }
1129
1245
  if (first === "projects" && second === "list" || first === "get" && second === "projects") {
1130
1246
  const projects = await client.projects.list();
1131
1247
  write(projects, renderProjectList(projects));
@@ -1321,6 +1437,12 @@ function resolveCommandExecution(options, rest) {
1321
1437
  throw new Error("Missing command");
1322
1438
  }
1323
1439
  const commandArgs = normalizedRest.slice(1);
1440
+ if (command === "ps" && commandArgs.length === 0) {
1441
+ return {
1442
+ kind: "cli-help",
1443
+ text: PS_HELP_TEXT
1444
+ };
1445
+ }
1324
1446
  if (trailingHelp) {
1325
1447
  const cliOnlyHelp = findCliOnlyCommandHelp(normalizedRest);
1326
1448
  if (cliOnlyHelp) {
@@ -1394,6 +1516,36 @@ function resolveCommandExecution(options, rest) {
1394
1516
  }
1395
1517
  throw new Error("Unknown auth command");
1396
1518
  }
1519
+ if (command === "ps") {
1520
+ const subcommand = commandArgs[0];
1521
+ if (subcommand === "list") {
1522
+ const commandFilters = parsePsListArgs(commandArgs.slice(1));
1523
+ const filters = {
1524
+ ...commandFilters,
1525
+ ...options.project ? { project: options.project } : {},
1526
+ ...options.branch ? { branch: options.branch } : {},
1527
+ ...options.session ? { session: options.session } : {}
1528
+ };
1529
+ const pluginArgs = ["ps", "list"];
1530
+ for (const [flag, value] of [
1531
+ ["--project", filters.project],
1532
+ ["--branch", filters.branch],
1533
+ ["--session", filters.session],
1534
+ ["--worker", filters.worker]
1535
+ ]) {
1536
+ if (value) pluginArgs.push(flag, value);
1537
+ }
1538
+ return { kind: "plugin", pluginArgs };
1539
+ }
1540
+ if (subcommand === "stop") {
1541
+ const runId = requireValue(commandArgs[1], "Missing process run id");
1542
+ if (commandArgs.length > 2) {
1543
+ throw new Error(`Unexpected ps stop value: ${commandArgs[2]}`);
1544
+ }
1545
+ return { kind: "plugin", pluginArgs: ["ps", "stop", runId] };
1546
+ }
1547
+ throw new Error(`Unknown ps command: ${subcommand ?? ""}`.trim());
1548
+ }
1397
1549
  if (command === "get") {
1398
1550
  const target = commandArgs[0];
1399
1551
  if (target === "projects") {
@@ -1573,6 +1725,21 @@ function resolveCommandExecution(options, rest) {
1573
1725
  pluginArgs: ["conversation", sessionId, ...commandArgs]
1574
1726
  };
1575
1727
  }
1728
+ if (command === "shell") {
1729
+ if (!options.project) {
1730
+ throw new Error("--project/-p is required for `shell`");
1731
+ }
1732
+ if (!options.branch) {
1733
+ throw new Error("--branch/-b is required for `shell`");
1734
+ }
1735
+ if (options.json) {
1736
+ throw new Error("--json is not supported for `shell`");
1737
+ }
1738
+ return {
1739
+ kind: "shell",
1740
+ ...parseShellArgs(commandArgs)
1741
+ };
1742
+ }
1576
1743
  if (command === "prompt") {
1577
1744
  const sessionId = options.session;
1578
1745
  if (!sessionId) {
@@ -1696,31 +1863,43 @@ async function runCommand(argv) {
1696
1863
  const { options, rest } = parseGlobalArgs(argv);
1697
1864
  if (options.version) {
1698
1865
  printVersion();
1699
- return;
1866
+ return 0;
1700
1867
  }
1701
1868
  if (options.help || rest.length === 0) {
1702
1869
  printHelp();
1703
- return;
1870
+ return 0;
1704
1871
  }
1705
1872
  const config = parseConfig(options.configPath);
1706
- const client = new import_r5d_api.R5dctlClient(resolveClientOptions(options, config));
1873
+ const clientOptions = resolveClientOptions(options, config);
1874
+ const client = new import_r5d_api.R5dctlClient(clientOptions);
1707
1875
  const execution = resolveCommandExecution(options, rest);
1708
1876
  if (execution.kind === "auth-login") {
1709
1877
  await handleAuthLogin(client, options, execution.commandArgs, config);
1710
- return;
1878
+ return 0;
1711
1879
  }
1712
1880
  if (execution.kind === "auth-api-key-create") {
1713
1881
  await handleAuthApiKeyCreate(client, options, config, execution.commandArgs);
1714
- return;
1882
+ return 0;
1715
1883
  }
1716
1884
  if (execution.kind === "cli-help") {
1717
1885
  process.stdout.write(execution.text);
1718
- return;
1886
+ return 0;
1887
+ }
1888
+ if (execution.kind === "shell") {
1889
+ const project = await client.projects.describe(options.project);
1890
+ return await (0, import_shell.runR5dctlShell)({
1891
+ baseUrl: clientOptions.baseUrl ?? "https://r5d.dev",
1892
+ credential: clientOptions.token ?? clientOptions.apiKey ?? "",
1893
+ projectId: project.id,
1894
+ branchName: options.branch,
1895
+ command: execution.command
1896
+ });
1719
1897
  }
1720
1898
  await executeR5dctlCommand(client, options.json, execution.pluginArgs);
1721
1899
  if (execution.clearAuthOnSuccess) {
1722
1900
  writeConfig(options.configPath, clearAuthFromConfig(config, options));
1723
1901
  }
1902
+ return 0;
1724
1903
  }
1725
1904
  function extractApiErrorMessage(error) {
1726
1905
  if (typeof error.body === "object" && error.body !== null) {
@@ -1733,8 +1912,7 @@ function extractApiErrorMessage(error) {
1733
1912
  }
1734
1913
  async function runR5dctlCli(argv) {
1735
1914
  try {
1736
- await runCommand(argv);
1737
- return 0;
1915
+ return await runCommand(argv);
1738
1916
  } catch (error) {
1739
1917
  if (error instanceof import_r5d_api.R5dctlApiError) {
1740
1918
  process.stderr.write(`${extractApiErrorMessage(error)}
@@ -1749,11 +1927,12 @@ async function runR5dctlCli(argv) {
1749
1927
  async function main(argv = process.argv.slice(2)) {
1750
1928
  const exitCode = await runR5dctlCli(argv);
1751
1929
  if (exitCode !== 0) {
1752
- process.exit(exitCode);
1930
+ process.exitCode = exitCode;
1753
1931
  }
1754
1932
  }
1755
1933
  // Annotate the CommonJS export names for ESM import in node:
1756
1934
  0 && (module.exports = {
1935
+ formatProcessAge,
1757
1936
  getCliHelpText,
1758
1937
  getR5dctlVersion,
1759
1938
  main,
@@ -1762,9 +1941,12 @@ async function main(argv = process.argv.slice(2)) {
1762
1941
  parseEnvFlags,
1763
1942
  parseGlobalArgs,
1764
1943
  parsePromptArgs,
1944
+ parsePsListArgs,
1765
1945
  parseSetEnvArgs,
1946
+ parseShellArgs,
1766
1947
  readDotenvFile,
1767
1948
  renderConversationResponse,
1949
+ renderProcessList,
1768
1950
  resolveCommandExecution,
1769
1951
  runR5dctlCli,
1770
1952
  summarizeEnvData
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "commonjs"
5
5
  }