@botbuddy/cli 1.36.2 → 1.36.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/stack.mjs +50 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.36.2",
3
+ "version": "1.36.3",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/stack.mjs CHANGED
@@ -1279,21 +1279,62 @@ export const STACK_DISPOSITIONS = Object.freeze(["destroy", "stop"]);
1279
1279
  * the project's data volumes (`supabase_db_/storage_/edge_runtime_<project_id>`); `--workdir`
1280
1280
  * scopes that to the stack's own project_id, never `--all`. `stop` keeps the volumes for a
1281
1281
  * cheap re-provision. An unknown disposition throws before Docker is touched.
1282
+ *
1283
+ * BOT-2001: when `supabase stop` fails (2026-10-02: a transient exit 1 whose real error sat
1284
+ * behind the CLI's deprecation WARN), log the tail of its output and fall back to a
1285
+ * label-scoped teardown of exactly this project (`com.supabase.cli.project=<project_id>`),
1286
+ * on the same validated daemon. Success requires the post-check `docker ps -a` for that
1287
+ * label to be empty; no readable project_id → no fallback, never an unscoped `docker rm`.
1282
1288
  */
1283
- export function localTeardown(stackDir, dockerTarget, disposition, spawn = spawnSync, env = process.env) {
1289
+ export function localTeardown(stackDir, dockerTarget, disposition, spawn = spawnSync, env = process.env, {
1290
+ readProjectId = projectIdUnder, log = (line) => process.stderr.write(line),
1291
+ } = {}) {
1284
1292
  if (!STACK_DISPOSITIONS.includes(disposition)) {
1285
1293
  throw new Error(`localTeardown: unknown disposition ${JSON.stringify(disposition)} (expected one of ${STACK_DISPOSITIONS.join(", ")})`);
1286
1294
  }
1287
1295
  const args = ["stop", "--workdir", stackDir, ...(disposition === "destroy" ? ["--no-backup"] : [])];
1288
- process.stderr.write(`${yellow("⚠ LOCAL-EXEC FALLBACK")} — running ${bold(`supabase ${args.join(" ")}`)} (disposition ${disposition}).\n`);
1289
- const res = spawn("supabase", args, {
1290
- encoding: "utf8",
1291
- env: dockerEnvForTarget(dockerTarget, env),
1292
- });
1293
- if (res.status !== 0) {
1294
- process.stderr.write(`${yellow("⚠")} stack: supabase stop returned ${res.status}: ${(res.stderr || res.stdout || "").slice(0, 300)}\n`);
1296
+ log(`${yellow("⚠ LOCAL-EXEC FALLBACK")} — running ${bold(`supabase ${args.join(" ")}`)} (disposition ${disposition}).\n`);
1297
+ const dockerEnv = dockerEnvForTarget(dockerTarget, env);
1298
+ const res = spawn("supabase", args, { encoding: "utf8", env: dockerEnv });
1299
+ if (res.status === 0) return true;
1300
+ // The real error is LAST (WARN/upgrade notices lead), so keep the tail, not the head.
1301
+ const output = `${res.stderr || ""}${res.stdout || ""}`.trim();
1302
+ log(`${yellow("⚠")} stack: supabase stop returned ${res.status}:\n${output.slice(-2000)}\n`);
1303
+ const projectId = readProjectId(stackDir);
1304
+ if (!projectId) {
1305
+ log(`${yellow("⚠")} stack: no readable project_id under ${stackDir} — not attempting a label-scoped teardown.\n`);
1306
+ return false;
1307
+ }
1308
+ return labelScopedTeardown(projectId, disposition, spawn, dockerEnv, log);
1309
+ }
1310
+
1311
+ /** BOT-2001: remove exactly one Supabase project's containers, network and (for `destroy`)
1312
+ * volumes by the CLI's project label; true iff a re-list finds NONE of the resource classes
1313
+ * this disposition removes (Codex #1012: a busy volume or network fails closed too). */
1314
+ function labelScopedTeardown(projectId, disposition, spawn, dockerEnv, log) {
1315
+ const filter = ["--filter", `label=com.supabase.cli.project=${projectId}`];
1316
+ const docker = (args) => spawn("docker", args, { encoding: "utf8", env: dockerEnv });
1317
+ const ids = (res) => (res.status === 0 ? (res.stdout || "").split(/\s+/).filter(Boolean) : null);
1318
+ const classes = [
1319
+ { name: "container", list: ["ps", "-aq", ...filter], remove: ["rm", "-f"] },
1320
+ ...(disposition === "destroy" ? [{ name: "volume", list: ["volume", "ls", "-q", ...filter], remove: ["volume", "rm", "-f"] }] : []),
1321
+ { name: "network", list: ["network", "ls", "-q", ...filter], remove: ["network", "rm"] },
1322
+ ];
1323
+ log(`${yellow("⚠")} stack: falling back to a label-scoped teardown of project ${bold(projectId)}.\n`);
1324
+ for (const c of classes) {
1325
+ const found = ids(docker(c.list));
1326
+ if (found?.length) {
1327
+ const rm = docker([...c.remove, ...found]);
1328
+ if (rm.status !== 0) log(`${yellow("⚠")} stack: removing ${c.name}(s) of ${projectId} failed: ${`${rm.stderr || rm.stdout || ""}`.trim().slice(-500)}\n`);
1329
+ }
1330
+ }
1331
+ const left = classes.map((c) => ({ name: c.name, ids: ids(docker(c.list)) })).filter((c) => c.ids === null || c.ids.length);
1332
+ if (left.length) {
1333
+ log(`${yellow("⚠")} stack: project ${projectId} still has ${left.map((c) => (c.ids === null ? `unverifiable ${c.name}s` : `${c.ids.length} ${c.name}(s)`)).join(", ")} after the fallback — the slot stays fenced.\n`);
1334
+ return false;
1295
1335
  }
1296
- return res.status === 0;
1336
+ log(`${yellow("⚠")} stack: label-scoped teardown of project ${projectId} succeeded.\n`);
1337
+ return true;
1297
1338
  }
1298
1339
 
1299
1340
  function emit(receipt, opts, code) {