@botbuddy/cli 1.33.10 → 1.33.11
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/package.json +1 -1
- package/src/stack.mjs +62 -6
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -1264,10 +1264,25 @@ export function localProvision(opts, dockerTarget, execution, spawn = spawnSync,
|
|
|
1264
1264
|
return conn;
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
-
/**
|
|
1268
|
-
export
|
|
1269
|
-
|
|
1270
|
-
|
|
1267
|
+
/** Release dispositions `stack done` accepts (`--destroy` is the default, `--stop` keeps volumes). */
|
|
1268
|
+
export const STACK_DISPOSITIONS = Object.freeze(["destroy", "stop"]);
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* LOUD local-exec fallback: tear the stack down in `stackDir`. Returns true iff it succeeded.
|
|
1272
|
+
*
|
|
1273
|
+
* BOT-1887: the teardown honours the lease's release `disposition`. `destroy` (the `done`
|
|
1274
|
+
* default, and what the receipt reports) runs `supabase stop --no-backup`, which also deletes
|
|
1275
|
+
* the project's data volumes (`supabase_db_/storage_/edge_runtime_<project_id>`); `--workdir`
|
|
1276
|
+
* scopes that to the stack's own project_id, never `--all`. `stop` keeps the volumes for a
|
|
1277
|
+
* cheap re-provision. An unknown disposition throws before Docker is touched.
|
|
1278
|
+
*/
|
|
1279
|
+
export function localTeardown(stackDir, dockerTarget, disposition, spawn = spawnSync, env = process.env) {
|
|
1280
|
+
if (!STACK_DISPOSITIONS.includes(disposition)) {
|
|
1281
|
+
throw new Error(`localTeardown: unknown disposition ${JSON.stringify(disposition)} (expected one of ${STACK_DISPOSITIONS.join(", ")})`);
|
|
1282
|
+
}
|
|
1283
|
+
const args = ["stop", "--workdir", stackDir, ...(disposition === "destroy" ? ["--no-backup"] : [])];
|
|
1284
|
+
process.stderr.write(`${yellow("⚠ LOCAL-EXEC FALLBACK")} — running ${bold(`supabase ${args.join(" ")}`)} (disposition ${disposition}).\n`);
|
|
1285
|
+
const res = spawn("supabase", args, {
|
|
1271
1286
|
encoding: "utf8",
|
|
1272
1287
|
env: dockerEnvForTarget(dockerTarget, env),
|
|
1273
1288
|
});
|
|
@@ -1608,7 +1623,14 @@ export async function cmdDone(leaseId, opts, {
|
|
|
1608
1623
|
let dockerTarget = null;
|
|
1609
1624
|
let legacyTargetProof = null;
|
|
1610
1625
|
let teardownDir = process.cwd();
|
|
1626
|
+
let leaseBefore = null;
|
|
1611
1627
|
if (opts.localExec) {
|
|
1628
|
+
// BOT-1887: the disposition picks the local teardown (destroy deletes the project's
|
|
1629
|
+
// volumes); refuse an unknown one BEFORE the lease is released into `reaping`.
|
|
1630
|
+
if (!STACK_DISPOSITIONS.includes(opts.disposition)) {
|
|
1631
|
+
return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId,
|
|
1632
|
+
error: `unknown disposition ${JSON.stringify(opts.disposition)} (expected one of ${STACK_DISPOSITIONS.join(", ")})` }), opts, EXIT.INVALID);
|
|
1633
|
+
}
|
|
1612
1634
|
const current = await call("get_stack_lease", { lease_id: leaseId });
|
|
1613
1635
|
if (!current.ok || !current.data?.success) {
|
|
1614
1636
|
return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId,
|
|
@@ -1618,6 +1640,7 @@ export async function cmdDone(leaseId, opts, {
|
|
|
1618
1640
|
// BOT-1711: tear down in the directory the stack was PROVISIONED in (the lease's
|
|
1619
1641
|
// recorded worktree_root/stack_path), not the invoking cwd — otherwise `supabase
|
|
1620
1642
|
// stop` at the worktree root would stop the shared canonical stack.
|
|
1643
|
+
leaseBefore = current.data;
|
|
1621
1644
|
teardownDir = leaseStackDir(current.data);
|
|
1622
1645
|
// BOT-1711 (Codex P1, R4): refuse local-exec teardown of ANY lease whose target is the
|
|
1623
1646
|
// worktree root / default project — BEFORE branching on the persisted Docker target.
|
|
@@ -1706,13 +1729,46 @@ export async function cmdDone(leaseId, opts, {
|
|
|
1706
1729
|
if (!r.ok) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: r.error }), opts, r.auth ? EXIT.AUTH : EXIT.BACKEND);
|
|
1707
1730
|
if (!r.data.success) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, code: r.data.code, error: r.data.code }), opts, EXIT.BACKEND);
|
|
1708
1731
|
let state = r.data.state;
|
|
1732
|
+
// BOT-1887 (Codex #972 P1): a lease that is ALREADY reaping (an idle reap that
|
|
1733
|
+
// established `stop`, or a retried `destroy`) keeps its in-flight teardown contract —
|
|
1734
|
+
// `release_stack_lease` returns that established `reap_disposition` and ignores the
|
|
1735
|
+
// requested one. Tear down (and report) by the established contract, never the flag,
|
|
1736
|
+
// so a retry can neither delete volumes a `stop` meant to keep nor keep a `destroy`'s.
|
|
1737
|
+
// Codex #972 R2 (P2): a lease that was ALREADY reaped answers `noop` with no
|
|
1738
|
+
// reap_disposition — nothing was torn down by this call, so report the disposition it
|
|
1739
|
+
// was actually reaped with (the lease row carries it), never the newly requested flag;
|
|
1740
|
+
// unknown → null. A never-provisioned lease is terminalized as `destroy` server-side.
|
|
1741
|
+
let established = r.data.reap_disposition;
|
|
1742
|
+
const alreadyReaped = established == null && state === "reaped" && r.data.noop === true;
|
|
1743
|
+
if (alreadyReaped) {
|
|
1744
|
+
// Codex #972 R3: the pre-release read is only authoritative if it ALREADY saw the
|
|
1745
|
+
// lease reaped — a Helper can finalize it between that read and the release. Otherwise
|
|
1746
|
+
// re-read, and refuse to emit an unverifiable receipt if that read fails.
|
|
1747
|
+
let stored = leaseBefore?.state === "reaped" ? leaseBefore : null;
|
|
1748
|
+
if (!stored) {
|
|
1749
|
+
const got = await call("get_stack_lease", { lease_id: leaseId });
|
|
1750
|
+
stored = got.ok && got.data?.success ? got.data : null;
|
|
1751
|
+
if (!stored) {
|
|
1752
|
+
return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, state,
|
|
1753
|
+
error: `the lease was already reaped but its reap disposition could not be read (${got.error || got.data?.code || "unreadable"}); nothing was torn down` }), opts, got.auth ? EXIT.AUTH : EXIT.BACKEND);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
established = stored.reap_disposition ?? null;
|
|
1757
|
+
} else if (established == null && r.data.never_provisioned === true) {
|
|
1758
|
+
established = "destroy";
|
|
1759
|
+
}
|
|
1760
|
+
if (established != null && !STACK_DISPOSITIONS.includes(established)) {
|
|
1761
|
+
return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, state,
|
|
1762
|
+
error: `the lease's established reap disposition ${JSON.stringify(established)} is unknown — NOT tearing down; the slot stays fenced` }), opts, EXIT.BACKEND);
|
|
1763
|
+
}
|
|
1764
|
+
const disposition = alreadyReaped ? established : (established ?? opts.disposition);
|
|
1709
1765
|
if (opts.localExec && state === "reaping") {
|
|
1710
1766
|
// Only finalize once the stack is PROVABLY down. A failed `supabase stop` (Docker
|
|
1711
1767
|
// unavailable, etc.) must NOT finalize — finalize frees the slot lock and promotes
|
|
1712
1768
|
// the next queued lease, which would collide with containers still running on this
|
|
1713
1769
|
// slot (Codex P1). Leave the lease in `reaping` (slot stays fenced) for a retry /
|
|
1714
1770
|
// the reaper. Fail with a non-zero exit so the caller knows teardown is incomplete.
|
|
1715
|
-
if (!localTeardownFn(teardownDir, dockerTarget)) {
|
|
1771
|
+
if (!localTeardownFn(teardownDir, dockerTarget, disposition)) {
|
|
1716
1772
|
return emitResult(buildReceipt({
|
|
1717
1773
|
command: "done", outcome: "error", lease_id: leaseId, state,
|
|
1718
1774
|
error: "local `supabase stop` failed — NOT finalizing; the slot stays fenced. Tear the stack down and re-run `stack done --local-exec`, or let the reaper reconcile.",
|
|
@@ -1722,7 +1778,7 @@ export async function cmdDone(leaseId, opts, {
|
|
|
1722
1778
|
if (fin.ok && fin.data?.success) state = fin.data.state;
|
|
1723
1779
|
else process.stderr.write(`${yellow("⚠")} stack: finalize failed (${fin.error || fin.data?.code}); the reaper will reconcile.\n`);
|
|
1724
1780
|
}
|
|
1725
|
-
return emitResult(buildReceipt({ command: "done", outcome: "released", lease_id: leaseId, state, disposition
|
|
1781
|
+
return emitResult(buildReceipt({ command: "done", outcome: "released", lease_id: leaseId, state, disposition,
|
|
1726
1782
|
...(legacyTargetProof ? { legacy_target_proof: legacyTargetProof } : {}) }), opts, EXIT.OK);
|
|
1727
1783
|
}
|
|
1728
1784
|
|