@botbuddy/cli 1.66.1 → 1.68.0

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.
@@ -0,0 +1,126 @@
1
+ // BOT-3152: `bb status-band` — the bb-status band's host side, run from the
2
+ // installed CLI (never a repo checkout), and its one-time Claude Code install.
3
+ //
4
+ // bb status-band read --harness-session-id <uuid> [--ticket-id K[,K…]] [--skip-epic]
5
+ // bb status-band relay --cond <condition>… [--since <cursor>]
6
+ // bb status-band install
7
+ //
8
+ // `read` and `relay` print one JSON line for the mod. `install` points the
9
+ // `botbuddy-labs` Claude Code marketplace at the plugin shipped in this package
10
+ // and installs `bb-status` at user scope, so every worktree gets the band.
11
+ import { execFile as nodeExecFile } from "node:child_process";
12
+ import { readFile } from "node:fs/promises";
13
+ import { dirname, join, resolve, sep } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { promisify } from "node:util";
16
+
17
+ export const MARKETPLACE = "botbuddy-labs";
18
+ export const PLUGIN = "bb-status";
19
+ const PLUGIN_ID = `${PLUGIN}@${MARKETPLACE}`;
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+ export const PACKAGE_ROOT = resolve(HERE, "..");
22
+ export const MARKETPLACE_DIR = join(PACKAGE_ROOT, "claude-plugin");
23
+
24
+ const HELP = `bb status-band <read|relay|install>
25
+
26
+ read --harness-session-id <uuid> [--ticket-id KEY[,KEY…]] [--skip-epic]
27
+ One JSON snapshot of this worktree's BB session for the bb-status band.
28
+ relay --cond <condition>… [--since <cursor>]
29
+ Arm one bb wait on the band's subjects and print how it ended.
30
+ install
31
+ Install the bb-status band into Claude Code once, at user scope, from
32
+ this installed CLI package. bb setup and bb update run it for you.`;
33
+
34
+ export async function cmdStatusBand(args, { log = (l) => console.log(l), out = process.stdout } = {}) {
35
+ const [sub, ...rest] = args;
36
+ if (sub === "read") {
37
+ const { main } = await import("./status-band-read.mjs");
38
+ return main(rest, { out });
39
+ }
40
+ if (sub === "relay") {
41
+ const { main } = await import("./status-band-relay.mjs");
42
+ return main(rest, { out });
43
+ }
44
+ if (sub === "install") {
45
+ const result = await installStatusBand();
46
+ log(statusBandLine(result));
47
+ return result.status === "failed" ? 1 : 0;
48
+ }
49
+ log(HELP);
50
+ return sub === undefined || sub === "--help" || sub === "-h" || sub === "help" ? 0 : 2;
51
+ }
52
+
53
+ // A plugin served from a repo checkout dies with that checkout (the BOT-3152
54
+ // fragility), so only a package installed under node_modules is offered.
55
+ export function isInstalledPackage(root = PACKAGE_ROOT) {
56
+ return root.split(sep).includes("node_modules");
57
+ }
58
+
59
+ async function claudeJson(run, args) {
60
+ const { stdout } = await run(["plugin", ...args, "--json"]);
61
+ return JSON.parse(stdout);
62
+ }
63
+
64
+ /**
65
+ * Point `botbuddy-labs` at this package's plugin folder and install `bb-status`
66
+ * at user scope. Idempotent; never throws. Returns { status, … } where status is
67
+ * installed | updated | unchanged | skipped | failed.
68
+ */
69
+ export async function installStatusBand({
70
+ marketplaceDir = MARKETPLACE_DIR,
71
+ packageRoot = PACKAGE_ROOT,
72
+ claude = "claude",
73
+ execFileImpl = promisify(nodeExecFile),
74
+ readJson = async (p) => JSON.parse(await readFile(p, "utf8")),
75
+ } = {}) {
76
+ if (!isInstalledPackage(packageRoot)) return { status: "skipped", reason: "not_installed_package" };
77
+ const run = (args) => execFileImpl(claude, args, { timeout: 60_000, maxBuffer: 4 * 1024 * 1024 });
78
+ try {
79
+ await run(["--version"]);
80
+ } catch {
81
+ return { status: "skipped", reason: "claude_missing" };
82
+ }
83
+ let shipped;
84
+ try {
85
+ shipped = (await readJson(join(marketplaceDir, PLUGIN, ".claude-plugin", "plugin.json"))).version;
86
+ } catch {
87
+ return { status: "failed", reason: "plugin_missing_from_package" };
88
+ }
89
+ try {
90
+ const markets = await claudeJson(run, ["marketplace", "list"]);
91
+ const current = Array.isArray(markets) ? markets.find((m) => m?.name === MARKETPLACE) : null;
92
+ const repointed = Boolean(current) && resolve(String(current.path ?? "")) !== resolve(marketplaceDir);
93
+ if (repointed) await run(["plugin", "marketplace", "remove", MARKETPLACE]);
94
+ if (!current || repointed) await run(["plugin", "marketplace", "add", marketplaceDir, "--scope", "user"]);
95
+ else await run(["plugin", "marketplace", "update", MARKETPLACE]);
96
+ const plugins = await claudeJson(run, ["list"]);
97
+ const mine = Array.isArray(plugins) ? plugins.find((p) => p?.id === PLUGIN_ID && p?.scope === "user") : null;
98
+ if (mine && mine.version === shipped && mine.enabled !== false && !repointed) {
99
+ return { status: "unchanged", version: shipped };
100
+ }
101
+ if (mine && !repointed) await run(["plugin", "update", PLUGIN_ID, "--scope", "user"]);
102
+ else await run(["plugin", "install", PLUGIN_ID, "--scope", "user"]);
103
+ if (mine && mine.enabled === false) await run(["plugin", "enable", PLUGIN_ID, "--scope", "user"]);
104
+ return { status: mine && !repointed ? "updated" : "installed", version: shipped, ...(repointed ? { replaced: String(current.path ?? "") } : {}) };
105
+ } catch (error) {
106
+ const detail = String(error?.stderr || error?.message || error).trim().split("\n")[0].slice(0, 200);
107
+ return { status: "failed", reason: "claude_plugin_command_failed", detail };
108
+ }
109
+ }
110
+
111
+ export function statusBandLine(result) {
112
+ switch (result?.status) {
113
+ case "installed":
114
+ return `bb-status band installed in Claude Code (user scope, v${result.version}); new sessions show it. Run /reload-plugins in open ones.`;
115
+ case "updated":
116
+ return `bb-status band updated in Claude Code to v${result.version}; new sessions show it. Run /reload-plugins in open ones.`;
117
+ case "unchanged":
118
+ return `bb-status band already installed in Claude Code (v${result.version}).`;
119
+ case "skipped":
120
+ return result.reason === "claude_missing"
121
+ ? "bb-status band skipped: no claude command on PATH."
122
+ : "bb-status band skipped: this bb runs from a checkout, not an installed package (npm i -g @botbuddy/cli).";
123
+ default:
124
+ return `bb-status band could not be installed (${result?.reason ?? "unknown"}${result?.detail ? `: ${result.detail}` : ""}); run bb status-band install to retry.`;
125
+ }
126
+ }
@@ -144,7 +144,7 @@ export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl
144
144
 
145
145
  // `botbuddy update` waits for npm because the terminal result, rather than the
146
146
  // child-process launch, is the only trustworthy confirmation an update worked.
147
- export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, readInstalledVersion: installedVersion = readInstalledVersion, refreshPools = refreshPoolsFromNewPackage, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
147
+ export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, readInstalledVersion: installedVersion = readInstalledVersion, refreshPools = refreshPoolsFromNewPackage, installBand = installBandFromNewPackage, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
148
148
  // BOT-1566 (Codex P1/P2): on Windows npm is the `npm.cmd` shim, and Node
149
149
  // cannot spawn a .cmd file shell-free (docs) — run it through a shell on
150
150
  // win32 (args are all static constants, so no injection surface) and select
@@ -208,6 +208,11 @@ export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = sp
208
208
  .catch((error) => {
209
209
  try { stderr.write(`botbuddy update: the installed staging pools were not refreshed (${error?.message ?? error}); run bb staging-pool status\n`); } catch { /* ignore */ }
210
210
  })
211
+ // BOT-3152: the new package re-points the bb-status band at itself; never an exit code.
212
+ .then(() => installBand({ npmBin, shellOpt, probeImpl, log }))
213
+ .catch((error) => {
214
+ try { stderr.write(`botbuddy update: the bb-status band was not refreshed (${error?.message ?? error}); run bb status-band install\n`); } catch { /* ignore */ }
215
+ })
211
216
  .finally(() => resolve(0));
212
217
  });
213
218
  });
@@ -299,6 +304,15 @@ export async function refreshInstalledPools({
299
304
  return results;
300
305
  }
301
306
 
307
+ // BOT-3152: run the NEW package's `status-band install` (this process is the old
308
+ // one), so Claude Code's bb-status band and its marketplace match the update.
309
+ export async function installBandFromNewPackage({ npmBin, shellOpt, probeImpl, log, nodeBin = process.execPath, execFileImpl = promisify(execFile) }) {
310
+ const root = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
311
+ const { stdout } = await execFileImpl(nodeBin, [join(root, "@botbuddy", "cli", "bin", "botbuddy.mjs"), "status-band", "install"], { timeout: 180_000, maxBuffer: 1024 * 1024 });
312
+ const line = String(stdout).trim().split("\n").filter(Boolean).pop();
313
+ if (line) log(line);
314
+ }
315
+
302
316
  // The default for `cmdUpdate`: locate the newly installed package and refresh with it.
303
317
  export async function refreshPoolsFromNewPackage({ installedVersion, npmBin, shellOpt, probeImpl, log, stderr }) {
304
318
  const root = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
package/src/wait-core.mjs CHANGED
@@ -119,6 +119,9 @@ export const SIGNAL_TYPES = Object.freeze([
119
119
  // BOT-3136: one signal per member-issue change of a Linear project (an epic),
120
120
  // subject = the project's lower-case UUID. Tenant-primary like linear_issue.
121
121
  "linear_project",
122
+ // BOT-3152: a staging incident of the caller's tenant opening or closing
123
+ // (subject = owner/repo). The bb-status band's staging banner wakes on it.
124
+ "staging_incident",
122
125
  ]);
123
126
 
124
127
  const LOCK_SUBTYPES = Object.freeze(["playwright_lane", "vite_port", "backend_port", "supabase_local"]);
package/src/wait.mjs CHANGED
@@ -255,6 +255,9 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
255
255
  event:type=linear_project,subject=<Linear project UUID>;
256
256
  it wakes when any issue in that project changes, joins
257
257
  or leaves it (the band's epic row, BOT-3136).
258
+ Staging state uses event:type=staging_incident;
259
+ it wakes when one of your tenant's staging incidents
260
+ opens or closes (the band's staging banner, BOT-3152).
258
261
 
259
262
  OPTIONS
260
263
  Review intervention: exact-head Codex waits have a durable one-shot diagnostic.