@ricsam/r5dctl 0.0.121 → 0.0.122

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
@@ -59,6 +59,8 @@ r5dctl -p <project> create branch <branch> --source worktree:<branch> [--working
59
59
  r5dctl -p <project> describe branch-operation <operation-id>
60
60
  r5dctl -p <project> delete branch <branch> [<branch>...] # preview the sessions that would be deleted, exit 1
61
61
  r5dctl -p <project> delete branch <branch> [<branch>...] --confirm # delete the branches and their sessions through the platform
62
+ r5dctl -p <project> delete branch <branch> --confirm --no-wait # accept the deletion and print its branch id instead of waiting
63
+ r5dctl -p <project> describe branch-deletion <branch-id> # pending, failed, or completed
62
64
  r5dctl -p <project> get envs
63
65
  r5dctl -p <project> get envs --show-values
64
66
  r5dctl -p <project> get env API_KEY
@@ -127,7 +129,7 @@ pull requests.
127
129
 
128
130
  `create branch` creates a linked worktree branch through the platform from an existing branch's checkout, with the same `--source worktree:<branch>` and `--working-tree carry|clean` semantics as `session start --new-worktree` (carry is the default) and no session prompt. `--worker <label>` selects the connected worker that cuts the checkout; omitted, the platform chooses one, and an r5d worker shell's temporary credential is always bound to its own worker. The command returns as soon as the operation is accepted and prints its id; `describe branch-operation <operation-id>` reports `provisioning`, `ready`, or `failed` with the worker and any error. Neither prints a commit hash: read the branch with git in its checkout once it is ready.
129
131
 
130
- `delete branch` removes one or more branches and every session on them through the platform, which is the only supported way to remove a linked worktree: the platform owns those checkouts, so never run `git worktree remove`, `git worktree prune`, or `git branch -D` inside a project checkout. Without `--confirm` it prints each branch and its sessions and exits non-zero without deleting anything. With `--confirm` the 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.
132
+ `delete branch` removes one or more branches and every session on them through the platform, which is the only supported way to remove a linked worktree: the platform owns those checkouts, so never run `git worktree remove`, `git worktree prune`, or `git branch -D` inside a project checkout. Without `--confirm` it prints each branch and its sessions and exits non-zero without deleting anything. With `--confirm` each branch is resolved to the exact managed branch it names today, its deletion is accepted at once by the platform, and the command waits (polling every 2 seconds, up to 10 minutes) until the platform reports it completed before requesting the next one; a failure does not stop the remaining deletions, and the exit status is non-zero if any branch failed. `--no-wait` prints the accepted branch id instead of waiting; `describe branch-deletion <branch-id>` reports `pending` (with the retry reason when the worker has not answered yet), `failed` with the platform's error, or `completed`, and answers `completed` for a branch id that no longer exists. The deletion stays bound to that branch id: a same-name branch created afterwards is never deleted by an old request, and re-running the command for a branch whose deletion failed retries it. The primary branch `main` is refused before anything is deleted, as is a branch with a running agent or active process (stop them first); a name that only has sessions and no managed branch is refused too (delete those sessions with `delete session`). On a server that predates the route, the command explains that the server does not support `delete branch` yet instead of reporting a missing branch.
131
133
 
132
134
  `get branches` lists every branch the project knows about, including managed branches without sessions. A worker whose workspace sync found a configured branch's checkout directory missing, or present but not a linked worktree of the project, skips that branch and reports it; the listing then shows `checkout=missing on <worker>` or `checkout=not_a_worktree on <worker>`, and `workspace status` prints a `Missing checkouts on <worker>` line. Recover either by deleting the branch with `delete branch <branch> --confirm` or by reconnecting that worker, which recreates the checkout without snapshotting the project's other checkouts.
133
135
 
package/dist/cjs/cli.cjs CHANGED
@@ -28,6 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var cli_exports = {};
30
30
  __export(cli_exports, {
31
+ BRANCH_DELETION_POLL_INTERVAL_MS: () => BRANCH_DELETION_POLL_INTERVAL_MS,
32
+ BRANCH_DELETION_WAIT_TIMEOUT_MS: () => BRANCH_DELETION_WAIT_TIMEOUT_MS,
31
33
  advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
32
34
  branchDeletionConfirmationMessage: () => branchDeletionConfirmationMessage,
33
35
  branchDeletionsConfirmationMessage: () => branchDeletionsConfirmationMessage,
@@ -77,6 +79,7 @@ __export(cli_exports, {
77
79
  renderBranchDeletion: () => renderBranchDeletion,
78
80
  renderBranchDeletionPreview: () => renderBranchDeletionPreview,
79
81
  renderBranchDeletionResult: () => renderBranchDeletionResult,
82
+ renderBranchDeletionStatus: () => renderBranchDeletionStatus,
80
83
  renderBranchList: () => renderBranchList,
81
84
  renderBranchOperation: () => renderBranchOperation,
82
85
  renderConversationForkResponse: () => renderConversationForkResponse,
@@ -102,6 +105,7 @@ __export(cli_exports, {
102
105
  resolveCommandExecution: () => resolveCommandExecution,
103
106
  runR5dctlCli: () => runR5dctlCli,
104
107
  summarizeEnvData: () => summarizeEnvData,
108
+ waitForBranchDeletion: () => waitForBranchDeletion,
105
109
  waitForSessionProvisioning: () => waitForSessionProvisioning,
106
110
  writeConfig: () => writeConfig
107
111
  });
@@ -247,8 +251,13 @@ const SHARED_HELP_ENTRIES = [
247
251
  },
248
252
  {
249
253
  section: "branches",
250
- usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
251
- description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
254
+ usage: "-p <project> delete branch <branch> [<branch>...] [--confirm] [--no-wait]",
255
+ description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted. Each deletion is accepted at once and awaited until the platform finishes it; --no-wait prints the accepted branch ids instead."
256
+ },
257
+ {
258
+ section: "branches",
259
+ usage: "-p <project> describe branch-deletion <branch-id>",
260
+ description: "Show whether an accepted branch deletion is pending, failed, or completed."
252
261
  },
253
262
  {
254
263
  section: "envs",
@@ -656,16 +665,21 @@ function parseBranchCreateArgs(args) {
656
665
  function parseBranchDeleteArgs(args) {
657
666
  const branchNames = [];
658
667
  let confirmed = false;
668
+ let noWait = false;
659
669
  for (const arg of args) {
660
670
  if (arg === "--confirm") {
661
671
  confirmed = true;
662
672
  continue;
663
673
  }
674
+ if (arg === "--no-wait") {
675
+ noWait = true;
676
+ continue;
677
+ }
664
678
  if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
665
679
  if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
666
680
  branchNames.push(arg);
667
681
  }
668
- return { branchNames, confirmed };
682
+ return { branchNames, confirmed, noWait };
669
683
  }
670
684
  const PRIMARY_BRANCH_NAME = "main";
671
685
  function isUnsupportedServerRoute(error) {
@@ -693,12 +707,66 @@ async function createBranchThroughPlatform(client, projectRef, input) {
693
707
  })
694
708
  );
695
709
  }
710
+ const BRANCH_DELETION_POLL_INTERVAL_MS = 2e3;
711
+ const BRANCH_DELETION_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
712
+ async function waitForBranchDeletion(client, projectRef, branchId, options = {}) {
713
+ const pollIntervalMs = options.pollIntervalMs ?? BRANCH_DELETION_POLL_INTERVAL_MS;
714
+ const timeoutMs = options.timeoutMs ?? BRANCH_DELETION_WAIT_TIMEOUT_MS;
715
+ const wait = options.sleep ?? ((ms) => (0, import_promises.setTimeout)(ms));
716
+ const deadline = Date.now() + timeoutMs;
717
+ for (; ; ) {
718
+ const status = await client.projects.branches.describeDeletion(projectRef, branchId);
719
+ if (status.status === "completed") return;
720
+ if (status.status === "failed") throw new Error(status.error ?? "Branch deletion failed");
721
+ if (Date.now() >= deadline) {
722
+ throw new Error(
723
+ `Branch deletion ${branchId} is still pending after ${Math.round(timeoutMs / 1e3)}s${status.retryReason ? ` (${status.retryReason})` : ""}; it continues on the platform, re-run to keep waiting`
724
+ );
725
+ }
726
+ await wait(pollIntervalMs);
727
+ }
728
+ }
729
+ async function resolveUncertainAcceptance(client, projectRef, branchId, error) {
730
+ if (error.status !== 503) return null;
731
+ try {
732
+ const status = await client.projects.branches.describeDeletion(projectRef, branchId);
733
+ return { status: status.status };
734
+ } catch {
735
+ return null;
736
+ }
737
+ }
696
738
  async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
697
739
  if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
698
740
  const branch = await client.projects.branches.describe(projectRef, branchName);
699
741
  if (!options.confirmed) return { kind: "preview", branch };
700
- const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
701
- return { kind: "deleted", result };
742
+ if (!branch.branchId) {
743
+ throw new Error(
744
+ `Branch ${branchName} has no managed checkout, only ${branch.sessions.length} session(s); delete those with \`delete session <id>\``
745
+ );
746
+ }
747
+ const branchId = branch.branchId;
748
+ let accepted;
749
+ try {
750
+ accepted = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName, { branchId }));
751
+ } catch (error) {
752
+ const landed = error instanceof import_r5d_api.R5dctlApiError ? await resolveUncertainAcceptance(client, projectRef, branchId, error) : null;
753
+ if (!landed) throw error;
754
+ const sessionIds = branch.sessions.map((session) => session.id);
755
+ accepted = landed.status === "completed" ? { status: "completed", branchId, branchName, sessionIds } : { status: "pending", branchId, branchName, workerLabel: "", sessionIds };
756
+ }
757
+ if (accepted.status === "completed" && accepted.sessionIds.length > 0) {
758
+ return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
759
+ }
760
+ if (accepted.status === "completed") return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: [] } };
761
+ if (options.noWait) {
762
+ return { kind: "accepted", result: { branchName, branchId, sessionIds: accepted.sessionIds, workerLabel: accepted.workerLabel } };
763
+ }
764
+ await waitForBranchDeletion(client, projectRef, branchId, {
765
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
766
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
767
+ ...options.sleep ? { sleep: options.sleep } : {}
768
+ });
769
+ return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
702
770
  }
703
771
  async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
704
772
  if (branchNames.length === 0) throw new Error("Missing branch name");
@@ -716,9 +784,15 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
716
784
  for (const branchName of branchNames) {
717
785
  let result;
718
786
  try {
719
- const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, { confirmed: true });
720
- if (deletion.kind !== "deleted") throw new Error(`Branch ${branchName} was only previewed`);
721
- result = { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds };
787
+ const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
788
+ confirmed: true,
789
+ ...options.noWait !== void 0 ? { noWait: options.noWait } : {},
790
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
791
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
792
+ ...options.sleep ? { sleep: options.sleep } : {}
793
+ });
794
+ if (deletion.kind === "preview") throw new Error(`Branch ${branchName} was only previewed`);
795
+ result = deletion.kind === "deleted" ? { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds } : { branchName, deleted: false, accepted: true, branchId: deletion.result.branchId, sessionIds: deletion.result.sessionIds };
722
796
  } catch (error) {
723
797
  result = {
724
798
  branchName,
@@ -732,7 +806,21 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
732
806
  return { kind: "deleted", results };
733
807
  }
734
808
  function renderBranchDeletionResult(result) {
735
- return result.deleted ? renderBranchDeletion(result) : `Failed to delete branch ${result.branchName}: ${result.error}
809
+ if (result.deleted) return renderBranchDeletion(result);
810
+ if (result.accepted) {
811
+ return `Accepted deletion of branch ${result.branchName} (${result.sessionIds.length} session(s)); it continues on the platform. Check with \`describe branch-deletion ${result.branchId}\`.
812
+ `;
813
+ }
814
+ return `Failed to delete branch ${result.branchName}: ${result.error}
815
+ `;
816
+ }
817
+ function renderBranchDeletionStatus(status) {
818
+ const lines = [`Branch: ${status.branchName ?? "(deleted)"}`, `Branch id: ${status.branchId}`, `Status: ${status.status}`];
819
+ if (status.workerLabel) lines.push(`Worker: ${status.workerLabel}`);
820
+ if (status.attempts !== void 0) lines.push(`Attempts: ${status.attempts}`);
821
+ if (status.retryReason) lines.push(`Retrying: ${status.retryReason}`);
822
+ if (status.error) lines.push(`Error: ${status.error}`);
823
+ return `${lines.join("\n")}
736
824
  `;
737
825
  }
738
826
  function branchDeletionsConfirmationMessage(branches) {
@@ -742,7 +830,7 @@ function branchDeletionsConfirmationMessage(branches) {
742
830
  return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
743
831
  }
744
832
  function branchDeletionsFailureMessage(results) {
745
- return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
833
+ return `Failed to delete ${results.filter((result) => !result.deleted && !result.accepted).length} of ${results.length} branch(es).`;
746
834
  }
747
835
  function renderBranchDeletionPreview(branch) {
748
836
  const count = branch.sessions.length;
@@ -1830,7 +1918,8 @@ function renderBranchList(branches) {
1830
1918
  if (branches.length === 0) return "No branches found.\n";
1831
1919
  const lines = branches.map((branch) => {
1832
1920
  const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
1833
- return `${branch.branchName} sessions=${branch.sessionCount}${checkout}`;
1921
+ const deletion = branch.deletion ? ` deletion=${branch.deletion.status}${branch.deletion.error ? ` (${branch.deletion.error})` : ""}` : "";
1922
+ return `${branch.branchName} sessions=${branch.sessionCount}${checkout}${deletion}`;
1834
1923
  });
1835
1924
  if (branches.some((branch) => branch.checkoutState)) {
1836
1925
  lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
@@ -3091,10 +3180,15 @@ async function executeR5dctlCommand(client, json, args) {
3091
3180
  requireValue(args[offset], "Missing project reference"),
3092
3181
  requireValue(args[offset + 1], "Missing branch name")
3093
3182
  );
3094
- write(result, `Project: ${result.project.path}
3183
+ write(
3184
+ result,
3185
+ `Project: ${result.project.path}
3095
3186
  Branch: ${result.branchName}
3096
- Sessions: ${result.sessions.length}
3097
- `);
3187
+ Branch id: ${result.branchId ?? "(no managed branch)"}
3188
+ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error ? ` (${result.deletion.error})` : ""}
3189
+ ` : ""}Sessions: ${result.sessions.length}
3190
+ `
3191
+ );
3098
3192
  return;
3099
3193
  }
3100
3194
  if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
@@ -3122,9 +3216,10 @@ Sessions: ${result.sessions.length}
3122
3216
  if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
3123
3217
  const offset = first === "delete" ? 2 : 3;
3124
3218
  const projectRef = requireValue(args[offset], "Missing project reference");
3125
- const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
3219
+ const { branchNames, confirmed, noWait } = parseBranchDeleteArgs(args.slice(offset + 1));
3126
3220
  const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
3127
3221
  confirmed,
3222
+ noWait,
3128
3223
  // Each deletion is its own round trip, so a reader sees results as they land.
3129
3224
  onResult: (result) => {
3130
3225
  if (!json) process.stdout.write(renderBranchDeletionResult(result));
@@ -3135,7 +3230,15 @@ Sessions: ${result.sessions.length}
3135
3230
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
3136
3231
  }
3137
3232
  if (json) write({ results: deletion.results }, "");
3138
- if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3233
+ if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3234
+ return;
3235
+ }
3236
+ if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
3237
+ const offset = first === "describe" ? 2 : 3;
3238
+ const projectRef = requireValue(args[offset], "Missing project reference");
3239
+ const branchId = requireValue(args[offset + 1], "Missing branch id");
3240
+ const status = await requireServerRouteSupport("describe branch-deletion", () => client.projects.branches.describeDeletion(projectRef, branchId));
3241
+ write(status, renderBranchDeletionStatus(status));
3139
3242
  return;
3140
3243
  }
3141
3244
  if (first === "get" && second === "envs") {
@@ -3928,6 +4031,8 @@ async function main(argv = process.argv.slice(2)) {
3928
4031
  }
3929
4032
  // Annotate the CommonJS export names for ESM import in node:
3930
4033
  0 && (module.exports = {
4034
+ BRANCH_DELETION_POLL_INTERVAL_MS,
4035
+ BRANCH_DELETION_WAIT_TIMEOUT_MS,
3931
4036
  advanceTransientDevicePollBackoff,
3932
4037
  branchDeletionConfirmationMessage,
3933
4038
  branchDeletionsConfirmationMessage,
@@ -3977,6 +4082,7 @@ async function main(argv = process.argv.slice(2)) {
3977
4082
  renderBranchDeletion,
3978
4083
  renderBranchDeletionPreview,
3979
4084
  renderBranchDeletionResult,
4085
+ renderBranchDeletionStatus,
3980
4086
  renderBranchList,
3981
4087
  renderBranchOperation,
3982
4088
  renderConversationForkResponse,
@@ -4002,6 +4108,7 @@ async function main(argv = process.argv.slice(2)) {
4002
4108
  resolveCommandExecution,
4003
4109
  runR5dctlCli,
4004
4110
  summarizeEnvData,
4111
+ waitForBranchDeletion,
4005
4112
  waitForSessionProvisioning,
4006
4113
  writeConfig
4007
4114
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.121",
3
+ "version": "0.0.122",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -142,8 +142,13 @@ const SHARED_HELP_ENTRIES = [
142
142
  },
143
143
  {
144
144
  section: "branches",
145
- usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
146
- description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
145
+ usage: "-p <project> delete branch <branch> [<branch>...] [--confirm] [--no-wait]",
146
+ description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted. Each deletion is accepted at once and awaited until the platform finishes it; --no-wait prints the accepted branch ids instead."
147
+ },
148
+ {
149
+ section: "branches",
150
+ usage: "-p <project> describe branch-deletion <branch-id>",
151
+ description: "Show whether an accepted branch deletion is pending, failed, or completed."
147
152
  },
148
153
  {
149
154
  section: "envs",
@@ -551,16 +556,21 @@ function parseBranchCreateArgs(args) {
551
556
  function parseBranchDeleteArgs(args) {
552
557
  const branchNames = [];
553
558
  let confirmed = false;
559
+ let noWait = false;
554
560
  for (const arg of args) {
555
561
  if (arg === "--confirm") {
556
562
  confirmed = true;
557
563
  continue;
558
564
  }
565
+ if (arg === "--no-wait") {
566
+ noWait = true;
567
+ continue;
568
+ }
559
569
  if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
560
570
  if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
561
571
  branchNames.push(arg);
562
572
  }
563
- return { branchNames, confirmed };
573
+ return { branchNames, confirmed, noWait };
564
574
  }
565
575
  const PRIMARY_BRANCH_NAME = "main";
566
576
  function isUnsupportedServerRoute(error) {
@@ -588,12 +598,66 @@ async function createBranchThroughPlatform(client, projectRef, input) {
588
598
  })
589
599
  );
590
600
  }
601
+ const BRANCH_DELETION_POLL_INTERVAL_MS = 2e3;
602
+ const BRANCH_DELETION_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
603
+ async function waitForBranchDeletion(client, projectRef, branchId, options = {}) {
604
+ const pollIntervalMs = options.pollIntervalMs ?? BRANCH_DELETION_POLL_INTERVAL_MS;
605
+ const timeoutMs = options.timeoutMs ?? BRANCH_DELETION_WAIT_TIMEOUT_MS;
606
+ const wait = options.sleep ?? ((ms) => sleep(ms));
607
+ const deadline = Date.now() + timeoutMs;
608
+ for (; ; ) {
609
+ const status = await client.projects.branches.describeDeletion(projectRef, branchId);
610
+ if (status.status === "completed") return;
611
+ if (status.status === "failed") throw new Error(status.error ?? "Branch deletion failed");
612
+ if (Date.now() >= deadline) {
613
+ throw new Error(
614
+ `Branch deletion ${branchId} is still pending after ${Math.round(timeoutMs / 1e3)}s${status.retryReason ? ` (${status.retryReason})` : ""}; it continues on the platform, re-run to keep waiting`
615
+ );
616
+ }
617
+ await wait(pollIntervalMs);
618
+ }
619
+ }
620
+ async function resolveUncertainAcceptance(client, projectRef, branchId, error) {
621
+ if (error.status !== 503) return null;
622
+ try {
623
+ const status = await client.projects.branches.describeDeletion(projectRef, branchId);
624
+ return { status: status.status };
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
591
629
  async function deleteBranchThroughPlatform(client, projectRef, branchName, options) {
592
630
  if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
593
631
  const branch = await client.projects.branches.describe(projectRef, branchName);
594
632
  if (!options.confirmed) return { kind: "preview", branch };
595
- const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
596
- return { kind: "deleted", result };
633
+ if (!branch.branchId) {
634
+ throw new Error(
635
+ `Branch ${branchName} has no managed checkout, only ${branch.sessions.length} session(s); delete those with \`delete session <id>\``
636
+ );
637
+ }
638
+ const branchId = branch.branchId;
639
+ let accepted;
640
+ try {
641
+ accepted = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName, { branchId }));
642
+ } catch (error) {
643
+ const landed = error instanceof R5dctlApiError ? await resolveUncertainAcceptance(client, projectRef, branchId, error) : null;
644
+ if (!landed) throw error;
645
+ const sessionIds = branch.sessions.map((session) => session.id);
646
+ accepted = landed.status === "completed" ? { status: "completed", branchId, branchName, sessionIds } : { status: "pending", branchId, branchName, workerLabel: "", sessionIds };
647
+ }
648
+ if (accepted.status === "completed" && accepted.sessionIds.length > 0) {
649
+ return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
650
+ }
651
+ if (accepted.status === "completed") return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: [] } };
652
+ if (options.noWait) {
653
+ return { kind: "accepted", result: { branchName, branchId, sessionIds: accepted.sessionIds, workerLabel: accepted.workerLabel } };
654
+ }
655
+ await waitForBranchDeletion(client, projectRef, branchId, {
656
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
657
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
658
+ ...options.sleep ? { sleep: options.sleep } : {}
659
+ });
660
+ return { kind: "deleted", result: { branchName, branchId, deletedSessionIds: accepted.sessionIds } };
597
661
  }
598
662
  async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
599
663
  if (branchNames.length === 0) throw new Error("Missing branch name");
@@ -611,9 +675,15 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
611
675
  for (const branchName of branchNames) {
612
676
  let result;
613
677
  try {
614
- const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, { confirmed: true });
615
- if (deletion.kind !== "deleted") throw new Error(`Branch ${branchName} was only previewed`);
616
- result = { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds };
678
+ const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, {
679
+ confirmed: true,
680
+ ...options.noWait !== void 0 ? { noWait: options.noWait } : {},
681
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
682
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
683
+ ...options.sleep ? { sleep: options.sleep } : {}
684
+ });
685
+ if (deletion.kind === "preview") throw new Error(`Branch ${branchName} was only previewed`);
686
+ result = deletion.kind === "deleted" ? { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds } : { branchName, deleted: false, accepted: true, branchId: deletion.result.branchId, sessionIds: deletion.result.sessionIds };
617
687
  } catch (error) {
618
688
  result = {
619
689
  branchName,
@@ -627,7 +697,21 @@ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, op
627
697
  return { kind: "deleted", results };
628
698
  }
629
699
  function renderBranchDeletionResult(result) {
630
- return result.deleted ? renderBranchDeletion(result) : `Failed to delete branch ${result.branchName}: ${result.error}
700
+ if (result.deleted) return renderBranchDeletion(result);
701
+ if (result.accepted) {
702
+ return `Accepted deletion of branch ${result.branchName} (${result.sessionIds.length} session(s)); it continues on the platform. Check with \`describe branch-deletion ${result.branchId}\`.
703
+ `;
704
+ }
705
+ return `Failed to delete branch ${result.branchName}: ${result.error}
706
+ `;
707
+ }
708
+ function renderBranchDeletionStatus(status) {
709
+ const lines = [`Branch: ${status.branchName ?? "(deleted)"}`, `Branch id: ${status.branchId}`, `Status: ${status.status}`];
710
+ if (status.workerLabel) lines.push(`Worker: ${status.workerLabel}`);
711
+ if (status.attempts !== void 0) lines.push(`Attempts: ${status.attempts}`);
712
+ if (status.retryReason) lines.push(`Retrying: ${status.retryReason}`);
713
+ if (status.error) lines.push(`Error: ${status.error}`);
714
+ return `${lines.join("\n")}
631
715
  `;
632
716
  }
633
717
  function branchDeletionsConfirmationMessage(branches) {
@@ -637,7 +721,7 @@ function branchDeletionsConfirmationMessage(branches) {
637
721
  return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
638
722
  }
639
723
  function branchDeletionsFailureMessage(results) {
640
- return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
724
+ return `Failed to delete ${results.filter((result) => !result.deleted && !result.accepted).length} of ${results.length} branch(es).`;
641
725
  }
642
726
  function renderBranchDeletionPreview(branch) {
643
727
  const count = branch.sessions.length;
@@ -1725,7 +1809,8 @@ function renderBranchList(branches) {
1725
1809
  if (branches.length === 0) return "No branches found.\n";
1726
1810
  const lines = branches.map((branch) => {
1727
1811
  const checkout = branch.checkoutState ? ` checkout=${branch.checkoutState}${branch.checkoutStateWorkerLabel ? ` on ${branch.checkoutStateWorkerLabel}` : ""}` : "";
1728
- return `${branch.branchName} sessions=${branch.sessionCount}${checkout}`;
1812
+ const deletion = branch.deletion ? ` deletion=${branch.deletion.status}${branch.deletion.error ? ` (${branch.deletion.error})` : ""}` : "";
1813
+ return `${branch.branchName} sessions=${branch.sessionCount}${checkout}${deletion}`;
1729
1814
  });
1730
1815
  if (branches.some((branch) => branch.checkoutState)) {
1731
1816
  lines.push(`Workspace sync skips a branch whose checkout is missing or not a worktree; ${CHECKOUT_RECOVERY_HINT}.`);
@@ -2986,10 +3071,15 @@ async function executeR5dctlCommand(client, json, args) {
2986
3071
  requireValue(args[offset], "Missing project reference"),
2987
3072
  requireValue(args[offset + 1], "Missing branch name")
2988
3073
  );
2989
- write(result, `Project: ${result.project.path}
3074
+ write(
3075
+ result,
3076
+ `Project: ${result.project.path}
2990
3077
  Branch: ${result.branchName}
2991
- Sessions: ${result.sessions.length}
2992
- `);
3078
+ Branch id: ${result.branchId ?? "(no managed branch)"}
3079
+ ${result.deletion ? `Deletion: ${result.deletion.status}${result.deletion.error ? ` (${result.deletion.error})` : ""}
3080
+ ` : ""}Sessions: ${result.sessions.length}
3081
+ `
3082
+ );
2993
3083
  return;
2994
3084
  }
2995
3085
  if (first === "projects" && second === "branches" && third === "create" || first === "create" && second === "branch") {
@@ -3017,9 +3107,10 @@ Sessions: ${result.sessions.length}
3017
3107
  if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
3018
3108
  const offset = first === "delete" ? 2 : 3;
3019
3109
  const projectRef = requireValue(args[offset], "Missing project reference");
3020
- const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
3110
+ const { branchNames, confirmed, noWait } = parseBranchDeleteArgs(args.slice(offset + 1));
3021
3111
  const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
3022
3112
  confirmed,
3113
+ noWait,
3023
3114
  // Each deletion is its own round trip, so a reader sees results as they land.
3024
3115
  onResult: (result) => {
3025
3116
  if (!json) process.stdout.write(renderBranchDeletionResult(result));
@@ -3030,7 +3121,15 @@ Sessions: ${result.sessions.length}
3030
3121
  throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
3031
3122
  }
3032
3123
  if (json) write({ results: deletion.results }, "");
3033
- if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3124
+ if (deletion.results.some((result) => !result.deleted && !result.accepted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
3125
+ return;
3126
+ }
3127
+ if (first === "projects" && second === "branches" && third === "describe-deletion" || first === "describe" && second === "branch-deletion") {
3128
+ const offset = first === "describe" ? 2 : 3;
3129
+ const projectRef = requireValue(args[offset], "Missing project reference");
3130
+ const branchId = requireValue(args[offset + 1], "Missing branch id");
3131
+ const status = await requireServerRouteSupport("describe branch-deletion", () => client.projects.branches.describeDeletion(projectRef, branchId));
3132
+ write(status, renderBranchDeletionStatus(status));
3034
3133
  return;
3035
3134
  }
3036
3135
  if (first === "get" && second === "envs") {
@@ -3822,6 +3921,8 @@ async function main(argv = process.argv.slice(2)) {
3822
3921
  }
3823
3922
  }
3824
3923
  export {
3924
+ BRANCH_DELETION_POLL_INTERVAL_MS,
3925
+ BRANCH_DELETION_WAIT_TIMEOUT_MS,
3825
3926
  advanceTransientDevicePollBackoff,
3826
3927
  branchDeletionConfirmationMessage,
3827
3928
  branchDeletionsConfirmationMessage,
@@ -3871,6 +3972,7 @@ export {
3871
3972
  renderBranchDeletion,
3872
3973
  renderBranchDeletionPreview,
3873
3974
  renderBranchDeletionResult,
3975
+ renderBranchDeletionStatus,
3874
3976
  renderBranchList,
3875
3977
  renderBranchOperation,
3876
3978
  renderConversationForkResponse,
@@ -3896,6 +3998,7 @@ export {
3896
3998
  resolveCommandExecution,
3897
3999
  runR5dctlCli,
3898
4000
  summarizeEnvData,
4001
+ waitForBranchDeletion,
3899
4002
  waitForSessionProvisioning,
3900
4003
  writeConfig
3901
4004
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.121",
3
+ "version": "0.0.122",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,4 @@
1
- import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlK8sResourceHistory, type R5dctlK8sUsage, type R5dctlK8sWorkload, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlBranch, type R5dctlBranchDeleteResponse, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
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 R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  export type GlobalOptions = {
3
3
  baseUrl?: string;
4
4
  json: boolean;
@@ -90,6 +90,7 @@ export declare function parseBranchCreateArgs(args: string[]): R5dctlBranchCreat
90
90
  export declare function parseBranchDeleteArgs(args: string[]): {
91
91
  branchNames: string[];
92
92
  confirmed: boolean;
93
+ noWait: boolean;
93
94
  };
94
95
  /** An older server strips unknown routes and answers with this generic 404; a route's own not-found carries a specific message. */
95
96
  export declare function isUnsupportedServerRoute(error: unknown): error is R5dctlApiError;
@@ -101,10 +102,39 @@ export type R5dctlBranchDeletionOutcome = {
101
102
  branch: R5dctlBranchDescription;
102
103
  } | {
103
104
  kind: "deleted";
104
- result: R5dctlBranchDeleteResponse;
105
+ result: {
106
+ branchName: string;
107
+ branchId: string;
108
+ deletedSessionIds: string[];
109
+ };
110
+ } | {
111
+ kind: "accepted";
112
+ result: {
113
+ branchName: string;
114
+ branchId: string;
115
+ sessionIds: string[];
116
+ workerLabel: string;
117
+ };
105
118
  };
119
+ /** The platform answers an accepted deletion at once; the checkout and chats disappear behind this poll. */
120
+ export declare const BRANCH_DELETION_POLL_INTERVAL_MS = 2000;
121
+ export declare const BRANCH_DELETION_WAIT_TIMEOUT_MS: number;
122
+ /**
123
+ * Wait for one accepted deletion, keyed by its exact branch incarnation. A
124
+ * `completed` status is the terminal success; a `failed` status carries the
125
+ * platform's reason and is not retried here (re-run the command to retry).
126
+ */
127
+ export declare function waitForBranchDeletion(client: R5dctlClient, projectRef: string, branchId: string, options?: {
128
+ pollIntervalMs?: number;
129
+ timeoutMs?: number;
130
+ sleep?: (ms: number) => Promise<void>;
131
+ }): Promise<void>;
106
132
  export declare function deleteBranchThroughPlatform(client: R5dctlClient, projectRef: string, branchName: string, options: {
107
133
  confirmed: boolean;
134
+ noWait?: boolean;
135
+ pollIntervalMs?: number;
136
+ timeoutMs?: number;
137
+ sleep?: (ms: number) => Promise<void>;
108
138
  }): Promise<R5dctlBranchDeletionOutcome>;
109
139
  export type R5dctlBranchDeletionResult = {
110
140
  branchName: string;
@@ -113,6 +143,13 @@ export type R5dctlBranchDeletionResult = {
113
143
  } | {
114
144
  branchName: string;
115
145
  deleted: false;
146
+ accepted: true;
147
+ branchId: string;
148
+ sessionIds: string[];
149
+ } | {
150
+ branchName: string;
151
+ deleted: false;
152
+ accepted?: false;
116
153
  error: string;
117
154
  };
118
155
  export type R5dctlBranchDeletionsOutcome = {
@@ -126,18 +163,29 @@ export type R5dctlBranchDeletionsOutcome = {
126
163
  * Delete several branches one request at a time: the server route stays
127
164
  * one-branch-per-request. Every name is checked before the first request, a
128
165
  * preview describes each branch, and a confirmed run continues past a failed
129
- * branch so one refused deletion does not strand the rest of the list.
166
+ * branch so one refused deletion does not strand the rest of the list. Each
167
+ * accepted deletion is awaited before the next is requested, so the worker
168
+ * handles them in order and a failure is reported where it happened.
130
169
  */
131
170
  export declare function deleteBranchesThroughPlatform(client: R5dctlClient, projectRef: string, branchNames: readonly string[], options: {
132
171
  confirmed: boolean;
172
+ noWait?: boolean;
173
+ pollIntervalMs?: number;
174
+ timeoutMs?: number;
175
+ sleep?: (ms: number) => Promise<void>;
133
176
  onResult?: (result: R5dctlBranchDeletionResult) => void;
134
177
  }): Promise<R5dctlBranchDeletionsOutcome>;
135
178
  export declare function renderBranchDeletionResult(result: R5dctlBranchDeletionResult): string;
179
+ /** Operation facts of one deletion, keyed by its branch incarnation. */
180
+ export declare function renderBranchDeletionStatus(status: R5dctlBranchDeletionStatus): string;
136
181
  export declare function branchDeletionsConfirmationMessage(branches: readonly R5dctlBranchDescription[]): string;
137
182
  export declare function branchDeletionsFailureMessage(results: readonly R5dctlBranchDeletionResult[]): string;
138
183
  export declare function renderBranchDeletionPreview(branch: R5dctlBranchDescription): string;
139
184
  export declare function branchDeletionConfirmationMessage(branch: R5dctlBranchDescription): string;
140
- export declare function renderBranchDeletion(result: R5dctlBranchDeleteResponse): string;
185
+ export declare function renderBranchDeletion(result: {
186
+ branchName: string;
187
+ deletedSessionIds: string[];
188
+ }): string;
141
189
  /** Operation facts only: no commit hash, because a hash is stale as soon as the branch commits. */
142
190
  export declare function renderBranchOperation(operation: R5dctlBranchOperation, projectRef: string): string;
143
191
  export declare function parseSessionRunCommand(options: Pick<GlobalOptions, "project" | "branch" | "session">, args: string[], env?: Record<string, string | undefined>): R5dctlSessionRunCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.121",
3
+ "version": "0.0.122",
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.121",
29
+ "@ricsam/r5d-api": "^0.0.122",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"