@botbuddy/cli 1.18.0 → 1.19.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.
- package/package.json +1 -1
- package/src/docker-hygiene.mjs +158 -3
- package/src/stack.mjs +45 -6
package/package.json
CHANGED
package/src/docker-hygiene.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { hostname } from "node:os";
|
|
9
9
|
import { callToolJson } from "./api.mjs";
|
|
10
10
|
import { acquireStackLock, lockPathForProject, projectIdFromConfig } from "./stack-file-lock.mjs";
|
|
11
|
+
import { VERSION } from "./version.mjs";
|
|
11
12
|
import { machineUuid } from "./machine-id.mjs";
|
|
12
13
|
|
|
13
14
|
export const SCHEMA_VERSION = 1;
|
|
@@ -18,6 +19,7 @@ const INSPECT_BATCH_SIZE = 10;
|
|
|
18
19
|
const MAX_APPLY_BUDGET_MS = 5 * 60 * 1000;
|
|
19
20
|
const MAX_DOCKER_COMMAND_MS = 30 * 1000;
|
|
20
21
|
const LOCK_EXPIRY_RESERVE_MS = 60 * 1000;
|
|
22
|
+
const TELEMETRY_TIMEOUT_MS = 2 * 1000;
|
|
21
23
|
|
|
22
24
|
export const EXIT = Object.freeze({
|
|
23
25
|
OK: 0,
|
|
@@ -79,6 +81,18 @@ SAFETY
|
|
|
79
81
|
• Re-inspects every candidate immediately before deletion.
|
|
80
82
|
• Never force-removes a resource, never runs a prune command, and never removes volumes.
|
|
81
83
|
|
|
84
|
+
RELIABILITY MEASUREMENT
|
|
85
|
+
After the local safety decision is complete, the CLI makes one best-effort
|
|
86
|
+
aggregate reliability report. Docker IDs, names, endpoints, labels, paths,
|
|
87
|
+
commands, and raw errors are not uploaded. Reporting is observational only:
|
|
88
|
+
success, rejection, unavailability, or timeout never changes cleanup safety,
|
|
89
|
+
command arguments, or the local exit code.
|
|
90
|
+
|
|
91
|
+
LIFECYCLE
|
|
92
|
+
Use one worktree and one ticket-scoped managed stack for one test/fix batch;
|
|
93
|
+
finish or reap that stack before starting another. Run preflight before start,
|
|
94
|
+
and ticket-scoped dry-run hygiene when pressure warns or refuses admission.
|
|
95
|
+
|
|
82
96
|
PERSISTENT ORBSTACK GHOSTS
|
|
83
97
|
If an exact ID remains listed but OrbStack cannot inspect it, inventory is
|
|
84
98
|
reported but cleanup refuses. Restart or repair OrbStack, then rerun the
|
|
@@ -329,6 +343,11 @@ function selected(runDocker, selector, args) {
|
|
|
329
343
|
return runChecked(runDocker, [...selector, ...args]);
|
|
330
344
|
}
|
|
331
345
|
|
|
346
|
+
function isDockerDaemonUnavailable(error) {
|
|
347
|
+
return /(?:cannot connect to (?:the )?docker daemon|docker daemon is not running|error during connect|dial unix[^\n]*(?:connection refused|no such file or directory)|orbstack[^\n]*daemon[^\n]*unavailable)/i
|
|
348
|
+
.test(String(error?.message ?? error ?? ""));
|
|
349
|
+
}
|
|
350
|
+
|
|
332
351
|
function nonEmptyLines(text) {
|
|
333
352
|
return text.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
334
353
|
}
|
|
@@ -655,15 +674,26 @@ function readInventory(runDocker, selector) {
|
|
|
655
674
|
|
|
656
675
|
function validateOrbStack(runDocker, opts) {
|
|
657
676
|
let resolvedEndpoint = opts.endpoint;
|
|
677
|
+
let targetHintsOrbStack = /orbstack/i.test(String(opts.endpoint || ""));
|
|
658
678
|
if (opts.context) {
|
|
659
679
|
const inspected = parseJson(runChecked(runDocker, ["context", "inspect", opts.context]), "docker context inspect");
|
|
660
680
|
const context = Array.isArray(inspected) ? inspected[0] : inspected;
|
|
661
681
|
resolvedEndpoint = context?.Endpoints?.docker?.Host || null;
|
|
662
682
|
if (!resolvedEndpoint) throw new Error(`Docker context ${opts.context} has no Docker endpoint`);
|
|
683
|
+
targetHintsOrbStack = /orbstack/i.test(`${context?.Name || opts.context} ${context?.Metadata?.Description || ""} ${resolvedEndpoint}`);
|
|
663
684
|
}
|
|
664
685
|
|
|
665
686
|
const selector = selectorArgs(opts);
|
|
666
|
-
|
|
687
|
+
let info;
|
|
688
|
+
try {
|
|
689
|
+
info = parseJson(selected(runDocker, selector, ["info", "--format", "{{json .}}"]), "docker info");
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (targetHintsOrbStack && isDockerDaemonUnavailable(error)) {
|
|
692
|
+
error.reliabilityEventKind = "orbstack_unavailable";
|
|
693
|
+
error.resolvedEndpoint = resolvedEndpoint;
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
667
697
|
const osIsOrbStack = info?.OperatingSystem === "OrbStack";
|
|
668
698
|
const identity = `${info?.KernelVersion || ""} ${info?.Name || ""} ${info?.ServerVersion || ""}`;
|
|
669
699
|
if (!osIsOrbStack || !/orbstack/i.test(identity)) {
|
|
@@ -741,6 +771,7 @@ function baseReceipt(command, opts, now) {
|
|
|
741
771
|
warnings: [],
|
|
742
772
|
errors: [],
|
|
743
773
|
recommendation: null,
|
|
774
|
+
telemetry: { status: "not_attempted" },
|
|
744
775
|
authority: {
|
|
745
776
|
ticket: opts.ticket,
|
|
746
777
|
worktree_branch: null,
|
|
@@ -751,6 +782,119 @@ function baseReceipt(command, opts, now) {
|
|
|
751
782
|
};
|
|
752
783
|
}
|
|
753
784
|
|
|
785
|
+
export function buildReliabilityTelemetry(receipt, { dedupeKey } = {}) {
|
|
786
|
+
const unavailable = receipt.context?.validation === "orbstack_unavailable";
|
|
787
|
+
const ghostInventory = (receipt.inventory_errors?.length ?? 0) > 0;
|
|
788
|
+
// A successful target check is not a capacity measurement. If discovery
|
|
789
|
+
// fails before evaluatePressure produces a decision, suppress the generic
|
|
790
|
+
// observation so the evidence view cannot count a zero-filled false pass.
|
|
791
|
+
// Classified daemon and persistent-inventory faults remain reportable.
|
|
792
|
+
if (receipt.command === "preflight" && !receipt.pressure && !unavailable && !ghostInventory) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const eventKind = unavailable
|
|
796
|
+
? "orbstack_unavailable"
|
|
797
|
+
: ghostInventory
|
|
798
|
+
? "ghost_inventory"
|
|
799
|
+
: receipt.command === "preflight"
|
|
800
|
+
? "capacity_observed"
|
|
801
|
+
: receipt.apply ? "hygiene_apply" : "hygiene_dry_run";
|
|
802
|
+
const event = {
|
|
803
|
+
schema_version: SCHEMA_VERSION,
|
|
804
|
+
event_kind: eventKind,
|
|
805
|
+
dedupe_key: dedupeKey,
|
|
806
|
+
observed_at: receipt.timestamp,
|
|
807
|
+
outcome: receipt.outcome,
|
|
808
|
+
active_supabase_projects: receipt.active_projects?.length ?? 0,
|
|
809
|
+
bridge_networks: receipt.inventory?.user_bridge_networks ?? 0,
|
|
810
|
+
attached_endpoints: receipt.inventory?.attached_endpoints ?? 0,
|
|
811
|
+
unknown_resources: (receipt.inventory_errors?.length ?? 0)
|
|
812
|
+
+ (receipt.skipped?.filter((item) => item.reason === "missing_project_identity").length ?? 0),
|
|
813
|
+
candidate_count: receipt.candidates?.length ?? 0,
|
|
814
|
+
deleted_count: receipt.deleted?.length ?? 0,
|
|
815
|
+
skipped_count: receipt.skipped?.length ?? 0,
|
|
816
|
+
reclaimed_bytes: receipt.reclaimed_space_bytes ?? 0,
|
|
817
|
+
current_pressure_units: receipt.pressure?.current_pressure_units ?? null,
|
|
818
|
+
projected_pressure_units: receipt.pressure?.projected_pressure_units ?? null,
|
|
819
|
+
preflight_status: receipt.pressure
|
|
820
|
+
? ({ ok: "admissible", warn: "warn", fail: "refused" }[receipt.pressure.status] ?? "unknown")
|
|
821
|
+
: null,
|
|
822
|
+
...(unavailable || ghostInventory ? { fault_class: eventKind } : {}),
|
|
823
|
+
cli_version: VERSION,
|
|
824
|
+
// Docker exposes the OrbStack VM kernel build, not the OrbStack app
|
|
825
|
+
// release. Leaving the version absent is scientifically safer than
|
|
826
|
+
// stratifying observations on a mislabeled kernel dimension.
|
|
827
|
+
docker_version: receipt.context?.server?.version ?? null,
|
|
828
|
+
};
|
|
829
|
+
// The parser treats omitted and null optional values equivalently, while
|
|
830
|
+
// JSON Schema correctly declares only the concrete scalar types. Keep the
|
|
831
|
+
// official CLI payload schema-valid by omitting absence rather than sending
|
|
832
|
+
// explicit nulls through strict MCP gateways.
|
|
833
|
+
return Object.fromEntries(Object.entries(event).filter(([, value]) => value != null));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function reportReliability(result, options) {
|
|
837
|
+
const reportableOutage = result?.receipt?.context?.validation === "orbstack_unavailable";
|
|
838
|
+
if (!result?.receipt?.context?.server && !reportableOutage) return result;
|
|
839
|
+
const callTool = options.callTool ?? callToolJson;
|
|
840
|
+
const createDedupeKey = options.telemetryDedupeKey
|
|
841
|
+
?? (() => `cli:${result.receipt.command}:${crypto.randomUUID()}`);
|
|
842
|
+
const telemetryNow = options.telemetryNow ?? options.workflowOptions?.now ?? (() => new Date());
|
|
843
|
+
let observedAt;
|
|
844
|
+
try {
|
|
845
|
+
observedAt = telemetryNow().toISOString();
|
|
846
|
+
} catch {
|
|
847
|
+
result.receipt.telemetry = { status: "unavailable", code: "invalid_observation_time" };
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
const telemetry = buildReliabilityTelemetry(result.receipt, { dedupeKey: createDedupeKey() });
|
|
851
|
+
if (!telemetry) return result;
|
|
852
|
+
const event = {
|
|
853
|
+
...telemetry,
|
|
854
|
+
// The receipt timestamp marks command start. Measurement correlation must
|
|
855
|
+
// instead use the instant the complete inventory/safety decision exists;
|
|
856
|
+
// otherwise a bounded but slow Docker census can age a just-in-time
|
|
857
|
+
// preflight outside the stack-attempt correlation window.
|
|
858
|
+
observed_at: observedAt,
|
|
859
|
+
};
|
|
860
|
+
const controller = new AbortController();
|
|
861
|
+
const timeoutMs = options.telemetryTimeoutMs ?? TELEMETRY_TIMEOUT_MS;
|
|
862
|
+
// Race the ENTIRE tool call — credential resolution (resolveCallAuth, which can
|
|
863
|
+
// block on a macOS Keychain prompt) AND the HTTP request — against the timeout.
|
|
864
|
+
// runLocalExecPreflight routes through this wrapper and cmdUp awaits it, so
|
|
865
|
+
// aborting only the eventual fetch would let a blocked Keychain read hang stack
|
|
866
|
+
// admission despite the timer (BOT-1421 review). The abort is still fired.
|
|
867
|
+
let timer;
|
|
868
|
+
const TIMED_OUT = Symbol("reliability-telemetry-timeout");
|
|
869
|
+
const timeout = new Promise((resolve) => {
|
|
870
|
+
timer = setTimeout(() => {
|
|
871
|
+
controller.abort(new Error("telemetry timeout"));
|
|
872
|
+
resolve(TIMED_OUT);
|
|
873
|
+
}, timeoutMs);
|
|
874
|
+
});
|
|
875
|
+
try {
|
|
876
|
+
const reported = await Promise.race([
|
|
877
|
+
callTool("report_host_reliability", event, { signal: controller.signal }),
|
|
878
|
+
timeout,
|
|
879
|
+
]);
|
|
880
|
+
result.receipt.telemetry = reported === TIMED_OUT
|
|
881
|
+
? { status: "unavailable", code: "report_timeout" }
|
|
882
|
+
: reported?.ok && !reported.isError && reported.data?.success !== false
|
|
883
|
+
? { status: "reported", inserted: reported.data?.inserted !== false }
|
|
884
|
+
: reported?.ok
|
|
885
|
+
? { status: "rejected", code: String(reported.data?.code || "report_rejected").slice(0, 80) }
|
|
886
|
+
: { status: "unavailable", code: String(reported?.error || "report_unavailable").slice(0, 80) };
|
|
887
|
+
} catch (error) {
|
|
888
|
+
result.receipt.telemetry = {
|
|
889
|
+
status: "unavailable",
|
|
890
|
+
code: error?.name === "AbortError" ? "report_timeout" : "report_unavailable",
|
|
891
|
+
};
|
|
892
|
+
} finally {
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
}
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
|
|
754
898
|
function addSkipOnce(receipt, item) {
|
|
755
899
|
if (!receipt.skipped.some((existing) => existing.type === item.type && existing.id === item.id && existing.reason === item.reason)) {
|
|
756
900
|
receipt.skipped.push(item);
|
|
@@ -833,6 +977,16 @@ export function runDockerWorkflow(argv, {
|
|
|
833
977
|
} catch (error) {
|
|
834
978
|
receipt.outcome = "error";
|
|
835
979
|
receipt.errors = [error.message];
|
|
980
|
+
if (error.reliabilityEventKind === "orbstack_unavailable") {
|
|
981
|
+
receipt.context = {
|
|
982
|
+
requested: parsed.opts.context
|
|
983
|
+
? { type: "context", value: parsed.opts.context }
|
|
984
|
+
: { type: "endpoint", value: parsed.opts.endpoint },
|
|
985
|
+
resolved_endpoint: error.resolvedEndpoint || parsed.opts.endpoint || null,
|
|
986
|
+
validation: "orbstack_unavailable",
|
|
987
|
+
server: null,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
836
990
|
return { exitCode: EXIT.DOCKER, receipt, json: parsed.opts.json };
|
|
837
991
|
}
|
|
838
992
|
|
|
@@ -1025,6 +1179,7 @@ export function formatHumanReceipt(receipt) {
|
|
|
1025
1179
|
if (receipt.pressure) {
|
|
1026
1180
|
lines.push(`Pressure: ${receipt.pressure.projected_pressure_units} projected (${receipt.pressure.status}; warn ${receipt.pressure.warn_pressure}, fail ${receipt.pressure.fail_pressure})`);
|
|
1027
1181
|
}
|
|
1182
|
+
lines.push(`Reliability telemetry: ${receipt.telemetry?.status || "not_attempted"}`);
|
|
1028
1183
|
for (const warning of receipt.warnings) lines.push(`Warning: ${warning}`);
|
|
1029
1184
|
for (const error of receipt.errors) lines.push(`Error: ${error}`);
|
|
1030
1185
|
if (receipt.recommendation) lines.push(`Next: ${receipt.recommendation}`);
|
|
@@ -1080,7 +1235,7 @@ export async function runDockerCommand(argv, options = {}) {
|
|
|
1080
1235
|
const runWorkflow = options.runWorkflow ?? runDockerWorkflow;
|
|
1081
1236
|
const workflowOptions = options.workflowOptions ?? {};
|
|
1082
1237
|
if (parsed.errors.length > 0 || parsed.command !== "hygiene" || !parsed.opts.apply) {
|
|
1083
|
-
return runWorkflow(argv, workflowOptions);
|
|
1238
|
+
return reportReliability(await runWorkflow(argv, workflowOptions), options);
|
|
1084
1239
|
}
|
|
1085
1240
|
|
|
1086
1241
|
const resolveProjectId = options.resolveProjectId ?? resolveLocalProjectId;
|
|
@@ -1104,7 +1259,6 @@ export async function runDockerCommand(argv, options = {}) {
|
|
|
1104
1259
|
if (result?.receipt?.authority) {
|
|
1105
1260
|
result.receipt.authority.file_lock = { project_id: projectId, path: fileLockPath, held: true, released: false };
|
|
1106
1261
|
}
|
|
1107
|
-
return result;
|
|
1108
1262
|
} finally {
|
|
1109
1263
|
fileLock.release();
|
|
1110
1264
|
if (result?.receipt?.authority?.file_lock) {
|
|
@@ -1112,6 +1266,7 @@ export async function runDockerCommand(argv, options = {}) {
|
|
|
1112
1266
|
result.receipt.authority.file_lock.released = true;
|
|
1113
1267
|
}
|
|
1114
1268
|
}
|
|
1269
|
+
return reportReliability(result, options);
|
|
1115
1270
|
}
|
|
1116
1271
|
|
|
1117
1272
|
async function runDockerCommandWithBotBuddyLock(argv, {
|
package/src/stack.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { randomUUID } from "crypto";
|
|
|
30
30
|
import { callToolJson } from "./api.mjs";
|
|
31
31
|
import { SERVER_URL, getConfig } from "./config.mjs";
|
|
32
32
|
import { resolveOwnerToken, resolveAgentKey } from "./cli-credentials.mjs";
|
|
33
|
-
import { runDockerWorkflow } from "./docker-hygiene.mjs";
|
|
33
|
+
import { runDockerCommand, runDockerWorkflow } from "./docker-hygiene.mjs";
|
|
34
34
|
import { bold, dim, yellow } from "./utils.mjs";
|
|
35
35
|
|
|
36
36
|
export const STACK_SCHEMA_VERSION = 1;
|
|
@@ -497,7 +497,15 @@ const nonQueued = (s) => s != null && s !== "queued";
|
|
|
497
497
|
const isReaped = (s) => s === "reaping" || s === "reaped";
|
|
498
498
|
|
|
499
499
|
/** Read-only pressure gate used before local managed-stack provisioning. */
|
|
500
|
-
export function runLocalExecPreflight(opts,
|
|
500
|
+
export async function runLocalExecPreflight(opts, runCommand = runDockerCommand) {
|
|
501
|
+
const selector = opts.dockerContext
|
|
502
|
+
? ["--context", opts.dockerContext]
|
|
503
|
+
: ["--endpoint", opts.dockerEndpoint];
|
|
504
|
+
return runCommand(["preflight", ...selector, "--json"]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Read-only daemon identity check for teardown; never report provision telemetry. */
|
|
508
|
+
export async function runLocalExecTargetCheck(opts, runWorkflow = runDockerWorkflow) {
|
|
501
509
|
const selector = opts.dockerContext
|
|
502
510
|
? ["--context", opts.dockerContext]
|
|
503
511
|
: ["--endpoint", opts.dockerEndpoint];
|
|
@@ -670,7 +678,7 @@ export async function cmdUp(opts, {
|
|
|
670
678
|
let localPreflight = null;
|
|
671
679
|
let localDockerTarget = null;
|
|
672
680
|
if (opts.localExec) {
|
|
673
|
-
const checked = runPreflight(opts);
|
|
681
|
+
const checked = await runPreflight(opts);
|
|
674
682
|
localPreflight = compactPreflight(checked.receipt);
|
|
675
683
|
localDockerTarget = dockerTargetFromPreflight(checked.receipt);
|
|
676
684
|
if (checked.exitCode !== 0 || !localDockerTarget) {
|
|
@@ -725,7 +733,7 @@ export async function cmdUp(opts, {
|
|
|
725
733
|
// Capacity may have changed while this command was queued. Re-run the
|
|
726
734
|
// non-mutating gate immediately before `supabase start`; on refusal,
|
|
727
735
|
// release the minted lease and never invoke the local provisioner.
|
|
728
|
-
const checked = runPreflight(opts);
|
|
736
|
+
const checked = await runPreflight(opts);
|
|
729
737
|
localPreflight = compactPreflight(checked.receipt);
|
|
730
738
|
localDockerTarget = dockerTargetFromPreflight(checked.receipt);
|
|
731
739
|
if (checked.exitCode !== 0 || !localDockerTarget) {
|
|
@@ -740,10 +748,41 @@ export async function cmdUp(opts, {
|
|
|
740
748
|
lease_cancellation: leaseCancellation,
|
|
741
749
|
}), opts, EXIT.LEASE_FAILED);
|
|
742
750
|
}
|
|
751
|
+
// Reserve the queued provision job for THIS agent BEFORE `supabase start`.
|
|
752
|
+
// Otherwise a Helper can claim it while the local provisioner runs, winning
|
|
753
|
+
// the later activation race and leaving an untracked local stack (two
|
|
754
|
+
// provisioners contending for one slot). If the reservation is LOST, nothing
|
|
755
|
+
// local has started yet, so it is safe to atomically release the minted lease
|
|
756
|
+
// and free the slot (BOT-1421 review).
|
|
757
|
+
// Persist the validated Docker target with the reservation so that if the
|
|
758
|
+
// provisioner partially starts a stack and then fails, the fenced lease can
|
|
759
|
+
// still be torn down by `stack done --local-exec` (BOT-1421 review).
|
|
760
|
+
const reserved = await callTool("reserve_stack_lease", {
|
|
761
|
+
lease_id: leaseId,
|
|
762
|
+
connection: connectionWithDockerTarget({}, localDockerTarget),
|
|
763
|
+
});
|
|
764
|
+
if (!reserved.ok || !reserved.data?.success) {
|
|
765
|
+
const cancelled = await callTool("cancel_unclaimed_stack_lease", { lease_id: leaseId });
|
|
766
|
+
const leaseCancellation = cancelled.ok && cancelled.data?.success
|
|
767
|
+
? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
|
|
768
|
+
: { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
|
|
769
|
+
return emitResult(buildReceipt({
|
|
770
|
+
command: "up", outcome: "error", lease_id: leaseId, state, slot,
|
|
771
|
+
error: reserved.data?.code || reserved.error || "could not reserve the provision job for local execution",
|
|
772
|
+
lease_cancellation: leaseCancellation,
|
|
773
|
+
}), opts, EXIT.LEASE_FAILED);
|
|
774
|
+
}
|
|
743
775
|
let conn;
|
|
744
776
|
try {
|
|
745
777
|
conn = connectionWithDockerTarget(localProvisionFn(opts, localDockerTarget), localDockerTarget);
|
|
746
778
|
} catch (e) {
|
|
779
|
+
// Once the local provisioner has run, `supabase start` may have created
|
|
780
|
+
// (or fully started) containers even on a nonzero exit or a status-parse
|
|
781
|
+
// failure. Cancelling here would reap the lease and free the slot while
|
|
782
|
+
// that stack is still up — the exact orphaned/contended-slot hazard this
|
|
783
|
+
// change closes. Leave the lease FENCED (provisioning, reserved to us) so
|
|
784
|
+
// `botbuddy stack done` / the reaper tears the stack down first (BOT-1421
|
|
785
|
+
// review); do NOT cancel.
|
|
747
786
|
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
|
|
748
787
|
}
|
|
749
788
|
const act = await callTool("activate_stack_lease", { lease_id: leaseId, connection: conn });
|
|
@@ -795,7 +834,7 @@ async function cmdTouch(leaseId, opts) {
|
|
|
795
834
|
|
|
796
835
|
export async function cmdDone(leaseId, opts, {
|
|
797
836
|
callTool = callToolJson,
|
|
798
|
-
runPreflight =
|
|
837
|
+
runPreflight = runLocalExecTargetCheck,
|
|
799
838
|
proveLegacyTarget = proveLegacyLocalExecTarget,
|
|
800
839
|
localTeardownFn = localTeardown,
|
|
801
840
|
emitResult = emit,
|
|
@@ -811,7 +850,7 @@ export async function cmdDone(leaseId, opts, {
|
|
|
811
850
|
}
|
|
812
851
|
const expected = current.data.connection?.botbuddy_docker_target;
|
|
813
852
|
let checked;
|
|
814
|
-
try { checked = runPreflight(opts); } catch (error) {
|
|
853
|
+
try { checked = await runPreflight(opts); } catch (error) {
|
|
815
854
|
return emitResult(buildReceipt({ command: "done", outcome: "refused", lease_id: leaseId,
|
|
816
855
|
error: `could not validate teardown Docker target: ${error.message}` }), opts, EXIT.LEASE_FAILED);
|
|
817
856
|
}
|