@ricsam/r5dctl 0.0.104 → 0.0.106

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
@@ -57,14 +57,14 @@ r5dctl -p <project> get branches
57
57
  r5dctl -p <project> describe branch <branch>
58
58
  r5dctl -p <project> create branch <branch> --source worktree:<branch> [--working-tree carry|clean] [--worker <label>]
59
59
  r5dctl -p <project> describe branch-operation <operation-id>
60
- r5dctl -p <project> delete branch <branch> # preview the sessions that would be deleted, exit 1
61
- r5dctl -p <project> delete branch <branch> --confirm # delete the branch and its sessions through the platform
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
62
62
  r5dctl -p <project> get envs
63
63
  r5dctl -p <project> get envs --show-values
64
64
  r5dctl -p <project> get env API_KEY
65
65
  r5dctl -p <project> set envs --from-env-file ~/.env --only-missing
66
66
  r5dctl -p <project> set envs -e API_KEY=value --description "API credential"
67
- r5dctl -p <project> get sessions --branch <branch>
67
+ r5dctl -p <project> get sessions --branch <branch> # each session's live state and last activity
68
68
  r5dctl -p <project> create session -b <branch> --name "Spec session"
69
69
  r5dctl describe session <session-id>
70
70
  r5dctl -s <session-id> update session --name "Renamed session"
@@ -125,12 +125,16 @@ pull requests.
125
125
 
126
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
127
 
128
- `delete branch` removes a branch and every session on it 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 the branch and its sessions and exits non-zero without deleting anything. The primary branch `main` is refused, as is a branch with a running agent or active process (stop them first). 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.
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
129
 
130
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
131
 
132
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.
133
133
 
134
+ `get sessions` lists a project's sessions, newest first, and appends each session's live state (`running`, `stopping`, `paused`, `provisioning`, `queued`, or `idle`) and how long ago its conversation last advanced (`running, last active 12s ago`). Several sessions can work in the same branch checkout at once (task chips, shared-mode sub-agents, sessions the user starts), so run it with `--branch` before relying on a working tree or reviewing what changed in it. A server that predates the fields prints the id, branch, and name only.
135
+
136
+ `session stop` also stops every sub-agent session the stopped session started, directly or through its own children, and `session status` prints `Child sessions: <active> active of <total>` for a session that launched sub-agents.
137
+
134
138
  `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.
135
139
 
136
140
  `session process-log` prints a shell run's stored stdout and stderr from the server, where r5d keeps every run's full output; the `logPath` in tool results names that server-side store, not a file on the worker. Without flags it prints the whole log, `--tail <n>` keeps the last n lines, and `--grep <pattern>` keeps the lines matching a JavaScript regular expression (combine both for the last n matches). Each stream is capped at 512 KB per call and printed under a `==> stdout <==` or `==> stderr <==` header; a one-line summary with byte totals, the selection, and any truncation goes to stderr so stdout stays clean for `grep`, `head`, and `tail`. Inside an r5d worker shell the temporary credential can only read runs on that shell's worker.
package/dist/cjs/cli.cjs CHANGED
@@ -30,9 +30,12 @@ var cli_exports = {};
30
30
  __export(cli_exports, {
31
31
  advanceTransientDevicePollBackoff: () => advanceTransientDevicePollBackoff,
32
32
  branchDeletionConfirmationMessage: () => branchDeletionConfirmationMessage,
33
+ branchDeletionsConfirmationMessage: () => branchDeletionsConfirmationMessage,
34
+ branchDeletionsFailureMessage: () => branchDeletionsFailureMessage,
33
35
  collectK8sUsage: () => collectK8sUsage,
34
36
  createBranchThroughPlatform: () => createBranchThroughPlatform,
35
37
  deleteBranchThroughPlatform: () => deleteBranchThroughPlatform,
38
+ deleteBranchesThroughPlatform: () => deleteBranchesThroughPlatform,
36
39
  dispatchSessionRunCommand: () => dispatchSessionRunCommand,
37
40
  followR5dctlSession: () => followR5dctlSession,
38
41
  formatK8sCpu: () => formatK8sCpu,
@@ -70,6 +73,7 @@ __export(cli_exports, {
70
73
  readDotenvFile: () => readDotenvFile,
71
74
  renderBranchDeletion: () => renderBranchDeletion,
72
75
  renderBranchDeletionPreview: () => renderBranchDeletionPreview,
76
+ renderBranchDeletionResult: () => renderBranchDeletionResult,
73
77
  renderBranchList: () => renderBranchList,
74
78
  renderBranchOperation: () => renderBranchOperation,
75
79
  renderConversationForkResponse: () => renderConversationForkResponse,
@@ -85,6 +89,7 @@ __export(cli_exports, {
85
89
  renderProcessInspection: () => renderProcessInspection,
86
90
  renderProcessList: () => renderProcessList,
87
91
  renderProcessLog: () => renderProcessLog,
92
+ renderSessionList: () => renderSessionList,
88
93
  renderSessionRunStart: () => renderSessionRunStart,
89
94
  renderSessionRunStatus: () => renderSessionRunStatus,
90
95
  renderWorkspaceStatus: () => renderWorkspaceStatus,
@@ -221,8 +226,8 @@ const SHARED_HELP_ENTRIES = [
221
226
  },
222
227
  {
223
228
  section: "branches",
224
- usage: "-p <project> delete branch <branch> [--confirm]",
225
- description: "Delete a branch and its sessions through the platform; without --confirm, only preview what would be deleted."
229
+ usage: "-p <project> delete branch <branch> [<branch>...] [--confirm]",
230
+ description: "Delete one or more branches and their sessions through the platform; without --confirm, only preview what would be deleted."
226
231
  },
227
232
  {
228
233
  section: "envs",
@@ -610,15 +615,18 @@ function parseBranchCreateArgs(args) {
610
615
  };
611
616
  }
612
617
  function parseBranchDeleteArgs(args) {
618
+ const branchNames = [];
613
619
  let confirmed = false;
614
620
  for (const arg of args) {
615
621
  if (arg === "--confirm") {
616
622
  confirmed = true;
617
623
  continue;
618
624
  }
619
- throw new Error(arg.startsWith("-") ? `Unknown delete branch flag: ${arg}` : `Unexpected delete branch argument: ${arg}`);
625
+ if (arg.startsWith("-")) throw new Error(`Unknown delete branch flag: ${arg}`);
626
+ if (branchNames.includes(arg)) throw new Error(`Branch ${arg} was given more than once`);
627
+ branchNames.push(arg);
620
628
  }
621
- return { confirmed };
629
+ return { branchNames, confirmed };
622
630
  }
623
631
  const PRIMARY_BRANCH_NAME = "main";
624
632
  function isUnsupportedServerRoute(error) {
@@ -653,6 +661,50 @@ async function deleteBranchThroughPlatform(client, projectRef, branchName, optio
653
661
  const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
654
662
  return { kind: "deleted", result };
655
663
  }
664
+ async function deleteBranchesThroughPlatform(client, projectRef, branchNames, options) {
665
+ if (branchNames.length === 0) throw new Error("Missing branch name");
666
+ for (const branchName of branchNames) {
667
+ if (branchName === PRIMARY_BRANCH_NAME) throw new Error(`The primary branch ${branchName} cannot be deleted`);
668
+ }
669
+ if (!options.confirmed) {
670
+ const branches = [];
671
+ for (const branchName of branchNames) {
672
+ branches.push(await client.projects.branches.describe(projectRef, branchName));
673
+ }
674
+ return { kind: "preview", branches };
675
+ }
676
+ const results = [];
677
+ for (const branchName of branchNames) {
678
+ let result;
679
+ try {
680
+ const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, { confirmed: true });
681
+ if (deletion.kind !== "deleted") throw new Error(`Branch ${branchName} was only previewed`);
682
+ result = { branchName, deleted: true, deletedSessionIds: deletion.result.deletedSessionIds };
683
+ } catch (error) {
684
+ result = {
685
+ branchName,
686
+ deleted: false,
687
+ error: error instanceof import_r5d_api.R5dctlApiError ? extractApiErrorMessage(error) : error instanceof Error ? error.message : String(error)
688
+ };
689
+ }
690
+ results.push(result);
691
+ options.onResult?.(result);
692
+ }
693
+ return { kind: "deleted", results };
694
+ }
695
+ function renderBranchDeletionResult(result) {
696
+ return result.deleted ? renderBranchDeletion(result) : `Failed to delete branch ${result.branchName}: ${result.error}
697
+ `;
698
+ }
699
+ function branchDeletionsConfirmationMessage(branches) {
700
+ const [only] = branches;
701
+ if (branches.length === 1 && only) return branchDeletionConfirmationMessage(only);
702
+ const sessionCount = branches.reduce((count, branch) => count + branch.sessions.length, 0);
703
+ return `Nothing was deleted. Re-run with --confirm to delete ${branches.length} branches and their ${sessionCount} session(s).`;
704
+ }
705
+ function branchDeletionsFailureMessage(results) {
706
+ return `Failed to delete ${results.filter((result) => !result.deleted).length} of ${results.length} branch(es).`;
707
+ }
656
708
  function renderBranchDeletionPreview(branch) {
657
709
  const count = branch.sessions.length;
658
710
  const lines = [
@@ -1584,11 +1636,15 @@ function renderProjectList(projects) {
1584
1636
  Mode: ${project.mode}`).join("\n\n")}
1585
1637
  `;
1586
1638
  }
1587
- function renderSessionList(sessions) {
1639
+ function formatSessionActivity(session, nowMs) {
1640
+ if (!session.runState) return void 0;
1641
+ return session.lastActivityAt ? `${session.runState}, last active ${formatProcessAge(session.lastActivityAt, nowMs)} ago` : session.runState;
1642
+ }
1643
+ function renderSessionList(sessions, nowMs = Date.now()) {
1588
1644
  if (sessions.length === 0) {
1589
1645
  return "No sessions found.\n";
1590
1646
  }
1591
- return `${sessions.map((session) => `${session.id} ${session.branchName} ${session.name ?? "(unnamed)"}`).join("\n")}
1647
+ return `${sessions.map((session) => [session.id, session.branchName, formatSessionActivity(session, nowMs), session.name ?? "(unnamed)"].filter(Boolean).join(" ")).join("\n")}
1592
1648
  `;
1593
1649
  }
1594
1650
  function renderRecentSessionList(sessions) {
@@ -2079,6 +2135,13 @@ function formatProvisioningQueueLine(session) {
2079
2135
  if (position === 0) return `Provisioning: worker started the branch operation at ${since}`;
2080
2136
  return `Provisioning: queued behind ${position} worker operation${position === 1 ? "" : "s"} since ${since}`;
2081
2137
  }
2138
+ function formatChildSessionsLine(session) {
2139
+ const children = session.childSessions;
2140
+ if (!children || typeof children !== "object") return void 0;
2141
+ const { total, active } = children;
2142
+ if (!Number.isInteger(total) || !Number.isInteger(active) || total <= 0 || active < 0) return void 0;
2143
+ return `Child sessions: ${active} active of ${total}`;
2144
+ }
2082
2145
  function renderSessionRunStatus(session) {
2083
2146
  const status = formatStatusValue(session.status) ?? "unknown";
2084
2147
  const lines = [`Session: ${responseString(session, "sessionId") ?? "(unknown)"}`, `Status: ${status}`];
@@ -2099,6 +2162,8 @@ function renderSessionRunStatus(session) {
2099
2162
  const value = responseString(session, key);
2100
2163
  if (value) lines.push(`${label}: ${value}`);
2101
2164
  }
2165
+ const childSessions = formatChildSessionsLine(session);
2166
+ if (childSessions) lines.push(childSessions);
2102
2167
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2103
2168
  const error = responseString(session, "error");
2104
2169
  if (error) lines.push(`Error: ${error}`);
@@ -2846,13 +2911,20 @@ Sessions: ${result.sessions.length}
2846
2911
  if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
2847
2912
  const offset = first === "delete" ? 2 : 3;
2848
2913
  const projectRef = requireValue(args[offset], "Missing project reference");
2849
- const branchName = requireValue(args[offset + 1], "Missing branch name");
2850
- const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, parseBranchDeleteArgs(args.slice(offset + 2)));
2914
+ const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
2915
+ const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
2916
+ confirmed,
2917
+ // Each deletion is its own round trip, so a reader sees results as they land.
2918
+ onResult: (result) => {
2919
+ if (!json) process.stdout.write(renderBranchDeletionResult(result));
2920
+ }
2921
+ });
2851
2922
  if (deletion.kind === "preview") {
2852
- write({ ...deletion.branch, confirmed: false }, renderBranchDeletionPreview(deletion.branch));
2853
- throw new Error(branchDeletionConfirmationMessage(deletion.branch));
2923
+ write({ branches: deletion.branches, confirmed: false }, deletion.branches.map(renderBranchDeletionPreview).join("\n"));
2924
+ throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
2854
2925
  }
2855
- write(deletion.result, renderBranchDeletion(deletion.result));
2926
+ if (json) write({ results: deletion.results }, "");
2927
+ if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
2856
2928
  return;
2857
2929
  }
2858
2930
  if (first === "get" && second === "envs") {
@@ -3387,17 +3459,14 @@ function resolveCommandExecution(options, rest) {
3387
3459
  if (!options.project) {
3388
3460
  throw new Error("--project/-p is required for `delete branch`");
3389
3461
  }
3390
- const rawArgs = commandArgs.slice(1);
3391
- const positionalBranch = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs[0] : void 0;
3392
- const branch = positionalBranch ?? options.branch;
3393
- if (!branch) {
3462
+ const parsed = parseBranchDeleteArgs(commandArgs.slice(1));
3463
+ const branchNames = parsed.branchNames.length > 0 ? parsed.branchNames : options.branch ? [options.branch] : [];
3464
+ if (branchNames.length === 0) {
3394
3465
  throw new Error("Branch name is required for `delete branch`");
3395
3466
  }
3396
- const flagArgs = positionalBranch ? rawArgs.slice(1) : rawArgs;
3397
- parseBranchDeleteArgs(flagArgs);
3398
3467
  return {
3399
3468
  kind: "plugin",
3400
- pluginArgs: ["delete", "branch", options.project, branch, ...flagArgs]
3469
+ pluginArgs: ["delete", "branch", options.project, ...branchNames, ...parsed.confirmed ? ["--confirm"] : []]
3401
3470
  };
3402
3471
  }
3403
3472
  throw new Error("Unknown delete command");
@@ -3628,9 +3697,12 @@ async function main(argv = process.argv.slice(2)) {
3628
3697
  0 && (module.exports = {
3629
3698
  advanceTransientDevicePollBackoff,
3630
3699
  branchDeletionConfirmationMessage,
3700
+ branchDeletionsConfirmationMessage,
3701
+ branchDeletionsFailureMessage,
3631
3702
  collectK8sUsage,
3632
3703
  createBranchThroughPlatform,
3633
3704
  deleteBranchThroughPlatform,
3705
+ deleteBranchesThroughPlatform,
3634
3706
  dispatchSessionRunCommand,
3635
3707
  followR5dctlSession,
3636
3708
  formatK8sCpu,
@@ -3668,6 +3740,7 @@ async function main(argv = process.argv.slice(2)) {
3668
3740
  readDotenvFile,
3669
3741
  renderBranchDeletion,
3670
3742
  renderBranchDeletionPreview,
3743
+ renderBranchDeletionResult,
3671
3744
  renderBranchList,
3672
3745
  renderBranchOperation,
3673
3746
  renderConversationForkResponse,
@@ -3683,6 +3756,7 @@ async function main(argv = process.argv.slice(2)) {
3683
3756
  renderProcessInspection,
3684
3757
  renderProcessList,
3685
3758
  renderProcessLog,
3759
+ renderSessionList,
3686
3760
  renderSessionRunStart,
3687
3761
  renderSessionRunStatus,
3688
3762
  renderWorkspaceStatus,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.104",
3
+ "version": "0.0.106",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/cli.mjs CHANGED
@@ -126,8 +126,8 @@ const SHARED_HELP_ENTRIES = [
126
126
  },
127
127
  {
128
128
  section: "branches",
129
- usage: "-p <project> delete branch <branch> [--confirm]",
130
- description: "Delete a branch and its sessions through the platform; without --confirm, only preview what would be deleted."
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
131
  },
132
132
  {
133
133
  section: "envs",
@@ -515,15 +515,18 @@ function parseBranchCreateArgs(args) {
515
515
  };
516
516
  }
517
517
  function parseBranchDeleteArgs(args) {
518
+ const branchNames = [];
518
519
  let confirmed = false;
519
520
  for (const arg of args) {
520
521
  if (arg === "--confirm") {
521
522
  confirmed = true;
522
523
  continue;
523
524
  }
524
- throw new Error(arg.startsWith("-") ? `Unknown delete branch flag: ${arg}` : `Unexpected delete branch argument: ${arg}`);
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);
525
528
  }
526
- return { confirmed };
529
+ return { branchNames, confirmed };
527
530
  }
528
531
  const PRIMARY_BRANCH_NAME = "main";
529
532
  function isUnsupportedServerRoute(error) {
@@ -558,6 +561,50 @@ async function deleteBranchThroughPlatform(client, projectRef, branchName, optio
558
561
  const result = await requireServerRouteSupport("delete branch", () => client.projects.branches.delete(projectRef, branchName));
559
562
  return { kind: "deleted", result };
560
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
+ }
561
608
  function renderBranchDeletionPreview(branch) {
562
609
  const count = branch.sessions.length;
563
610
  const lines = [
@@ -1489,11 +1536,15 @@ function renderProjectList(projects) {
1489
1536
  Mode: ${project.mode}`).join("\n\n")}
1490
1537
  `;
1491
1538
  }
1492
- function renderSessionList(sessions) {
1539
+ function formatSessionActivity(session, nowMs) {
1540
+ if (!session.runState) return void 0;
1541
+ return session.lastActivityAt ? `${session.runState}, last active ${formatProcessAge(session.lastActivityAt, nowMs)} ago` : session.runState;
1542
+ }
1543
+ function renderSessionList(sessions, nowMs = Date.now()) {
1493
1544
  if (sessions.length === 0) {
1494
1545
  return "No sessions found.\n";
1495
1546
  }
1496
- return `${sessions.map((session) => `${session.id} ${session.branchName} ${session.name ?? "(unnamed)"}`).join("\n")}
1547
+ return `${sessions.map((session) => [session.id, session.branchName, formatSessionActivity(session, nowMs), session.name ?? "(unnamed)"].filter(Boolean).join(" ")).join("\n")}
1497
1548
  `;
1498
1549
  }
1499
1550
  function renderRecentSessionList(sessions) {
@@ -1984,6 +2035,13 @@ function formatProvisioningQueueLine(session) {
1984
2035
  if (position === 0) return `Provisioning: worker started the branch operation at ${since}`;
1985
2036
  return `Provisioning: queued behind ${position} worker operation${position === 1 ? "" : "s"} since ${since}`;
1986
2037
  }
2038
+ function formatChildSessionsLine(session) {
2039
+ const children = session.childSessions;
2040
+ if (!children || typeof children !== "object") return void 0;
2041
+ const { total, active } = children;
2042
+ if (!Number.isInteger(total) || !Number.isInteger(active) || total <= 0 || active < 0) return void 0;
2043
+ return `Child sessions: ${active} active of ${total}`;
2044
+ }
1987
2045
  function renderSessionRunStatus(session) {
1988
2046
  const status = formatStatusValue(session.status) ?? "unknown";
1989
2047
  const lines = [`Session: ${responseString(session, "sessionId") ?? "(unknown)"}`, `Status: ${status}`];
@@ -2004,6 +2062,8 @@ function renderSessionRunStatus(session) {
2004
2062
  const value = responseString(session, key);
2005
2063
  if (value) lines.push(`${label}: ${value}`);
2006
2064
  }
2065
+ const childSessions = formatChildSessionsLine(session);
2066
+ if (childSessions) lines.push(childSessions);
2007
2067
  lines.push(`Branch state: ${formatBranchStateHint(branch)}`);
2008
2068
  const error = responseString(session, "error");
2009
2069
  if (error) lines.push(`Error: ${error}`);
@@ -2751,13 +2811,20 @@ Sessions: ${result.sessions.length}
2751
2811
  if (first === "projects" && second === "branches" && third === "delete" || first === "delete" && second === "branch") {
2752
2812
  const offset = first === "delete" ? 2 : 3;
2753
2813
  const projectRef = requireValue(args[offset], "Missing project reference");
2754
- const branchName = requireValue(args[offset + 1], "Missing branch name");
2755
- const deletion = await deleteBranchThroughPlatform(client, projectRef, branchName, parseBranchDeleteArgs(args.slice(offset + 2)));
2814
+ const { branchNames, confirmed } = parseBranchDeleteArgs(args.slice(offset + 1));
2815
+ const deletion = await deleteBranchesThroughPlatform(client, projectRef, branchNames, {
2816
+ confirmed,
2817
+ // Each deletion is its own round trip, so a reader sees results as they land.
2818
+ onResult: (result) => {
2819
+ if (!json) process.stdout.write(renderBranchDeletionResult(result));
2820
+ }
2821
+ });
2756
2822
  if (deletion.kind === "preview") {
2757
- write({ ...deletion.branch, confirmed: false }, renderBranchDeletionPreview(deletion.branch));
2758
- throw new Error(branchDeletionConfirmationMessage(deletion.branch));
2823
+ write({ branches: deletion.branches, confirmed: false }, deletion.branches.map(renderBranchDeletionPreview).join("\n"));
2824
+ throw new Error(branchDeletionsConfirmationMessage(deletion.branches));
2759
2825
  }
2760
- write(deletion.result, renderBranchDeletion(deletion.result));
2826
+ if (json) write({ results: deletion.results }, "");
2827
+ if (deletion.results.some((result) => !result.deleted)) throw new Error(branchDeletionsFailureMessage(deletion.results));
2761
2828
  return;
2762
2829
  }
2763
2830
  if (first === "get" && second === "envs") {
@@ -3292,17 +3359,14 @@ function resolveCommandExecution(options, rest) {
3292
3359
  if (!options.project) {
3293
3360
  throw new Error("--project/-p is required for `delete branch`");
3294
3361
  }
3295
- const rawArgs = commandArgs.slice(1);
3296
- const positionalBranch = rawArgs[0] && !rawArgs[0].startsWith("-") ? rawArgs[0] : void 0;
3297
- const branch = positionalBranch ?? options.branch;
3298
- if (!branch) {
3362
+ const parsed = parseBranchDeleteArgs(commandArgs.slice(1));
3363
+ const branchNames = parsed.branchNames.length > 0 ? parsed.branchNames : options.branch ? [options.branch] : [];
3364
+ if (branchNames.length === 0) {
3299
3365
  throw new Error("Branch name is required for `delete branch`");
3300
3366
  }
3301
- const flagArgs = positionalBranch ? rawArgs.slice(1) : rawArgs;
3302
- parseBranchDeleteArgs(flagArgs);
3303
3367
  return {
3304
3368
  kind: "plugin",
3305
- pluginArgs: ["delete", "branch", options.project, branch, ...flagArgs]
3369
+ pluginArgs: ["delete", "branch", options.project, ...branchNames, ...parsed.confirmed ? ["--confirm"] : []]
3306
3370
  };
3307
3371
  }
3308
3372
  throw new Error("Unknown delete command");
@@ -3532,9 +3596,12 @@ async function main(argv = process.argv.slice(2)) {
3532
3596
  export {
3533
3597
  advanceTransientDevicePollBackoff,
3534
3598
  branchDeletionConfirmationMessage,
3599
+ branchDeletionsConfirmationMessage,
3600
+ branchDeletionsFailureMessage,
3535
3601
  collectK8sUsage,
3536
3602
  createBranchThroughPlatform,
3537
3603
  deleteBranchThroughPlatform,
3604
+ deleteBranchesThroughPlatform,
3538
3605
  dispatchSessionRunCommand,
3539
3606
  followR5dctlSession,
3540
3607
  formatK8sCpu,
@@ -3572,6 +3639,7 @@ export {
3572
3639
  readDotenvFile,
3573
3640
  renderBranchDeletion,
3574
3641
  renderBranchDeletionPreview,
3642
+ renderBranchDeletionResult,
3575
3643
  renderBranchList,
3576
3644
  renderBranchOperation,
3577
3645
  renderConversationForkResponse,
@@ -3587,6 +3655,7 @@ export {
3587
3655
  renderProcessInspection,
3588
3656
  renderProcessList,
3589
3657
  renderProcessLog,
3658
+ renderSessionList,
3590
3659
  renderSessionRunStart,
3591
3660
  renderSessionRunStatus,
3592
3661
  renderWorkspaceStatus,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.104",
3
+ "version": "0.0.106",
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 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 R5dctlSessionSummary, 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;
@@ -86,6 +86,7 @@ export type R5dctlBranchCreateArgs = {
86
86
  };
87
87
  export declare function parseBranchCreateArgs(args: string[]): R5dctlBranchCreateArgs;
88
88
  export declare function parseBranchDeleteArgs(args: string[]): {
89
+ branchNames: string[];
89
90
  confirmed: boolean;
90
91
  };
91
92
  /** An older server strips unknown routes and answers with this generic 404; a route's own not-found carries a specific message. */
@@ -103,6 +104,35 @@ export type R5dctlBranchDeletionOutcome = {
103
104
  export declare function deleteBranchThroughPlatform(client: R5dctlClient, projectRef: string, branchName: string, options: {
104
105
  confirmed: boolean;
105
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;
106
136
  export declare function renderBranchDeletionPreview(branch: R5dctlBranchDescription): string;
107
137
  export declare function branchDeletionConfirmationMessage(branch: R5dctlBranchDescription): string;
108
138
  export declare function renderBranchDeletion(result: R5dctlBranchDeleteResponse): string;
@@ -174,6 +204,7 @@ export declare function advanceTransientDevicePollBackoff(intervalMs: number, tr
174
204
  nextBackoffMs: number;
175
205
  };
176
206
  export declare function handleAuthLogin(client: R5dctlClient, options: GlobalOptions, commandArgs: string[], config: R5dctlConfig): Promise<void>;
207
+ export declare function renderSessionList(sessions: R5dctlSessionSummary[], nowMs?: number): string;
177
208
  export declare function renderWorkspaceStatus(status: R5dctlWorkspaceStatus): string;
178
209
  export declare function renderBranchList(branches: R5dctlBranch[]): string;
179
210
  export declare function renderWorkspaceSync(result: R5dctlWorkspaceSyncResult): string;
@@ -254,6 +285,11 @@ export type R5dctlCommandResponse = {
254
285
  error?: string;
255
286
  summary?: string;
256
287
  diffSummary?: string;
288
+ /** Sub-agent sessions this session launched; older servers omit it. */
289
+ childSessions?: {
290
+ total: number;
291
+ active: number;
292
+ };
257
293
  };
258
294
  export type R5dctlSessionRunClient = {
259
295
  projects: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.104",
3
+ "version": "0.0.106",
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.104",
29
+ "@ricsam/r5d-api": "^0.0.106",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"