@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/dist/mjs/cli.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  R5dctlApiError,
9
9
  R5dctlClient
10
10
  } from "@ricsam/r5d-api";
11
+ import { runR5dctlShell } from "./shell.mjs";
11
12
  const CHAT_MODES = /* @__PURE__ */ new Set([
12
13
  "ask",
13
14
  "plan",
@@ -40,14 +41,32 @@ const CLI_ONLY_HELP_ENTRIES = [
40
41
  section: "auth",
41
42
  usage: "auth login [--no-open]",
42
43
  description: "Start the browser login flow."
44
+ },
45
+ {
46
+ section: "shell",
47
+ usage: "-p <project> -b <branch> shell [-c <command>]",
48
+ description: "Open an interactive worker shell or run one command through a PTY."
43
49
  }
44
50
  ];
45
51
  const CLI_ONLY_COMMAND_HELP = [
46
52
  {
47
53
  path: ["auth", "login"],
48
54
  usage: "auth login [--no-open]"
55
+ },
56
+ {
57
+ path: ["shell"],
58
+ usage: "-p <project> -b <branch> shell [-c <command>]"
49
59
  }
50
60
  ];
61
+ const PS_HELP_TEXT = [
62
+ "Usage:",
63
+ " r5dctl ps list [--worker <label>]",
64
+ " r5dctl ps stop <run-id>",
65
+ "",
66
+ "List or gracefully stop active r5d-managed worker processes.",
67
+ "Use -p/--project, -b/--branch, or -s/--session to filter ps list.",
68
+ ""
69
+ ].join("\n");
51
70
  const SHARED_HELP_ENTRIES = [
52
71
  { section: "auth", usage: "auth status", description: "Show the current authenticated user and credential source." },
53
72
  { section: "auth", usage: "auth logout", description: "Revoke the current credential and clear saved auth." },
@@ -71,12 +90,12 @@ const SHARED_HELP_ENTRIES = [
71
90
  },
72
91
  {
73
92
  section: "envs",
74
- usage: "-p <project> -b <branch> set envs --from-env-file <path> [--target <backend|frontend>] [--only-missing]",
93
+ usage: "-p <project> -b <branch> set envs --from-env-file <path> [--only-missing]",
75
94
  description: "Securely import branch envs from a dotenv file without putting values in command arguments."
76
95
  },
77
96
  {
78
97
  section: "envs",
79
- usage: "-p <project> -b <branch> set envs -e backend/KEY=value [--description <text>] [--only-missing]",
98
+ usage: "-p <project> -b <branch> set envs -e KEY=value [--description <text>] [--only-missing]",
80
99
  description: "Set one or more branch envs directly."
81
100
  },
82
101
  {
@@ -109,9 +128,15 @@ const SHARED_HELP_ENTRIES = [
109
128
  },
110
129
  {
111
130
  section: "sessions",
112
- usage: "-s <session-id> apply-required-envs -e backend/KEY=value -e frontend/KEY=value",
113
- description: "Apply required backend or frontend env values."
131
+ usage: "-s <session-id> apply-required-envs -e KEY=value",
132
+ description: "Apply required environment variable values."
133
+ },
134
+ {
135
+ section: "processes",
136
+ usage: "ps list [--worker <label>]",
137
+ description: "List active r5d-managed worker processes; global project, branch, and session filters apply."
114
138
  },
139
+ { section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
115
140
  {
116
141
  section: "agents",
117
142
  usage: '-p <project> start-agent <src-branch> <agent-type> "<prompt>"',
@@ -127,13 +152,25 @@ const SHARED_HELP_ENTRIES = [
127
152
  { section: "merges", usage: "-p <project> continue-merge <target-branch>", description: "Commit a resolved merge." },
128
153
  { section: "merges", usage: "-p <project> abort-merge <target-branch>", description: "Abort an in-progress merge." }
129
154
  ];
130
- const HELP_SECTION_ORDER = ["auth", "projects", "branches", "envs", "sessions", "agents", "merges"];
155
+ const HELP_SECTION_ORDER = [
156
+ "auth",
157
+ "projects",
158
+ "branches",
159
+ "envs",
160
+ "sessions",
161
+ "processes",
162
+ "shell",
163
+ "agents",
164
+ "merges"
165
+ ];
131
166
  const HELP_SECTION_TITLES = {
132
167
  auth: "Auth",
133
168
  projects: "Projects",
134
169
  branches: "Branches",
135
170
  envs: "Environment variables",
136
171
  sessions: "Sessions",
172
+ processes: "Processes",
173
+ shell: "Shell",
137
174
  agents: "Agents",
138
175
  merges: "Merges"
139
176
  };
@@ -233,7 +270,7 @@ function renderCliOnlyCommandHelp(entry) {
233
270
  }
234
271
  function renderSharedCommandHelp(pathSegments) {
235
272
  const entry = SHARED_HELP_ENTRIES.find((candidate) => {
236
- const usageSegments = candidate.usage.split(" ").filter((segment) => !segment.startsWith("-") && !segment.startsWith("<") && !segment.startsWith("["));
273
+ const usageSegments = candidate.usage.split(" ").map((segment) => segment.replace(/^\[/, "").replace(/\]$/, "")).filter((segment) => !segment.startsWith("-") && !segment.startsWith("<") && !segment.startsWith("["));
237
274
  return usageSegments.length <= pathSegments.length && usageSegments.every((segment, index) => segment === pathSegments[index]);
238
275
  });
239
276
  if (!entry) {
@@ -469,33 +506,24 @@ function parseAnswerFlags(args) {
469
506
  function validateEnvAssignment(value) {
470
507
  const equalsIndex = value.indexOf("=");
471
508
  if (equalsIndex <= 0) {
472
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
473
- }
474
- const assignment = value.slice(0, equalsIndex);
475
- const slashIndex = assignment.indexOf("/");
476
- if (slashIndex <= 0) {
477
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
509
+ throw new Error(`Invalid env assignment '${value}'. Expected KEY=value`);
478
510
  }
479
- const target = assignment.slice(0, slashIndex);
480
- const key = assignment.slice(slashIndex + 1);
481
- if (target !== "backend" && target !== "frontend" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
482
- throw new Error(`Invalid env assignment '${value}'. Expected backend/KEY=value or frontend/KEY=value`);
511
+ const key = value.slice(0, equalsIndex);
512
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
513
+ throw new Error(`Invalid env assignment '${value}'. Expected KEY=value`);
483
514
  }
484
515
  }
485
516
  function parseEnvAssignment(value) {
486
517
  validateEnvAssignment(value);
487
518
  const equalsIndex = value.indexOf("=");
488
- const slashIndex = value.indexOf("/");
489
519
  return {
490
- target: value.slice(0, slashIndex),
491
- name: value.slice(slashIndex + 1, equalsIndex),
520
+ name: value.slice(0, equalsIndex),
492
521
  value: value.slice(equalsIndex + 1)
493
522
  };
494
523
  }
495
524
  function parseSetEnvArgs(args) {
496
525
  const parsed = {
497
526
  assignments: [],
498
- target: "backend",
499
527
  onlyMissing: false,
500
528
  includeEmpty: false
501
529
  };
@@ -525,23 +553,6 @@ function parseSetEnvArgs(args) {
525
553
  parsed.fromEnvFile = requireValue(inlineFile, "Missing value for --from-env-file");
526
554
  continue;
527
555
  }
528
- if (arg === "--target") {
529
- const target = requireValue(args[index + 1], "Missing value for --target");
530
- if (target !== "backend" && target !== "frontend") {
531
- throw new Error("Invalid --target. Expected backend or frontend");
532
- }
533
- parsed.target = target;
534
- index += 1;
535
- continue;
536
- }
537
- const inlineTarget = parseLongOptionWithEquals(arg, "--target");
538
- if (inlineTarget !== void 0) {
539
- if (inlineTarget !== "backend" && inlineTarget !== "frontend") {
540
- throw new Error("Invalid --target. Expected backend or frontend");
541
- }
542
- parsed.target = inlineTarget;
543
- continue;
544
- }
545
556
  if (arg === "--description") {
546
557
  parsed.description = requireValue(args[index + 1], "Missing value for --description");
547
558
  index += 1;
@@ -615,7 +626,7 @@ function parseEnvFlags(args) {
615
626
  if (arg.startsWith("-")) {
616
627
  throw new Error(`Unknown env flag: ${arg}`);
617
628
  }
618
- throw new Error(`Unexpected value '${arg}'. Use -e backend/KEY=value.`);
629
+ throw new Error(`Unexpected value '${arg}'. Use -e KEY=value.`);
619
630
  }
620
631
  if (envs.length === 0) {
621
632
  throw new Error("At least one -e/--env value is required.");
@@ -682,6 +693,67 @@ function parsePromptArgs(args) {
682
693
  message
683
694
  };
684
695
  }
696
+ function parseShellArgs(args) {
697
+ let command;
698
+ for (let index = 0; index < args.length; index += 1) {
699
+ const arg = args[index];
700
+ if (!arg) {
701
+ continue;
702
+ }
703
+ if (arg === "-c" || arg === "--command") {
704
+ if (command !== void 0) {
705
+ throw new Error("Use only one -c/--command value");
706
+ }
707
+ command = requireValue(args[index + 1], `Missing value for ${arg}`);
708
+ index += 1;
709
+ continue;
710
+ }
711
+ const inlineCommand = parseLongOptionWithEquals(arg, "--command");
712
+ if (inlineCommand !== void 0) {
713
+ if (command !== void 0) {
714
+ throw new Error("Use only one -c/--command value");
715
+ }
716
+ command = requireValue(inlineCommand, "Missing value for --command");
717
+ continue;
718
+ }
719
+ throw new Error(arg.startsWith("-") ? `Unknown shell flag: ${arg}` : `Unexpected shell value: ${arg}`);
720
+ }
721
+ return command === void 0 ? {} : { command };
722
+ }
723
+ function parsePsListArgs(args) {
724
+ const input = {};
725
+ const definitions = [
726
+ { flag: "--project", key: "project" },
727
+ { flag: "--branch", key: "branch" },
728
+ { flag: "--session", key: "session" },
729
+ { flag: "--worker", key: "worker" }
730
+ ];
731
+ const assign = (key, value, flag) => {
732
+ if (input[key] !== void 0) {
733
+ throw new Error(`Use only one ${flag} value`);
734
+ }
735
+ input[key] = requireValue(value, `Missing value for ${flag}`);
736
+ };
737
+ for (let index = 0; index < args.length; index += 1) {
738
+ const arg = args[index];
739
+ if (!arg) continue;
740
+ const definition = definitions.find(({ flag }) => arg === flag || arg.startsWith(`${flag}=`));
741
+ if (!definition) {
742
+ throw new Error(arg.startsWith("-") ? `Unknown ps list flag: ${arg}` : `Unexpected ps list value: ${arg}`);
743
+ }
744
+ if (arg === definition.flag) {
745
+ assign(definition.key, requireValue(args[index + 1], `Missing value for ${definition.flag}`), definition.flag);
746
+ index += 1;
747
+ continue;
748
+ }
749
+ assign(
750
+ definition.key,
751
+ requireValue(parseLongOptionWithEquals(arg, definition.flag), `Missing value for ${definition.flag}`),
752
+ definition.flag
753
+ );
754
+ }
755
+ return input;
756
+ }
685
757
  async function openInBrowser(url) {
686
758
  if (process.platform === "darwin") {
687
759
  spawn("open", [url], { stdio: "ignore", detached: true }).unref();
@@ -823,34 +895,66 @@ function renderSessionList(sessions) {
823
895
  function renderSessionDescription(session) {
824
896
  return [`Session: ${session.id}`, `Project: ${session.projectPath}`, `Branch: ${session.branchName}`].join("\n") + "\n";
825
897
  }
898
+ function formatProcessAge(startedAt, nowMs = Date.now()) {
899
+ const startedAtMs = Date.parse(startedAt);
900
+ if (!Number.isFinite(startedAtMs)) {
901
+ return "unknown";
902
+ }
903
+ const totalSeconds = Math.max(0, Math.floor((nowMs - startedAtMs) / 1e3));
904
+ if (totalSeconds < 60) {
905
+ return `${totalSeconds}s`;
906
+ }
907
+ if (totalSeconds < 60 * 60) {
908
+ const minutes = Math.floor(totalSeconds / 60);
909
+ const seconds = totalSeconds % 60;
910
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
911
+ }
912
+ if (totalSeconds < 24 * 60 * 60) {
913
+ const hours2 = Math.floor(totalSeconds / (60 * 60));
914
+ const minutes = Math.floor(totalSeconds % (60 * 60) / 60);
915
+ return `${hours2}h ${String(minutes).padStart(2, "0")}m`;
916
+ }
917
+ const days = Math.floor(totalSeconds / (24 * 60 * 60));
918
+ const hours = Math.floor(totalSeconds % (24 * 60 * 60) / (60 * 60));
919
+ return `${days}d ${hours}h`;
920
+ }
921
+ function renderProcessList(processes, nowMs = Date.now()) {
922
+ if (processes.length === 0) {
923
+ return "No active processes.\n";
924
+ }
925
+ const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "PROJECT", "BRANCH", "SESSION", "COMMAND"];
926
+ const rows = processes.map((process2) => [
927
+ process2.runId,
928
+ process2.status,
929
+ formatProcessAge(process2.startedAt, nowMs),
930
+ process2.workerLabel,
931
+ process2.projectPath,
932
+ process2.branchName,
933
+ process2.sessionId,
934
+ process2.command
935
+ ]);
936
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)));
937
+ const formatRow = (row) => row.map((value, column) => column === row.length - 1 ? value : value.padEnd(widths[column] ?? value.length)).join(" ");
938
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
939
+ return `${[formatRow(headers), separator, ...rows.map(formatRow)].join("\n")}
940
+ `;
941
+ }
826
942
  function summarizeEnvData(data) {
827
- const summary = {};
828
- for (const target of ["backend", "frontend"]) {
829
- const entries = data[target];
830
- if (!entries) continue;
831
- summary[target] = Object.fromEntries(
832
- Object.entries(entries).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
833
- name,
834
- {
835
- optional: env.optional ?? false,
836
- description: env.description,
837
- hasValue: env.value !== null
838
- }
839
- ])
840
- );
841
- }
842
- return summary;
943
+ return Object.fromEntries(
944
+ Object.entries(data).sort(([left], [right]) => left.localeCompare(right)).map(([name, env]) => [
945
+ name,
946
+ {
947
+ optional: env.optional ?? false,
948
+ description: env.description,
949
+ hasValue: env.value !== null
950
+ }
951
+ ])
952
+ );
843
953
  }
844
954
  function renderEnvSummary(data) {
845
955
  const lines = [];
846
- for (const target of ["backend", "frontend"]) {
847
- const entries = Object.entries(data[target] ?? {});
848
- if (entries.length === 0) continue;
849
- if (lines.length > 0) lines.push("");
850
- lines.push(`${target}:`);
851
- for (const [name, env] of entries) {
852
- lines.push(` ${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
853
- }
956
+ for (const [name, env] of Object.entries(data)) {
957
+ lines.push(`${name} ${env.hasValue ? "set" : "missing"} ${env.optional ? "optional" : "required"} ${env.description}`);
854
958
  }
855
959
  return lines.length > 0 ? `${lines.join("\n")}
856
960
  ` : "No environment variables declared.\n";
@@ -862,7 +966,7 @@ function renderEnvSetResult(result) {
862
966
  if (result.skippedExisting.length > 0) lines.push(`Skipped ${result.skippedExisting.length} existing value(s).`);
863
967
  if (result.skippedEmpty.length > 0) lines.push(`Skipped ${result.skippedEmpty.length} empty value(s).`);
864
968
  if (result.updated.length > 0) {
865
- lines.push(...result.updated.map(({ target, name }) => ` ${target}/${name}`));
969
+ lines.push(...result.updated.map(({ name }) => ` ${name}`));
866
970
  }
867
971
  return `${lines.join("\n")}
868
972
  `;
@@ -875,10 +979,10 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
875
979
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
876
980
  throw new Error(`Invalid environment variable name in dotenv file: ${name}`);
877
981
  }
878
- return { target: options.target, name, value };
982
+ return { name, value };
879
983
  }) : options.assignments.map(parseEnvAssignment);
880
984
  const uniqueAssignments = /* @__PURE__ */ new Map();
881
- for (const assignment of assignments) uniqueAssignments.set(`${assignment.target}/${assignment.name}`, assignment);
985
+ for (const assignment of assignments) uniqueAssignments.set(assignment.name, assignment);
882
986
  const update = {};
883
987
  const result = {
884
988
  success: true,
@@ -891,12 +995,12 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
891
995
  const missingDescriptions = [];
892
996
  const importedDescription = options.fromEnvFile ? `Imported from ${path.basename(options.fromEnvFile)}.` : void 0;
893
997
  for (const assignment of uniqueAssignments.values()) {
894
- const identity = { target: assignment.target, name: assignment.name };
998
+ const identity = { name: assignment.name };
895
999
  if (!options.includeEmpty && assignment.value.length === 0) {
896
1000
  result.skippedEmpty.push(identity);
897
1001
  continue;
898
1002
  }
899
- const existing = current[assignment.target]?.[assignment.name];
1003
+ const existing = current[assignment.name];
900
1004
  if (options.onlyMissing && existing?.value !== null && existing?.value !== void 0) {
901
1005
  result.skippedExisting.push(identity);
902
1006
  continue;
@@ -906,9 +1010,8 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
906
1010
  missingDescriptions.push(identity);
907
1011
  continue;
908
1012
  }
909
- const targetUpdate = update[assignment.target] ??= {};
910
1013
  const optional = options.optional ?? existing?.optional;
911
- targetUpdate[assignment.name] = {
1014
+ update[assignment.name] = {
912
1015
  description,
913
1016
  value: assignment.value,
914
1017
  ...optional === void 0 ? {} : { optional }
@@ -916,9 +1019,7 @@ async function setBranchEnvs(client, projectRef, branchName, args) {
916
1019
  result.updated.push(identity);
917
1020
  }
918
1021
  if (missingDescriptions.length > 0) {
919
- throw new Error(
920
- `--description is required when declaring new envs with -e: ${missingDescriptions.map(({ target, name }) => `${target}/${name}`).join(", ")}`
921
- );
1022
+ throw new Error(`--description is required when declaring new envs with -e: ${missingDescriptions.map(({ name }) => name).join(", ")}`);
922
1023
  }
923
1024
  if (result.updated.length > 0) {
924
1025
  await client.projects.env.update(project.id, branchName, update);
@@ -1083,6 +1184,17 @@ async function executeR5dctlCommand(client, json, args) {
1083
1184
  write(result, "API key revoked.\n");
1084
1185
  return;
1085
1186
  }
1187
+ if (first === "ps" && second === "list") {
1188
+ const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
1189
+ write(processes, renderProcessList(processes));
1190
+ return;
1191
+ }
1192
+ if (first === "ps" && second === "stop") {
1193
+ const process2 = await client.processes.stop(requireValue(args[2], "Missing process run id"));
1194
+ write(process2, `Stopped process ${process2.runId}: ${process2.command}
1195
+ `);
1196
+ return;
1197
+ }
1086
1198
  if (first === "projects" && second === "list" || first === "get" && second === "projects") {
1087
1199
  const projects = await client.projects.list();
1088
1200
  write(projects, renderProjectList(projects));
@@ -1278,6 +1390,12 @@ function resolveCommandExecution(options, rest) {
1278
1390
  throw new Error("Missing command");
1279
1391
  }
1280
1392
  const commandArgs = normalizedRest.slice(1);
1393
+ if (command === "ps" && commandArgs.length === 0) {
1394
+ return {
1395
+ kind: "cli-help",
1396
+ text: PS_HELP_TEXT
1397
+ };
1398
+ }
1281
1399
  if (trailingHelp) {
1282
1400
  const cliOnlyHelp = findCliOnlyCommandHelp(normalizedRest);
1283
1401
  if (cliOnlyHelp) {
@@ -1351,6 +1469,36 @@ function resolveCommandExecution(options, rest) {
1351
1469
  }
1352
1470
  throw new Error("Unknown auth command");
1353
1471
  }
1472
+ if (command === "ps") {
1473
+ const subcommand = commandArgs[0];
1474
+ if (subcommand === "list") {
1475
+ const commandFilters = parsePsListArgs(commandArgs.slice(1));
1476
+ const filters = {
1477
+ ...commandFilters,
1478
+ ...options.project ? { project: options.project } : {},
1479
+ ...options.branch ? { branch: options.branch } : {},
1480
+ ...options.session ? { session: options.session } : {}
1481
+ };
1482
+ const pluginArgs = ["ps", "list"];
1483
+ for (const [flag, value] of [
1484
+ ["--project", filters.project],
1485
+ ["--branch", filters.branch],
1486
+ ["--session", filters.session],
1487
+ ["--worker", filters.worker]
1488
+ ]) {
1489
+ if (value) pluginArgs.push(flag, value);
1490
+ }
1491
+ return { kind: "plugin", pluginArgs };
1492
+ }
1493
+ if (subcommand === "stop") {
1494
+ const runId = requireValue(commandArgs[1], "Missing process run id");
1495
+ if (commandArgs.length > 2) {
1496
+ throw new Error(`Unexpected ps stop value: ${commandArgs[2]}`);
1497
+ }
1498
+ return { kind: "plugin", pluginArgs: ["ps", "stop", runId] };
1499
+ }
1500
+ throw new Error(`Unknown ps command: ${subcommand ?? ""}`.trim());
1501
+ }
1354
1502
  if (command === "get") {
1355
1503
  const target = commandArgs[0];
1356
1504
  if (target === "projects") {
@@ -1530,6 +1678,21 @@ function resolveCommandExecution(options, rest) {
1530
1678
  pluginArgs: ["conversation", sessionId, ...commandArgs]
1531
1679
  };
1532
1680
  }
1681
+ if (command === "shell") {
1682
+ if (!options.project) {
1683
+ throw new Error("--project/-p is required for `shell`");
1684
+ }
1685
+ if (!options.branch) {
1686
+ throw new Error("--branch/-b is required for `shell`");
1687
+ }
1688
+ if (options.json) {
1689
+ throw new Error("--json is not supported for `shell`");
1690
+ }
1691
+ return {
1692
+ kind: "shell",
1693
+ ...parseShellArgs(commandArgs)
1694
+ };
1695
+ }
1533
1696
  if (command === "prompt") {
1534
1697
  const sessionId = options.session;
1535
1698
  if (!sessionId) {
@@ -1653,31 +1816,43 @@ async function runCommand(argv) {
1653
1816
  const { options, rest } = parseGlobalArgs(argv);
1654
1817
  if (options.version) {
1655
1818
  printVersion();
1656
- return;
1819
+ return 0;
1657
1820
  }
1658
1821
  if (options.help || rest.length === 0) {
1659
1822
  printHelp();
1660
- return;
1823
+ return 0;
1661
1824
  }
1662
1825
  const config = parseConfig(options.configPath);
1663
- const client = new R5dctlClient(resolveClientOptions(options, config));
1826
+ const clientOptions = resolveClientOptions(options, config);
1827
+ const client = new R5dctlClient(clientOptions);
1664
1828
  const execution = resolveCommandExecution(options, rest);
1665
1829
  if (execution.kind === "auth-login") {
1666
1830
  await handleAuthLogin(client, options, execution.commandArgs, config);
1667
- return;
1831
+ return 0;
1668
1832
  }
1669
1833
  if (execution.kind === "auth-api-key-create") {
1670
1834
  await handleAuthApiKeyCreate(client, options, config, execution.commandArgs);
1671
- return;
1835
+ return 0;
1672
1836
  }
1673
1837
  if (execution.kind === "cli-help") {
1674
1838
  process.stdout.write(execution.text);
1675
- return;
1839
+ return 0;
1840
+ }
1841
+ if (execution.kind === "shell") {
1842
+ const project = await client.projects.describe(options.project);
1843
+ return await runR5dctlShell({
1844
+ baseUrl: clientOptions.baseUrl ?? "https://r5d.dev",
1845
+ credential: clientOptions.token ?? clientOptions.apiKey ?? "",
1846
+ projectId: project.id,
1847
+ branchName: options.branch,
1848
+ command: execution.command
1849
+ });
1676
1850
  }
1677
1851
  await executeR5dctlCommand(client, options.json, execution.pluginArgs);
1678
1852
  if (execution.clearAuthOnSuccess) {
1679
1853
  writeConfig(options.configPath, clearAuthFromConfig(config, options));
1680
1854
  }
1855
+ return 0;
1681
1856
  }
1682
1857
  function extractApiErrorMessage(error) {
1683
1858
  if (typeof error.body === "object" && error.body !== null) {
@@ -1690,8 +1865,7 @@ function extractApiErrorMessage(error) {
1690
1865
  }
1691
1866
  async function runR5dctlCli(argv) {
1692
1867
  try {
1693
- await runCommand(argv);
1694
- return 0;
1868
+ return await runCommand(argv);
1695
1869
  } catch (error) {
1696
1870
  if (error instanceof R5dctlApiError) {
1697
1871
  process.stderr.write(`${extractApiErrorMessage(error)}
@@ -1706,10 +1880,11 @@ async function runR5dctlCli(argv) {
1706
1880
  async function main(argv = process.argv.slice(2)) {
1707
1881
  const exitCode = await runR5dctlCli(argv);
1708
1882
  if (exitCode !== 0) {
1709
- process.exit(exitCode);
1883
+ process.exitCode = exitCode;
1710
1884
  }
1711
1885
  }
1712
1886
  export {
1887
+ formatProcessAge,
1713
1888
  getCliHelpText,
1714
1889
  getR5dctlVersion,
1715
1890
  main,
@@ -1718,9 +1893,12 @@ export {
1718
1893
  parseEnvFlags,
1719
1894
  parseGlobalArgs,
1720
1895
  parsePromptArgs,
1896
+ parsePsListArgs,
1721
1897
  parseSetEnvArgs,
1898
+ parseShellArgs,
1722
1899
  readDotenvFile,
1723
1900
  renderConversationResponse,
1901
+ renderProcessList,
1724
1902
  resolveCommandExecution,
1725
1903
  runR5dctlCli,
1726
1904
  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": "module"
5
5
  }