@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 +16 -0
- package/dist/cjs/cli.cjs +164 -23
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +163 -23
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +23 -1
- package/package.json +2 -2
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
|
-
|
|
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,
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/cjs/package.json
CHANGED
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
|
-
|
|
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,
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
29
|
+
"@ricsam/r5d-api": "^0.0.74",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|