@ricsam/r5dctl 0.0.103 → 0.0.105
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 +10 -0
- package/dist/cjs/cli.cjs +302 -10
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/cli.mjs +288 -10
- package/dist/mjs/package.json +1 -1
- package/dist/types/cli.d.ts +61 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ r5dctl delete project <namespace/name|id>
|
|
|
55
55
|
|
|
56
56
|
r5dctl -p <project> get branches
|
|
57
57
|
r5dctl -p <project> describe branch <branch>
|
|
58
|
+
r5dctl -p <project> create branch <branch> --source worktree:<branch> [--working-tree carry|clean] [--worker <label>]
|
|
59
|
+
r5dctl -p <project> describe branch-operation <operation-id>
|
|
60
|
+
r5dctl -p <project> delete branch <branch> [<branch>...] # preview the sessions that would be deleted, exit 1
|
|
61
|
+
r5dctl -p <project> delete branch <branch> [<branch>...] --confirm # delete the branches and their sessions through the platform
|
|
58
62
|
r5dctl -p <project> get envs
|
|
59
63
|
r5dctl -p <project> get envs --show-values
|
|
60
64
|
r5dctl -p <project> get env API_KEY
|
|
@@ -119,6 +123,12 @@ repository. Trust applies to that workflow path on every Git ref and grants
|
|
|
119
123
|
cluster-admin, so protect workflow changes and do not invoke it for untrusted
|
|
120
124
|
pull requests.
|
|
121
125
|
|
|
126
|
+
`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.
|
|
127
|
+
|
|
128
|
+
`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 branches are deleted one after another, each prints its own result line, a failure does not stop the remaining deletions, and the exit status is non-zero if any branch failed. 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 worker that is still applying a configuration refresh makes a deletion wait on the server for up to 45 seconds; if the refresh is still running after that, the result line says so and the branch can be retried. 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.
|
|
129
|
+
|
|
130
|
+
`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.
|
|
131
|
+
|
|
122
132
|
`session start` creates an ordinary agent-mode chat on an explicitly selected existing worktree, or creates a named linked worktree from an explicit `worktree:<branch>` source. A new worktree is cut from the source branch's current commit; `--working-tree carry` (the default, `git switch -c` semantics) also copies the source checkout's uncommitted changes into it, while `--working-tree clean` checks out that commit only. The new session's first message states which mode applies, and the start output prints `Source` and `Working tree` lines. A clean request fails with a clear error when the selected worker predates the option, because such a worker would silently carry instead. It requires project, worker, model, and prompt and never infers them from the current shell. Start returns a durable `provisioning` handle immediately; workspace synchronization, linked-worktree creation, and prompt enqueue continue in the background and are visible through `session status`. Idempotent request retries return the same handle. Prompts return after durable enqueue. Task specialization belongs in the prompt; there are no predefined debug, explore, research, or test agents. `session stop` returns as soon as the agent run is stopped; the stopped worktree's workspace Git synchronization finishes asynchronously behind the response.
|
|
123
133
|
|
|
124
134
|
`session status` reports the run lifecycle and the launch-time `Source` and `Working tree` facts only. It deliberately prints no commit hash for the worktree, not even the launch baseline: a hash would be stale by the time a caller acts on it, because commits can land between the read and the action. Find the base and the branch's commits with git in the worker checkout instead, for example `git merge-base <your-branch> <branch>`, `git log --oneline <your-branch>..<branch>`, and `git status` in the branch folder, as the `Branch state` line reminds you. While a new worktree is still provisioning, a `Provisioning` line reports where its creation stands in the worker's operation queue and since when (`Provisioning: queued behind 3 worker operations since 2026-09-03T11:41:30Z`); a worker that predates operation receipts leaves that line out.
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -29,7 +29,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var cli_exports = {};
|
|
30
30
|
__export(cli_exports, {
|
|
31
31
|
advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
|
|
32
|
+
branchDeletionConfirmationMessage: () => branchDeletionConfirmationMessage,
|
|
33
|
+
branchDeletionsConfirmationMessage: () => branchDeletionsConfirmationMessage,
|
|
34
|
+
branchDeletionsFailureMessage: () => branchDeletionsFailureMessage,
|
|
32
35
|
collectK8sUsage: () => collectK8sUsage,
|
|
36
|
+
createBranchThroughPlatform: () => createBranchThroughPlatform,
|
|
37
|
+
deleteBranchThroughPlatform: () => deleteBranchThroughPlatform,
|
|
38
|
+
deleteBranchesThroughPlatform: () => deleteBranchesThroughPlatform,
|
|
33
39
|
dispatchSessionRunCommand: () => dispatchSessionRunCommand,
|
|
34
40
|
followR5dctlSession: () => followR5dctlSession,
|
|
35
41
|
formatK8sCpu: () => formatK8sCpu,
|
|
@@ -41,10 +47,13 @@ __export(cli_exports, {
|
|
|
41
47
|
getR5dctlVersion: () => getR5dctlVersion,
|
|
42
48
|
getTransientDevicePollDelay: () => getTransientDevicePollDelay,
|
|
43
49
|
handleAuthLogin: () => handleAuthLogin,
|
|
50
|
+
isUnsupportedServerRoute: () => isUnsupportedServerRoute,
|
|
44
51
|
main: () => main,
|
|
45
52
|
parseAnswerEnvRequestCommandArgs: () => parseAnswerEnvRequestCommandArgs,
|
|
46
53
|
parseAnswerFlags: () => parseAnswerFlags,
|
|
47
54
|
parseAnswerQuestionsCommandArgs: () => parseAnswerQuestionsCommandArgs,
|
|
55
|
+
parseBranchCreateArgs: () => parseBranchCreateArgs,
|
|
56
|
+
parseBranchDeleteArgs: () => parseBranchDeleteArgs,
|
|
48
57
|
parseConversationForkArgs: () => parseConversationForkArgs,
|
|
49
58
|
parseConversationInspectWorkArgs: () => parseConversationInspectWorkArgs,
|
|
50
59
|
parseConversationOverviewArgs: () => parseConversationOverviewArgs,
|
|
@@ -62,6 +71,11 @@ __export(cli_exports, {
|
|
|
62
71
|
parseSetEnvArgs: () => parseSetEnvArgs,
|
|
63
72
|
parseShellArgs: () => parseShellArgs,
|
|
64
73
|
readDotenvFile: () => readDotenvFile,
|
|
74
|
+
renderBranchDeletion: () => renderBranchDeletion,
|
|
75
|
+
renderBranchDeletionPreview: () => renderBranchDeletionPreview,
|
|
76
|
+
renderBranchDeletionResult: () => renderBranchDeletionResult,
|
|
77
|
+
renderBranchList: () => renderBranchList,
|
|
78
|
+
renderBranchOperation: () => renderBranchOperation,
|
|
65
79
|
renderConversationForkResponse: () => renderConversationForkResponse,
|
|
66
80
|
renderConversationHeadResponse: () => renderConversationHeadResponse,
|
|
67
81
|
renderConversationNodeResponse: () => renderConversationNodeResponse,
|
|
@@ -199,6 +213,21 @@ const SHARED_HELP_ENTRIES = [
|
|
|
199
213
|
{ section: "projects", usage: "delete project <namespace/name|id>", description: "Delete a project." },
|
|
200
214
|
{ section: "branches", usage: "-p <project> get branches", description: "List branches for a project." },
|
|
201
215
|
{ section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
|
|
216
|
+
{
|
|
217
|
+
section: "branches",
|
|
218
|
+
usage: "-p <project> create branch <branch> --source worktree:<branch> [--working-tree <carry|clean>] [--worker <label>]",
|
|
219
|
+
description: "Create a linked worktree branch from an existing branch's checkout and print the operation id to poll."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
section: "branches",
|
|
223
|
+
usage: "-p <project> describe branch-operation <operation-id>",
|
|
224
|
+
description: "Show whether a branch creation is provisioning, ready, or failed."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
section: "branches",
|
|
228
|
+
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
|
|
229
|
+
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
|
|
230
|
+
},
|
|
202
231
|
{
|
|
203
232
|
section: "envs",
|
|
204
233
|
usage: "-p <project> get envs [--show-values]",
|
|
@@ -545,8 +574,8 @@ function parseCommandOperands(args, valueFlags, booleanFlags = /* @__PURE__ */ n
|
|
|
545
574
|
function requireWorker(values) {
|
|
546
575
|
return requireValue(values.get("--worker")?.trim(), "--worker <label> is required");
|
|
547
576
|
}
|
|
548
|
-
function parseWorktreeSource(rawValue) {
|
|
549
|
-
const raw = requireValue(rawValue?.trim(),
|
|
577
|
+
function parseWorktreeSource(rawValue, requiredMessage = "--source worktree:<branch> is required with --new-worktree") {
|
|
578
|
+
const raw = requireValue(rawValue?.trim(), requiredMessage);
|
|
550
579
|
const separator = raw.indexOf(":");
|
|
551
580
|
const type = raw.slice(0, separator);
|
|
552
581
|
const branchName = raw.slice(separator + 1).trim();
|
|
@@ -573,6 +602,150 @@ function requireSessionMode(value) {
|
|
|
573
602
|
}
|
|
574
603
|
return mode;
|
|
575
604
|
}
|
|
605
|
+
function parseBranchCreateArgs(args) {
|
|
606
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--source", "--working-tree", "--worker"]), /* @__PURE__ */ new Map(), "create branch", 1);
|
|
607
|
+
if (parsed.positionals.length > 0) throw new Error(`Unexpected create branch argument: ${parsed.positionals[0]}`);
|
|
608
|
+
const worker = parsed.values.get("--worker")?.trim();
|
|
609
|
+
if (parsed.values.has("--worker") && !worker) throw new Error("Missing value for --worker");
|
|
610
|
+
return {
|
|
611
|
+
source: parseWorktreeSource(parsed.values.get("--source"), "--source worktree:<branch> is required for `create branch`"),
|
|
612
|
+
workingTree: parseWorkingTreeMode(parsed.values.get("--working-tree")),
|
|
613
|
+
...worker ? { worker } : {}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function parseBranchDeleteArgs(args) {
|
|
617
|
+
const branchNames = [];
|
|
618
|
+
let confirmed = false;
|
|
619
|
+
for (const arg of args) {
|
|
620
|
+
if (arg === "--confirm") {
|
|
621
|
+
confirmed = true;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
|
|
625
|
+
if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
|
|
626
|
+
branchNames.push(arg);
|
|
627
|
+
}
|
|
628
|
+
return { branchNames, confirmed };
|
|
629
|
+
}
|
|
630
|
+
const PRIMARY_BRANCH_NAME = "main";
|
|
631
|
+
function isUnsupportedServerRoute(error) {
|
|
632
|
+
return error instanceof import_r5d_api.R5dctlApiError && error.status === 404 && extractApiErrorMessage(error) === "Not Found";
|
|
633
|
+
}
|
|
634
|
+
async function requireServerRouteSupport(command, request) {
|
|
635
|
+
try {
|
|
636
|
+
return await request();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (isUnsupportedServerRoute(error)) {
|
|
639
|
+
throw new Error(`This r5d server does not support \`${command}\` yet. Update the server, or manage the branch from the web sidebar.`);
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function createBranchThroughPlatform(client, projectRef, input) {
|
|
645
|
+
return await requireServerRouteSupport(
|
|
646
|
+
"create branch",
|
|
647
|
+
() => client.projects.branches.create(projectRef, {
|
|
648
|
+
requestId: (0, import_node_crypto.randomUUID)(),
|
|
649
|
+
branchName: input.branchName,
|
|
650
|
+
source: input.source,
|
|
651
|
+
workingTree: input.workingTree,
|
|
652
|
+
...input.worker ? { worker: input.worker } : {}
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
|
|
657
|
+
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
658
|
+
const branch = await client.projects.branches.describe(projectRef, branchName);
|
|
659
|
+
if (!options.confirmed) return { kind: "preview", branch };
|
|
660
|
+
const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
|
|
661
|
+
return { kind: "deleted", result };
|
|
662
|
+
}
|
|
663
|
+
async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
|
|
664
|
+
if (branchNames.length === 0) throw new Error("Missing branch name");
|
|
665
|
+
for (const branchName of branchNames) {
|
|
666
|
+
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
667
|
+
}
|
|
668
|
+
if (!options.confirmed) {
|
|
669
|
+
const branches = [];
|
|
670
|
+
for (const branchName of branchNames) {
|
|
671
|
+
branches.push(await client.projects.branches.describe(projectRef, branchName));
|
|
672
|
+
}
|
|
673
|
+
return { kind: "preview", branches };
|
|
674
|
+
}
|
|
675
|
+
const results = [];
|
|
676
|
+
for (const branchName of branchNames) {
|
|
677
|
+
let result;
|
|
678
|
+
try {
|
|
679
|
+
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, { confirmed: true });
|
|
680
|
+
if (deletion.kind !== "deleted") throw new Error(`Branch ${branchName} was only previewed`);
|
|
681
|
+
result = { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds };
|
|
682
|
+
} catch (error) {
|
|
683
|
+
result = {
|
|
684
|
+
branchName,
|
|
685
|
+
deleted: false,
|
|
686
|
+
error: error instanceof import_r5d_api.R5dctlApiError ? extractApiErrorMessage(error) : error instanceof Error ? error.message : String(error)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
results.push(result);
|
|
690
|
+
options.onResult?.(result);
|
|
691
|
+
}
|
|
692
|
+
return { kind: "deleted", results };
|
|
693
|
+
}
|
|
694
|
+
function renderBranchDeletionResult(result) {
|
|
695
|
+
return result.deleted ? renderBranchDeletion(result) : `Failed to delete branch ${result.branchName}: ${result.error}
|
|
696
|
+
`;
|
|
697
|
+
}
|
|
698
|
+
function branchDeletionsConfirmationMessage(branches) {
|
|
699
|
+
const [only] = branches;
|
|
700
|
+
if (branches.length === 1 && only) return branchDeletionConfirmationMessage(only);
|
|
701
|
+
const sessionCount = branches.reduce((count, branch) => count + branch.sessions.length, 0);
|
|
702
|
+
return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
|
|
703
|
+
}
|
|
704
|
+
function branchDeletionsFailureMessage(results) {
|
|
705
|
+
return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
|
|
706
|
+
}
|
|
707
|
+
function renderBranchDeletionPreview(branch) {
|
|
708
|
+
const count = branch.sessions.length;
|
|
709
|
+
const lines = [
|
|
710
|
+
`Project: ${branch.project.path}`,
|
|
711
|
+
`Branch: ${branch.branchName}`,
|
|
712
|
+
`Sessions: ${count}`,
|
|
713
|
+
...branch.sessions.map((session) => ` ${session.id} ${session.name ?? "(unnamed)"}`),
|
|
714
|
+
`Deleting this branch removes its checkout from every connected worker and deletes the ${count} session(s) above.`
|
|
715
|
+
];
|
|
716
|
+
return `${lines.join("\n")}
|
|
717
|
+
`;
|
|
718
|
+
}
|
|
719
|
+
function branchDeletionConfirmationMessage(branch) {
|
|
720
|
+
return `Nothing was deleted. Re-run with --confirm to delete branch ${branch.branchName} and its ${branch.sessions.length} session(s).`;
|
|
721
|
+
}
|
|
722
|
+
function renderBranchDeletion(result) {
|
|
723
|
+
const lines = [
|
|
724
|
+
`Deleted branch ${result.branchName} and ${result.deletedSessionIds.length} session(s).`,
|
|
725
|
+
...result.deletedSessionIds.map((sessionId) => ` ${sessionId}`)
|
|
726
|
+
];
|
|
727
|
+
return `${lines.join("\n")}
|
|
728
|
+
`;
|
|
729
|
+
}
|
|
730
|
+
function renderBranchOperation(operation, projectRef) {
|
|
731
|
+
const lines = [
|
|
732
|
+
`Operation: ${operation.id}`,
|
|
733
|
+
`Branch: ${operation.branchName}`,
|
|
734
|
+
`Source: worktree:${operation.source.branchName}`,
|
|
735
|
+
`Working tree: ${operation.workingTree}`,
|
|
736
|
+
`Worker: ${operation.worker ?? "(pending)"}`,
|
|
737
|
+
`Status: ${operation.status}`
|
|
738
|
+
];
|
|
739
|
+
if (operation.error) lines.push(`Error: ${operation.error}`);
|
|
740
|
+
if (operation.cleanupPending) lines.push("Cleanup: pending");
|
|
741
|
+
if (operation.status === "provisioning") {
|
|
742
|
+
lines.push(`Check: r5dctl -p ${projectRef} describe branch-operation ${operation.id}`);
|
|
743
|
+
} else if (operation.status === "ready") {
|
|
744
|
+
lines.push(`Branch state: ${formatBranchStateHint(operation.branchName)}`);
|
|
745
|
+
}
|
|
746
|
+
return `${lines.join("\n")}
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
576
749
|
function rejectSessionRunGlobalScopes(options, allowProject) {
|
|
577
750
|
if (!allowProject && options.project) throw new Error("--project/-p is only supported for `session start`");
|
|
578
751
|
if (options.branch) throw new Error("--branch/-b is not supported for `session`; select the worktree explicitly");
|
|
@@ -1479,10 +1652,34 @@ function renderWorkspaceStatus(status) {
|
|
|
1479
1652
|
`Workspace HEAD: ${status.head ?? "(not initialized)"}`,
|
|
1480
1653
|
`Latest sync: ${status.latestSync ? `${status.latestSync.outcome} on ${status.latestSync.workerLabel}` : "(none)"}`,
|
|
1481
1654
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1482
|
-
...status.workers.
|
|
1483
|
-
|
|
1484
|
-
|
|
1655
|
+
...status.workers.flatMap((worker) => [
|
|
1656
|
+
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1657
|
+
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? [])
|
|
1658
|
+
])
|
|
1659
|
+
];
|
|
1660
|
+
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1661
|
+
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1662
|
+
}
|
|
1663
|
+
return `${lines.join("\n")}
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
const CHECKOUT_RECOVERY_HINT = "delete the branch with `r5dctl -p <project> delete branch <branch> --confirm`, or reconnect the worker to recreate the checkout";
|
|
1667
|
+
function renderMissingCheckouts(workerLabel, missingCheckouts) {
|
|
1668
|
+
if (missingCheckouts.length === 0) return [];
|
|
1669
|
+
return [
|
|
1670
|
+
`Missing checkouts on ${workerLabel}: ${missingCheckouts.map((checkout) => `${checkout.projectId}/${checkout.branchName} (${checkout.reason})`).join(", ")}`,
|
|
1671
|
+
` Workspace sync skips these branches; ${CHECKOUT_RECOVERY_HINT}.`
|
|
1485
1672
|
];
|
|
1673
|
+
}
|
|
1674
|
+
function renderBranchList(branches) {
|
|
1675
|
+
if (branches.length === 0) return "No branches found.\n";
|
|
1676
|
+
const lines = branches.map((branch) => {
|
|
1677
|
+
const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
|
|
1678
|
+
return `${branch.branchName} sessions=${branch.sessionCount}${checkout}`;
|
|
1679
|
+
});
|
|
1680
|
+
if (branches.some((branch) => branch.checkoutState)) {
|
|
1681
|
+
lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
|
|
1682
|
+
}
|
|
1486
1683
|
return `${lines.join("\n")}
|
|
1487
1684
|
`;
|
|
1488
1685
|
}
|
|
@@ -2660,11 +2857,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2660
2857
|
if (first === "projects" && second === "branches" && third === "list" || first === "get" && second === "branches") {
|
|
2661
2858
|
const projectRef = requireValue(first === "get" ? args[2] : args[3], "Missing project reference");
|
|
2662
2859
|
const branches = await client.projects.branches.list(projectRef);
|
|
2663
|
-
write(
|
|
2664
|
-
branches,
|
|
2665
|
-
branches.length === 0 ? "No branches found.\n" : `${branches.map((branch) => `${branch.branchName} sessions=${branch.sessionCount}`).join("\n")}
|
|
2666
|
-
`
|
|
2667
|
-
);
|
|
2860
|
+
write(branches, renderBranchList(branches));
|
|
2668
2861
|
return;
|
|
2669
2862
|
}
|
|
2670
2863
|
if (first === "projects" && second === "branches" && third === "describe" || first === "describe" && second === "branch") {
|
|
@@ -2679,6 +2872,47 @@ Sessions: ${result.sessions.length}
|
|
|
2679
2872
|
`);
|
|
2680
2873
|
return;
|
|
2681
2874
|
}
|
|
2875
|
+
if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
|
|
2876
|
+
const offset = first === "create" ? 2 : 3;
|
|
2877
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2878
|
+
const branchName = requireValue(args[offset + 1], "Missing branch name");
|
|
2879
|
+
const operation = await createBranchThroughPlatform(client, projectRef, {
|
|
2880
|
+
branchName,
|
|
2881
|
+
...parseBranchCreateArgs(args.slice(offset + 2))
|
|
2882
|
+
});
|
|
2883
|
+
write(operation, renderBranchOperation(operation, projectRef));
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (first === "projects" && second === "branches" && third === "operation" || first === "describe" && second === "branch-operation") {
|
|
2887
|
+
const offset = first === "describe" ? 2 : 3;
|
|
2888
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2889
|
+
const operationId = requireValue(args[offset + 1], "Missing operation id");
|
|
2890
|
+
const operation = await requireServerRouteSupport(
|
|
2891
|
+
"describe branch-operation",
|
|
2892
|
+
() => client.projects.branches.describeOperation(projectRef, operationId)
|
|
2893
|
+
);
|
|
2894
|
+
write(operation, renderBranchOperation(operation, projectRef));
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
|
|
2898
|
+
const offset = first === "delete" ? 2 : 3;
|
|
2899
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2900
|
+
const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
2901
|
+
const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
|
|
2902
|
+
confirmed,
|
|
2903
|
+
// Each deletion is its own round trip, so a reader sees results as they land.
|
|
2904
|
+
onResult: (result) => {
|
|
2905
|
+
if (!json) process.stdout.write(renderBranchDeletionResult(result));
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
if (deletion.kind === "preview") {
|
|
2909
|
+
write({ branches: deletion.branches, confirmed: false }, deletion.branches.map(renderBranchDeletionPreview).join("\n"));
|
|
2910
|
+
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
2911
|
+
}
|
|
2912
|
+
if (json) write({ results: deletion.results }, "");
|
|
2913
|
+
if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2682
2916
|
if (first === "get" && second === "envs") {
|
|
2683
2917
|
const projectRef = requireValue(args[2], "Missing project reference");
|
|
2684
2918
|
const options = parseGetEnvsArgs(args.slice(3));
|
|
@@ -3095,6 +3329,19 @@ function resolveCommandExecution(options, rest) {
|
|
|
3095
3329
|
pluginArgs: ["describe", "branch", options.project, branch]
|
|
3096
3330
|
};
|
|
3097
3331
|
}
|
|
3332
|
+
if (target === "branch-operation") {
|
|
3333
|
+
if (!options.project) {
|
|
3334
|
+
throw new Error("--project/-p is required for `describe branch-operation`");
|
|
3335
|
+
}
|
|
3336
|
+
const operationId = requireValue(commandArgs[1], "Operation id is required for `describe branch-operation`");
|
|
3337
|
+
if (commandArgs.length > 2) {
|
|
3338
|
+
throw new Error(`Unexpected describe branch-operation argument: ${commandArgs[2]}`);
|
|
3339
|
+
}
|
|
3340
|
+
return {
|
|
3341
|
+
kind: "plugin",
|
|
3342
|
+
pluginArgs: ["describe", "branch-operation", options.project, operationId]
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3098
3345
|
if (target === "session") {
|
|
3099
3346
|
const sessionId = commandArgs[1] ?? options.session;
|
|
3100
3347
|
if (!sessionId) {
|
|
@@ -3127,6 +3374,23 @@ function resolveCommandExecution(options, rest) {
|
|
|
3127
3374
|
pluginArgs: ["create", "session", projectRef, branch, ...flagArgs]
|
|
3128
3375
|
};
|
|
3129
3376
|
}
|
|
3377
|
+
if (target === "branch") {
|
|
3378
|
+
if (!options.project) {
|
|
3379
|
+
throw new Error("--project/-p is required for `create branch`");
|
|
3380
|
+
}
|
|
3381
|
+
const rawArgs = commandArgs.slice(1);
|
|
3382
|
+
const positionalBranch = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs[0] : void 0;
|
|
3383
|
+
const branch = positionalBranch ?? options.branch;
|
|
3384
|
+
if (!branch) {
|
|
3385
|
+
throw new Error("Branch name is required for `create branch`");
|
|
3386
|
+
}
|
|
3387
|
+
const flagArgs = positionalBranch ? rawArgs.slice(1) : rawArgs;
|
|
3388
|
+
parseBranchCreateArgs(flagArgs);
|
|
3389
|
+
return {
|
|
3390
|
+
kind: "plugin",
|
|
3391
|
+
pluginArgs: ["create", "branch", options.project, branch, ...flagArgs]
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3130
3394
|
throw new Error("Unknown create command");
|
|
3131
3395
|
}
|
|
3132
3396
|
if (command === "update") {
|
|
@@ -3177,6 +3441,20 @@ function resolveCommandExecution(options, rest) {
|
|
|
3177
3441
|
pluginArgs: ["delete", "session", sessionId]
|
|
3178
3442
|
};
|
|
3179
3443
|
}
|
|
3444
|
+
if (target === "branch") {
|
|
3445
|
+
if (!options.project) {
|
|
3446
|
+
throw new Error("--project/-p is required for `delete branch`");
|
|
3447
|
+
}
|
|
3448
|
+
const parsed = parseBranchDeleteArgs(commandArgs.slice(1));
|
|
3449
|
+
const branchNames = parsed.branchNames.length > 0 ? parsed.branchNames : options.branch ? [options.branch] : [];
|
|
3450
|
+
if (branchNames.length === 0) {
|
|
3451
|
+
throw new Error("Branch name is required for `delete branch`");
|
|
3452
|
+
}
|
|
3453
|
+
return {
|
|
3454
|
+
kind: "plugin",
|
|
3455
|
+
pluginArgs: ["delete", "branch", options.project, ...branchNames, ...parsed.confirmed ? ["--confirm"] : []]
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3180
3458
|
throw new Error("Unknown delete command");
|
|
3181
3459
|
}
|
|
3182
3460
|
if (command === "conversation") {
|
|
@@ -3404,7 +3682,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3404
3682
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3405
3683
|
0 && (module.exports = {
|
|
3406
3684
|
advanceTransientDevicePollBackoff,
|
|
3685
|
+
branchDeletionConfirmationMessage,
|
|
3686
|
+
branchDeletionsConfirmationMessage,
|
|
3687
|
+
branchDeletionsFailureMessage,
|
|
3407
3688
|
collectK8sUsage,
|
|
3689
|
+
createBranchThroughPlatform,
|
|
3690
|
+
deleteBranchThroughPlatform,
|
|
3691
|
+
deleteBranchesThroughPlatform,
|
|
3408
3692
|
dispatchSessionRunCommand,
|
|
3409
3693
|
followR5dctlSession,
|
|
3410
3694
|
formatK8sCpu,
|
|
@@ -3416,10 +3700,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3416
3700
|
getR5dctlVersion,
|
|
3417
3701
|
getTransientDevicePollDelay,
|
|
3418
3702
|
handleAuthLogin,
|
|
3703
|
+
isUnsupportedServerRoute,
|
|
3419
3704
|
main,
|
|
3420
3705
|
parseAnswerEnvRequestCommandArgs,
|
|
3421
3706
|
parseAnswerFlags,
|
|
3422
3707
|
parseAnswerQuestionsCommandArgs,
|
|
3708
|
+
parseBranchCreateArgs,
|
|
3709
|
+
parseBranchDeleteArgs,
|
|
3423
3710
|
parseConversationForkArgs,
|
|
3424
3711
|
parseConversationInspectWorkArgs,
|
|
3425
3712
|
parseConversationOverviewArgs,
|
|
@@ -3437,6 +3724,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3437
3724
|
parseSetEnvArgs,
|
|
3438
3725
|
parseShellArgs,
|
|
3439
3726
|
readDotenvFile,
|
|
3727
|
+
renderBranchDeletion,
|
|
3728
|
+
renderBranchDeletionPreview,
|
|
3729
|
+
renderBranchDeletionResult,
|
|
3730
|
+
renderBranchList,
|
|
3731
|
+
renderBranchOperation,
|
|
3440
3732
|
renderConversationForkResponse,
|
|
3441
3733
|
renderConversationHeadResponse,
|
|
3442
3734
|
renderConversationNodeResponse,
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/cli.mjs
CHANGED
|
@@ -114,6 +114,21 @@ const SHARED_HELP_ENTRIES = [
|
|
|
114
114
|
{ section: "projects", usage: "delete project <namespace/name|id>", description: "Delete a project." },
|
|
115
115
|
{ section: "branches", usage: "-p <project> get branches", description: "List branches for a project." },
|
|
116
116
|
{ section: "branches", usage: "-p <project> describe branch <branch>", description: "Show branch URLs and sessions." },
|
|
117
|
+
{
|
|
118
|
+
section: "branches",
|
|
119
|
+
usage: "-p <project> create branch <branch> --source worktree:<branch> [--working-tree <carry|clean>] [--worker <label>]",
|
|
120
|
+
description: "Create a linked worktree branch from an existing branch's checkout and print the operation id to poll."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
section: "branches",
|
|
124
|
+
usage: "-p <project> describe branch-operation <operation-id>",
|
|
125
|
+
description: "Show whether a branch creation is provisioning, ready, or failed."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
section: "branches",
|
|
129
|
+
usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
|
|
130
|
+
description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
|
|
131
|
+
},
|
|
117
132
|
{
|
|
118
133
|
section: "envs",
|
|
119
134
|
usage: "-p <project> get envs [--show-values]",
|
|
@@ -460,8 +475,8 @@ function parseCommandOperands(args, valueFlags, booleanFlags = /* @__PURE__ */ n
|
|
|
460
475
|
function requireWorker(values) {
|
|
461
476
|
return requireValue(values.get("--worker")?.trim(), "--worker <label> is required");
|
|
462
477
|
}
|
|
463
|
-
function parseWorktreeSource(rawValue) {
|
|
464
|
-
const raw = requireValue(rawValue?.trim(),
|
|
478
|
+
function parseWorktreeSource(rawValue, requiredMessage = "--source worktree:<branch> is required with --new-worktree") {
|
|
479
|
+
const raw = requireValue(rawValue?.trim(), requiredMessage);
|
|
465
480
|
const separator = raw.indexOf(":");
|
|
466
481
|
const type = raw.slice(0, separator);
|
|
467
482
|
const branchName = raw.slice(separator + 1).trim();
|
|
@@ -488,6 +503,150 @@ function requireSessionMode(value) {
|
|
|
488
503
|
}
|
|
489
504
|
return mode;
|
|
490
505
|
}
|
|
506
|
+
function parseBranchCreateArgs(args) {
|
|
507
|
+
const parsed = parseCommandOperands(args, /* @__PURE__ */ new Set(["--source", "--working-tree", "--worker"]), /* @__PURE__ */ new Map(), "create branch", 1);
|
|
508
|
+
if (parsed.positionals.length > 0) throw new Error(`Unexpected create branch argument: ${parsed.positionals[0]}`);
|
|
509
|
+
const worker = parsed.values.get("--worker")?.trim();
|
|
510
|
+
if (parsed.values.has("--worker") && !worker) throw new Error("Missing value for --worker");
|
|
511
|
+
return {
|
|
512
|
+
source: parseWorktreeSource(parsed.values.get("--source"), "--source worktree:<branch> is required for `create branch`"),
|
|
513
|
+
workingTree: parseWorkingTreeMode(parsed.values.get("--working-tree")),
|
|
514
|
+
...worker ? { worker } : {}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function parseBranchDeleteArgs(args) {
|
|
518
|
+
const branchNames = [];
|
|
519
|
+
let confirmed = false;
|
|
520
|
+
for (const arg of args) {
|
|
521
|
+
if (arg === "--confirm") {
|
|
522
|
+
confirmed = true;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
|
|
526
|
+
if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
|
|
527
|
+
branchNames.push(arg);
|
|
528
|
+
}
|
|
529
|
+
return { branchNames, confirmed };
|
|
530
|
+
}
|
|
531
|
+
const PRIMARY_BRANCH_NAME = "main";
|
|
532
|
+
function isUnsupportedServerRoute(error) {
|
|
533
|
+
return error instanceof R5dctlApiError && error.status === 404 && extractApiErrorMessage(error) === "Not Found";
|
|
534
|
+
}
|
|
535
|
+
async function requireServerRouteSupport(command, request) {
|
|
536
|
+
try {
|
|
537
|
+
return await request();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (isUnsupportedServerRoute(error)) {
|
|
540
|
+
throw new Error(`This r5d server does not support \`${command}\` yet. Update the server, or manage the branch from the web sidebar.`);
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function createBranchThroughPlatform(client, projectRef, input) {
|
|
546
|
+
return await requireServerRouteSupport(
|
|
547
|
+
"create branch",
|
|
548
|
+
() => client.projects.branches.create(projectRef, {
|
|
549
|
+
requestId: randomUUID(),
|
|
550
|
+
branchName: input.branchName,
|
|
551
|
+
source: input.source,
|
|
552
|
+
workingTree: input.workingTree,
|
|
553
|
+
...input.worker ? { worker: input.worker } : {}
|
|
554
|
+
})
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
|
|
558
|
+
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
559
|
+
const branch = await client.projects.branches.describe(projectRef, branchName);
|
|
560
|
+
if (!options.confirmed) return { kind: "preview", branch };
|
|
561
|
+
const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
|
|
562
|
+
return { kind: "deleted", result };
|
|
563
|
+
}
|
|
564
|
+
async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
|
|
565
|
+
if (branchNames.length === 0) throw new Error("Missing branch name");
|
|
566
|
+
for (const branchName of branchNames) {
|
|
567
|
+
if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
|
|
568
|
+
}
|
|
569
|
+
if (!options.confirmed) {
|
|
570
|
+
const branches = [];
|
|
571
|
+
for (const branchName of branchNames) {
|
|
572
|
+
branches.push(await client.projects.branches.describe(projectRef, branchName));
|
|
573
|
+
}
|
|
574
|
+
return { kind: "preview", branches };
|
|
575
|
+
}
|
|
576
|
+
const results = [];
|
|
577
|
+
for (const branchName of branchNames) {
|
|
578
|
+
let result;
|
|
579
|
+
try {
|
|
580
|
+
const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, { confirmed: true });
|
|
581
|
+
if (deletion.kind !== "deleted") throw new Error(`Branch ${branchName} was only previewed`);
|
|
582
|
+
result = { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds };
|
|
583
|
+
} catch (error) {
|
|
584
|
+
result = {
|
|
585
|
+
branchName,
|
|
586
|
+
deleted: false,
|
|
587
|
+
error: error instanceof R5dctlApiError ? extractApiErrorMessage(error) : error instanceof Error ? error.message : String(error)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
results.push(result);
|
|
591
|
+
options.onResult?.(result);
|
|
592
|
+
}
|
|
593
|
+
return { kind: "deleted", results };
|
|
594
|
+
}
|
|
595
|
+
function renderBranchDeletionResult(result) {
|
|
596
|
+
return result.deleted ? renderBranchDeletion(result) : `Failed to delete branch ${result.branchName}: ${result.error}
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
function branchDeletionsConfirmationMessage(branches) {
|
|
600
|
+
const [only] = branches;
|
|
601
|
+
if (branches.length === 1 && only) return branchDeletionConfirmationMessage(only);
|
|
602
|
+
const sessionCount = branches.reduce((count, branch) => count + branch.sessions.length, 0);
|
|
603
|
+
return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
|
|
604
|
+
}
|
|
605
|
+
function branchDeletionsFailureMessage(results) {
|
|
606
|
+
return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
|
|
607
|
+
}
|
|
608
|
+
function renderBranchDeletionPreview(branch) {
|
|
609
|
+
const count = branch.sessions.length;
|
|
610
|
+
const lines = [
|
|
611
|
+
`Project: ${branch.project.path}`,
|
|
612
|
+
`Branch: ${branch.branchName}`,
|
|
613
|
+
`Sessions: ${count}`,
|
|
614
|
+
...branch.sessions.map((session) => ` ${session.id} ${session.name ?? "(unnamed)"}`),
|
|
615
|
+
`Deleting this branch removes its checkout from every connected worker and deletes the ${count} session(s) above.`
|
|
616
|
+
];
|
|
617
|
+
return `${lines.join("\n")}
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
function branchDeletionConfirmationMessage(branch) {
|
|
621
|
+
return `Nothing was deleted. Re-run with --confirm to delete branch ${branch.branchName} and its ${branch.sessions.length} session(s).`;
|
|
622
|
+
}
|
|
623
|
+
function renderBranchDeletion(result) {
|
|
624
|
+
const lines = [
|
|
625
|
+
`Deleted branch ${result.branchName} and ${result.deletedSessionIds.length} session(s).`,
|
|
626
|
+
...result.deletedSessionIds.map((sessionId) => ` ${sessionId}`)
|
|
627
|
+
];
|
|
628
|
+
return `${lines.join("\n")}
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
function renderBranchOperation(operation, projectRef) {
|
|
632
|
+
const lines = [
|
|
633
|
+
`Operation: ${operation.id}`,
|
|
634
|
+
`Branch: ${operation.branchName}`,
|
|
635
|
+
`Source: worktree:${operation.source.branchName}`,
|
|
636
|
+
`Working tree: ${operation.workingTree}`,
|
|
637
|
+
`Worker: ${operation.worker ?? "(pending)"}`,
|
|
638
|
+
`Status: ${operation.status}`
|
|
639
|
+
];
|
|
640
|
+
if (operation.error) lines.push(`Error: ${operation.error}`);
|
|
641
|
+
if (operation.cleanupPending) lines.push("Cleanup: pending");
|
|
642
|
+
if (operation.status === "provisioning") {
|
|
643
|
+
lines.push(`Check: r5dctl -p ${projectRef} describe branch-operation ${operation.id}`);
|
|
644
|
+
} else if (operation.status === "ready") {
|
|
645
|
+
lines.push(`Branch state: ${formatBranchStateHint(operation.branchName)}`);
|
|
646
|
+
}
|
|
647
|
+
return `${lines.join("\n")}
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
491
650
|
function rejectSessionRunGlobalScopes(options, allowProject) {
|
|
492
651
|
if (!allowProject && options.project) throw new Error("--project/-p is only supported for `session start`");
|
|
493
652
|
if (options.branch) throw new Error("--branch/-b is not supported for `session`; select the worktree explicitly");
|
|
@@ -1394,10 +1553,34 @@ function renderWorkspaceStatus(status) {
|
|
|
1394
1553
|
`Workspace HEAD: ${status.head ?? "(not initialized)"}`,
|
|
1395
1554
|
`Latest sync: ${status.latestSync ? `${status.latestSync.outcome} on ${status.latestSync.workerLabel}` : "(none)"}`,
|
|
1396
1555
|
`Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
|
|
1397
|
-
...status.workers.
|
|
1398
|
-
|
|
1399
|
-
|
|
1556
|
+
...status.workers.flatMap((worker) => [
|
|
1557
|
+
`Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
|
|
1558
|
+
...renderMissingCheckouts(worker.workerLabel, worker.missingCheckouts ?? [])
|
|
1559
|
+
])
|
|
1560
|
+
];
|
|
1561
|
+
if (status.latestSync?.missingCheckouts?.length && !status.workers.some((worker) => worker.missingCheckouts?.length)) {
|
|
1562
|
+
lines.push(...renderMissingCheckouts(status.latestSync.workerLabel, status.latestSync.missingCheckouts));
|
|
1563
|
+
}
|
|
1564
|
+
return `${lines.join("\n")}
|
|
1565
|
+
`;
|
|
1566
|
+
}
|
|
1567
|
+
const CHECKOUT_RECOVERY_HINT = "delete the branch with `r5dctl -p <project> delete branch <branch> --confirm`, or reconnect the worker to recreate the checkout";
|
|
1568
|
+
function renderMissingCheckouts(workerLabel, missingCheckouts) {
|
|
1569
|
+
if (missingCheckouts.length === 0) return [];
|
|
1570
|
+
return [
|
|
1571
|
+
`Missing checkouts on ${workerLabel}: ${missingCheckouts.map((checkout) => `${checkout.projectId}/${checkout.branchName} (${checkout.reason})`).join(", ")}`,
|
|
1572
|
+
` Workspace sync skips these branches; ${CHECKOUT_RECOVERY_HINT}.`
|
|
1400
1573
|
];
|
|
1574
|
+
}
|
|
1575
|
+
function renderBranchList(branches) {
|
|
1576
|
+
if (branches.length === 0) return "No branches found.\n";
|
|
1577
|
+
const lines = branches.map((branch) => {
|
|
1578
|
+
const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
|
|
1579
|
+
return `${branch.branchName} sessions=${branch.sessionCount}${checkout}`;
|
|
1580
|
+
});
|
|
1581
|
+
if (branches.some((branch) => branch.checkoutState)) {
|
|
1582
|
+
lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
|
|
1583
|
+
}
|
|
1401
1584
|
return `${lines.join("\n")}
|
|
1402
1585
|
`;
|
|
1403
1586
|
}
|
|
@@ -2575,11 +2758,7 @@ async function executeR5dctlCommand(client, json, args) {
|
|
|
2575
2758
|
if (first === "projects" && second === "branches" && third === "list" || first === "get" && second === "branches") {
|
|
2576
2759
|
const projectRef = requireValue(first === "get" ? args[2] : args[3], "Missing project reference");
|
|
2577
2760
|
const branches = await client.projects.branches.list(projectRef);
|
|
2578
|
-
write(
|
|
2579
|
-
branches,
|
|
2580
|
-
branches.length === 0 ? "No branches found.\n" : `${branches.map((branch) => `${branch.branchName} sessions=${branch.sessionCount}`).join("\n")}
|
|
2581
|
-
`
|
|
2582
|
-
);
|
|
2761
|
+
write(branches, renderBranchList(branches));
|
|
2583
2762
|
return;
|
|
2584
2763
|
}
|
|
2585
2764
|
if (first === "projects" && second === "branches" && third === "describe" || first === "describe" && second === "branch") {
|
|
@@ -2594,6 +2773,47 @@ Sessions: ${result.sessions.length}
|
|
|
2594
2773
|
`);
|
|
2595
2774
|
return;
|
|
2596
2775
|
}
|
|
2776
|
+
if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
|
|
2777
|
+
const offset = first === "create" ? 2 : 3;
|
|
2778
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2779
|
+
const branchName = requireValue(args[offset + 1], "Missing branch name");
|
|
2780
|
+
const operation = await createBranchThroughPlatform(client, projectRef, {
|
|
2781
|
+
branchName,
|
|
2782
|
+
...parseBranchCreateArgs(args.slice(offset + 2))
|
|
2783
|
+
});
|
|
2784
|
+
write(operation, renderBranchOperation(operation, projectRef));
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
if (first === "projects" && second === "branches" && third === "operation" || first === "describe" && second === "branch-operation") {
|
|
2788
|
+
const offset = first === "describe" ? 2 : 3;
|
|
2789
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2790
|
+
const operationId = requireValue(args[offset + 1], "Missing operation id");
|
|
2791
|
+
const operation = await requireServerRouteSupport(
|
|
2792
|
+
"describe branch-operation",
|
|
2793
|
+
() => client.projects.branches.describeOperation(projectRef, operationId)
|
|
2794
|
+
);
|
|
2795
|
+
write(operation, renderBranchOperation(operation, projectRef));
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
|
|
2799
|
+
const offset = first === "delete" ? 2 : 3;
|
|
2800
|
+
const projectRef = requireValue(args[offset], "Missing project reference");
|
|
2801
|
+
const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
|
|
2802
|
+
const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
|
|
2803
|
+
confirmed,
|
|
2804
|
+
// Each deletion is its own round trip, so a reader sees results as they land.
|
|
2805
|
+
onResult: (result) => {
|
|
2806
|
+
if (!json) process.stdout.write(renderBranchDeletionResult(result));
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
if (deletion.kind === "preview") {
|
|
2810
|
+
write({ branches: deletion.branches, confirmed: false }, deletion.branches.map(renderBranchDeletionPreview).join("\n"));
|
|
2811
|
+
throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
|
|
2812
|
+
}
|
|
2813
|
+
if (json) write({ results: deletion.results }, "");
|
|
2814
|
+
if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2597
2817
|
if (first === "get" && second === "envs") {
|
|
2598
2818
|
const projectRef = requireValue(args[2], "Missing project reference");
|
|
2599
2819
|
const options = parseGetEnvsArgs(args.slice(3));
|
|
@@ -3010,6 +3230,19 @@ function resolveCommandExecution(options, rest) {
|
|
|
3010
3230
|
pluginArgs: ["describe", "branch", options.project, branch]
|
|
3011
3231
|
};
|
|
3012
3232
|
}
|
|
3233
|
+
if (target === "branch-operation") {
|
|
3234
|
+
if (!options.project) {
|
|
3235
|
+
throw new Error("--project/-p is required for `describe branch-operation`");
|
|
3236
|
+
}
|
|
3237
|
+
const operationId = requireValue(commandArgs[1], "Operation id is required for `describe branch-operation`");
|
|
3238
|
+
if (commandArgs.length > 2) {
|
|
3239
|
+
throw new Error(`Unexpected describe branch-operation argument: ${commandArgs[2]}`);
|
|
3240
|
+
}
|
|
3241
|
+
return {
|
|
3242
|
+
kind: "plugin",
|
|
3243
|
+
pluginArgs: ["describe", "branch-operation", options.project, operationId]
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3013
3246
|
if (target === "session") {
|
|
3014
3247
|
const sessionId = commandArgs[1] ?? options.session;
|
|
3015
3248
|
if (!sessionId) {
|
|
@@ -3042,6 +3275,23 @@ function resolveCommandExecution(options, rest) {
|
|
|
3042
3275
|
pluginArgs: ["create", "session", projectRef, branch, ...flagArgs]
|
|
3043
3276
|
};
|
|
3044
3277
|
}
|
|
3278
|
+
if (target === "branch") {
|
|
3279
|
+
if (!options.project) {
|
|
3280
|
+
throw new Error("--project/-p is required for `create branch`");
|
|
3281
|
+
}
|
|
3282
|
+
const rawArgs = commandArgs.slice(1);
|
|
3283
|
+
const positionalBranch = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs[0] : void 0;
|
|
3284
|
+
const branch = positionalBranch ?? options.branch;
|
|
3285
|
+
if (!branch) {
|
|
3286
|
+
throw new Error("Branch name is required for `create branch`");
|
|
3287
|
+
}
|
|
3288
|
+
const flagArgs = positionalBranch ? rawArgs.slice(1) : rawArgs;
|
|
3289
|
+
parseBranchCreateArgs(flagArgs);
|
|
3290
|
+
return {
|
|
3291
|
+
kind: "plugin",
|
|
3292
|
+
pluginArgs: ["create", "branch", options.project, branch, ...flagArgs]
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3045
3295
|
throw new Error("Unknown create command");
|
|
3046
3296
|
}
|
|
3047
3297
|
if (command === "update") {
|
|
@@ -3092,6 +3342,20 @@ function resolveCommandExecution(options, rest) {
|
|
|
3092
3342
|
pluginArgs: ["delete", "session", sessionId]
|
|
3093
3343
|
};
|
|
3094
3344
|
}
|
|
3345
|
+
if (target === "branch") {
|
|
3346
|
+
if (!options.project) {
|
|
3347
|
+
throw new Error("--project/-p is required for `delete branch`");
|
|
3348
|
+
}
|
|
3349
|
+
const parsed = parseBranchDeleteArgs(commandArgs.slice(1));
|
|
3350
|
+
const branchNames = parsed.branchNames.length > 0 ? parsed.branchNames : options.branch ? [options.branch] : [];
|
|
3351
|
+
if (branchNames.length === 0) {
|
|
3352
|
+
throw new Error("Branch name is required for `delete branch`");
|
|
3353
|
+
}
|
|
3354
|
+
return {
|
|
3355
|
+
kind: "plugin",
|
|
3356
|
+
pluginArgs: ["delete", "branch", options.project, ...branchNames, ...parsed.confirmed ? ["--confirm"] : []]
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3095
3359
|
throw new Error("Unknown delete command");
|
|
3096
3360
|
}
|
|
3097
3361
|
if (command === "conversation") {
|
|
@@ -3318,7 +3582,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3318
3582
|
}
|
|
3319
3583
|
export {
|
|
3320
3584
|
advanceTransientDevicePollBackoff,
|
|
3585
|
+
branchDeletionConfirmationMessage,
|
|
3586
|
+
branchDeletionsConfirmationMessage,
|
|
3587
|
+
branchDeletionsFailureMessage,
|
|
3321
3588
|
collectK8sUsage,
|
|
3589
|
+
createBranchThroughPlatform,
|
|
3590
|
+
deleteBranchThroughPlatform,
|
|
3591
|
+
deleteBranchesThroughPlatform,
|
|
3322
3592
|
dispatchSessionRunCommand,
|
|
3323
3593
|
followR5dctlSession,
|
|
3324
3594
|
formatK8sCpu,
|
|
@@ -3330,10 +3600,13 @@ export {
|
|
|
3330
3600
|
getR5dctlVersion,
|
|
3331
3601
|
getTransientDevicePollDelay,
|
|
3332
3602
|
handleAuthLogin,
|
|
3603
|
+
isUnsupportedServerRoute,
|
|
3333
3604
|
main,
|
|
3334
3605
|
parseAnswerEnvRequestCommandArgs,
|
|
3335
3606
|
parseAnswerFlags,
|
|
3336
3607
|
parseAnswerQuestionsCommandArgs,
|
|
3608
|
+
parseBranchCreateArgs,
|
|
3609
|
+
parseBranchDeleteArgs,
|
|
3337
3610
|
parseConversationForkArgs,
|
|
3338
3611
|
parseConversationInspectWorkArgs,
|
|
3339
3612
|
parseConversationOverviewArgs,
|
|
@@ -3351,6 +3624,11 @@ export {
|
|
|
3351
3624
|
parseSetEnvArgs,
|
|
3352
3625
|
parseShellArgs,
|
|
3353
3626
|
readDotenvFile,
|
|
3627
|
+
renderBranchDeletion,
|
|
3628
|
+
renderBranchDeletionPreview,
|
|
3629
|
+
renderBranchDeletionResult,
|
|
3630
|
+
renderBranchList,
|
|
3631
|
+
renderBranchOperation,
|
|
3354
3632
|
renderConversationForkResponse,
|
|
3355
3633
|
renderConversationHeadResponse,
|
|
3356
3634
|
renderConversationNodeResponse,
|
package/dist/mjs/package.json
CHANGED
package/dist/types/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { 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 R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
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 R5dctlBranchDeleteResponse, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
|
|
2
2
|
export type GlobalOptions = {
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
json: boolean;
|
|
@@ -79,6 +79,65 @@ export type R5dctlSessionRunCommand = R5dctlSessionStartCommand | {
|
|
|
79
79
|
kind: "stop";
|
|
80
80
|
sessionId: string;
|
|
81
81
|
};
|
|
82
|
+
export type R5dctlBranchCreateArgs = {
|
|
83
|
+
source: R5dctlWorktreeSource;
|
|
84
|
+
workingTree: R5dctlWorkingTreeMode;
|
|
85
|
+
worker?: string;
|
|
86
|
+
};
|
|
87
|
+
export declare function parseBranchCreateArgs(args: string[]): R5dctlBranchCreateArgs;
|
|
88
|
+
export declare function parseBranchDeleteArgs(args: string[]): {
|
|
89
|
+
branchNames: string[];
|
|
90
|
+
confirmed: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** An older server strips unknown routes and answers with this generic 404; a route's own not-found carries a specific message. */
|
|
93
|
+
export declare function isUnsupportedServerRoute(error: unknown): error is R5dctlApiError;
|
|
94
|
+
export declare function createBranchThroughPlatform(client: R5dctlClient, projectRef: string, input: {
|
|
95
|
+
branchName: string;
|
|
96
|
+
} & R5dctlBranchCreateArgs): Promise<R5dctlBranchOperation>;
|
|
97
|
+
export type R5dctlBranchDeletionOutcome = {
|
|
98
|
+
kind: "preview";
|
|
99
|
+
branch: R5dctlBranchDescription;
|
|
100
|
+
} | {
|
|
101
|
+
kind: "deleted";
|
|
102
|
+
result: R5dctlBranchDeleteResponse;
|
|
103
|
+
};
|
|
104
|
+
export declare function deleteBranchThroughPlatform(client: R5dctlClient, projectRef: string, branchName: string, options: {
|
|
105
|
+
confirmed: boolean;
|
|
106
|
+
}): Promise<R5dctlBranchDeletionOutcome>;
|
|
107
|
+
export type R5dctlBranchDeletionResult = {
|
|
108
|
+
branchName: string;
|
|
109
|
+
deleted: true;
|
|
110
|
+
deletedSessionIds: string[];
|
|
111
|
+
} | {
|
|
112
|
+
branchName: string;
|
|
113
|
+
deleted: false;
|
|
114
|
+
error: string;
|
|
115
|
+
};
|
|
116
|
+
export type R5dctlBranchDeletionsOutcome = {
|
|
117
|
+
kind: "preview";
|
|
118
|
+
branches: R5dctlBranchDescription[];
|
|
119
|
+
} | {
|
|
120
|
+
kind: "deleted";
|
|
121
|
+
results: R5dctlBranchDeletionResult[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Delete several branches one request at a time: the server route stays
|
|
125
|
+
* one-branch-per-request. Every name is checked before the first request, a
|
|
126
|
+
* preview describes each branch, and a confirmed run continues past a failed
|
|
127
|
+
* branch so one refused deletion does not strand the rest of the list.
|
|
128
|
+
*/
|
|
129
|
+
export declare function deleteBranchesThroughPlatform(client: R5dctlClient, projectRef: string, branchNames: readonly string[], options: {
|
|
130
|
+
confirmed: boolean;
|
|
131
|
+
onResult?: (result: R5dctlBranchDeletionResult) => void;
|
|
132
|
+
}): Promise<R5dctlBranchDeletionsOutcome>;
|
|
133
|
+
export declare function renderBranchDeletionResult(result: R5dctlBranchDeletionResult): string;
|
|
134
|
+
export declare function branchDeletionsConfirmationMessage(branches: readonly R5dctlBranchDescription[]): string;
|
|
135
|
+
export declare function branchDeletionsFailureMessage(results: readonly R5dctlBranchDeletionResult[]): string;
|
|
136
|
+
export declare function renderBranchDeletionPreview(branch: R5dctlBranchDescription): string;
|
|
137
|
+
export declare function branchDeletionConfirmationMessage(branch: R5dctlBranchDescription): string;
|
|
138
|
+
export declare function renderBranchDeletion(result: R5dctlBranchDeleteResponse): string;
|
|
139
|
+
/** Operation facts only: no commit hash, because a hash is stale as soon as the branch commits. */
|
|
140
|
+
export declare function renderBranchOperation(operation: R5dctlBranchOperation, projectRef: string): string;
|
|
82
141
|
export declare function parseSessionRunCommand(options: Pick<GlobalOptions, "project" | "branch" | "session">, args: string[]): R5dctlSessionRunCommand;
|
|
83
142
|
export type R5dctlSessionProcessLogArgs = {
|
|
84
143
|
runId: string;
|
|
@@ -146,6 +205,7 @@ export declare function advanceTransientDevicePollBackoff(intervalMs: number, tr
|
|
|
146
205
|
};
|
|
147
206
|
export declare function handleAuthLogin(client: R5dctlClient, options: GlobalOptions, commandArgs: string[], config: R5dctlConfig): Promise<void>;
|
|
148
207
|
export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
|
|
208
|
+
export declare function renderBranchList(branches: R5dctlBranch[]): string;
|
|
149
209
|
export declare function renderWorkspaceSync(result: R5dctlWorkspaceSyncResult): string;
|
|
150
210
|
export type K8sUsageSummary = {
|
|
151
211
|
cpuMinNanoCores: number | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5dctl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.105",
|
|
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.105",
|
|
30
30
|
"dotenv": "^17",
|
|
31
31
|
"qrcode": "^1.5.4",
|
|
32
32
|
"ws": "^8.18.3"
|