@ricsam/r5dctl 0.0.72 → 0.0.73
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 +6 -0
- package/dist/cjs/cli.cjs +101 -19
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +100 -19
- 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
|
|
@@ -115,6 +117,10 @@ Add `-f`/`--follow`/`--watch` to `session start` to attach after enqueue, or to
|
|
|
115
117
|
|
|
116
118
|
`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
119
|
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
118
124
|
`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
125
|
|
|
120
126
|
`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 = [
|
|
@@ -174,10 +167,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
174
167
|
].join("\n");
|
|
175
168
|
const MERGE_HELP_TEXT = [
|
|
176
169
|
"Usage:",
|
|
177
|
-
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
170
|
+
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
171
|
+
" r5dctl -p <project> merge status <attempt-id>",
|
|
178
172
|
' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
179
173
|
"",
|
|
180
174
|
"Merge a managed worktree into its recorded parent worktree.",
|
|
175
|
+
"Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
|
|
181
176
|
""
|
|
182
177
|
].join("\n");
|
|
183
178
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -328,9 +323,14 @@ const SHARED_HELP_ENTRIES = [
|
|
|
328
323
|
{ section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
|
|
329
324
|
{
|
|
330
325
|
section: "merge",
|
|
331
|
-
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
326
|
+
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
332
327
|
description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
|
|
333
328
|
},
|
|
329
|
+
{
|
|
330
|
+
section: "merge",
|
|
331
|
+
usage: "-p <project> merge status <attempt-id>",
|
|
332
|
+
description: "Show a merge attempt's provisioning and resolution status."
|
|
333
|
+
},
|
|
334
334
|
{
|
|
335
335
|
section: "merge",
|
|
336
336
|
usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
@@ -567,7 +567,7 @@ function requireModel(value) {
|
|
|
567
567
|
function requireSessionMode(value) {
|
|
568
568
|
const mode = requireValue(value?.trim(), "--mode <mode> is required");
|
|
569
569
|
if (!R5DCTL_SESSION_MODES.has(mode)) {
|
|
570
|
-
throw new Error("Invalid --mode. Expected one of: ask, plan,
|
|
570
|
+
throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
|
|
571
571
|
}
|
|
572
572
|
return mode;
|
|
573
573
|
}
|
|
@@ -653,12 +653,19 @@ function parseMergeCommand(options, args) {
|
|
|
653
653
|
...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
|
|
654
654
|
};
|
|
655
655
|
}
|
|
656
|
+
if (args[0] === "status") {
|
|
657
|
+
const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
|
|
658
|
+
const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
|
|
659
|
+
if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
|
|
660
|
+
return { kind: "status", project, attemptId };
|
|
661
|
+
}
|
|
656
662
|
const parsed = parseCommandOperands(
|
|
657
663
|
args,
|
|
658
664
|
/* @__PURE__ */ new Set(["--worker"]),
|
|
659
665
|
/* @__PURE__ */ new Map([
|
|
660
666
|
["--create-conflict-worktree", "--create-conflict-worktree"],
|
|
661
|
-
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
|
|
667
|
+
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
|
|
668
|
+
["--no-wait", "--no-wait"]
|
|
662
669
|
]),
|
|
663
670
|
"merge"
|
|
664
671
|
);
|
|
@@ -671,7 +678,14 @@ function parseMergeCommand(options, args) {
|
|
|
671
678
|
}
|
|
672
679
|
const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
|
|
673
680
|
const worker = parsed.values.get("--worker")?.trim();
|
|
674
|
-
return {
|
|
681
|
+
return {
|
|
682
|
+
kind: "merge",
|
|
683
|
+
project,
|
|
684
|
+
sourceWorktree,
|
|
685
|
+
conflictMode,
|
|
686
|
+
...worker ? { worker } : {},
|
|
687
|
+
...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
|
|
688
|
+
};
|
|
675
689
|
}
|
|
676
690
|
function assertAuthLoginArgs(args) {
|
|
677
691
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
@@ -1959,17 +1973,22 @@ function renderSessionRunStatus(session) {
|
|
|
1959
1973
|
function renderWorktreeMerge(result, input) {
|
|
1960
1974
|
const lines = [];
|
|
1961
1975
|
appendMergeDetails(lines, result);
|
|
1976
|
+
const status = responseString(result, "status");
|
|
1962
1977
|
const conflictWorktree = responseString(result, "conflictWorktree");
|
|
1963
1978
|
const resolverSessionId = responseString(result, "resolverSessionId");
|
|
1979
|
+
const attemptId = responseString(result, "attemptId");
|
|
1964
1980
|
const worker = responseString(result, "worker") ?? input.worker;
|
|
1965
1981
|
if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
|
|
1966
|
-
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(
|
|
1982
|
+
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
|
|
1967
1983
|
lines.push(
|
|
1968
1984
|
"",
|
|
1969
1985
|
"Finish after resolving and reviewing the conflict worktree:",
|
|
1970
1986
|
` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
|
|
1971
1987
|
);
|
|
1972
1988
|
}
|
|
1989
|
+
if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
|
|
1990
|
+
lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
|
|
1991
|
+
}
|
|
1973
1992
|
return `${lines.join("\n")}
|
|
1974
1993
|
`;
|
|
1975
1994
|
}
|
|
@@ -2009,13 +2028,64 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
2009
2028
|
const data = await client.sessions.stop(command.sessionId);
|
|
2010
2029
|
return { data, human: renderSessionRunStatus(data) };
|
|
2011
2030
|
}
|
|
2012
|
-
|
|
2031
|
+
const MERGE_POLL_INTERVAL_MS = 2e3;
|
|
2032
|
+
const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
|
|
2033
|
+
const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
2034
|
+
function isMergeAttemptSettled(response) {
|
|
2035
|
+
const status = responseString(response, "status");
|
|
2036
|
+
if (!status) return true;
|
|
2037
|
+
if (status === "resolving") return response.resolverAgentRunning !== false;
|
|
2038
|
+
return status !== "provisioning";
|
|
2039
|
+
}
|
|
2040
|
+
async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
|
|
2041
|
+
const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
|
|
2042
|
+
const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
|
|
2043
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2044
|
+
const onProgress = options.onProgress ?? (() => {
|
|
2045
|
+
});
|
|
2046
|
+
let latest = initial;
|
|
2047
|
+
let worktreeReadyReported = latest.worktreeReady === true;
|
|
2048
|
+
let consecutiveFailures = 0;
|
|
2049
|
+
let waitedMs = 0;
|
|
2050
|
+
while (!isMergeAttemptSettled(latest)) {
|
|
2051
|
+
if (waitedMs >= timeoutMs) {
|
|
2052
|
+
onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
|
|
2053
|
+
return latest;
|
|
2054
|
+
}
|
|
2055
|
+
await sleep2(intervalMs);
|
|
2056
|
+
waitedMs += intervalMs;
|
|
2057
|
+
try {
|
|
2058
|
+
latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
|
|
2059
|
+
consecutiveFailures = 0;
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
consecutiveFailures += 1;
|
|
2062
|
+
if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (!worktreeReadyReported && latest.worktreeReady === true) {
|
|
2066
|
+
worktreeReadyReported = true;
|
|
2067
|
+
onProgress("Conflict worktree ready.");
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return latest;
|
|
2071
|
+
}
|
|
2072
|
+
async function dispatchMergeCommand(client, command, pollOptions = {}) {
|
|
2013
2073
|
if (command.kind === "merge") {
|
|
2014
2074
|
const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
|
|
2015
2075
|
conflictMode: command.conflictMode,
|
|
2016
2076
|
...command.worker ? { worker: command.worker } : {}
|
|
2017
2077
|
});
|
|
2018
|
-
|
|
2078
|
+
const attemptId = responseString(data2, "attemptId");
|
|
2079
|
+
if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
|
|
2080
|
+
return { data: data2, human: renderWorktreeMerge(data2, command) };
|
|
2081
|
+
}
|
|
2082
|
+
pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
|
|
2083
|
+
const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
|
|
2084
|
+
return { data: settled, human: renderWorktreeMerge(settled, command) };
|
|
2085
|
+
}
|
|
2086
|
+
if (command.kind === "status") {
|
|
2087
|
+
const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
|
|
2088
|
+
return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
|
|
2019
2089
|
}
|
|
2020
2090
|
const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
|
|
2021
2091
|
worker: command.worker,
|
|
@@ -3284,7 +3354,18 @@ async function runCommand(argv) {
|
|
|
3284
3354
|
});
|
|
3285
3355
|
}
|
|
3286
3356
|
if (execution.kind === "merge") {
|
|
3287
|
-
|
|
3357
|
+
if (!options.json) {
|
|
3358
|
+
if (execution.command.kind === "merge") {
|
|
3359
|
+
process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
|
|
3360
|
+
} else if (execution.command.kind === "finish") {
|
|
3361
|
+
process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
|
|
3362
|
+
`);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
const result = await dispatchMergeCommand(client, execution.command, {
|
|
3366
|
+
...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
|
|
3367
|
+
`) }
|
|
3368
|
+
});
|
|
3288
3369
|
writeDataOutput(options.json, result.data, result.human);
|
|
3289
3370
|
return 0;
|
|
3290
3371
|
}
|
|
@@ -3360,6 +3441,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3360
3441
|
getR5dctlVersion,
|
|
3361
3442
|
getTransientDevicePollDelay,
|
|
3362
3443
|
handleAuthLogin,
|
|
3444
|
+
isMergeAttemptSettled,
|
|
3363
3445
|
main,
|
|
3364
3446
|
parseAnswerEnvRequestCommandArgs,
|
|
3365
3447
|
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 = [
|
|
@@ -89,10 +81,12 @@ const SESSION_RUN_HELP_TEXT = [
|
|
|
89
81
|
].join("\n");
|
|
90
82
|
const MERGE_HELP_TEXT = [
|
|
91
83
|
"Usage:",
|
|
92
|
-
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
84
|
+
" r5dctl -p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
85
|
+
" r5dctl -p <project> merge status <attempt-id>",
|
|
93
86
|
' r5dctl -p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
94
87
|
"",
|
|
95
88
|
"Merge a managed worktree into its recorded parent worktree.",
|
|
89
|
+
"Conflict worktrees provision in the background; merge waits by default and --no-wait returns the attempt immediately.",
|
|
96
90
|
""
|
|
97
91
|
].join("\n");
|
|
98
92
|
const SHARED_HELP_ENTRIES = [
|
|
@@ -243,9 +237,14 @@ const SHARED_HELP_ENTRIES = [
|
|
|
243
237
|
{ section: "processes", usage: "ps stop <run-id>", description: "Gracefully stop an active process with SIGTERM." },
|
|
244
238
|
{
|
|
245
239
|
section: "merge",
|
|
246
|
-
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] <source-worktree>",
|
|
240
|
+
usage: "-p <project> merge [--worker <label>] [--create-conflict-worktree|--launch-conflict-resolution-agent] [--no-wait] <source-worktree>",
|
|
247
241
|
description: "Merge a worktree into its recorded parent, with an explicit conflict strategy."
|
|
248
242
|
},
|
|
243
|
+
{
|
|
244
|
+
section: "merge",
|
|
245
|
+
usage: "-p <project> merge status <attempt-id>",
|
|
246
|
+
description: "Show a merge attempt's provisioning and resolution status."
|
|
247
|
+
},
|
|
249
248
|
{
|
|
250
249
|
section: "merge",
|
|
251
250
|
usage: '-p <project> merge finish --worker <label> [--summary "<summary>"] <conflict-worktree>',
|
|
@@ -482,7 +481,7 @@ function requireModel(value) {
|
|
|
482
481
|
function requireSessionMode(value) {
|
|
483
482
|
const mode = requireValue(value?.trim(), "--mode <mode> is required");
|
|
484
483
|
if (!R5DCTL_SESSION_MODES.has(mode)) {
|
|
485
|
-
throw new Error("Invalid --mode. Expected one of: ask, plan,
|
|
484
|
+
throw new Error("Invalid --mode. Expected one of: ask, plan, agent");
|
|
486
485
|
}
|
|
487
486
|
return mode;
|
|
488
487
|
}
|
|
@@ -568,12 +567,19 @@ function parseMergeCommand(options, args) {
|
|
|
568
567
|
...parsed2.values.get("--summary")?.trim() ? { summary: parsed2.values.get("--summary").trim() } : {}
|
|
569
568
|
};
|
|
570
569
|
}
|
|
570
|
+
if (args[0] === "status") {
|
|
571
|
+
const parsed2 = parseCommandOperands(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), "merge status");
|
|
572
|
+
const attemptId = requireValue(parsed2.positionals[0], "Attempt id is required for `merge status`");
|
|
573
|
+
if (parsed2.positionals.length > 1) throw new Error(`Unexpected merge status argument: ${parsed2.positionals[1]}`);
|
|
574
|
+
return { kind: "status", project, attemptId };
|
|
575
|
+
}
|
|
571
576
|
const parsed = parseCommandOperands(
|
|
572
577
|
args,
|
|
573
578
|
/* @__PURE__ */ new Set(["--worker"]),
|
|
574
579
|
/* @__PURE__ */ new Map([
|
|
575
580
|
["--create-conflict-worktree", "--create-conflict-worktree"],
|
|
576
|
-
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"]
|
|
581
|
+
["--launch-conflict-resolution-agent", "--launch-conflict-resolution-agent"],
|
|
582
|
+
["--no-wait", "--no-wait"]
|
|
577
583
|
]),
|
|
578
584
|
"merge"
|
|
579
585
|
);
|
|
@@ -586,7 +592,14 @@ function parseMergeCommand(options, args) {
|
|
|
586
592
|
}
|
|
587
593
|
const conflictMode = createWorktree ? "worktree" : launchAgent ? "agent" : "none";
|
|
588
594
|
const worker = parsed.values.get("--worker")?.trim();
|
|
589
|
-
return {
|
|
595
|
+
return {
|
|
596
|
+
kind: "merge",
|
|
597
|
+
project,
|
|
598
|
+
sourceWorktree,
|
|
599
|
+
conflictMode,
|
|
600
|
+
...worker ? { worker } : {},
|
|
601
|
+
...parsed.booleans.has("--no-wait") ? { noWait: true } : {}
|
|
602
|
+
};
|
|
590
603
|
}
|
|
591
604
|
function assertAuthLoginArgs(args) {
|
|
592
605
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
@@ -1874,17 +1887,22 @@ function renderSessionRunStatus(session) {
|
|
|
1874
1887
|
function renderWorktreeMerge(result, input) {
|
|
1875
1888
|
const lines = [];
|
|
1876
1889
|
appendMergeDetails(lines, result);
|
|
1890
|
+
const status = responseString(result, "status");
|
|
1877
1891
|
const conflictWorktree = responseString(result, "conflictWorktree");
|
|
1878
1892
|
const resolverSessionId = responseString(result, "resolverSessionId");
|
|
1893
|
+
const attemptId = responseString(result, "attemptId");
|
|
1879
1894
|
const worker = responseString(result, "worker") ?? input.worker;
|
|
1880
1895
|
if (resolverSessionId) lines.push("", `Resolver session: ${resolverSessionId}`, ` r5dctl session stop ${resolverSessionId}`);
|
|
1881
|
-
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(
|
|
1896
|
+
if (conflictWorktree && worker && ["conflicts", "conflict_worktree_created", "resolving"].includes(status ?? "")) {
|
|
1882
1897
|
lines.push(
|
|
1883
1898
|
"",
|
|
1884
1899
|
"Finish after resolving and reviewing the conflict worktree:",
|
|
1885
1900
|
` r5dctl -p ${input.project} merge finish --worker ${worker} ${conflictWorktree} --summary "<summary>"`
|
|
1886
1901
|
);
|
|
1887
1902
|
}
|
|
1903
|
+
if (attemptId && (status === "provisioning" || status === "resolving" && result.resolverAgentRunning === false)) {
|
|
1904
|
+
lines.push("", "Check progress:", ` r5dctl -p ${input.project} merge status ${attemptId}`);
|
|
1905
|
+
}
|
|
1888
1906
|
return `${lines.join("\n")}
|
|
1889
1907
|
`;
|
|
1890
1908
|
}
|
|
@@ -1924,13 +1942,64 @@ async function dispatchSessionRunCommand(client, command) {
|
|
|
1924
1942
|
const data = await client.sessions.stop(command.sessionId);
|
|
1925
1943
|
return { data, human: renderSessionRunStatus(data) };
|
|
1926
1944
|
}
|
|
1927
|
-
|
|
1945
|
+
const MERGE_POLL_INTERVAL_MS = 2e3;
|
|
1946
|
+
const MERGE_POLL_TIMEOUT_MS = 20 * 6e4;
|
|
1947
|
+
const MERGE_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
1948
|
+
function isMergeAttemptSettled(response) {
|
|
1949
|
+
const status = responseString(response, "status");
|
|
1950
|
+
if (!status) return true;
|
|
1951
|
+
if (status === "resolving") return response.resolverAgentRunning !== false;
|
|
1952
|
+
return status !== "provisioning";
|
|
1953
|
+
}
|
|
1954
|
+
async function pollMergeAttemptUntilSettled(client, project, attemptId, initial, options) {
|
|
1955
|
+
const intervalMs = options.pollIntervalMs ?? MERGE_POLL_INTERVAL_MS;
|
|
1956
|
+
const timeoutMs = options.pollTimeoutMs ?? MERGE_POLL_TIMEOUT_MS;
|
|
1957
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1958
|
+
const onProgress = options.onProgress ?? (() => {
|
|
1959
|
+
});
|
|
1960
|
+
let latest = initial;
|
|
1961
|
+
let worktreeReadyReported = latest.worktreeReady === true;
|
|
1962
|
+
let consecutiveFailures = 0;
|
|
1963
|
+
let waitedMs = 0;
|
|
1964
|
+
while (!isMergeAttemptSettled(latest)) {
|
|
1965
|
+
if (waitedMs >= timeoutMs) {
|
|
1966
|
+
onProgress(`Still waiting after ${Math.round(timeoutMs / 6e4)} minutes; check later with: r5dctl -p ${project} merge status ${attemptId}`);
|
|
1967
|
+
return latest;
|
|
1968
|
+
}
|
|
1969
|
+
await sleep2(intervalMs);
|
|
1970
|
+
waitedMs += intervalMs;
|
|
1971
|
+
try {
|
|
1972
|
+
latest = await client.projects.worktrees.mergeAttempt(project, attemptId);
|
|
1973
|
+
consecutiveFailures = 0;
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
consecutiveFailures += 1;
|
|
1976
|
+
if (consecutiveFailures >= MERGE_POLL_MAX_CONSECUTIVE_FAILURES) throw error;
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
if (!worktreeReadyReported && latest.worktreeReady === true) {
|
|
1980
|
+
worktreeReadyReported = true;
|
|
1981
|
+
onProgress("Conflict worktree ready.");
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return latest;
|
|
1985
|
+
}
|
|
1986
|
+
async function dispatchMergeCommand(client, command, pollOptions = {}) {
|
|
1928
1987
|
if (command.kind === "merge") {
|
|
1929
1988
|
const data2 = await client.projects.worktrees.merge(command.project, command.sourceWorktree, {
|
|
1930
1989
|
conflictMode: command.conflictMode,
|
|
1931
1990
|
...command.worker ? { worker: command.worker } : {}
|
|
1932
1991
|
});
|
|
1933
|
-
|
|
1992
|
+
const attemptId = responseString(data2, "attemptId");
|
|
1993
|
+
if (command.noWait || !attemptId || isMergeAttemptSettled(data2)) {
|
|
1994
|
+
return { data: data2, human: renderWorktreeMerge(data2, command) };
|
|
1995
|
+
}
|
|
1996
|
+
pollOptions.onProgress?.(`Provisioning the conflict worktree (attempt ${attemptId})...`);
|
|
1997
|
+
const settled = await pollMergeAttemptUntilSettled(client, command.project, attemptId, data2, pollOptions);
|
|
1998
|
+
return { data: settled, human: renderWorktreeMerge(settled, command) };
|
|
1999
|
+
}
|
|
2000
|
+
if (command.kind === "status") {
|
|
2001
|
+
const data2 = await client.projects.worktrees.mergeAttempt(command.project, command.attemptId);
|
|
2002
|
+
return { data: data2, human: renderWorktreeMerge(data2, { project: command.project }) };
|
|
1934
2003
|
}
|
|
1935
2004
|
const data = await client.projects.worktrees.finishMerge(command.project, command.conflictWorktree, {
|
|
1936
2005
|
worker: command.worker,
|
|
@@ -3199,7 +3268,18 @@ async function runCommand(argv) {
|
|
|
3199
3268
|
});
|
|
3200
3269
|
}
|
|
3201
3270
|
if (execution.kind === "merge") {
|
|
3202
|
-
|
|
3271
|
+
if (!options.json) {
|
|
3272
|
+
if (execution.command.kind === "merge") {
|
|
3273
|
+
process.stderr.write("Merging (publishing the workspace and computing the merge)...\n");
|
|
3274
|
+
} else if (execution.command.kind === "finish") {
|
|
3275
|
+
process.stderr.write(`Finalizing ${execution.command.conflictWorktree} (publishing the resolver workspace)...
|
|
3276
|
+
`);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
const result = await dispatchMergeCommand(client, execution.command, {
|
|
3280
|
+
...options.json ? {} : { onProgress: (line) => process.stderr.write(`${line}
|
|
3281
|
+
`) }
|
|
3282
|
+
});
|
|
3203
3283
|
writeDataOutput(options.json, result.data, result.human);
|
|
3204
3284
|
return 0;
|
|
3205
3285
|
}
|
|
@@ -3274,6 +3354,7 @@ export {
|
|
|
3274
3354
|
getR5dctlVersion,
|
|
3275
3355
|
getTransientDevicePollDelay,
|
|
3276
3356
|
handleAuthLogin,
|
|
3357
|
+
isMergeAttemptSettled,
|
|
3277
3358
|
main,
|
|
3278
3359
|
parseAnswerEnvRequestCommandArgs,
|
|
3279
3360
|
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.73",
|
|
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.73",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|