@ricsam/r5dctl 0.0.121 → 0.0.123
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 -2
- package/dist/cjs/cli.cjs +153 -18
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +147 -18
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +56 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ r5dctl -p <project> create branch <branch> --source worktree:<branch> [--working
|
|
|
59
59
|
r5dctl -p <project> describe branch-operation <operation-id>
|
|
60
60
|
r5dctl -p <project> delete branch <branch> [<branch>...] # preview the sessions that would be deleted, exit 1
|
|
61
61
|
r5dctl -p <project> delete branch <branch> [<branch>...] --confirm # delete the branches and their sessions through the platform
|
|
62
|
+
r5dctl -p <project> delete branch <branch> --confirm --no-wait # accept the deletion and print its branch id instead of waiting
|
|
63
|
+
r5dctl -p <project> describe branch-deletion <branch-id> # pending, failed, or completed
|
|
62
64
|
r5dctl -p <project> get envs
|
|
63
65
|
r5dctl -p <project> get envs --show-values
|
|
64
66
|
r5dctl -p <project> get env API_KEY
|
|
@@ -127,7 +129,7 @@ pull requests.
|
|
|
127
129
|
|
|
128
130
|
`create branch` creates a linked worktree branch through the platform from an existing branch's checkout, with the same `--source worktree:<branch>` and `--working-tree carry|clean` semantics as `session start --new-worktree` (carry is the default) and no session prompt. `--worker <label>` selects the connected worker that cuts the checkout; omitted, the platform chooses one, and an r5d worker shell's temporary credential is always bound to its own worker. The command returns as soon as the operation is accepted and prints its id; `describe branch-operation <operation-id>` reports `provisioning`, `ready`, or `failed` with the worker and any error. Neither prints a commit hash: read the branch with git in its checkout once it is ready.
|
|
129
131
|
|
|
130
|
-
`delete branch` removes one or more branches and every session on them through the platform, which is the only supported way to remove a linked worktree: the platform owns those checkouts, so never run `git worktree remove`, `git worktree prune`, or `git branch -D` inside a project checkout. Without `--confirm` it prints each branch and its sessions and exits non-zero without deleting anything. With `--confirm` the
|
|
132
|
+
`delete branch` removes one or more branches and every session on them through the platform, which is the only supported way to remove a linked worktree: the platform owns those checkouts, so never run `git worktree remove`, `git worktree prune`, or `git branch -D` inside a project checkout. Without `--confirm` it prints each branch and its sessions and exits non-zero without deleting anything. With `--confirm` each branch is resolved to the exact managed branch it names today, its deletion is accepted at once by the platform, and the command waits (polling every 2 seconds, up to 10 minutes) until the platform reports it completed before requesting the next one; a failure does not stop the remaining deletions, and the exit status is non-zero if any branch failed. `--no-wait` prints the accepted branch id instead of waiting; `describe branch-deletion <branch-id>` reports `pending` (with the retry reason when the worker has not answered yet), `failed` with the platform's error, or `completed`, and answers `completed` for a branch id that no longer exists. The deletion stays bound to that branch id: a same-name branch created afterwards is never deleted by an old request, and re-running the command for a branch whose deletion failed retries it. The primary branch `main` is refused before anything is deleted, as is a branch with a running agent or active process (stop them first); a name that only has sessions and no managed branch is refused too (delete those sessions with `delete session`). On a server that predates the route, the command explains that the server does not support `delete branch` yet instead of reporting a missing branch.
|
|
131
133
|
|
|
132
134
|
`get branches` lists every branch the project knows about, including managed branches without sessions. A worker whose workspace sync found a configured branch's checkout directory missing, or present but not a linked worktree of the project, skips that branch and reports it; the listing then shows `checkout=missing on <worker>` or `checkout=not_a_worktree on <worker>`, and `workspace status` prints a `Missing checkouts on <worker>` line. Recover either by deleting the branch with `delete branch <branch> --confirm` or by reconnecting that worker, which recreates the checkout without snapshotting the project's other checkouts.
|
|
133
135
|
|
|
@@ -153,7 +155,9 @@ Add `-f`/`--follow`/`--watch` to `session start` to poll the provisioning handle
|
|
|
153
155
|
|
|
154
156
|
`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.
|
|
155
157
|
|
|
156
|
-
`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.
|
|
158
|
+
`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. The `LANE` column names the worker command lane each run holds (`general`, `utility`, or `control`) or waits for (`queued:general`), from the worker's latest capacity report; `-` means the worker did not report the run. `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.
|
|
159
|
+
|
|
160
|
+
A managed worker runs two general shells at a time; a third waits up to 60 seconds and then fails with "command capacity is busy". Coordination does not compete for those slots: when an agent shell runs one plain `r5dctl` coordination command (`ps list`, `ps stop`, `workspace status`, `session status|stop|prompt|claim|claims|process-log`, `describe …`, `get …`, `sessions recent`, the `conversation` read forms, `create|update|delete session`, `answer-questions`, `answer-env-request`) with plain or simply quoted arguments, the platform runs it in a reserved control lane: two small slots, no shell, a 120-second limit. Shell operators, pipes, redirections, `$` expansions, environment assignments, wrappers, paths to r5dctl, `--`, `--follow`, and credential or endpoint flags make it an ordinary shell command again. `workspace status` prints each worker's lane totals, holders, and queue.
|
|
157
161
|
|
|
158
162
|
`k8s usage` shows current and rolling CPU and memory usage for every workload container in your managed vCluster. The default window is `24h`; pass `--window 7d` for seven days. Filter with `--namespace`, `--workload`, or `--container`; a workload may be identified by name, `namespace/name`, or Kubernetes UID. Human-readable output shows current totals across sampled replicas and the selected window's maximum. Global `--json` includes min/max summaries, the complete five-minute history envelope, per-pod series, coverage, and freshness metadata.
|
|
159
163
|
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -28,6 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var cli_exports = {};
|
|
30
30
|
__export(cli_exports, {
|
|
31
|
+
BRANCH_DELETION_POLL_INTERVAL_MS: () => BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
32
|
+
BRANCH_DELETION_WAIT_TIMEOUT_MS: () => BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
31
33
|
advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
|
|
32
34
|
branchDeletionConfirmationMessage: () => branchDeletionConfirmationMessage,
|
|
33
35
|
branchDeletionsConfirmationMessage: () => branchDeletionsConfirmationMessage,
|
|
@@ -41,6 +43,7 @@ __export(cli_exports, {
|
|
|
41
43
|
formatK8sCpu: () => formatK8sCpu,
|
|
42
44
|
formatK8sMemory: () => formatK8sMemory,
|
|
43
45
|
formatProcessAge: () => formatProcessAge,
|
|
46
|
+
formatProcessLane: () => formatProcessLane,
|
|
44
47
|
getCliHelpText: () => getCliHelpText,
|
|
45
48
|
getK8sUsageState: () => getK8sUsageState,
|
|
46
49
|
getProjectEnvValue: () => getProjectEnvValue,
|
|
@@ -77,6 +80,7 @@ __export(cli_exports, {
|
|
|
77
80
|
renderBranchDeletion: () => renderBranchDeletion,
|
|
78
81
|
renderBranchDeletionPreview: () => renderBranchDeletionPreview,
|
|
79
82
|
renderBranchDeletionResult: () => renderBranchDeletionResult,
|
|
83
|
+
renderBranchDeletionStatus: () => renderBranchDeletionStatus,
|
|
80
84
|
renderBranchList: () => renderBranchList,
|
|
81
85
|
renderBranchOperation: () => renderBranchOperation,
|
|
82
86
|
renderConversationForkResponse: () => renderConversationForkResponse,
|
|
@@ -97,11 +101,13 @@ __export(cli_exports, {
|
|
|
97
101
|
renderSessionList: () => renderSessionList,
|
|
98
102
|
renderSessionRunStart: () => renderSessionRunStart,
|
|
99
103
|
renderSessionRunStatus: () => renderSessionRunStatus,
|
|
104
|
+
renderWorkerCapacity: () => renderWorkerCapacity,
|
|
100
105
|
renderWorkspaceStatus: () => renderWorkspaceStatus,
|
|
101
106
|
renderWorkspaceSync: () => renderWorkspaceSync,
|
|
102
107
|
resolveCommandExecution: () => resolveCommandExecution,
|
|
103
108
|
runR5dctlCli: () => runR5dctlCli,
|
|
104
109
|
summarizeEnvData: () => summarizeEnvData,
|
|
110
|
+
waitForBranchDeletion: () => waitForBranchDeletion,
|
|
105
111
|
waitForSessionProvisioning: () => waitForSessionProvisioning,
|
|
106
112
|
writeConfig: () => writeConfig
|
|
107
113
|
});
|
|
@@ -247,8 +253,13 @@ const SHARED_HELP_ENTRIES = [
|
|
|
247
253
|
},
|
|
248
254
|
{
|
|
249
255
|
section: "branches",
|
|
250
|
-
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
|
|
251
|
-
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
|
|
256
|
+
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm] [--no-wait]",
|
|
257
|
+
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted. Each deletion is accepted at once and awaited until the platform finishes it; --no-wait prints the accepted branch ids instead."
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
section: "branches",
|
|
261
|
+
usage: "-p <project> describe branch-deletion <branch-id>",
|
|
262
|
+
description: "Show whether an accepted branch deletion is pending, failed, or completed."
|
|
252
263
|
},
|
|
253
264
|
{
|
|
254
265
|
section: "envs",
|
|
@@ -656,16 +667,21 @@ function parseBranchCreateArgs(args) {
|
|
|
656
667
|
function parseBranchDeleteArgs(args) {
|
|
657
668
|
const branchNames = [];
|
|
658
669
|
let confirmed = false;
|
|
670
|
+
let noWait = false;
|
|
659
671
|
for (const arg of args) {
|
|
660
672
|
if (arg === "--confirm") {
|
|
661
673
|
confirmed = true;
|
|
662
674
|
continue;
|
|
663
675
|
}
|
|
676
|
+
if (arg === "--no-wait") {
|
|
677
|
+
noWait = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
664
680
|
if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
|
|
665
681
|
if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
|
|
666
682
|
branchNames.push(arg);
|
|
667
683
|
}
|
|
668
|
-
return { branchNames, confirmed };
|
|
684
|
+
return { branchNames, confirmed, noWait };
|
|
669
685
|
}
|
|
670
686
|
const PRIMARY_BRANCH_NAME = "main";
|
|
671
687
|
function isUnsupportedServerRoute(error) {
|
|
@@ -693,12 +709,66 @@ async function createBranchThroughPlatform(client, projectRef, input) {
|
|
|
693
709
|
})
|
|
694
710
|
);
|
|
695
711
|
}
|
|
712
|
+
const BRANCH_DELETION_POLL_INTERVAL_MS = 2e3;
|
|
713
|
+
const BRANCH_DELETION_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
714
|
+
async function waitForBranchDeletion(client, projectRef, branchId, options = {}) {
|
|
715
|
+
const pollIntervalMs = options.pollIntervalMs ?? BRANCH_DELETION_POLL_INTERVAL_MS;
|
|
716
|
+
const timeoutMs = options.timeoutMs ?? BRANCH_DELETION_WAIT_TIMEOUT_MS;
|
|
717
|
+
const wait = options.sleep ?? ((ms) => (0, import_promises.setTimeout)(ms));
|
|
718
|
+
const deadline = Date.now() + timeoutMs;
|
|
719
|
+
for (; ; ) {
|
|
720
|
+
const status = await client.projects.branches.describeDeletion(projectRef, branchId);
|
|
721
|
+
if (status.status === "completed") return;
|
|
722
|
+
if (status.status === "failed") throw new Error(status.error ?? "Branch deletion failed");
|
|
723
|
+
if (Date.now() >= deadline) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`Branch deletion ${branchId} is still pending after ${Math.round(timeoutMs / 1e3)}s${status.retryReason ? ` (${status.retryReason})` : ""}; it continues on the platform, re-run to keep waiting`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
await wait(pollIntervalMs);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function resolveUncertainAcceptance(client, projectRef, branchId, error) {
|
|
732
|
+
if (error.status !== 503) return null;
|
|
733
|
+
try {
|
|
734
|
+
const status = await client.projects.branches.describeDeletion(projectRef, branchId);
|
|
735
|
+
return { status: status.status };
|
|
736
|
+
} catch {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
696
740
|
async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
|
|
697
741
|
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
698
742
|
const branch = await client.projects.branches.describe(projectRef, branchName);
|
|
699
743
|
if (!options.confirmed) return { kind: "preview", branch };
|
|
700
|
-
|
|
701
|
-
|
|
744
|
+
if (!branch.branchId) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`Branch ${branchName} has no managed checkout, only ${branch.sessions.length} session(s); delete those with \`delete session <id>\``
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const branchId = branch.branchId;
|
|
750
|
+
let accepted;
|
|
751
|
+
try {
|
|
752
|
+
accepted = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName, { branchId }));
|
|
753
|
+
} catch (error) {
|
|
754
|
+
const landed = error instanceof import_r5d_api.R5dctlApiError ? await resolveUncertainAcceptance(client, projectRef, branchId, error) : null;
|
|
755
|
+
if (!landed) throw error;
|
|
756
|
+
const sessionIds = branch.sessions.map((session) => session.id);
|
|
757
|
+
accepted = landed.status === "completed" ? { status: "completed", branchId, branchName, sessionIds } : { status: "pending", branchId, branchName, workerLabel: "", sessionIds };
|
|
758
|
+
}
|
|
759
|
+
if (accepted.status === "completed" && accepted.sessionIds.length > 0) {
|
|
760
|
+
return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
|
|
761
|
+
}
|
|
762
|
+
if (accepted.status === "completed") return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: [] } };
|
|
763
|
+
if (options.noWait) {
|
|
764
|
+
return { kind: "accepted", result: { branchName, branchId, sessionIds: accepted.sessionIds, workerLabel: accepted.workerLabel } };
|
|
765
|
+
}
|
|
766
|
+
await waitForBranchDeletion(client, projectRef, branchId, {
|
|
767
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
768
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
769
|
+
...options.sleep ? { sleep: options.sleep } : {}
|
|
770
|
+
});
|
|
771
|
+
return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
|
|
702
772
|
}
|
|
703
773
|
async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
|
|
704
774
|
if (branchNames.length === 0) throw new Error("Missing branch name");
|
|
@@ -716,9 +786,15 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
|
|
|
716
786
|
for (const branchName of branchNames) {
|
|
717
787
|
let result;
|
|
718
788
|
try {
|
|
719
|
-
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
|
|
720
|
-
|
|
721
|
-
|
|
789
|
+
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
|
|
790
|
+
confirmed: true,
|
|
791
|
+
...options.noWait !== void 0 ? { noWait: options.noWait } : {},
|
|
792
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
793
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
794
|
+
...options.sleep ? { sleep: options.sleep } : {}
|
|
795
|
+
});
|
|
796
|
+
if (deletion.kind === "preview") throw new Error(`Branch ${branchName} was only previewed`);
|
|
797
|
+
result = deletion.kind === "deleted" ? { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds } : { branchName, deleted: false, accepted: true, branchId: deletion.result.branchId, sessionIds: deletion.result.sessionIds };
|
|
722
798
|
} catch (error) {
|
|
723
799
|
result = {
|
|
724
800
|
branchName,
|
|
@@ -732,7 +808,21 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
|
|
|
732
808
|
return { kind: "deleted", results };
|
|
733
809
|
}
|
|
734
810
|
function renderBranchDeletionResult(result) {
|
|
735
|
-
|
|
811
|
+
if (result.deleted) return renderBranchDeletion(result);
|
|
812
|
+
if (result.accepted) {
|
|
813
|
+
return `Accepted deletion of branch ${result.branchName} (${result.sessionIds.length} session(s)); it continues on the platform. Check with \`describe branch-deletion ${result.branchId}\`.
|
|
814
|
+
`;
|
|
815
|
+
}
|
|
816
|
+
return `Failed to delete branch ${result.branchName}: ${result.error}
|
|
817
|
+
`;
|
|
818
|
+
}
|
|
819
|
+
function renderBranchDeletionStatus(status) {
|
|
820
|
+
const lines = [`Branch: ${status.branchName ?? "(deleted)"}`, `Branch id: ${status.branchId}`, `Status: ${status.status}`];
|
|
821
|
+
if (status.workerLabel) lines.push(`Worker: ${status.workerLabel}`);
|
|
822
|
+
if (status.attempts !== void 0) lines.push(`Attempts: ${status.attempts}`);
|
|
823
|
+
if (status.retryReason) lines.push(`Retrying: ${status.retryReason}`);
|
|
824
|
+
if (status.error) lines.push(`Error: ${status.error}`);
|
|
825
|
+
return `${lines.join("\n")}
|
|
736
826
|
`;
|
|
737
827
|
}
|
|
738
828
|
function branchDeletionsConfirmationMessage(branches) {
|
|
@@ -742,7 +832,7 @@ function branchDeletionsConfirmationMessage(branches) {
|
|
|
742
832
|
return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
|
|
743
833
|
}
|
|
744
834
|
function branchDeletionsFailureMessage(results) {
|
|
745
|
-
return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
|
|
835
|
+
return `Failed to delete ${results.filter((result) => !result.deleted && !result.accepted).length} of ${results.length} branch(es).`;
|
|
746
836
|
}
|
|
747
837
|
function renderBranchDeletionPreview(branch) {
|
|
748
838
|
const count = branch.sessions.length;
|
|
@@ -1802,6 +1892,23 @@ function renderRecentSessionList(sessions) {
|
|
|
1802
1892
|
return `${sessions.map((session) => `${session.id} ${session.projectPath}/${session.branchName} ${session.name ?? "(unnamed)"}`).join("\n")}
|
|
1803
1893
|
`;
|
|
1804
1894
|
}
|
|
1895
|
+
function renderWorkerCapacity(workerLabel, capacity) {
|
|
1896
|
+
if (!capacity) return [`Command capacity on ${workerLabel}: not reported yet`];
|
|
1897
|
+
if (!capacity.bounded) return [`Command capacity on ${workerLabel}: unbounded (no managed launcher)`];
|
|
1898
|
+
const describeLane = (lane) => {
|
|
1899
|
+
const report = capacity.lanes[lane];
|
|
1900
|
+
const slots = report.slots === null ? "?" : String(report.slots);
|
|
1901
|
+
const holders = report.active.map((entry) => `${entry.id}${entry.sessionId ? ` (session ${entry.sessionId})` : ""}`);
|
|
1902
|
+
const queued = report.queued.map((entry) => `${entry.id}${entry.sessionId ? ` (session ${entry.sessionId})` : ""}`);
|
|
1903
|
+
return ` ${lane}: ${report.active.length}/${slots} busy${holders.length ? `, held by ${holders.join(", ")}` : ""}${queued.length ? `; queued: ${queued.join(", ")}` : ""}`;
|
|
1904
|
+
};
|
|
1905
|
+
return [
|
|
1906
|
+
`Command capacity on ${workerLabel} (reported ${capacity.reportedAt}):`,
|
|
1907
|
+
describeLane("general"),
|
|
1908
|
+
describeLane("utility"),
|
|
1909
|
+
`${describeLane("control")}${capacity.controlRuntimeMs !== null ? ` (plain r5dctl coordination, ${Math.round(capacity.controlRuntimeMs / 1e3)}s limit)` : ""}`
|
|
1910
|
+
];
|
|
1911
|
+
}
|
|
1805
1912
|
function renderWorkspaceStatus(status) {
|
|
1806
1913
|
const lines = [
|
|
1807
1914
|
`Workspace HEAD: ${status.head ?? "(not initialized)"}`,
|
|
@@ -1809,7 +1916,8 @@ function renderWorkspaceStatus(status) {
|
|
|
1809
1916
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1810
1917
|
...status.workers.flatMap((worker) => [
|
|
1811
1918
|
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1812
|
-
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? [])
|
|
1919
|
+
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1920
|
+
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1813
1921
|
])
|
|
1814
1922
|
];
|
|
1815
1923
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
@@ -1830,7 +1938,8 @@ function renderBranchList(branches) {
|
|
|
1830
1938
|
if (branches.length === 0) return "No branches found.\n";
|
|
1831
1939
|
const lines = branches.map((branch) => {
|
|
1832
1940
|
const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
|
|
1833
|
-
|
|
1941
|
+
const deletion = branch.deletion ? ` deletion=${branch.deletion.status}${branch.deletion.error ? ` (${branch.deletion.error})` : ""}` : "";
|
|
1942
|
+
return `${branch.branchName} sessions=${branch.sessionCount}${checkout}${deletion}`;
|
|
1834
1943
|
});
|
|
1835
1944
|
if (branches.some((branch) => branch.checkoutState)) {
|
|
1836
1945
|
lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
|
|
@@ -2042,10 +2151,11 @@ function renderProcessList(processes, nowMs = Date.now()) {
|
|
|
2042
2151
|
if (processes.length === 0) {
|
|
2043
2152
|
return "No active processes.\n";
|
|
2044
2153
|
}
|
|
2045
|
-
const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
|
|
2154
|
+
const headers = ["RUN ID", "STATUS", "LANE", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
|
|
2046
2155
|
const rows = processes.map((process2) => [
|
|
2047
2156
|
process2.runId,
|
|
2048
2157
|
process2.status,
|
|
2158
|
+
formatProcessLane(process2),
|
|
2049
2159
|
formatProcessAge(process2.startedAt, nowMs),
|
|
2050
2160
|
process2.workerLabel,
|
|
2051
2161
|
process2.projectPath,
|
|
@@ -2059,6 +2169,10 @@ function renderProcessList(processes, nowMs = Date.now()) {
|
|
|
2059
2169
|
return `${[formatRow(headers), separator, ...rows.map(formatRow)].join("\n")}
|
|
2060
2170
|
`;
|
|
2061
2171
|
}
|
|
2172
|
+
function formatProcessLane(process2) {
|
|
2173
|
+
if (process2.admission) return process2.admission.phase === "queued" ? `queued:${process2.admission.lane}` : process2.admission.lane;
|
|
2174
|
+
return process2.commandClass ?? "-";
|
|
2175
|
+
}
|
|
2062
2176
|
function renderProcessHistory(processes, nowMs = Date.now()) {
|
|
2063
2177
|
return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
|
|
2064
2178
|
}
|
|
@@ -2096,6 +2210,7 @@ function renderProcessInspection(process2) {
|
|
|
2096
2210
|
`Worker: ${process2.workerLabel} (${process2.platform ?? "unknown"}/${process2.arch ?? "unknown"})`,
|
|
2097
2211
|
`Target: ${process2.projectPath} ${process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile}`,
|
|
2098
2212
|
`Session: ${process2.sessionId}`,
|
|
2213
|
+
`Lane: ${formatProcessLane(process2)}${process2.admission ? ` since ${process2.admission.since}` : ""}`,
|
|
2099
2214
|
`Command: ${process2.command}`,
|
|
2100
2215
|
`Argv: ${JSON.stringify(process2.argv)}`,
|
|
2101
2216
|
`CWD: ${process2.cwd ?? "(execution root)"}`,
|
|
@@ -3091,10 +3206,15 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
3091
3206
|
requireValue(args[offset], "Missing project reference"),
|
|
3092
3207
|
requireValue(args[offset + 1], "Missing branch name")
|
|
3093
3208
|
);
|
|
3094
|
-
write(
|
|
3209
|
+
write(
|
|
3210
|
+
result,
|
|
3211
|
+
`Project: ${result.project.path}
|
|
3095
3212
|
Branch: ${result.branchName}
|
|
3096
|
-
|
|
3097
|
-
`)
|
|
3213
|
+
Branch id: ${result.branchId ?? "(no managed branch)"}
|
|
3214
|
+
${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error ? ` (${result.deletion.error})` : ""}
|
|
3215
|
+
` : ""}Sessions: ${result.sessions.length}
|
|
3216
|
+
`
|
|
3217
|
+
);
|
|
3098
3218
|
return;
|
|
3099
3219
|
}
|
|
3100
3220
|
if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
|
|
@@ -3122,9 +3242,10 @@ Sessions: ${result.sessions.length}
|
|
|
3122
3242
|
if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
|
|
3123
3243
|
const offset = first === "delete" ? 2 : 3;
|
|
3124
3244
|
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
3125
|
-
const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
3245
|
+
const { branchNames, confirmed, noWait } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
3126
3246
|
const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
|
|
3127
3247
|
confirmed,
|
|
3248
|
+
noWait,
|
|
3128
3249
|
// Each deletion is its own round trip, so a reader sees results as they land.
|
|
3129
3250
|
onResult: (result) => {
|
|
3130
3251
|
if (!json) process.stdout.write(renderBranchDeletionResult(result));
|
|
@@ -3135,7 +3256,15 @@ Sessions: ${result.sessions.length}
|
|
|
3135
3256
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
3136
3257
|
}
|
|
3137
3258
|
if (json) write({ results: deletion.results }, "");
|
|
3138
|
-
if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
3259
|
+
if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
3263
|
+
const offset = first === "describe" ? 2 : 3;
|
|
3264
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
3265
|
+
const branchId = requireValue(args[offset + 1], "Missing branch id");
|
|
3266
|
+
const status = await requireServerRouteSupport("describe branch-deletion", () => client.projects.branches.describeDeletion(projectRef, branchId));
|
|
3267
|
+
write(status, renderBranchDeletionStatus(status));
|
|
3139
3268
|
return;
|
|
3140
3269
|
}
|
|
3141
3270
|
if (first === "get" && second === "envs") {
|
|
@@ -3928,6 +4057,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3928
4057
|
}
|
|
3929
4058
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3930
4059
|
0 && (module.exports = {
|
|
4060
|
+
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
4061
|
+
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
3931
4062
|
advanceTransientDevicePollBackoff,
|
|
3932
4063
|
branchDeletionConfirmationMessage,
|
|
3933
4064
|
branchDeletionsConfirmationMessage,
|
|
@@ -3941,6 +4072,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3941
4072
|
formatK8sCpu,
|
|
3942
4073
|
formatK8sMemory,
|
|
3943
4074
|
formatProcessAge,
|
|
4075
|
+
formatProcessLane,
|
|
3944
4076
|
getCliHelpText,
|
|
3945
4077
|
getK8sUsageState,
|
|
3946
4078
|
getProjectEnvValue,
|
|
@@ -3977,6 +4109,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3977
4109
|
renderBranchDeletion,
|
|
3978
4110
|
renderBranchDeletionPreview,
|
|
3979
4111
|
renderBranchDeletionResult,
|
|
4112
|
+
renderBranchDeletionStatus,
|
|
3980
4113
|
renderBranchList,
|
|
3981
4114
|
renderBranchOperation,
|
|
3982
4115
|
renderConversationForkResponse,
|
|
@@ -3997,11 +4130,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3997
4130
|
renderSessionList,
|
|
3998
4131
|
renderSessionRunStart,
|
|
3999
4132
|
renderSessionRunStatus,
|
|
4133
|
+
renderWorkerCapacity,
|
|
4000
4134
|
renderWorkspaceStatus,
|
|
4001
4135
|
renderWorkspaceSync,
|
|
4002
4136
|
resolveCommandExecution,
|
|
4003
4137
|
runR5dctlCli,
|
|
4004
4138
|
summarizeEnvData,
|
|
4139
|
+
waitForBranchDeletion,
|
|
4005
4140
|
waitForSessionProvisioning,
|
|
4006
4141
|
writeConfig
|
|
4007
4142
|
});
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -142,8 +142,13 @@ const SHARED_HELP_ENTRIES = [
|
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
section: "branches",
|
|
145
|
-
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
|
|
146
|
-
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
|
|
145
|
+
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm] [--no-wait]",
|
|
146
|
+
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted. Each deletion is accepted at once and awaited until the platform finishes it; --no-wait prints the accepted branch ids instead."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
section: "branches",
|
|
150
|
+
usage: "-p <project> describe branch-deletion <branch-id>",
|
|
151
|
+
description: "Show whether an accepted branch deletion is pending, failed, or completed."
|
|
147
152
|
},
|
|
148
153
|
{
|
|
149
154
|
section: "envs",
|
|
@@ -551,16 +556,21 @@ function parseBranchCreateArgs(args) {
|
|
|
551
556
|
function parseBranchDeleteArgs(args) {
|
|
552
557
|
const branchNames = [];
|
|
553
558
|
let confirmed = false;
|
|
559
|
+
let noWait = false;
|
|
554
560
|
for (const arg of args) {
|
|
555
561
|
if (arg === "--confirm") {
|
|
556
562
|
confirmed = true;
|
|
557
563
|
continue;
|
|
558
564
|
}
|
|
565
|
+
if (arg === "--no-wait") {
|
|
566
|
+
noWait = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
559
569
|
if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
|
|
560
570
|
if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
|
|
561
571
|
branchNames.push(arg);
|
|
562
572
|
}
|
|
563
|
-
return { branchNames, confirmed };
|
|
573
|
+
return { branchNames, confirmed, noWait };
|
|
564
574
|
}
|
|
565
575
|
const PRIMARY_BRANCH_NAME = "main";
|
|
566
576
|
function isUnsupportedServerRoute(error) {
|
|
@@ -588,12 +598,66 @@ async function createBranchThroughPlatform(client, projectRef, input) {
|
|
|
588
598
|
})
|
|
589
599
|
);
|
|
590
600
|
}
|
|
601
|
+
const BRANCH_DELETION_POLL_INTERVAL_MS = 2e3;
|
|
602
|
+
const BRANCH_DELETION_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
603
|
+
async function waitForBranchDeletion(client, projectRef, branchId, options = {}) {
|
|
604
|
+
const pollIntervalMs = options.pollIntervalMs ?? BRANCH_DELETION_POLL_INTERVAL_MS;
|
|
605
|
+
const timeoutMs = options.timeoutMs ?? BRANCH_DELETION_WAIT_TIMEOUT_MS;
|
|
606
|
+
const wait = options.sleep ?? ((ms) => sleep(ms));
|
|
607
|
+
const deadline = Date.now() + timeoutMs;
|
|
608
|
+
for (; ; ) {
|
|
609
|
+
const status = await client.projects.branches.describeDeletion(projectRef, branchId);
|
|
610
|
+
if (status.status === "completed") return;
|
|
611
|
+
if (status.status === "failed") throw new Error(status.error ?? "Branch deletion failed");
|
|
612
|
+
if (Date.now() >= deadline) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`Branch deletion ${branchId} is still pending after ${Math.round(timeoutMs / 1e3)}s${status.retryReason ? ` (${status.retryReason})` : ""}; it continues on the platform, re-run to keep waiting`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
await wait(pollIntervalMs);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function resolveUncertainAcceptance(client, projectRef, branchId, error) {
|
|
621
|
+
if (error.status !== 503) return null;
|
|
622
|
+
try {
|
|
623
|
+
const status = await client.projects.branches.describeDeletion(projectRef, branchId);
|
|
624
|
+
return { status: status.status };
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
591
629
|
async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
|
|
592
630
|
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
593
631
|
const branch = await client.projects.branches.describe(projectRef, branchName);
|
|
594
632
|
if (!options.confirmed) return { kind: "preview", branch };
|
|
595
|
-
|
|
596
|
-
|
|
633
|
+
if (!branch.branchId) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`Branch ${branchName} has no managed checkout, only ${branch.sessions.length} session(s); delete those with \`delete session <id>\``
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const branchId = branch.branchId;
|
|
639
|
+
let accepted;
|
|
640
|
+
try {
|
|
641
|
+
accepted = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName, { branchId }));
|
|
642
|
+
} catch (error) {
|
|
643
|
+
const landed = error instanceof R5dctlApiError ? await resolveUncertainAcceptance(client, projectRef, branchId, error) : null;
|
|
644
|
+
if (!landed) throw error;
|
|
645
|
+
const sessionIds = branch.sessions.map((session) => session.id);
|
|
646
|
+
accepted = landed.status === "completed" ? { status: "completed", branchId, branchName, sessionIds } : { status: "pending", branchId, branchName, workerLabel: "", sessionIds };
|
|
647
|
+
}
|
|
648
|
+
if (accepted.status === "completed" && accepted.sessionIds.length > 0) {
|
|
649
|
+
return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
|
|
650
|
+
}
|
|
651
|
+
if (accepted.status === "completed") return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: [] } };
|
|
652
|
+
if (options.noWait) {
|
|
653
|
+
return { kind: "accepted", result: { branchName, branchId, sessionIds: accepted.sessionIds, workerLabel: accepted.workerLabel } };
|
|
654
|
+
}
|
|
655
|
+
await waitForBranchDeletion(client, projectRef, branchId, {
|
|
656
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
657
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
658
|
+
...options.sleep ? { sleep: options.sleep } : {}
|
|
659
|
+
});
|
|
660
|
+
return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
|
|
597
661
|
}
|
|
598
662
|
async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
|
|
599
663
|
if (branchNames.length === 0) throw new Error("Missing branch name");
|
|
@@ -611,9 +675,15 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
|
|
|
611
675
|
for (const branchName of branchNames) {
|
|
612
676
|
let result;
|
|
613
677
|
try {
|
|
614
|
-
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
|
|
615
|
-
|
|
616
|
-
|
|
678
|
+
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
|
|
679
|
+
confirmed: true,
|
|
680
|
+
...options.noWait !== void 0 ? { noWait: options.noWait } : {},
|
|
681
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
682
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
683
|
+
...options.sleep ? { sleep: options.sleep } : {}
|
|
684
|
+
});
|
|
685
|
+
if (deletion.kind === "preview") throw new Error(`Branch ${branchName} was only previewed`);
|
|
686
|
+
result = deletion.kind === "deleted" ? { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds } : { branchName, deleted: false, accepted: true, branchId: deletion.result.branchId, sessionIds: deletion.result.sessionIds };
|
|
617
687
|
} catch (error) {
|
|
618
688
|
result = {
|
|
619
689
|
branchName,
|
|
@@ -627,7 +697,21 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
|
|
|
627
697
|
return { kind: "deleted", results };
|
|
628
698
|
}
|
|
629
699
|
function renderBranchDeletionResult(result) {
|
|
630
|
-
|
|
700
|
+
if (result.deleted) return renderBranchDeletion(result);
|
|
701
|
+
if (result.accepted) {
|
|
702
|
+
return `Accepted deletion of branch ${result.branchName} (${result.sessionIds.length} session(s)); it continues on the platform. Check with \`describe branch-deletion ${result.branchId}\`.
|
|
703
|
+
`;
|
|
704
|
+
}
|
|
705
|
+
return `Failed to delete branch ${result.branchName}: ${result.error}
|
|
706
|
+
`;
|
|
707
|
+
}
|
|
708
|
+
function renderBranchDeletionStatus(status) {
|
|
709
|
+
const lines = [`Branch: ${status.branchName ?? "(deleted)"}`, `Branch id: ${status.branchId}`, `Status: ${status.status}`];
|
|
710
|
+
if (status.workerLabel) lines.push(`Worker: ${status.workerLabel}`);
|
|
711
|
+
if (status.attempts !== void 0) lines.push(`Attempts: ${status.attempts}`);
|
|
712
|
+
if (status.retryReason) lines.push(`Retrying: ${status.retryReason}`);
|
|
713
|
+
if (status.error) lines.push(`Error: ${status.error}`);
|
|
714
|
+
return `${lines.join("\n")}
|
|
631
715
|
`;
|
|
632
716
|
}
|
|
633
717
|
function branchDeletionsConfirmationMessage(branches) {
|
|
@@ -637,7 +721,7 @@ function branchDeletionsConfirmationMessage(branches) {
|
|
|
637
721
|
return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
|
|
638
722
|
}
|
|
639
723
|
function branchDeletionsFailureMessage(results) {
|
|
640
|
-
return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
|
|
724
|
+
return `Failed to delete ${results.filter((result) => !result.deleted && !result.accepted).length} of ${results.length} branch(es).`;
|
|
641
725
|
}
|
|
642
726
|
function renderBranchDeletionPreview(branch) {
|
|
643
727
|
const count = branch.sessions.length;
|
|
@@ -1697,6 +1781,23 @@ function renderRecentSessionList(sessions) {
|
|
|
1697
1781
|
return `${sessions.map((session) => `${session.id} ${session.projectPath}/${session.branchName} ${session.name ?? "(unnamed)"}`).join("\n")}
|
|
1698
1782
|
`;
|
|
1699
1783
|
}
|
|
1784
|
+
function renderWorkerCapacity(workerLabel, capacity) {
|
|
1785
|
+
if (!capacity) return [`Command capacity on ${workerLabel}: not reported yet`];
|
|
1786
|
+
if (!capacity.bounded) return [`Command capacity on ${workerLabel}: unbounded (no managed launcher)`];
|
|
1787
|
+
const describeLane = (lane) => {
|
|
1788
|
+
const report = capacity.lanes[lane];
|
|
1789
|
+
const slots = report.slots === null ? "?" : String(report.slots);
|
|
1790
|
+
const holders = report.active.map((entry) => `${entry.id}${entry.sessionId ? ` (session ${entry.sessionId})` : ""}`);
|
|
1791
|
+
const queued = report.queued.map((entry) => `${entry.id}${entry.sessionId ? ` (session ${entry.sessionId})` : ""}`);
|
|
1792
|
+
return ` ${lane}: ${report.active.length}/${slots} busy${holders.length ? `, held by ${holders.join(", ")}` : ""}${queued.length ? `; queued: ${queued.join(", ")}` : ""}`;
|
|
1793
|
+
};
|
|
1794
|
+
return [
|
|
1795
|
+
`Command capacity on ${workerLabel} (reported ${capacity.reportedAt}):`,
|
|
1796
|
+
describeLane("general"),
|
|
1797
|
+
describeLane("utility"),
|
|
1798
|
+
`${describeLane("control")}${capacity.controlRuntimeMs !== null ? ` (plain r5dctl coordination, ${Math.round(capacity.controlRuntimeMs / 1e3)}s limit)` : ""}`
|
|
1799
|
+
];
|
|
1800
|
+
}
|
|
1700
1801
|
function renderWorkspaceStatus(status) {
|
|
1701
1802
|
const lines = [
|
|
1702
1803
|
`Workspace HEAD: ${status.head ?? "(not initialized)"}`,
|
|
@@ -1704,7 +1805,8 @@ function renderWorkspaceStatus(status) {
|
|
|
1704
1805
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1705
1806
|
...status.workers.flatMap((worker) => [
|
|
1706
1807
|
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1707
|
-
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? [])
|
|
1808
|
+
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? []),
|
|
1809
|
+
...renderWorkerCapacity(worker.workerLabel, worker.capacity)
|
|
1708
1810
|
])
|
|
1709
1811
|
];
|
|
1710
1812
|
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
@@ -1725,7 +1827,8 @@ function renderBranchList(branches) {
|
|
|
1725
1827
|
if (branches.length === 0) return "No branches found.\n";
|
|
1726
1828
|
const lines = branches.map((branch) => {
|
|
1727
1829
|
const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
|
|
1728
|
-
|
|
1830
|
+
const deletion = branch.deletion ? ` deletion=${branch.deletion.status}${branch.deletion.error ? ` (${branch.deletion.error})` : ""}` : "";
|
|
1831
|
+
return `${branch.branchName} sessions=${branch.sessionCount}${checkout}${deletion}`;
|
|
1729
1832
|
});
|
|
1730
1833
|
if (branches.some((branch) => branch.checkoutState)) {
|
|
1731
1834
|
lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
|
|
@@ -1937,10 +2040,11 @@ function renderProcessList(processes, nowMs = Date.now()) {
|
|
|
1937
2040
|
if (processes.length === 0) {
|
|
1938
2041
|
return "No active processes.\n";
|
|
1939
2042
|
}
|
|
1940
|
-
const headers = ["RUN ID", "STATUS", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
|
|
2043
|
+
const headers = ["RUN ID", "STATUS", "LANE", "AGE", "WORKER", "TARGET", "SCOPE", "SESSION", "COMMAND"];
|
|
1941
2044
|
const rows = processes.map((process2) => [
|
|
1942
2045
|
process2.runId,
|
|
1943
2046
|
process2.status,
|
|
2047
|
+
formatProcessLane(process2),
|
|
1944
2048
|
formatProcessAge(process2.startedAt, nowMs),
|
|
1945
2049
|
process2.workerLabel,
|
|
1946
2050
|
process2.projectPath,
|
|
@@ -1954,6 +2058,10 @@ function renderProcessList(processes, nowMs = Date.now()) {
|
|
|
1954
2058
|
return `${[formatRow(headers), separator, ...rows.map(formatRow)].join("\n")}
|
|
1955
2059
|
`;
|
|
1956
2060
|
}
|
|
2061
|
+
function formatProcessLane(process2) {
|
|
2062
|
+
if (process2.admission) return process2.admission.phase === "queued" ? `queued:${process2.admission.lane}` : process2.admission.lane;
|
|
2063
|
+
return process2.commandClass ?? "-";
|
|
2064
|
+
}
|
|
1957
2065
|
function renderProcessHistory(processes, nowMs = Date.now()) {
|
|
1958
2066
|
return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
|
|
1959
2067
|
}
|
|
@@ -1991,6 +2099,7 @@ function renderProcessInspection(process2) {
|
|
|
1991
2099
|
`Worker: ${process2.workerLabel} (${process2.platform ?? "unknown"}/${process2.arch ?? "unknown"})`,
|
|
1992
2100
|
`Target: ${process2.projectPath} ${process2.target.type === "project" ? process2.target.branchName : process2.target.rootProfile}`,
|
|
1993
2101
|
`Session: ${process2.sessionId}`,
|
|
2102
|
+
`Lane: ${formatProcessLane(process2)}${process2.admission ? ` since ${process2.admission.since}` : ""}`,
|
|
1994
2103
|
`Command: ${process2.command}`,
|
|
1995
2104
|
`Argv: ${JSON.stringify(process2.argv)}`,
|
|
1996
2105
|
`CWD: ${process2.cwd ?? "(execution root)"}`,
|
|
@@ -2986,10 +3095,15 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2986
3095
|
requireValue(args[offset], "Missing project reference"),
|
|
2987
3096
|
requireValue(args[offset + 1], "Missing branch name")
|
|
2988
3097
|
);
|
|
2989
|
-
write(
|
|
3098
|
+
write(
|
|
3099
|
+
result,
|
|
3100
|
+
`Project: ${result.project.path}
|
|
2990
3101
|
Branch: ${result.branchName}
|
|
2991
|
-
|
|
2992
|
-
`)
|
|
3102
|
+
Branch id: ${result.branchId ?? "(no managed branch)"}
|
|
3103
|
+
${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error ? ` (${result.deletion.error})` : ""}
|
|
3104
|
+
` : ""}Sessions: ${result.sessions.length}
|
|
3105
|
+
`
|
|
3106
|
+
);
|
|
2993
3107
|
return;
|
|
2994
3108
|
}
|
|
2995
3109
|
if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
|
|
@@ -3017,9 +3131,10 @@ Sessions: ${result.sessions.length}
|
|
|
3017
3131
|
if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
|
|
3018
3132
|
const offset = first === "delete" ? 2 : 3;
|
|
3019
3133
|
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
3020
|
-
const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
3134
|
+
const { branchNames, confirmed, noWait } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
3021
3135
|
const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
|
|
3022
3136
|
confirmed,
|
|
3137
|
+
noWait,
|
|
3023
3138
|
// Each deletion is its own round trip, so a reader sees results as they land.
|
|
3024
3139
|
onResult: (result) => {
|
|
3025
3140
|
if (!json) process.stdout.write(renderBranchDeletionResult(result));
|
|
@@ -3030,7 +3145,15 @@ Sessions: ${result.sessions.length}
|
|
|
3030
3145
|
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
3031
3146
|
}
|
|
3032
3147
|
if (json) write({ results: deletion.results }, "");
|
|
3033
|
-
if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
3148
|
+
if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
|
|
3152
|
+
const offset = first === "describe" ? 2 : 3;
|
|
3153
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
3154
|
+
const branchId = requireValue(args[offset + 1], "Missing branch id");
|
|
3155
|
+
const status = await requireServerRouteSupport("describe branch-deletion", () => client.projects.branches.describeDeletion(projectRef, branchId));
|
|
3156
|
+
write(status, renderBranchDeletionStatus(status));
|
|
3034
3157
|
return;
|
|
3035
3158
|
}
|
|
3036
3159
|
if (first === "get" && second === "envs") {
|
|
@@ -3822,6 +3945,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3822
3945
|
}
|
|
3823
3946
|
}
|
|
3824
3947
|
export {
|
|
3948
|
+
BRANCH_DELETION_POLL_INTERVAL_MS,
|
|
3949
|
+
BRANCH_DELETION_WAIT_TIMEOUT_MS,
|
|
3825
3950
|
advanceTransientDevicePollBackoff,
|
|
3826
3951
|
branchDeletionConfirmationMessage,
|
|
3827
3952
|
branchDeletionsConfirmationMessage,
|
|
@@ -3835,6 +3960,7 @@ export {
|
|
|
3835
3960
|
formatK8sCpu,
|
|
3836
3961
|
formatK8sMemory,
|
|
3837
3962
|
formatProcessAge,
|
|
3963
|
+
formatProcessLane,
|
|
3838
3964
|
getCliHelpText,
|
|
3839
3965
|
getK8sUsageState,
|
|
3840
3966
|
getProjectEnvValue,
|
|
@@ -3871,6 +3997,7 @@ export {
|
|
|
3871
3997
|
renderBranchDeletion,
|
|
3872
3998
|
renderBranchDeletionPreview,
|
|
3873
3999
|
renderBranchDeletionResult,
|
|
4000
|
+
renderBranchDeletionStatus,
|
|
3874
4001
|
renderBranchList,
|
|
3875
4002
|
renderBranchOperation,
|
|
3876
4003
|
renderConversationForkResponse,
|
|
@@ -3891,11 +4018,13 @@ export {
|
|
|
3891
4018
|
renderSessionList,
|
|
3892
4019
|
renderSessionRunStart,
|
|
3893
4020
|
renderSessionRunStatus,
|
|
4021
|
+
renderWorkerCapacity,
|
|
3894
4022
|
renderWorkspaceStatus,
|
|
3895
4023
|
renderWorkspaceSync,
|
|
3896
4024
|
resolveCommandExecution,
|
|
3897
4025
|
runR5dctlCli,
|
|
3898
4026
|
summarizeEnvData,
|
|
4027
|
+
waitForBranchDeletion,
|
|
3899
4028
|
waitForSessionProvisioning,
|
|
3900
4029
|
writeConfig
|
|
3901
4030
|
};
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlBranch, type
|
|
1
|
+
import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlWorkerCapacityReport, type R5dctlBranch, type R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
2
2
|
export type GlobalOptions = {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
json: boolean;
|
|
@@ -90,6 +90,7 @@ export declare function parseBranchCreateArgs(args: string[]): R5dctlBranchCreat
|
|
|
90
90
|
export declare function parseBranchDeleteArgs(args: string[]): {
|
|
91
91
|
branchNames: string[];
|
|
92
92
|
confirmed: boolean;
|
|
93
|
+
noWait: boolean;
|
|
93
94
|
};
|
|
94
95
|
/** An older server strips unknown routes and answers with this generic 404; a route's own not-found carries a specific message. */
|
|
95
96
|
export declare function isUnsupportedServerRoute(error: unknown): error is R5dctlApiError;
|
|
@@ -101,10 +102,39 @@ export type R5dctlBranchDeletionOutcome = {
|
|
|
101
102
|
branch: R5dctlBranchDescription;
|
|
102
103
|
} | {
|
|
103
104
|
kind: "deleted";
|
|
104
|
-
result:
|
|
105
|
+
result: {
|
|
106
|
+
branchName: string;
|
|
107
|
+
branchId: string;
|
|
108
|
+
deletedSessionIds: string[];
|
|
109
|
+
};
|
|
110
|
+
} | {
|
|
111
|
+
kind: "accepted";
|
|
112
|
+
result: {
|
|
113
|
+
branchName: string;
|
|
114
|
+
branchId: string;
|
|
115
|
+
sessionIds: string[];
|
|
116
|
+
workerLabel: string;
|
|
117
|
+
};
|
|
105
118
|
};
|
|
119
|
+
/** The platform answers an accepted deletion at once; the checkout and chats disappear behind this poll. */
|
|
120
|
+
export declare const BRANCH_DELETION_POLL_INTERVAL_MS = 2000;
|
|
121
|
+
export declare const BRANCH_DELETION_WAIT_TIMEOUT_MS: number;
|
|
122
|
+
/**
|
|
123
|
+
* Wait for one accepted deletion, keyed by its exact branch incarnation. A
|
|
124
|
+
* `completed` status is the terminal success; a `failed` status carries the
|
|
125
|
+
* platform's reason and is not retried here (re-run the command to retry).
|
|
126
|
+
*/
|
|
127
|
+
export declare function waitForBranchDeletion(client: R5dctlClient, projectRef: string, branchId: string, options?: {
|
|
128
|
+
pollIntervalMs?: number;
|
|
129
|
+
timeoutMs?: number;
|
|
130
|
+
sleep?: (ms: number) => Promise<void>;
|
|
131
|
+
}): Promise<void>;
|
|
106
132
|
export declare function deleteBranchThroughPlatform(client: R5dctlClient, projectRef: string, branchName: string, options: {
|
|
107
133
|
confirmed: boolean;
|
|
134
|
+
noWait?: boolean;
|
|
135
|
+
pollIntervalMs?: number;
|
|
136
|
+
timeoutMs?: number;
|
|
137
|
+
sleep?: (ms: number) => Promise<void>;
|
|
108
138
|
}): Promise<R5dctlBranchDeletionOutcome>;
|
|
109
139
|
export type R5dctlBranchDeletionResult = {
|
|
110
140
|
branchName: string;
|
|
@@ -113,6 +143,13 @@ export type R5dctlBranchDeletionResult = {
|
|
|
113
143
|
} | {
|
|
114
144
|
branchName: string;
|
|
115
145
|
deleted: false;
|
|
146
|
+
accepted: true;
|
|
147
|
+
branchId: string;
|
|
148
|
+
sessionIds: string[];
|
|
149
|
+
} | {
|
|
150
|
+
branchName: string;
|
|
151
|
+
deleted: false;
|
|
152
|
+
accepted?: false;
|
|
116
153
|
error: string;
|
|
117
154
|
};
|
|
118
155
|
export type R5dctlBranchDeletionsOutcome = {
|
|
@@ -126,18 +163,29 @@ export type R5dctlBranchDeletionsOutcome = {
|
|
|
126
163
|
* Delete several branches one request at a time: the server route stays
|
|
127
164
|
* one-branch-per-request. Every name is checked before the first request, a
|
|
128
165
|
* preview describes each branch, and a confirmed run continues past a failed
|
|
129
|
-
* branch so one refused deletion does not strand the rest of the list.
|
|
166
|
+
* branch so one refused deletion does not strand the rest of the list. Each
|
|
167
|
+
* accepted deletion is awaited before the next is requested, so the worker
|
|
168
|
+
* handles them in order and a failure is reported where it happened.
|
|
130
169
|
*/
|
|
131
170
|
export declare function deleteBranchesThroughPlatform(client: R5dctlClient, projectRef: string, branchNames: readonly string[], options: {
|
|
132
171
|
confirmed: boolean;
|
|
172
|
+
noWait?: boolean;
|
|
173
|
+
pollIntervalMs?: number;
|
|
174
|
+
timeoutMs?: number;
|
|
175
|
+
sleep?: (ms: number) => Promise<void>;
|
|
133
176
|
onResult?: (result: R5dctlBranchDeletionResult) => void;
|
|
134
177
|
}): Promise<R5dctlBranchDeletionsOutcome>;
|
|
135
178
|
export declare function renderBranchDeletionResult(result: R5dctlBranchDeletionResult): string;
|
|
179
|
+
/** Operation facts of one deletion, keyed by its branch incarnation. */
|
|
180
|
+
export declare function renderBranchDeletionStatus(status: R5dctlBranchDeletionStatus): string;
|
|
136
181
|
export declare function branchDeletionsConfirmationMessage(branches: readonly R5dctlBranchDescription[]): string;
|
|
137
182
|
export declare function branchDeletionsFailureMessage(results: readonly R5dctlBranchDeletionResult[]): string;
|
|
138
183
|
export declare function renderBranchDeletionPreview(branch: R5dctlBranchDescription): string;
|
|
139
184
|
export declare function branchDeletionConfirmationMessage(branch: R5dctlBranchDescription): string;
|
|
140
|
-
export declare function renderBranchDeletion(result:
|
|
185
|
+
export declare function renderBranchDeletion(result: {
|
|
186
|
+
branchName: string;
|
|
187
|
+
deletedSessionIds: string[];
|
|
188
|
+
}): string;
|
|
141
189
|
/** Operation facts only: no commit hash, because a hash is stale as soon as the branch commits. */
|
|
142
190
|
export declare function renderBranchOperation(operation: R5dctlBranchOperation, projectRef: string): string;
|
|
143
191
|
export declare function parseSessionRunCommand(options: Pick<GlobalOptions, "project" | "branch" | "session">, args: string[], env?: Record<string, string | undefined>): R5dctlSessionRunCommand;
|
|
@@ -230,6 +278,8 @@ export declare function advanceTransientDevicePollBackoff(intervalMs: number, tr
|
|
|
230
278
|
};
|
|
231
279
|
export declare function handleAuthLogin(client: R5dctlClient, options: GlobalOptions, commandArgs: string[], config: R5dctlConfig): Promise<void>;
|
|
232
280
|
export declare function renderSessionList(sessions: R5dctlSessionSummary[], nowMs?: number): string;
|
|
281
|
+
/** One line per worker naming every lane's holders and queue, so an agent can see whom to ask to free capacity. */
|
|
282
|
+
export declare function renderWorkerCapacity(workerLabel: string, capacity: R5dctlWorkerCapacityReport | null | undefined): string[];
|
|
233
283
|
export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
|
|
234
284
|
export declare function renderBranchList(branches: R5dctlBranch[]): string;
|
|
235
285
|
export declare function renderWorkspaceSync(result: R5dctlWorkspaceSyncResult): string;
|
|
@@ -270,6 +320,8 @@ export declare function getK8sUsageState(entry: K8sUsageReport["entries"][number
|
|
|
270
320
|
export declare function renderK8sUsageReport(report: K8sUsageReport): string;
|
|
271
321
|
export declare function formatProcessAge(startedAt: string, nowMs?: number): string;
|
|
272
322
|
export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?: number): string;
|
|
323
|
+
/** `general`, `control`, `queued:general`, or `-` when the worker reported nothing for this run. */
|
|
324
|
+
export declare function formatProcessLane(process: Pick<R5dctlProcessRun, "admission" | "commandClass">): string;
|
|
273
325
|
export declare function renderProcessHistory(processes: R5dctlProcessRun[], nowMs?: number): string;
|
|
274
326
|
/** Streams go to stdout under `==> name <==` headers; the selection summary is returned separately for stderr. */
|
|
275
327
|
export declare function renderProcessLog(log: R5dctlProcessLog): {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5dctl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.123",
|
|
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.123",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|