@botbuddy/cli 1.64.16 → 1.64.18
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/commands.mjs +1 -1
- package/src/stack-run-outcome.mjs +17 -1
- package/src/stack.mjs +6 -3
- package/src/staging-pool/cli.mjs +23 -2
- package/src/staging-pool/help.mjs +4 -2
- package/src/update-check.mjs +121 -7
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.18",
|
|
3
|
+
"source_commit": "ce75c0db8999addc150c16458ebeace234a11ac7",
|
|
4
|
+
"source_sha256": "524adf0ce3779f1b458c5aaea9e6af52269423cecb0abec9a4fbf73b7da0f4ad",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "4f30f4a1f5be06733dc65e8930a13f66b5cd766cb7808b34a60ae63f9b1d8f25"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.18",
|
|
3
|
+
"source_commit": "ce75c0db8999addc150c16458ebeace234a11ac7",
|
|
4
|
+
"source_sha256": "524adf0ce3779f1b458c5aaea9e6af52269423cecb0abec9a4fbf73b7da0f4ad",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "5b07c264400be4a1790ddb3f8a0b0880465deff8eaccbb0cf18a7e734385326d"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.64.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.64.18",
|
|
4
|
+
"source_commit": "ce75c0db8999addc150c16458ebeace234a11ac7",
|
|
5
|
+
"source_sha256": "524adf0ce3779f1b458c5aaea9e6af52269423cecb0abec9a4fbf73b7da0f4ad",
|
|
6
6
|
"targets": {
|
|
7
7
|
"darwin-arm64": {
|
|
8
8
|
"goos": "darwin",
|
|
9
9
|
"goarch": "arm64",
|
|
10
10
|
"binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "4f30f4a1f5be06733dc65e8930a13f66b5cd766cb7808b34a60ae63f9b1d8f25",
|
|
12
12
|
"mode": "0755"
|
|
13
13
|
},
|
|
14
14
|
"linux-x64": {
|
|
15
15
|
"goos": "linux",
|
|
16
16
|
"goarch": "amd64",
|
|
17
17
|
"binary": "daemon/bin/linux-x64/botbuddy-daemon",
|
|
18
|
-
"sha256": "
|
|
18
|
+
"sha256": "5b07c264400be4a1790ddb3f8a0b0880465deff8eaccbb0cf18a7e734385326d",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -147,7 +147,7 @@ export async function run(argv, {
|
|
|
147
147
|
case "wait": return runWait(args);
|
|
148
148
|
case "pw": return runPw(args);
|
|
149
149
|
// BOT-2084: the human-installed staging session pool broker.
|
|
150
|
-
case "staging-pool": return (await import("./staging-pool/cli.mjs")).runStagingPool(args);
|
|
150
|
+
case "staging-pool": return (await import("./staging-pool/cli.mjs")).runStagingPool(args, { cliVersion: VERSION });
|
|
151
151
|
case "mcp": return cmdMcp(args);
|
|
152
152
|
case "resources": return callTool("list_resources");
|
|
153
153
|
case "agents": return callTool("list_agents");
|
|
@@ -185,6 +185,9 @@ export function buildStackRunOutcomePayload({ fields = {}, exitCode, runId = ran
|
|
|
185
185
|
const signal = pick(fields.stop_signal?.signal, SIGNAL_RE);
|
|
186
186
|
// BOT-2880: sent only when present, because an older server refuses an unknown key.
|
|
187
187
|
const callbackAuthorizationReason = pick(fields.callback_authorization_reason, AUTHORIZATION_REASON_RE);
|
|
188
|
+
const failureMechanism = code === "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE" &&
|
|
189
|
+
["origin_config_missing", "origin_digest_missing"].includes(fields.failure_mechanism)
|
|
190
|
+
? fields.failure_mechanism : null;
|
|
188
191
|
return {
|
|
189
192
|
run_id: runId,
|
|
190
193
|
lease_id: validStackLeaseId(fields.lease_id),
|
|
@@ -208,6 +211,7 @@ export function buildStackRunOutcomePayload({ fields = {}, exitCode, runId = ran
|
|
|
208
211
|
signal_parent: pick(fields.stop_signal.parent, TOKEN_RE),
|
|
209
212
|
} : {}),
|
|
210
213
|
...(callbackAuthorizationReason ? { callback_authorization_reason: callbackAuthorizationReason } : {}),
|
|
214
|
+
...(failureMechanism ? { failure_mechanism: failureMechanism } : {}),
|
|
211
215
|
};
|
|
212
216
|
}
|
|
213
217
|
|
|
@@ -236,7 +240,19 @@ export async function reportStackRunOutcome({ fields, exitCode, runId, opts, env
|
|
|
236
240
|
timer = setTimeout(() => { ac.abort(); resolve(TIMED_OUT); }, timeoutMs);
|
|
237
241
|
});
|
|
238
242
|
try {
|
|
239
|
-
const call = Promise.resolve().then(() =>
|
|
243
|
+
const call = Promise.resolve().then(async () => {
|
|
244
|
+
const first = await channel.callTool(STACK_RUN_OUTCOME_TOOL, payload, { auth: channel.auth, signal: ac.signal });
|
|
245
|
+
// Older servers reject unknown keys before writing. Retry only that exact,
|
|
246
|
+
// explicit rejection with the same run id; never retry an uncertain send.
|
|
247
|
+
if (!ac.signal.aborted && payload.failure_mechanism && first?.ok && first?.isError &&
|
|
248
|
+
first?.data?.code === "INVALID_STACK_RUN_OUTCOME" &&
|
|
249
|
+
typeof first?.data?.message === "string" &&
|
|
250
|
+
first.data.message.startsWith("unknown key 'failure_mechanism'")) {
|
|
251
|
+
const { failure_mechanism: _omitted, ...legacyPayload } = payload;
|
|
252
|
+
return await channel.callTool(STACK_RUN_OUTCOME_TOOL, legacyPayload, { auth: channel.auth, signal: ac.signal });
|
|
253
|
+
}
|
|
254
|
+
return first;
|
|
255
|
+
});
|
|
240
256
|
const result = await Promise.race([call.catch(() => null), timeout]);
|
|
241
257
|
if (result === TIMED_OUT) return { sent: false, reason: "timeout", payload };
|
|
242
258
|
const capture = result?.data?.feedback_capture;
|
package/src/stack.mjs
CHANGED
|
@@ -4250,7 +4250,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4250
4250
|
let hasDeclaration = false;
|
|
4251
4251
|
try { hasDeclaration = existsSync(declaration) && lstatSync(declaration).isFile(); } catch { hasDeclaration = false; }
|
|
4252
4252
|
if (remoteVariant && !originConfigPath) {
|
|
4253
|
-
const error = new Error("the requester-origin config variant has no materialized config to replace"); error.code = "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE"; throw error;
|
|
4253
|
+
const error = new Error("the requester-origin config variant has no materialized config to replace"); error.code = "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE"; error.failureMechanism = "origin_config_missing"; throw error;
|
|
4254
4254
|
}
|
|
4255
4255
|
return buildBundle(stackDirFor(execution), {
|
|
4256
4256
|
...(remoteVariant ? { readFile: (abs, ...rest) => abs === originConfigPath ? Buffer.from(execution.originConfigText, "utf8") : readFileSync(abs, ...rest) } : {}),
|
|
@@ -4288,7 +4288,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4288
4288
|
// hide it — and it happens before any lease exists.
|
|
4289
4289
|
if (executionRequest === "remote" || bundleInclude.length) {
|
|
4290
4290
|
const fixable = typeof error?.code === "string" && error.code.startsWith("BUNDLE_");
|
|
4291
|
-
return { exitCode: fixable ? EXIT.INVALID : EXIT.INTERNAL, outcome: "error", code: error?.code ?? "BUNDLE_FAILED", error: message
|
|
4291
|
+
return { exitCode: fixable ? EXIT.INVALID : EXIT.INTERNAL, outcome: "error", code: error?.code ?? "BUNDLE_FAILED", error: message,
|
|
4292
|
+
...(error?.code === "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE" && error?.failureMechanism === "origin_config_missing" ? { failureMechanism: error.failureMechanism } : {}) };
|
|
4292
4293
|
}
|
|
4293
4294
|
// Codex #1077 P2: without a digest, `auto` could still pick a remote host and be
|
|
4294
4295
|
// refused BUNDLE_REQUIRED instead of queueing here, so ask for this machine.
|
|
@@ -4346,7 +4347,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4346
4347
|
let renewIdentifiedRequest = null;
|
|
4347
4348
|
if (execution.admissionRequestId) {
|
|
4348
4349
|
if (!stackDigest || !bundle?.sha256) {
|
|
4349
|
-
return { exitCode: EXIT.INTERNAL, outcome: "error", code: "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE", error: "the requester-origin bundle digest could not be computed; no lease was requested" };
|
|
4350
|
+
return { exitCode: EXIT.INTERNAL, outcome: "error", code: "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE", failureMechanism: "origin_digest_missing", error: "the requester-origin bundle digest could not be computed; no lease was requested" };
|
|
4350
4351
|
}
|
|
4351
4352
|
leaseArgs.request_id = execution.admissionRequestId;
|
|
4352
4353
|
// Durable request-to-context association before any request can commit (recovery key).
|
|
@@ -5323,6 +5324,8 @@ export function runReceiptFields(result) {
|
|
|
5323
5324
|
...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
|
|
5324
5325
|
// BOT-2880: which check refused a CALLBACK_AUTHORIZATION_LOST (a bounded token, never a value).
|
|
5325
5326
|
...(result.callbackAuthorizationReason ? { callback_authorization_reason: result.callbackAuthorizationReason } : {}),
|
|
5327
|
+
...(result.code === "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE" && ["origin_config_missing", "origin_digest_missing"].includes(result.failureMechanism)
|
|
5328
|
+
? { failure_mechanism: result.failureMechanism } : {}),
|
|
5326
5329
|
// BOT-2957: the control-plane gaps this run's callback route rode through (never on a run that had none).
|
|
5327
5330
|
...(result.callbackOutage ? { callback_outage: result.callbackOutage } : {}),
|
|
5328
5331
|
// BOT-2780: the reserved requester origin generated auth links name (never a credential).
|
package/src/staging-pool/cli.mjs
CHANGED
|
@@ -398,6 +398,26 @@ export function upgradeNotice(ctx) {
|
|
|
398
398
|
return { upgrade: { reason: "installed_before_registry", message: "This pool was installed from a repository checkout; re-install it from the BotBuddy control plane (keeps counts and interval).", next: installCommandFor(ctx.tenant) } };
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// BOT-3016: the launchd job runs a pinned runtime copy, not this CLI. Status
|
|
402
|
+
// names both versions and, when they differ on a control-plane pool, the
|
|
403
|
+
// agent-safe refresh (bb update runs it too). cliVersion is null inside the
|
|
404
|
+
// bundled job copy, which has no package.json to read.
|
|
405
|
+
export function runtimeNotice(ctx, cliVersion) {
|
|
406
|
+
if (!ctx?.installed || ctx.installed.error) return {};
|
|
407
|
+
const runtimeVersion = ctx.installed.runtime?.version ?? null;
|
|
408
|
+
const versions = { runtimeVersion, ...(cliVersion ? { cliVersion } : {}) };
|
|
409
|
+
if (!cliVersion || runtimeVersion === cliVersion || ctx.installed.source !== "control-plane" || !ctx.tenant || !ctx.poolId) return versions;
|
|
410
|
+
const runs = runtimeVersion ? `runs v${runtimeVersion}` : "runs an untracked runtime copy";
|
|
411
|
+
return {
|
|
412
|
+
...versions,
|
|
413
|
+
runtimeBehind: {
|
|
414
|
+
reason: "runtime_behind",
|
|
415
|
+
message: `The pool job ${runs}, but this CLI is v${cliVersion}. bb update refreshes it; so does the next command.`,
|
|
416
|
+
next: `bb staging-pool refresh-runtime --tenant ${shellWord(String(ctx.tenant))} --pool ${shellWord(String(ctx.poolId))}`,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
401
421
|
export async function runStagingPool(argv, deps = {}) {
|
|
402
422
|
const {
|
|
403
423
|
env = process.env,
|
|
@@ -415,6 +435,7 @@ export async function runStagingPool(argv, deps = {}) {
|
|
|
415
435
|
nodeBin = process.execPath,
|
|
416
436
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
417
437
|
readSecretFile = readFileSync,
|
|
438
|
+
cliVersion = null,
|
|
418
439
|
} = deps;
|
|
419
440
|
|
|
420
441
|
const redactor = createRedactor();
|
|
@@ -599,10 +620,10 @@ export async function runStagingPool(argv, deps = {}) {
|
|
|
599
620
|
const entry = options.persona ? findPersona(ctx.poolBinding, options.persona) : null;
|
|
600
621
|
if (options.persona && !entry) assertPersonaAllowed(target, options.persona);
|
|
601
622
|
const health = await assessHealth({ ctx, pool, now, exec, uid, requestedPersona: entry?.email });
|
|
602
|
-
emit({ command: "status", check: true, ...health, ...upgradeNotice(ctx) });
|
|
623
|
+
emit({ command: "status", check: true, ...health, ...upgradeNotice(ctx), ...runtimeNotice(ctx, cliVersion) });
|
|
603
624
|
return health.ok ? 0 : 5;
|
|
604
625
|
}
|
|
605
|
-
emit({ ok: true, command: "status", ...(await pool.status()), ...upgradeNotice(ctx) });
|
|
626
|
+
emit({ ok: true, command: "status", ...(await pool.status()), ...upgradeNotice(ctx), ...runtimeNotice(ctx, cliVersion) });
|
|
606
627
|
return 0;
|
|
607
628
|
},
|
|
608
629
|
|
|
@@ -96,14 +96,16 @@ ANYONE (agents too) — existing runtime maintenance, never sign-in
|
|
|
96
96
|
and this published CLI package. Preserves credentials, sessions, counts,
|
|
97
97
|
interval and plist. The existing launchd job refills on its next tick.
|
|
98
98
|
No caller path or runtime content is accepted; no sign-in is performed.
|
|
99
|
+
bb update runs it for every installed pool after a confirmed upgrade.
|
|
99
100
|
|
|
100
101
|
HUMAN ONLY (Jono, from Terminal.app or iTerm) — refused inside a Claude or Codex session
|
|
101
102
|
These sign in with the persona password or change the standing sessions.
|
|
102
103
|
install --tenant <slug> [--pool <pool_id>] --botbuddy-bin <absolute path> [--interval <seconds>]
|
|
103
104
|
Reads the tenant's registered pool from the BotBuddy control plane, copies
|
|
104
105
|
the job to the pool home, writes its config and loads the launchd job. Run
|
|
105
|
-
it from any directory.
|
|
106
|
-
|
|
106
|
+
it from any directory. bb update refreshes the job's runtime, so re-run
|
|
107
|
+
install only when status or bb update names it; it keeps the counts and
|
|
108
|
+
interval. --pool is needed only when the tenant has several.
|
|
107
109
|
e.g. bb staging-pool install --tenant botbuddy --botbuddy-bin "$(command -v botbuddy)"
|
|
108
110
|
tick [--pool <pool_id>] [--home <pool home>]
|
|
109
111
|
One refresh and refill pass now (what the job runs).
|
package/src/update-check.mjs
CHANGED
|
@@ -11,14 +11,23 @@
|
|
|
11
11
|
// `cmdUpdate` deliberately does NOT update in-process (BOT-1499: a self-update
|
|
12
12
|
// that replaces its own running files can kill itself mid-install). It hands
|
|
13
13
|
// off to a detached, unref'd `npm install -g` and exits 0.
|
|
14
|
+
//
|
|
15
|
+
// BOT-3016: an installed staging session pool is a standing launchd job that
|
|
16
|
+
// runs a pinned copy of the CLI, so npm alone leaves it on old code. After a
|
|
17
|
+
// confirmed update, `cmdUpdate` refreshes each control-plane pool with the NEW
|
|
18
|
+
// package's agent-safe `staging-pool refresh-runtime` (BOT-2673), which derives
|
|
19
|
+
// its runtime from its own package path — hence a child of the new binary, not
|
|
20
|
+
// this process. A refusal is reported once and never fails the update.
|
|
14
21
|
|
|
15
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
22
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
16
23
|
import { readFileSync } from "node:fs";
|
|
17
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
24
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
18
25
|
import { homedir } from "node:os";
|
|
19
26
|
import { dirname, join } from "node:path";
|
|
27
|
+
import { promisify } from "node:util";
|
|
20
28
|
|
|
21
29
|
import { VERSION } from "./version.mjs";
|
|
30
|
+
import { installLine } from "./staging-pool/errors.mjs";
|
|
22
31
|
|
|
23
32
|
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
24
33
|
export const NPM_LATEST_URL = "https://registry.npmjs.org/@botbuddy/cli/latest";
|
|
@@ -118,11 +127,16 @@ export async function maybeWarnStale({
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
export function
|
|
130
|
+
export function readNpmGlobalRoot({ npmBin = "npm", shellOpt = {}, probeImpl = spawnSync } = {}) {
|
|
122
131
|
const root = probeImpl(npmBin, ["root", "-g"], { encoding: "utf8", ...shellOpt });
|
|
123
132
|
if (root?.error || root?.status !== 0) throw new Error(`npm global root lookup failed for ${npmBin}`);
|
|
124
133
|
const directory = String(root?.stdout ?? "").trim();
|
|
125
134
|
if (!directory) throw new Error("npm global root lookup returned no directory");
|
|
135
|
+
return directory;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl = spawnSync, readPackage = readFileSync } = {}) {
|
|
139
|
+
const directory = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
|
|
126
140
|
const packageJson = JSON.parse(readPackage(join(directory, "@botbuddy", "cli", "package.json"), "utf8"));
|
|
127
141
|
if (typeof packageJson?.version !== "string" || !parseSemver(packageJson.version)) throw new Error("installed @botbuddy/cli package has no valid version");
|
|
128
142
|
return packageJson.version;
|
|
@@ -130,7 +144,7 @@ export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl
|
|
|
130
144
|
|
|
131
145
|
// `botbuddy update` waits for npm because the terminal result, rather than the
|
|
132
146
|
// child-process launch, is the only trustworthy confirmation an update worked.
|
|
133
|
-
export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, readInstalledVersion: installedVersion = readInstalledVersion, 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, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
|
|
134
148
|
// BOT-1566 (Codex P1/P2): on Windows npm is the `npm.cmd` shim, and Node
|
|
135
149
|
// cannot spawn a .cmd file shell-free (docs) — run it through a shell on
|
|
136
150
|
// win32 (args are all static constants, so no injection surface) and select
|
|
@@ -179,14 +193,114 @@ export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = sp
|
|
|
179
193
|
resolve(1);
|
|
180
194
|
return;
|
|
181
195
|
}
|
|
196
|
+
let installed;
|
|
182
197
|
try {
|
|
183
|
-
|
|
184
|
-
log(`✓ Updated to v${installed}`);
|
|
185
|
-
resolve(0);
|
|
198
|
+
installed = installedVersion({ npmBin, shellOpt });
|
|
186
199
|
} catch (error) {
|
|
187
200
|
try { stderr.write(`botbuddy update: npm completed but could not verify the installed version (${error?.message ?? error})\n`); } catch { /* ignore */ }
|
|
188
201
|
resolve(1);
|
|
202
|
+
return;
|
|
189
203
|
}
|
|
204
|
+
log(`✓ Updated to v${installed}`);
|
|
205
|
+
// BOT-3016: the update succeeded; a pool refresh problem is reported, never an exit code.
|
|
206
|
+
Promise.resolve()
|
|
207
|
+
.then(() => refreshPools({ installedVersion: installed, npmBin, shellOpt, probeImpl, log, stderr }))
|
|
208
|
+
.catch((error) => {
|
|
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
|
+
})
|
|
211
|
+
.finally(() => resolve(0));
|
|
190
212
|
});
|
|
191
213
|
});
|
|
192
214
|
}
|
|
215
|
+
|
|
216
|
+
// ─── BOT-3016: refresh installed staging-pool runtimes after an update ───────
|
|
217
|
+
|
|
218
|
+
export const REFRESH_TIMEOUT_MS = 120_000;
|
|
219
|
+
const HUMAN_NEXT = "human, from Terminal.app or iTerm";
|
|
220
|
+
|
|
221
|
+
export function stagingPoolsRoot(home = homedir()) {
|
|
222
|
+
return join(home, ".botbuddy", "staging-session-pool");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function lastJsonLine(text) {
|
|
226
|
+
const lines = String(text ?? "").split("\n").filter((line) => line.startsWith("{"));
|
|
227
|
+
for (const line of lines.reverse()) {
|
|
228
|
+
try { return JSON.parse(line); } catch { /* keep looking */ }
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function refreshOne({ dirName, installedVersion, cliEntry, nodeBin, home, execFileImpl }) {
|
|
234
|
+
let config;
|
|
235
|
+
try {
|
|
236
|
+
config = JSON.parse(await readFile(join(stagingPoolsRoot(home), dirName, "config.json"), "utf8"));
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error?.code === "ENOENT") return null; // not a pool home
|
|
239
|
+
return { poolId: dirName, status: "unreadable", code: "config_unreadable", message: `Its config.json could not be read (${error?.message ?? error}).`, next: "bb staging-pool status --check --pool " + dirName, actor: "agent or human" };
|
|
240
|
+
}
|
|
241
|
+
const poolId = typeof config?.poolId === "string" ? config.poolId : dirName;
|
|
242
|
+
const tenant = config?.tenant ?? config?.binding?.tenant ?? null;
|
|
243
|
+
if (config?.source !== "control-plane" || !tenant) {
|
|
244
|
+
return { poolId, status: "legacy", code: "installed_before_registry", message: "It was installed before the control-plane registry, so only a human re-install can update it.", next: tenant ? installLine(tenant, poolId) : `bb staging-pool status --pool ${poolId}`, actor: HUMAN_NEXT };
|
|
245
|
+
}
|
|
246
|
+
const runtimeVersion = config?.runtime?.version ?? null;
|
|
247
|
+
if (runtimeVersion === installedVersion) return { poolId, status: "current", runtimeVersion };
|
|
248
|
+
const retry = `bb staging-pool refresh-runtime --tenant ${tenant} --pool ${poolId}`;
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await execFileImpl(nodeBin, [cliEntry, "staging-pool", "refresh-runtime", "--tenant", tenant, "--pool", poolId], { timeout: REFRESH_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
251
|
+
const receipt = lastJsonLine(stdout);
|
|
252
|
+
if (receipt?.ok === true) return { poolId, status: "refreshed", runtimeVersion: receipt.runtimeVersion ?? installedVersion };
|
|
253
|
+
return { poolId, status: "refused", code: receipt?.code ?? "refresh_failed", message: receipt?.message ?? "The refresh printed no success receipt.", next: retry, actor: "agent or human" };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// A refusal is one JSON line on stderr (then text guidance); stdout stays empty.
|
|
256
|
+
const receipt = lastJsonLine(error?.stderr) ?? lastJsonLine(error?.stdout);
|
|
257
|
+
if (receipt?.code) {
|
|
258
|
+
const human = /^human/i.test(String(receipt.actor ?? "")) || /install/.test(String(receipt.next ?? ""));
|
|
259
|
+
const actor = typeof receipt.actor === "string" && receipt.actor ? (human ? `${receipt.actor}, from Terminal.app or iTerm` : receipt.actor) : (human ? HUMAN_NEXT : "agent or human");
|
|
260
|
+
return { poolId, status: "refused", code: receipt.code, message: [receipt.message, receipt.detail].filter(Boolean).join(" "), next: receipt.next ?? retry, actor };
|
|
261
|
+
}
|
|
262
|
+
return { poolId, status: "refused", code: "refresh_failed", message: `The refresh did not finish (${error?.message ?? error}).`, next: retry, actor: "agent or human" };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Refresh every control-plane staging pool installed under ~/.botbuddy whose
|
|
268
|
+
* recorded runtime differs from `installedVersion`, by running the new
|
|
269
|
+
* package's `staging-pool refresh-runtime`. Reports one line per pool and
|
|
270
|
+
* never throws for a pool's refusal. Returns [{ poolId, status, ... }].
|
|
271
|
+
*/
|
|
272
|
+
export async function refreshInstalledPools({
|
|
273
|
+
installedVersion,
|
|
274
|
+
cliEntry,
|
|
275
|
+
nodeBin = process.execPath,
|
|
276
|
+
home = homedir(),
|
|
277
|
+
execFileImpl = promisify(execFile),
|
|
278
|
+
log = (line) => console.log(line),
|
|
279
|
+
stderr = process.stderr,
|
|
280
|
+
} = {}) {
|
|
281
|
+
let entries;
|
|
282
|
+
try {
|
|
283
|
+
entries = await readdir(stagingPoolsRoot(home), { withFileTypes: true });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error?.code === "ENOENT") return [];
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
const results = [];
|
|
289
|
+
for (const entry of entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
290
|
+
const result = await refreshOne({ dirName: entry.name, installedVersion, cliEntry, nodeBin, home, execFileImpl });
|
|
291
|
+
if (!result) continue;
|
|
292
|
+
results.push(result);
|
|
293
|
+
if (result.status === "refreshed") log(`✓ Staging pool ${result.poolId} now runs v${result.runtimeVersion}`);
|
|
294
|
+
else if (result.status === "current") log(`✓ Staging pool ${result.poolId} already runs v${result.runtimeVersion}`);
|
|
295
|
+
else {
|
|
296
|
+
try { stderr.write(`! Staging pool ${result.poolId} was not refreshed (${result.code}): ${result.message}\n Next (${result.actor}): ${result.next}\n`); } catch { /* ignore */ }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The default for `cmdUpdate`: locate the newly installed package and refresh with it.
|
|
303
|
+
export async function refreshPoolsFromNewPackage({ installedVersion, npmBin, shellOpt, probeImpl, log, stderr }) {
|
|
304
|
+
const root = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
|
|
305
|
+
return refreshInstalledPools({ installedVersion, cliEntry: join(root, "@botbuddy", "cli", "bin", "botbuddy.mjs"), log, stderr });
|
|
306
|
+
}
|