@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 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(), "--source worktree:<branch> is required with --new-worktree");
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.map(
1483
- (worker) => `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`
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,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.103",
3
+ "version": "0.0.105",
4
4
  "type": "commonjs"
5
5
  }
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(), "--source worktree:<branch> is required with --new-worktree");
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.map(
1398
- (worker) => `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`
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,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.103",
3
+ "version": "0.0.105",
4
4
  "type": "module"
5
5
  }
@@ -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.103",
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.103",
29
+ "@ricsam/r5d-api": "^0.0.105",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"