@botbuddy/cli 1.60.6 → 1.60.8
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/pw/args.mjs +6 -2
- package/src/pw/run.mjs +11 -7
- package/src/pw/staging-auth.mjs +38 -8
- package/src/staging-pool/binding.mjs +25 -40
- package/src/staging-pool/cli.mjs +246 -91
- package/src/staging-pool/config.mjs +5 -1
- package/src/staging-pool/errors.mjs +122 -47
- package/src/staging-pool/health.mjs +2 -2
- package/src/staging-pool/help.mjs +31 -22
- package/src/staging-pool/registry.mjs +79 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.8",
|
|
3
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
4
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "234246b7e1b8634420dbe2bceba35a4efa51a0ade206b09882f9c80a0aa66bfb"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.8",
|
|
3
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
4
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.60.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.60.8",
|
|
4
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
5
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
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": "234246b7e1b8634420dbe2bceba35a4efa51a0ade206b09882f9c80a0aa66bfb",
|
|
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": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/pw/args.mjs
CHANGED
|
@@ -67,16 +67,20 @@ export function planInvocation(argv, env = process.env) {
|
|
|
67
67
|
const authArgs = argv.slice(2);
|
|
68
68
|
const strayAuth = authArgs.find((value) => credentialFlagOf(value));
|
|
69
69
|
if (strayAuth) throw misplacedCredentialFlag(strayAuth);
|
|
70
|
-
const usage = "bb-pw: usage: bb pw --environment staging <lane> auth --persona <slug>";
|
|
70
|
+
const usage = "bb-pw: usage: bb pw --environment staging <lane> auth --persona <slug> [--pool <pool_id>]";
|
|
71
71
|
let persona = null;
|
|
72
|
+
let pool = null;
|
|
72
73
|
for (let i = 0; i < authArgs.length; i++) {
|
|
73
74
|
const token = String(authArgs[i]);
|
|
74
75
|
if (token === "--persona" && persona === null && authArgs[i + 1] !== undefined && !String(authArgs[i + 1]).startsWith("--")) { persona = String(authArgs[++i]); continue; }
|
|
75
76
|
if (token.startsWith("--persona=") && persona === null && token.length > "--persona=".length) { persona = token.slice("--persona=".length); continue; }
|
|
77
|
+
// BOT-2648: which installed pool, when several are installed.
|
|
78
|
+
if (token === "--pool" && pool === null && authArgs[i + 1] !== undefined && /^[a-z0-9][a-z0-9-]{0,63}$/.test(String(authArgs[i + 1]))) { pool = String(authArgs[++i]); continue; }
|
|
79
|
+
if (token.startsWith("--pool=") && pool === null && /^[a-z0-9][a-z0-9-]{0,63}$/.test(token.slice("--pool=".length))) { pool = token.slice("--pool=".length); continue; }
|
|
76
80
|
throw new Error(`${usage} (unexpected '${token}')`);
|
|
77
81
|
}
|
|
78
82
|
if (!persona) throw new Error(`${usage} (--persona is required)`);
|
|
79
|
-
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "auth", persona };
|
|
83
|
+
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "auth", persona, pool };
|
|
80
84
|
}
|
|
81
85
|
if (verb === "status") {
|
|
82
86
|
// `status` has no workload, so — like the global verbs — there is no `--`
|
package/src/pw/run.mjs
CHANGED
|
@@ -190,7 +190,10 @@ function consumeWrapperFlags(list, deps, start = 0) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// The non-secret values a staging-auth refusal names in its next command.
|
|
193
|
-
const authVars = (plan
|
|
193
|
+
const authVars = (plan, deps = {}) => {
|
|
194
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
195
|
+
return { lane: plan.lane, persona: plan.persona, cwd, ticketFlag: stagingAuth.ticketFlagFor(cwd) };
|
|
196
|
+
};
|
|
194
197
|
|
|
195
198
|
// `bb pw auth --help` / `bb pw [flags] <lane> auth --help`: auth's own help, before
|
|
196
199
|
// any credential, lock or daemon call.
|
|
@@ -229,8 +232,8 @@ async function runPwInner(argv, deps = {}) {
|
|
|
229
232
|
// local mode — before any credential, lock or issuer call.
|
|
230
233
|
if (plan.mode === "auth") {
|
|
231
234
|
const authEnvironment = resolveEnvironment({ flag: deps.environment ?? null, env });
|
|
232
|
-
if (authEnvironment.error || authEnvironment.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_staging_environment", 2, {}, authVars(plan));
|
|
233
|
-
if (env.BB_PW_NO_LOCK === "1") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_lane_lock", 3, {}, authVars(plan));
|
|
235
|
+
if (authEnvironment.error || authEnvironment.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_staging_environment", 2, {}, authVars(plan, deps));
|
|
236
|
+
if (env.BB_PW_NO_LOCK === "1") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_lane_lock", 3, {}, authVars(plan, deps));
|
|
234
237
|
}
|
|
235
238
|
const { spawnExec, socketRun } = deps.daemon ?? await import("./daemon.mjs");
|
|
236
239
|
if (plan.scope === "global") { if (plan.mode === "reap") { await (deps.reap ?? (await import("./reap.mjs")).reap)({ env, stdout }); return 0; } if (env.BB_PW_NO_LOCK !== "1") { stderr.write("bb-pw: close-all and kill-all require BB_PW_NO_LOCK=1 because they can affect lanes you do not own.\n"); return 3; } return spawnExec(plan, env); }
|
|
@@ -241,7 +244,8 @@ async function runPwInner(argv, deps = {}) {
|
|
|
241
244
|
try {
|
|
242
245
|
const credential = await (deps.resolveCredential ?? resolveAgentSessionCredential)({ argv, env, cwd: deps.cwd ?? process.cwd(), sessionId: deps.sessionId ?? env.BOTBUDDY_SESSION_ID ?? null, flagToken: null });
|
|
243
246
|
managedCredential = credential;
|
|
244
|
-
|
|
247
|
+
// BOT-2648 (Codex P2, #1323): the credential's tenant picks the installed staging pool.
|
|
248
|
+
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null, credentialTenant: credential.identity?.tenant ?? null };
|
|
245
249
|
} catch (error) {
|
|
246
250
|
stderr.write(selfHealErrorLine(error)); return 3;
|
|
247
251
|
}
|
|
@@ -278,7 +282,7 @@ async function runPwInner(argv, deps = {}) {
|
|
|
278
282
|
// A lane that is provably no longer ours (not a transient verify error):
|
|
279
283
|
// clear the session we installed and revoke its custody (AC-3).
|
|
280
284
|
if (auth.reason === "not_held" || auth.reason === "held_by_other") await stagingAuth.onLaneAuthorityLost(authCtx).catch(() => {});
|
|
281
|
-
if (plan.mode === "auth" && (auth.reason === "not_held" || auth.reason === "held_by_other")) return stagingAuth.refuse({ stdout, stderr }, "lane_not_claimed", 3, { detail: auth.message }, authVars(plan));
|
|
285
|
+
if (plan.mode === "auth" && (auth.reason === "not_held" || auth.reason === "held_by_other")) return stagingAuth.refuse({ stdout, stderr }, "lane_not_claimed", 3, { detail: auth.message }, authVars(plan, deps));
|
|
282
286
|
stderr.write(`${auth.message}\n`); return 3;
|
|
283
287
|
}
|
|
284
288
|
// Report the host the lock service actually stored (alias-collapsed), so the
|
|
@@ -305,10 +309,10 @@ async function runPwInner(argv, deps = {}) {
|
|
|
305
309
|
const me = await authCtx.resolveCallerId().catch(() => null);
|
|
306
310
|
if (me && me === auth.holder) { holderMatch = "self"; authCtx.callerIds = [...authCtx.callerIds, me]; }
|
|
307
311
|
}
|
|
308
|
-
if (holderMatch !== "self") return stagingAuth.refuse({ stdout, stderr }, "exact_lane_holder_required", 3, {}, authVars(plan));
|
|
312
|
+
if (holderMatch !== "self") return stagingAuth.refuse({ stdout, stderr }, "exact_lane_holder_required", 3, {}, authVars(plan, deps));
|
|
309
313
|
if (tracker) {
|
|
310
314
|
const browserSession = await tracker.status();
|
|
311
|
-
if (browserSession.active && browserSession.environment && browserSession.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "browser_session_not_staging", 2, {}, authVars(plan));
|
|
315
|
+
if (browserSession.active && browserSession.environment && browserSession.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "browser_session_not_staging", 2, {}, authVars(plan, deps));
|
|
312
316
|
}
|
|
313
317
|
return stagingAuth.runStagingAuth(authCtx);
|
|
314
318
|
}
|
package/src/pw/staging-auth.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// clear that fails blocks the lane until it is resolved.
|
|
16
16
|
// * The ledger (~/.botbuddy/pw-staging-auth/<host>/lane-<n>.json, 0600) holds
|
|
17
17
|
// non-secret install records only.
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, chmodSync } from "node:fs";
|
|
19
20
|
import { homedir } from "node:os";
|
|
20
21
|
import { dirname, join } from "node:path";
|
|
@@ -229,8 +230,24 @@ const GUIDED_FIELDS = ["message", "cause", "actor", "next", "doc", "help", "agen
|
|
|
229
230
|
* `extra` fields already rendered by the pool (an empty pool's health reason) win.
|
|
230
231
|
* `vars` are non-secret only: lane, persona alias, allowed aliases, checkout.
|
|
231
232
|
*/
|
|
233
|
+
/**
|
|
234
|
+
* ` --ticket BOT-123` from the checkout's branch name (`…/bot-123-…`), or "" so a
|
|
235
|
+
* lane-claim command never prints a <ticket> placeholder. Best effort, refusal-only.
|
|
236
|
+
*/
|
|
237
|
+
export function ticketFlagFor(cwd, { exec = execFileSync } = {}) {
|
|
238
|
+
try {
|
|
239
|
+
// symbolic-ref also names an unborn branch; a detached HEAD has no ticket.
|
|
240
|
+
const branch = String(exec("git", ["-C", cwd, "symbolic-ref", "--short", "-q", "HEAD"], { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] })).trim();
|
|
241
|
+
const match = /(?:^|[/_-])((?:bot|ent)-\d+)(?=$|[/_-])/i.exec(branch);
|
|
242
|
+
return match ? ` --ticket ${match[1].toUpperCase()}` : "";
|
|
243
|
+
} catch {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
232
248
|
export function refuse({ stdout, stderr }, code, exitCode = 3, extra = {}, vars = {}) {
|
|
233
|
-
|
|
249
|
+
// A real refusal: never a <placeholder> (AC-8 follow-up).
|
|
250
|
+
const g = guidance(isKnownCode(code) ? code : "pool_take_failed", { consequence: "bb pw auth has no pool to take a session from", ...vars }, { real: true });
|
|
234
251
|
const guided = { message: g.message, cause: g.cause, actor: g.actor_label, next: g.next, doc: g.doc, help: g.help, agentAction: g.agent_action, remedy: remedyLine(g) };
|
|
235
252
|
for (const field of GUIDED_FIELDS) if (extra[field] !== undefined) guided[field] = extra[field];
|
|
236
253
|
const text = {
|
|
@@ -256,15 +273,28 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
256
273
|
const poolModule = deps.stagingPoolModule ?? await loadPool();
|
|
257
274
|
const poolOptions = deps.stagingPool ?? {};
|
|
258
275
|
const cwd = deps.cwd ?? process.cwd();
|
|
259
|
-
const
|
|
260
|
-
|
|
276
|
+
const authLine = `bb pw --environment staging ${plan.lane} auth --persona ${plan.persona}`;
|
|
277
|
+
// The lane credential's tenant (resolved by run.mjs), else an explicit hint.
|
|
278
|
+
const tenantHint = deps.credentialTenant ?? deps.tenant ?? env.BOTBUDDY_TENANT ?? null;
|
|
279
|
+
const vars = { lane: plan.lane, persona: plan.persona, ticketFlag: ticketFlagFor(cwd), consequence: "bb pw auth has no pool to take a session from" };
|
|
280
|
+
// BOT-2648: the pool comes from the installed pool home (--pool, the only one,
|
|
281
|
+
// or the one for this tenant), never from the working directory.
|
|
282
|
+
let ctxPool;
|
|
261
283
|
try {
|
|
262
|
-
|
|
284
|
+
ctxPool = poolModule.resolveInstalledPool({ ...(poolOptions.home ? { home: poolOptions.home } : {}), poolId: plan.pool ?? null, tenantHint });
|
|
263
285
|
} catch (error) {
|
|
264
|
-
|
|
286
|
+
const poolIds = Array.isArray(error?.poolIds) ? error.poolIds : [];
|
|
287
|
+
const code = error?.code === "pool_ambiguous" ? "pool_ambiguous" : "pool_not_installed";
|
|
288
|
+
return refuse(io, code, 3, { detail: String(error?.message ?? error).slice(0, 300) }, {
|
|
289
|
+
...vars,
|
|
290
|
+
tenant: tenantHint ?? undefined,
|
|
291
|
+
poolIds,
|
|
292
|
+
poolId: plan.pool ?? undefined,
|
|
293
|
+
retryWithPool: poolIds.length ? `${authLine} --pool ${poolIds[0]}` : undefined,
|
|
294
|
+
});
|
|
265
295
|
}
|
|
266
|
-
|
|
267
|
-
Object.assign(vars, {
|
|
296
|
+
const found = { binding: ctxPool.poolBinding };
|
|
297
|
+
Object.assign(vars, { allowed: Object.keys(found.binding.personas ?? {}), tenant: ctxPool.tenant ?? undefined, poolId: ctxPool.poolId ?? undefined });
|
|
268
298
|
const { findPersona } = await loadBinding();
|
|
269
299
|
const persona = findPersona(found.binding, plan.persona);
|
|
270
300
|
if (!persona) return refuse(io, "persona_not_allowlisted", 2, {}, vars);
|
|
@@ -281,7 +311,7 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
281
311
|
dropGone(ctx, generation);
|
|
282
312
|
let claim;
|
|
283
313
|
try {
|
|
284
|
-
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`,
|
|
314
|
+
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`, poolId: ctxPool.poolId, tenantHint, ...poolOptions });
|
|
285
315
|
} catch (error) {
|
|
286
316
|
if (error?.code === "pool_empty") {
|
|
287
317
|
return refuse(io, "pool_empty", 3, Object.fromEntries(["reason", "detail", ...GUIDED_FIELDS].filter((key) => error[key] !== undefined).map((key) => [key, error[key]])), vars);
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
// BOT-2084: the
|
|
1
|
+
// BOT-2084 / BOT-2648: the staging session pool binding.
|
|
2
2
|
//
|
|
3
|
-
// A
|
|
4
|
-
//
|
|
5
|
-
// validates
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
import { dirname, join, parse } from "node:path";
|
|
10
|
-
|
|
11
|
-
export const AGENT_BINDING_FILE = ".botbuddy-agent.json";
|
|
3
|
+
// A tenant registers its binding in the BotBuddy control plane
|
|
4
|
+
// (register_staging_pool); `bb staging-pool install --tenant` reads it and
|
|
5
|
+
// this module parses and validates it into the camelCase shape the pool uses. It
|
|
6
|
+
// is never read from a repository checkout. Node-stdlib-only and self-contained:
|
|
7
|
+
// the installed job keeps a validated snapshot in its config.
|
|
12
8
|
|
|
13
9
|
export class BindingError extends Error {
|
|
14
10
|
constructor(message, code = "pool_binding_invalid") {
|
|
@@ -29,8 +25,21 @@ const PUBLIC_KEY_RE = /^(sb_publishable_[A-Za-z0-9_-]{16,128}|eyJ[A-Za-z0-9_-]+\
|
|
|
29
25
|
export const ADAPTERS = Object.freeze(["supabase-gotrue-totp"]);
|
|
30
26
|
export const MAX_POOL_SIZE = 4;
|
|
31
27
|
|
|
28
|
+
// A legacy Supabase key is a JWT; only the public `anon` role is a publishable key
|
|
29
|
+
// (Codex P1, #1323: a service_role JWT is a secret and is never installed).
|
|
30
|
+
export function isPublicAnonJwt(value) {
|
|
31
|
+
const parts = String(value).split(".");
|
|
32
|
+
if (parts.length !== 3) return false;
|
|
33
|
+
try {
|
|
34
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
35
|
+
return claims !== null && typeof claims === "object" && claims.role === "anon";
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
function need(condition, message) {
|
|
33
|
-
if (!condition) throw new BindingError(
|
|
42
|
+
if (!condition) throw new BindingError(`staging_session_pool binding: ${message}`);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// raw: the parsed .botbuddy-agent.json object. Returns null when the repo does not
|
|
@@ -48,12 +57,15 @@ export function parsePoolBinding(raw) {
|
|
|
48
57
|
need(typeof section.app_origin === "string", "app_origin is required");
|
|
49
58
|
need(typeof section.supabase_url === "string", "supabase_url is required");
|
|
50
59
|
need(typeof section.publishable_key === "string" && PUBLIC_KEY_RE.test(section.publishable_key), "publishable_key must be the public publishable/anon key");
|
|
60
|
+
need(!section.publishable_key.startsWith("eyJ") || isPublicAnonJwt(section.publishable_key), "a legacy JWT publishable_key must carry role anon (a service_role key is a secret)");
|
|
51
61
|
need(typeof section.storage_key === "string" && STORAGE_KEY_RE.test(section.storage_key), "storage_key must be the app's supabase-js auth storage key");
|
|
52
62
|
need(Array.isArray(section.production_origins ?? []), "production_origins must be an array");
|
|
53
63
|
need(typeof section.password_env_file === "string" && /^(~\/|\/)/.test(section.password_env_file), "password_env_file must be an absolute or ~/ path");
|
|
54
64
|
need(typeof section.launchd_label === "string" && LABEL_RE.test(section.launchd_label), "launchd_label must be a reverse-DNS label");
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
// BOT-2648: docs is optional, and install_command is retired (install reads the
|
|
66
|
+
// control plane by tenant); a pre-registry snapshot may still carry it.
|
|
67
|
+
need(section.docs === undefined || (typeof section.docs === "string" && section.docs.length > 0), "docs must be a non-empty string when present");
|
|
68
|
+
need(section.install_command === undefined || typeof section.install_command === "string", "install_command must be a string when present");
|
|
57
69
|
const personas = section.personas;
|
|
58
70
|
need(personas && typeof personas === "object" && Object.keys(personas).length > 0, "personas must name at least one persona");
|
|
59
71
|
const parsedPersonas = {};
|
|
@@ -84,7 +96,6 @@ export function parsePoolBinding(raw) {
|
|
|
84
96
|
personas: Object.freeze(parsedPersonas),
|
|
85
97
|
launchdLabel: section.launchd_label,
|
|
86
98
|
docs: section.docs,
|
|
87
|
-
installCommand: section.install_command,
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
|
|
@@ -94,29 +105,3 @@ export function findPersona(binding, aliasOrEmail) {
|
|
|
94
105
|
if (Object.hasOwn(binding.personas, key)) return binding.personas[key];
|
|
95
106
|
return Object.values(binding.personas).find((persona) => persona.email === key) ?? null;
|
|
96
107
|
}
|
|
97
|
-
|
|
98
|
-
// Walk up from cwd to the committed binding file; null when none or no section.
|
|
99
|
-
export async function readRepoPoolBinding(cwd = process.cwd(), { read = readFile } = {}) {
|
|
100
|
-
let dir = cwd;
|
|
101
|
-
const root = parse(dir).root;
|
|
102
|
-
while (true) {
|
|
103
|
-
let text = null;
|
|
104
|
-
try {
|
|
105
|
-
text = await read(join(dir, AGENT_BINDING_FILE), "utf8");
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error?.code !== "ENOENT") throw error;
|
|
108
|
-
}
|
|
109
|
-
if (text !== null) {
|
|
110
|
-
let raw;
|
|
111
|
-
try {
|
|
112
|
-
raw = JSON.parse(text);
|
|
113
|
-
} catch {
|
|
114
|
-
throw new BindingError(`${AGENT_BINDING_FILE} is not valid JSON`);
|
|
115
|
-
}
|
|
116
|
-
const binding = parsePoolBinding(raw);
|
|
117
|
-
return binding ? { binding, repoRoot: dir } : null;
|
|
118
|
-
}
|
|
119
|
-
if (dir === root) return null;
|
|
120
|
-
dir = dirname(dir);
|
|
121
|
-
}
|
|
122
|
-
}
|