@ricsam/r5dctl 0.0.72 → 0.0.74

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
@@ -93,6 +93,8 @@ r5dctl session stop <session-id>
93
93
  r5dctl -p <project> merge fix/tests
94
94
  r5dctl -p <project> merge --worker <label> --create-conflict-worktree fix/tests
95
95
  r5dctl -p <project> merge --worker <label> --launch-conflict-resolution-agent fix/tests
96
+ r5dctl -p <project> merge --worker <label> --launch-conflict-resolution-agent --no-wait fix/tests
97
+ r5dctl -p <project> merge status <attempt-id>
96
98
  r5dctl -p <project> merge finish --worker <label> conflicts/fix-tests --summary "Resolved the conflicts"
97
99
 
98
100
  r5dctl -p <project> -b <branch> shell
@@ -107,14 +109,28 @@ r5dctl k8s usage
107
109
  r5dctl k8s usage --namespace default --workload api --container server
108
110
  r5dctl k8s usage --window 7d
109
111
  r5dctl k8s usage --workload default/api --window 7d --json
112
+ r5dctl vcluster trusted-workflow add --repo owner/repository --workflow .github/workflows/deploy.yml
113
+ r5dctl vcluster trusted-workflow list
114
+ r5dctl vcluster trusted-workflow remove <workflow-id>
110
115
  ```
111
116
 
117
+ Trusted workflows authenticate Kubernetes through the dedicated
118
+ `ricsam/r5d-cluster-auth` GitHub Action. They do not run or authenticate
119
+ `r5dctl` inside GitHub Actions, and no R5D secret needs to be stored in the
120
+ repository. Trust applies to that workflow path on every Git ref and grants
121
+ cluster-admin, so protect workflow changes and do not invoke it for untrusted
122
+ pull requests.
123
+
112
124
  `session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named worktree from an explicit `worktree:<branch>` source. It requires project, worker, model, and prompt and never infers them from the current shell. Starts and prompts return after durable enqueue while execution continues asynchronously. Task specialization belongs in the prompt; there are no predefined debug, explore, research, or test agents. `session stop` returns as soon as the agent run is stopped; the stopped worktree's workspace publication finishes asynchronously behind the response, and `merge` republishes at its own boundary before reading the worktree.
113
125
 
114
126
  Add `-f`/`--follow`/`--watch` to `session start` to attach after enqueue, or to `conversation` to print the current transcript and follow new output. Assistant text is written to stdout and lifecycle/tool progress to stderr; `--tools` includes tool calls and results. With global `--json`, follow mode emits one JSON event per line. Ctrl-C only detaches and never stops the session. The CLI reconnects transiently interrupted event streams and resynchronizes committed conversation revisions without repeating assistant text.
115
127
 
116
128
  `merge` merges a managed source worktree into its recorded parent. By default conflicts are reported without creating more work. `--create-conflict-worktree` prepares a manual conflict worktree, while `--launch-conflict-resolution-agent` starts the dedicated merge-conflict resolver; those strategies are mutually exclusive. A worker-bound shell credential supplies its own worker automatically; otherwise pass `--worker`. Finish a reviewed manual resolution with `merge finish`.
117
129
 
130
+ Merging is lockless and optimistic: nothing blocks agents from continuing to work on the involved branches. Every merge pins the branch tips it previewed and publishes through an atomic compare-and-swap, and a concurrent publication simply retries. When a resolver agent finishes after the target advanced, the platform automatically continues the merge — publishing directly if the refreshed merge is conflict-free, or launching a fresh resolver attempt (whose prompt references the superseded session's resolution) — repeating until one resolution publishes. Superseded attempts surface the follow-up attempt id in `merge status`. Only manual conflict worktrees stop at `stale`: re-run `merge` to start a fresh resolution.
131
+
132
+ The server answers merge requests as soon as the result is durably recorded; the slow per-member workspace propagation always completes asynchronously behind the response. For a clean merge that means the merged commit is already published when the command returns. For a conflict strategy the server responds with the merge attempt in a `provisioning` state and prepares the conflict worktree (and starts the resolver agent) in the background; the CLI polls the attempt until the worktree is ready — and, for the agent strategy, until the resolver agent is running — before printing the final result. Pass `--no-wait` to skip the wait and get the attempt id immediately, and use `merge status <attempt-id>` at any time to re-check a provisioning, failed, or completed attempt (a provisioning failure surfaces there as a `failed` attempt with its reason). Ctrl-C during the wait only detaches the CLI; provisioning continues server-side.
133
+
118
134
  `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.
119
135
 
120
136
  `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.
package/dist/cjs/cli.cjs CHANGED
@@ -42,6 +42,7 @@ __export(cli_exports, {
42
42
  getR5dctlVersion: () => getR5dctlVersion,
43
43
  getTransientDevicePollDelay: () => getTransientDevicePollDelay,
44
44
  handleAuthLogin: () => handleAuthLogin,
45
+ isMergeAttemptSettled: () => isMergeAttemptSettled,
45
46
  main: () => main,
46
47
  parseAnswerEnvRequestCommandArgs: () => parseAnswerEnvRequestCommandArgs,
47
48
  parseAnswerFlags: () => parseAnswerFlags,
@@ -96,16 +97,8 @@ var import_promises = require("node:timers/promises");
96
97
  var import_dotenv = require("dotenv");
97
98
  var import_r5d_api = require("@ricsam/r5d-api");
98
99
  var import_shell = require("./shell.cjs");
99
- const CHAT_MODES = /* @__PURE__ */ new Set([
100
- "ask",
101
- "plan",
102
- "build",
103
- "agent",
104
- "security_review",
105
- "large_diff_remediation",
106
- "merge_conflict_resolution"
107
- ]);
108
- const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "build", "agent"]);
100
+ const CHAT_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent", "large_diff_remediation", "merge_conflict_resolution"]);
101
+ const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent"]);
109
102
  const MODEL_TIERS = /* @__PURE__ */ new Set(["low", "medium", "high", "max"]);
110
103
  const R5DCTL_PACKAGE_NAME = "@ricsam/r5dctl";
111
104
  const CLI_GLOBAL_OPTION_HELP = [
@@ -156,6 +149,9 @@ const PS_HELP_TEXT = [
156
149
  const K8S_HELP_TEXT = [
157
150
  "Usage:",
158
151
  " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
152
+ " r5dctl vcluster trusted-workflow add --repo <owner/repository> --workflow <.github/workflows/file.yml>",
153
+ " r5dctl vcluster trusted-workflow list",
154
+ " r5dctl vcluster trusted-workflow remove <workflow-id>",
159
155
  "",
160
156
  "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
161
157
  ""
@@ -174,10 +170,12 @@ const SESSION_RUN_HELP_TEXT = [
174
170
  ].join("\n");
175
171
  const MERGE_HELP_TEXT = [
176
172
  "Usage:",
177
- " r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
173
+ " r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
174
+ " r5dctl -p <project> merge status <attempt-id>",
178
175
  ' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
179
176
  "",
180
177
  "Merge a managed worktree into its recorded parent worktree.",
178
+ "Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
181
179
  ""
182
180
  ].join("\n");
183
181
  const SHARED_HELP_ENTRIES = [
@@ -310,6 +308,17 @@ const SHARED_HELP_ENTRIES = [
310
308
  usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
311
309
  description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
312
310
  },
311
+ {
312
+ section: "kubernetes",
313
+ usage: "vcluster trusted-workflow add --repo <owner/repository> --workflow <.github/workflows/file.yml>",
314
+ description: "Trust an exact GitHub Actions workflow path to authenticate kubectl against your managed vCluster."
315
+ },
316
+ { section: "kubernetes", usage: "vcluster trusted-workflow list", description: "List trusted GitHub Actions workflows." },
317
+ {
318
+ section: "kubernetes",
319
+ usage: "vcluster trusted-workflow remove <workflow-id>",
320
+ description: "Revoke a trusted GitHub Actions workflow and its outstanding credentials."
321
+ },
313
322
  {
314
323
  section: "processes",
315
324
  usage: "ps list [--worker <label>]",
@@ -328,9 +337,14 @@ const SHARED_HELP_ENTRIES = [
328
337
  { section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
329
338
  {
330
339
  section: "merge",
331
- usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
340
+ usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
332
341
  description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
333
342
  },
343
+ {
344
+ section: "merge",
345
+ usage: "-p <project> merge status <attempt-id>",
346
+ description: "Show a merge attempt's provisioning and resolution status."
347
+ },
334
348
  {
335
349
  section: "merge",
336
350
  usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
@@ -567,7 +581,7 @@ function requireModel(value) {
567
581
  function requireSessionMode(value) {
568
582
  const mode = requireValue(value?.trim(), "--mode <mode> is required");
569
583
  if (!R5DCTL_SESSION_MODES.has(mode)) {
570
- throw new Error("Invalid --mode. Expected one of: ask, plan, build, agent");
584
+ throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
571
585
  }
572
586
  return mode;
573
587
  }
@@ -653,12 +667,19 @@ function parseMergeCommand(options, args) {
653
667
  ...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
654
668
  };
655
669
  }
670
+ if (args[0] === "status") {
671
+ const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
672
+ const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
673
+ if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
674
+ return { kind: "status", project, attemptId };
675
+ }
656
676
  const parsed = parseCommandOperands(
657
677
  args,
658
678
  /* @__PURE__ */ new Set(["--worker"]),
659
679
  /* @__PURE__ */ new Map([
660
680
  ["--create-conflict-worktree", "--create-conflict-worktree"],
661
- ["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
681
+ ["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
682
+ ["--no-wait", "--no-wait"]
662
683
  ]),
663
684
  "merge"
664
685
  );
@@ -671,7 +692,14 @@ function parseMergeCommand(options, args) {
671
692
  }
672
693
  const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
673
694
  const worker = parsed.values.get("--worker")?.trim();
674
- return { kind: "merge", project, sourceWorktree, conflictMode, ...worker ? { worker } : {} };
695
+ return {
696
+ kind: "merge",
697
+ project,
698
+ sourceWorktree,
699
+ conflictMode,
700
+ ...worker ? { worker } : {},
701
+ ...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
702
+ };
675
703
  }
676
704
  function assertAuthLoginArgs(args) {
677
705
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
@@ -1959,17 +1987,22 @@ function renderSessionRunStatus(session) {
1959
1987
  function renderWorktreeMerge(result, input) {
1960
1988
  const lines = [];
1961
1989
  appendMergeDetails(lines, result);
1990
+ const status = responseString(result, "status");
1962
1991
  const conflictWorktree = responseString(result, "conflictWorktree");
1963
1992
  const resolverSessionId = responseString(result, "resolverSessionId");
1993
+ const attemptId = responseString(result, "attemptId");
1964
1994
  const worker = responseString(result, "worker") ?? input.worker;
1965
1995
  if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
1966
- if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(responseString(result, "status") ?? "")) {
1996
+ if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
1967
1997
  lines.push(
1968
1998
  "",
1969
1999
  "Finish after resolving and reviewing the conflict worktree:",
1970
2000
  ` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
1971
2001
  );
1972
2002
  }
2003
+ if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
2004
+ lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
2005
+ }
1973
2006
  return `${lines.join("\n")}
1974
2007
  `;
1975
2008
  }
@@ -2009,13 +2042,64 @@ async function dispatchSessionRunCommand(client, command) {
2009
2042
  const data = await client.sessions.stop(command.sessionId);
2010
2043
  return { data, human: renderSessionRunStatus(data) };
2011
2044
  }
2012
- async function dispatchMergeCommand(client, command) {
2045
+ const MERGE_POLL_INTERVAL_MS = 2e3;
2046
+ const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
2047
+ const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
2048
+ function isMergeAttemptSettled(response) {
2049
+ const status = responseString(response, "status");
2050
+ if (!status) return true;
2051
+ if (status === "resolving") return response.resolverAgentRunning !== false;
2052
+ return status !== "provisioning";
2053
+ }
2054
+ async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
2055
+ const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
2056
+ const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
2057
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2058
+ const onProgress = options.onProgress ?? (() => {
2059
+ });
2060
+ let latest = initial;
2061
+ let worktreeReadyReported = latest.worktreeReady === true;
2062
+ let consecutiveFailures = 0;
2063
+ let waitedMs = 0;
2064
+ while (!isMergeAttemptSettled(latest)) {
2065
+ if (waitedMs >= timeoutMs) {
2066
+ onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
2067
+ return latest;
2068
+ }
2069
+ await sleep2(intervalMs);
2070
+ waitedMs += intervalMs;
2071
+ try {
2072
+ latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
2073
+ consecutiveFailures = 0;
2074
+ } catch (error) {
2075
+ consecutiveFailures += 1;
2076
+ if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
2077
+ continue;
2078
+ }
2079
+ if (!worktreeReadyReported && latest.worktreeReady === true) {
2080
+ worktreeReadyReported = true;
2081
+ onProgress("Conflict worktree ready.");
2082
+ }
2083
+ }
2084
+ return latest;
2085
+ }
2086
+ async function dispatchMergeCommand(client, command, pollOptions = {}) {
2013
2087
  if (command.kind === "merge") {
2014
2088
  const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
2015
2089
  conflictMode: command.conflictMode,
2016
2090
  ...command.worker ? { worker: command.worker } : {}
2017
2091
  });
2018
- return { data: data2, human: renderWorktreeMerge(data2, command) };
2092
+ const attemptId = responseString(data2, "attemptId");
2093
+ if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
2094
+ return { data: data2, human: renderWorktreeMerge(data2, command) };
2095
+ }
2096
+ pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
2097
+ const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
2098
+ return { data: settled, human: renderWorktreeMerge(settled, command) };
2099
+ }
2100
+ if (command.kind === "status") {
2101
+ const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
2102
+ return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
2019
2103
  }
2020
2104
  const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
2021
2105
  worker: command.worker,
@@ -2572,6 +2656,40 @@ async function executeR5dctlCommand(client, json, args) {
2572
2656
  write(report, renderK8sUsageReport(report));
2573
2657
  return;
2574
2658
  }
2659
+ if (first === "vcluster" && second === "trusted-workflow") {
2660
+ const subcommand = requireValue(args[2], "Missing trusted-workflow command");
2661
+ if (subcommand === "add") {
2662
+ const parsed = parseCommandOperands(args.slice(3), /* @__PURE__ */ new Set(["--repo", "--workflow"]), /* @__PURE__ */ new Map(), "trusted-workflow add");
2663
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected trusted-workflow add value: ${parsed.positionals[0]}`);
2664
+ const workflow = await client.k8s.trustedWorkflows.add({
2665
+ repository: requireValue(parsed.values.get("--repo"), "--repo <owner/repository> is required"),
2666
+ workflowPath: requireValue(parsed.values.get("--workflow"), "--workflow <path> is required")
2667
+ });
2668
+ write(workflow, "Trusted GitHub workflow configured successfully.\n");
2669
+ return;
2670
+ }
2671
+ if (subcommand === "list") {
2672
+ if (args.length > 3) throw new Error(`Unexpected trusted-workflow list value: ${args[3]}`);
2673
+ const workflows = await client.k8s.trustedWorkflows.list();
2674
+ write(
2675
+ workflows,
2676
+ workflows.length === 0 ? "No trusted GitHub workflows.\n" : `${workflows.map(
2677
+ (workflow) => `${workflow.id} ${workflow.repository} ${workflow.workflowPath} ${workflow.revokedAt ? "revoked" : "active"}`
2678
+ ).join("\n")}
2679
+ `
2680
+ );
2681
+ return;
2682
+ }
2683
+ if (subcommand === "remove") {
2684
+ const workflowId = requireValue(args[3], "Missing trusted workflow id");
2685
+ if (args.length > 4) throw new Error(`Unexpected trusted-workflow remove value: ${args[4]}`);
2686
+ const result = await client.k8s.trustedWorkflows.revoke(workflowId);
2687
+ if (!result.success) throw new Error("Trusted GitHub workflow was not found or was already revoked");
2688
+ write(result, "Trusted GitHub workflow revoked.\n");
2689
+ return;
2690
+ }
2691
+ throw new Error(`Unknown trusted-workflow command: ${subcommand}`);
2692
+ }
2575
2693
  if (first === "ps" && second === "list") {
2576
2694
  const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
2577
2695
  write(processes, renderProcessList(processes));
@@ -2941,12 +3059,23 @@ function resolveCommandExecution(options, rest) {
2941
3059
  }
2942
3060
  if (command === "k8s") {
2943
3061
  const subcommand = commandArgs[0];
2944
- if (subcommand !== "usage") throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
2945
3062
  if (options.project || options.branch || options.session) {
2946
- throw new Error("Project, branch, and session scopes are not supported for `k8s usage`");
3063
+ throw new Error("Project, branch, and session scopes are not supported for `k8s`");
3064
+ }
3065
+ if (subcommand === "usage") {
3066
+ parseK8sUsageArgs(commandArgs.slice(1));
3067
+ return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
3068
+ }
3069
+ throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
3070
+ }
3071
+ if (command === "vcluster") {
3072
+ if (options.project || options.branch || options.session) {
3073
+ throw new Error("Project, branch, and session scopes are not supported for `vcluster`");
3074
+ }
3075
+ if (commandArgs[0] !== "trusted-workflow") {
3076
+ throw new Error(`Unknown vcluster command: ${commandArgs[0] ?? ""}`.trim());
2947
3077
  }
2948
- parseK8sUsageArgs(commandArgs.slice(1));
2949
- return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
3078
+ return { kind: "plugin", pluginArgs: ["vcluster", "trusted-workflow", ...commandArgs.slice(1)] };
2950
3079
  }
2951
3080
  if (command === "get") {
2952
3081
  const target = commandArgs[0];
@@ -3284,7 +3413,18 @@ async function runCommand(argv) {
3284
3413
  });
3285
3414
  }
3286
3415
  if (execution.kind === "merge") {
3287
- const result = await dispatchMergeCommand(client, execution.command);
3416
+ if (!options.json) {
3417
+ if (execution.command.kind === "merge") {
3418
+ process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
3419
+ } else if (execution.command.kind === "finish") {
3420
+ process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
3421
+ `);
3422
+ }
3423
+ }
3424
+ const result = await dispatchMergeCommand(client, execution.command, {
3425
+ ...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
3426
+ `) }
3427
+ });
3288
3428
  writeDataOutput(options.json, result.data, result.human);
3289
3429
  return 0;
3290
3430
  }
@@ -3360,6 +3500,7 @@ async function main(argv = process.argv.slice(2)) {
3360
3500
  getR5dctlVersion,
3361
3501
  getTransientDevicePollDelay,
3362
3502
  handleAuthLogin,
3503
+ isMergeAttemptSettled,
3363
3504
  main,
3364
3505
  parseAnswerEnvRequestCommandArgs,
3365
3506
  parseAnswerFlags,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.72",
3
+ "version": "0.0.74",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -11,16 +11,8 @@ import {
11
11
  R5dctlClient
12
12
  } from "@ricsam/r5d-api";
13
13
  import { runR5dctlShell } from "./shell.mjs";
14
- const CHAT_MODES = /* @__PURE__ */ new Set([
15
- "ask",
16
- "plan",
17
- "build",
18
- "agent",
19
- "security_review",
20
- "large_diff_remediation",
21
- "merge_conflict_resolution"
22
- ]);
23
- const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "build", "agent"]);
14
+ const CHAT_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent", "large_diff_remediation", "merge_conflict_resolution"]);
15
+ const R5DCTL_SESSION_MODES = /* @__PURE__ */ new Set(["ask", "plan", "agent"]);
24
16
  const MODEL_TIERS = /* @__PURE__ */ new Set(["low", "medium", "high", "max"]);
25
17
  const R5DCTL_PACKAGE_NAME = "@ricsam/r5dctl";
26
18
  const CLI_GLOBAL_OPTION_HELP = [
@@ -71,6 +63,9 @@ const PS_HELP_TEXT = [
71
63
  const K8S_HELP_TEXT = [
72
64
  "Usage:",
73
65
  " r5dctl k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
66
+ " r5dctl vcluster trusted-workflow add --repo <owner/repository> --workflow <.github/workflows/file.yml>",
67
+ " r5dctl vcluster trusted-workflow list",
68
+ " r5dctl vcluster trusted-workflow remove <workflow-id>",
74
69
  "",
75
70
  "Show current and rolling CPU and memory usage for managed-vCluster workload containers.",
76
71
  ""
@@ -89,10 +84,12 @@ const SESSION_RUN_HELP_TEXT = [
89
84
  ].join("\n");
90
85
  const MERGE_HELP_TEXT = [
91
86
  "Usage:",
92
- " r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
87
+ " r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
88
+ " r5dctl -p <project> merge status <attempt-id>",
93
89
  ' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
94
90
  "",
95
91
  "Merge a managed worktree into its recorded parent worktree.",
92
+ "Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
96
93
  ""
97
94
  ].join("\n");
98
95
  const SHARED_HELP_ENTRIES = [
@@ -225,6 +222,17 @@ const SHARED_HELP_ENTRIES = [
225
222
  usage: "k8s usage [--namespace <name>] [--workload <name|namespace/name|uid>] [--container <name>] [--window <24h|7d>]",
226
223
  description: "Show current and rolling CPU and memory usage for managed-vCluster workload containers."
227
224
  },
225
+ {
226
+ section: "kubernetes",
227
+ usage: "vcluster trusted-workflow add --repo <owner/repository> --workflow <.github/workflows/file.yml>",
228
+ description: "Trust an exact GitHub Actions workflow path to authenticate kubectl against your managed vCluster."
229
+ },
230
+ { section: "kubernetes", usage: "vcluster trusted-workflow list", description: "List trusted GitHub Actions workflows." },
231
+ {
232
+ section: "kubernetes",
233
+ usage: "vcluster trusted-workflow remove <workflow-id>",
234
+ description: "Revoke a trusted GitHub Actions workflow and its outstanding credentials."
235
+ },
228
236
  {
229
237
  section: "processes",
230
238
  usage: "ps list [--worker <label>]",
@@ -243,9 +251,14 @@ const SHARED_HELP_ENTRIES = [
243
251
  { section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
244
252
  {
245
253
  section: "merge",
246
- usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
254
+ usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
247
255
  description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
248
256
  },
257
+ {
258
+ section: "merge",
259
+ usage: "-p <project> merge status <attempt-id>",
260
+ description: "Show a merge attempt's provisioning and resolution status."
261
+ },
249
262
  {
250
263
  section: "merge",
251
264
  usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
@@ -482,7 +495,7 @@ function requireModel(value) {
482
495
  function requireSessionMode(value) {
483
496
  const mode = requireValue(value?.trim(), "--mode <mode> is required");
484
497
  if (!R5DCTL_SESSION_MODES.has(mode)) {
485
- throw new Error("Invalid --mode. Expected one of: ask, plan, build, agent");
498
+ throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
486
499
  }
487
500
  return mode;
488
501
  }
@@ -568,12 +581,19 @@ function parseMergeCommand(options, args) {
568
581
  ...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
569
582
  };
570
583
  }
584
+ if (args[0] === "status") {
585
+ const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
586
+ const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
587
+ if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
588
+ return { kind: "status", project, attemptId };
589
+ }
571
590
  const parsed = parseCommandOperands(
572
591
  args,
573
592
  /* @__PURE__ */ new Set(["--worker"]),
574
593
  /* @__PURE__ */ new Map([
575
594
  ["--create-conflict-worktree", "--create-conflict-worktree"],
576
- ["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
595
+ ["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
596
+ ["--no-wait", "--no-wait"]
577
597
  ]),
578
598
  "merge"
579
599
  );
@@ -586,7 +606,14 @@ function parseMergeCommand(options, args) {
586
606
  }
587
607
  const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
588
608
  const worker = parsed.values.get("--worker")?.trim();
589
- return { kind: "merge", project, sourceWorktree, conflictMode, ...worker ? { worker } : {} };
609
+ return {
610
+ kind: "merge",
611
+ project,
612
+ sourceWorktree,
613
+ conflictMode,
614
+ ...worker ? { worker } : {},
615
+ ...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
616
+ };
590
617
  }
591
618
  function assertAuthLoginArgs(args) {
592
619
  const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
@@ -1874,17 +1901,22 @@ function renderSessionRunStatus(session) {
1874
1901
  function renderWorktreeMerge(result, input) {
1875
1902
  const lines = [];
1876
1903
  appendMergeDetails(lines, result);
1904
+ const status = responseString(result, "status");
1877
1905
  const conflictWorktree = responseString(result, "conflictWorktree");
1878
1906
  const resolverSessionId = responseString(result, "resolverSessionId");
1907
+ const attemptId = responseString(result, "attemptId");
1879
1908
  const worker = responseString(result, "worker") ?? input.worker;
1880
1909
  if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
1881
- if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(responseString(result, "status") ?? "")) {
1910
+ if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
1882
1911
  lines.push(
1883
1912
  "",
1884
1913
  "Finish after resolving and reviewing the conflict worktree:",
1885
1914
  ` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
1886
1915
  );
1887
1916
  }
1917
+ if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
1918
+ lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
1919
+ }
1888
1920
  return `${lines.join("\n")}
1889
1921
  `;
1890
1922
  }
@@ -1924,13 +1956,64 @@ async function dispatchSessionRunCommand(client, command) {
1924
1956
  const data = await client.sessions.stop(command.sessionId);
1925
1957
  return { data, human: renderSessionRunStatus(data) };
1926
1958
  }
1927
- async function dispatchMergeCommand(client, command) {
1959
+ const MERGE_POLL_INTERVAL_MS = 2e3;
1960
+ const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
1961
+ const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
1962
+ function isMergeAttemptSettled(response) {
1963
+ const status = responseString(response, "status");
1964
+ if (!status) return true;
1965
+ if (status === "resolving") return response.resolverAgentRunning !== false;
1966
+ return status !== "provisioning";
1967
+ }
1968
+ async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
1969
+ const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
1970
+ const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
1971
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1972
+ const onProgress = options.onProgress ?? (() => {
1973
+ });
1974
+ let latest = initial;
1975
+ let worktreeReadyReported = latest.worktreeReady === true;
1976
+ let consecutiveFailures = 0;
1977
+ let waitedMs = 0;
1978
+ while (!isMergeAttemptSettled(latest)) {
1979
+ if (waitedMs >= timeoutMs) {
1980
+ onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
1981
+ return latest;
1982
+ }
1983
+ await sleep2(intervalMs);
1984
+ waitedMs += intervalMs;
1985
+ try {
1986
+ latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
1987
+ consecutiveFailures = 0;
1988
+ } catch (error) {
1989
+ consecutiveFailures += 1;
1990
+ if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
1991
+ continue;
1992
+ }
1993
+ if (!worktreeReadyReported && latest.worktreeReady === true) {
1994
+ worktreeReadyReported = true;
1995
+ onProgress("Conflict worktree ready.");
1996
+ }
1997
+ }
1998
+ return latest;
1999
+ }
2000
+ async function dispatchMergeCommand(client, command, pollOptions = {}) {
1928
2001
  if (command.kind === "merge") {
1929
2002
  const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
1930
2003
  conflictMode: command.conflictMode,
1931
2004
  ...command.worker ? { worker: command.worker } : {}
1932
2005
  });
1933
- return { data: data2, human: renderWorktreeMerge(data2, command) };
2006
+ const attemptId = responseString(data2, "attemptId");
2007
+ if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
2008
+ return { data: data2, human: renderWorktreeMerge(data2, command) };
2009
+ }
2010
+ pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
2011
+ const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
2012
+ return { data: settled, human: renderWorktreeMerge(settled, command) };
2013
+ }
2014
+ if (command.kind === "status") {
2015
+ const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
2016
+ return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
1934
2017
  }
1935
2018
  const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
1936
2019
  worker: command.worker,
@@ -2487,6 +2570,40 @@ async function executeR5dctlCommand(client, json, args) {
2487
2570
  write(report, renderK8sUsageReport(report));
2488
2571
  return;
2489
2572
  }
2573
+ if (first === "vcluster" && second === "trusted-workflow") {
2574
+ const subcommand = requireValue(args[2], "Missing trusted-workflow command");
2575
+ if (subcommand === "add") {
2576
+ const parsed = parseCommandOperands(args.slice(3), /* @__PURE__ */ new Set(["--repo", "--workflow"]), /* @__PURE__ */ new Map(), "trusted-workflow add");
2577
+ if (parsed.positionals.length > 0) throw new Error(`Unexpected trusted-workflow add value: ${parsed.positionals[0]}`);
2578
+ const workflow = await client.k8s.trustedWorkflows.add({
2579
+ repository: requireValue(parsed.values.get("--repo"), "--repo <owner/repository> is required"),
2580
+ workflowPath: requireValue(parsed.values.get("--workflow"), "--workflow <path> is required")
2581
+ });
2582
+ write(workflow, "Trusted GitHub workflow configured successfully.\n");
2583
+ return;
2584
+ }
2585
+ if (subcommand === "list") {
2586
+ if (args.length > 3) throw new Error(`Unexpected trusted-workflow list value: ${args[3]}`);
2587
+ const workflows = await client.k8s.trustedWorkflows.list();
2588
+ write(
2589
+ workflows,
2590
+ workflows.length === 0 ? "No trusted GitHub workflows.\n" : `${workflows.map(
2591
+ (workflow) => `${workflow.id} ${workflow.repository} ${workflow.workflowPath} ${workflow.revokedAt ? "revoked" : "active"}`
2592
+ ).join("\n")}
2593
+ `
2594
+ );
2595
+ return;
2596
+ }
2597
+ if (subcommand === "remove") {
2598
+ const workflowId = requireValue(args[3], "Missing trusted workflow id");
2599
+ if (args.length > 4) throw new Error(`Unexpected trusted-workflow remove value: ${args[4]}`);
2600
+ const result = await client.k8s.trustedWorkflows.revoke(workflowId);
2601
+ if (!result.success) throw new Error("Trusted GitHub workflow was not found or was already revoked");
2602
+ write(result, "Trusted GitHub workflow revoked.\n");
2603
+ return;
2604
+ }
2605
+ throw new Error(`Unknown trusted-workflow command: ${subcommand}`);
2606
+ }
2490
2607
  if (first === "ps" && second === "list") {
2491
2608
  const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
2492
2609
  write(processes, renderProcessList(processes));
@@ -2856,12 +2973,23 @@ function resolveCommandExecution(options, rest) {
2856
2973
  }
2857
2974
  if (command === "k8s") {
2858
2975
  const subcommand = commandArgs[0];
2859
- if (subcommand !== "usage") throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
2860
2976
  if (options.project || options.branch || options.session) {
2861
- throw new Error("Project, branch, and session scopes are not supported for `k8s usage`");
2977
+ throw new Error("Project, branch, and session scopes are not supported for `k8s`");
2978
+ }
2979
+ if (subcommand === "usage") {
2980
+ parseK8sUsageArgs(commandArgs.slice(1));
2981
+ return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
2982
+ }
2983
+ throw new Error(`Unknown k8s command: ${subcommand ?? ""}`.trim());
2984
+ }
2985
+ if (command === "vcluster") {
2986
+ if (options.project || options.branch || options.session) {
2987
+ throw new Error("Project, branch, and session scopes are not supported for `vcluster`");
2988
+ }
2989
+ if (commandArgs[0] !== "trusted-workflow") {
2990
+ throw new Error(`Unknown vcluster command: ${commandArgs[0] ?? ""}`.trim());
2862
2991
  }
2863
- parseK8sUsageArgs(commandArgs.slice(1));
2864
- return { kind: "plugin", pluginArgs: ["k8s", "usage", ...commandArgs.slice(1)] };
2992
+ return { kind: "plugin", pluginArgs: ["vcluster", "trusted-workflow", ...commandArgs.slice(1)] };
2865
2993
  }
2866
2994
  if (command === "get") {
2867
2995
  const target = commandArgs[0];
@@ -3199,7 +3327,18 @@ async function runCommand(argv) {
3199
3327
  });
3200
3328
  }
3201
3329
  if (execution.kind === "merge") {
3202
- const result = await dispatchMergeCommand(client, execution.command);
3330
+ if (!options.json) {
3331
+ if (execution.command.kind === "merge") {
3332
+ process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
3333
+ } else if (execution.command.kind === "finish") {
3334
+ process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
3335
+ `);
3336
+ }
3337
+ }
3338
+ const result = await dispatchMergeCommand(client, execution.command, {
3339
+ ...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
3340
+ `) }
3341
+ });
3203
3342
  writeDataOutput(options.json, result.data, result.human);
3204
3343
  return 0;
3205
3344
  }
@@ -3274,6 +3413,7 @@ export {
3274
3413
  getR5dctlVersion,
3275
3414
  getTransientDevicePollDelay,
3276
3415
  handleAuthLogin,
3416
+ isMergeAttemptSettled,
3277
3417
  main,
3278
3418
  parseAnswerEnvRequestCommandArgs,
3279
3419
  parseAnswerFlags,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.72",
3
+ "version": "0.0.74",
4
4
  "type": "module"
5
5
  }
@@ -85,6 +85,11 @@ export type R5dctlMergeCommand = {
85
85
  sourceWorktree: string;
86
86
  worker?: string;
87
87
  conflictMode: R5dctlMergeConflictMode;
88
+ noWait?: boolean;
89
+ } | {
90
+ kind: "status";
91
+ project: string;
92
+ attemptId: string;
88
93
  } | {
89
94
  kind: "finish";
90
95
  project: string;
@@ -229,6 +234,8 @@ export type R5dctlCommandResponse = {
229
234
  conflictedFiles?: string[];
230
235
  failureReason?: string;
231
236
  message?: string;
237
+ worktreeReady?: boolean;
238
+ resolverAgentRunning?: boolean;
232
239
  };
233
240
  export type R5dctlSessionRunClient = {
234
241
  projects: {
@@ -259,6 +266,7 @@ export type R5dctlMergeClient = {
259
266
  worker: string;
260
267
  summary?: string;
261
268
  }) => Promise<R5dctlCommandResponse>;
269
+ mergeAttempt: (projectRef: string, attemptId: string) => Promise<R5dctlCommandResponse>;
262
270
  };
263
271
  };
264
272
  };
@@ -273,7 +281,21 @@ export type R5dctlCommandDispatchResult = {
273
281
  human: string;
274
282
  };
275
283
  export declare function dispatchSessionRunCommand(client: R5dctlSessionRunClient, command: R5dctlSessionRunCommand): Promise<R5dctlCommandDispatchResult>;
276
- export declare function dispatchMergeCommand(client: R5dctlMergeClient, command: R5dctlMergeCommand): Promise<R5dctlCommandDispatchResult>;
284
+ export type R5dctlMergePollOptions = {
285
+ onProgress?: (line: string) => void;
286
+ pollIntervalMs?: number;
287
+ pollTimeoutMs?: number;
288
+ sleep?: (ms: number) => Promise<void>;
289
+ };
290
+ /**
291
+ * Whether a merge attempt response is settled for the CLI: terminal, waiting
292
+ * on the user (a ready manual conflict worktree), or actively resolving with
293
+ * its agent running. Anything else is still provisioning or starting. A
294
+ * "resolving" response without the resolverAgentRunning field comes from a
295
+ * server that starts the agent in-request, so it counts as settled.
296
+ */
297
+ export declare function isMergeAttemptSettled(response: R5dctlCommandResponse): boolean;
298
+ export declare function dispatchMergeCommand(client: R5dctlMergeClient, command: R5dctlMergeCommand, pollOptions?: R5dctlMergePollOptions): Promise<R5dctlCommandDispatchResult>;
277
299
  export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
278
300
  export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
279
301
  type R5dctlFollowClient = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.72",
3
+ "version": "0.0.74",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,7 +26,7 @@
26
26
  "r5dctl": "dist/cjs/main.cjs"
27
27
  },
28
28
  "dependencies": {
29
- "@ricsam/r5d-api": "^0.0.72",
29
+ "@ricsam/r5d-api": "^0.0.74",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"