@bridge_gpt/mcp-server 0.2.23 → 0.2.24
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/README.md +69 -17
- package/build/commands.generated.js +2 -2
- package/build/decision-page-template.js +9 -4
- package/build/docs.generated.js +5 -0
- package/build/index.js +762 -279
- package/build/init.js +29 -0
- package/build/install-bridge.js +480 -76
- package/build/readme.generated.js +1 -1
- package/build/sfcc/log-gate.js +85 -0
- package/build/sfcc/log-query.js +170 -0
- package/build/sfcc/register.js +10 -0
- package/build/sfcc/setup-status.js +33 -3
- package/build/version.generated.js +1 -1
- package/{CONDUCTOR.md → docs/CONDUCTOR.md} +2 -2
- package/docs/install/github-app.md +189 -0
- package/docs/install/mcp-tool-integrations.md +305 -0
- package/docs/install/sfcc-integration.md +140 -0
- package/package.json +5 -5
- package/public/js/main.min.js +46 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +3 -2
package/build/install-bridge.js
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* Collapses the previously five-step manual setup into a single CLI subcommand.
|
|
7
7
|
* It does the DETERMINISTIC work in the shell:
|
|
8
8
|
*
|
|
9
|
+
* Step 0 Resolve the repository name. `--repo` / `BAPI_REPO_NAME` short-circuit
|
|
10
|
+
* deterministically; otherwise, on the consume-an-existing-key flow, a
|
|
11
|
+
* compatible server resolves the unique repo from the API key via a
|
|
12
|
+
* read-only `GET <base>/setup/resolve-repo` (BAPI-616) BEFORE the
|
|
13
|
+
* connectivity check. An old server / unresolvable key / failure falls
|
|
14
|
+
* back to the existing local prompt/inference — never a hard failure.
|
|
9
15
|
* Step 1 Scaffold via `runInit` (commands, agents, pipelines, secret-free
|
|
10
16
|
* per-host MCP config placeholders, .bridge/config).
|
|
11
17
|
* Step 2 Verify connectivity over HTTP (`GET <base>/jira/ping?repo_name=…`
|
|
@@ -22,8 +28,10 @@
|
|
|
22
28
|
* (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) via
|
|
23
29
|
* `upsertBapiCredential`. Non-blocking (fail-open) — mirrors Stage 6.
|
|
24
30
|
*
|
|
25
|
-
* then SPAWNS a fresh agent session (Step 5) for the agentic
|
|
26
|
-
*
|
|
31
|
+
* then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
|
|
32
|
+
* remainder: `/install-bridge` config-field derivation → the read-after-write
|
|
33
|
+
* capability report → a single optional indexing-consent question. It does NOT
|
|
34
|
+
* chain `/learn-repository` and does NOT index without explicit consent. The
|
|
27
35
|
* fresh session is required because a CLI cannot force the running editor to
|
|
28
36
|
* reload the just-written `.mcp.json`, and field derivation needs an agent
|
|
29
37
|
* runtime the shell does not have.
|
|
@@ -85,15 +93,44 @@ export function buildPrewarmArgs() {
|
|
|
85
93
|
export function buildPrewarmCommandPreview() {
|
|
86
94
|
return `npx ${buildPrewarmArgs().join(" ")}`;
|
|
87
95
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The natural-language prompt handed to the spawned agent session. It is
|
|
98
|
+
* CONFIGURE-ONLY: it derives and applies configuration, presents the capability
|
|
99
|
+
* report, and ends with exactly one indexing-consent question. It never chains
|
|
100
|
+
* /learn-repository and never indexes before explicit consent.
|
|
101
|
+
*
|
|
102
|
+
* The index-consent beat is AGENT-OWNED (asked inside this spawned session), not
|
|
103
|
+
* parent-CLI-owned, because `spawnTerminalTab()` opens an asynchronous terminal
|
|
104
|
+
* session: the parent CLI returns immediately and cannot reliably ask a question
|
|
105
|
+
* that must appear AFTER the spawned session's capability report. Asking it via
|
|
106
|
+
* the parent's `deps.promptLine` would display the question before the report
|
|
107
|
+
* even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
|
|
108
|
+
* prompts (repository / overwrite / invite confirmation) and is intentionally
|
|
109
|
+
* NOT used for this post-report question.
|
|
110
|
+
*/
|
|
111
|
+
export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
|
|
112
|
+
"in the install-spawn context so it SKIPS its Stage 8 and Stage 9 offers — this session's only " +
|
|
113
|
+
"closing interaction is the single indexing question below). Do NOT run /learn-repository. Do NOT " +
|
|
114
|
+
"call parse_repository (or otherwise start indexing) before the capability report and explicit " +
|
|
115
|
+
"consent below. " +
|
|
116
|
+
"Complete the command's read-after-write five-section capability report first: 'Connected ✓', " +
|
|
117
|
+
"'Not yet connected ✗', 'Tools you can use now', 'Tools you'll unlock', and 'Recommended next " +
|
|
118
|
+
"step + why'. " +
|
|
119
|
+
"Only AFTER that report is fully presented, ask exactly one question using this visible prompt: " +
|
|
120
|
+
"'[Y/n] Index repository now?'. Only an explicit affirmative answer (e.g. 'y'/'yes') starts " +
|
|
121
|
+
"indexing; a blank answer, a negative answer, EOF, an unavailable interaction, and any " +
|
|
122
|
+
"non-interactive/headless run all resolve to NO. " +
|
|
123
|
+
"On an affirmative answer: call the parse_repository MCP tool exactly once, describe the accepted " +
|
|
124
|
+
"job as QUEUED, and direct later progress checks to get_parse_status or /check-parse-status " +
|
|
125
|
+
"(do NOT poll it to completion). If parse_repository returns a blocking refusal or error, do NOT " +
|
|
126
|
+
"claim the job was queued — report the sanitized result and leave indexing pending. " +
|
|
127
|
+
"On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
|
|
128
|
+
"continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
|
|
129
|
+
"Never request, echo, or transport any credential — only ever direct the human to the setup UI via " +
|
|
130
|
+
"the command's configure_in pointer. " +
|
|
131
|
+
"End with an explicit summary line stating how many config fields the apply_install_manifest call " +
|
|
132
|
+
"applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending — " +
|
|
133
|
+
"if 0 fields were applied, say so loudly and explain what is still pending.";
|
|
97
134
|
/** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
|
|
98
135
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
99
136
|
/** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
|
|
@@ -107,7 +144,7 @@ export function getInstallBridgeUsage() {
|
|
|
107
144
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
108
145
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
109
146
|
"routing credential, then opens a fresh agent session to derive the remaining",
|
|
110
|
-
"config and
|
|
147
|
+
"config, present a capability report, and offer optional repository indexing.",
|
|
111
148
|
"",
|
|
112
149
|
"Inputs (the only two irreducible ones):",
|
|
113
150
|
" --api-key <key> Bridge API key. Falls back to the BAPI_API_KEY env var,",
|
|
@@ -116,18 +153,35 @@ export function getInstallBridgeUsage() {
|
|
|
116
153
|
" key, it does not create one (--invite is the one exception:",
|
|
117
154
|
" it CREATES the project and its first admin key). NEVER",
|
|
118
155
|
" printed or logged.",
|
|
119
|
-
" --repo <name> Repository name.
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
" is
|
|
124
|
-
"
|
|
156
|
+
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
157
|
+
" priority and short-circuit before any network call. When",
|
|
158
|
+
" neither is set, a compatible server resolves the unique",
|
|
159
|
+
" repository from your existing API key automatically; if the",
|
|
160
|
+
" server is older, the key is unresolvable, or resolution",
|
|
161
|
+
" fails, it falls back to an inferred default you confirm",
|
|
162
|
+
" interactively (and to a required --repo when stdin is",
|
|
163
|
+
" non-interactive). MUST match the server-side repo",
|
|
164
|
+
" registration (it keys the credential store as bapi:<repo>).",
|
|
165
|
+
" With --invite it is the name your NEW project is created",
|
|
166
|
+
" under (globally unique).",
|
|
167
|
+
"",
|
|
168
|
+
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
169
|
+
" --email <addr> Create a brand-new Bridge workspace from just an email —",
|
|
170
|
+
" the primary path for a first-time user with nothing yet.",
|
|
171
|
+
" It requests a fresh workspace for that email, then creates",
|
|
172
|
+
" the project and mints your own admin API key in one command.",
|
|
173
|
+
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
174
|
+
" interactive prompt. The email is NOT a secret (it is shown",
|
|
175
|
+
" as you type), but it is never printed to a log. Mutually",
|
|
176
|
+
" exclusive with --api-key and --invite. No email verification",
|
|
177
|
+
" is performed and no message is sent to the address — it only",
|
|
178
|
+
" labels the new workspace.",
|
|
125
179
|
"",
|
|
126
180
|
"Bootstrap-invite onboarding (no web UI, no pre-existing key):",
|
|
127
|
-
" --invite [token] Redeem a bootstrap invite
|
|
128
|
-
" your own admin API key in one command.
|
|
129
|
-
" with --api-key (in this mode
|
|
130
|
-
" consumed).",
|
|
181
|
+
" --invite [token] Redeem a bootstrap invite you were already given: creates",
|
|
182
|
+
" the project and mints your own admin API key in one command.",
|
|
183
|
+
" Mutually exclusive with --api-key and --email (in this mode",
|
|
184
|
+
" the key is created, not consumed).",
|
|
131
185
|
"",
|
|
132
186
|
" Run it WITHOUT a value — `install-bridge --invite` — and the",
|
|
133
187
|
" token is read from an interactive prompt with echo",
|
|
@@ -157,8 +211,10 @@ export function getInstallBridgeUsage() {
|
|
|
157
211
|
"",
|
|
158
212
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
159
213
|
"(default docs/tmp) are read from the environment with the shown fallbacks.",
|
|
160
|
-
"
|
|
161
|
-
"
|
|
214
|
+
"BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
|
|
215
|
+
"visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token",
|
|
216
|
+
"non-interactively (scripting only — it is exposed to shell history; prefer the",
|
|
217
|
+
"prompt).",
|
|
162
218
|
].join("\n");
|
|
163
219
|
}
|
|
164
220
|
/**
|
|
@@ -178,10 +234,12 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
178
234
|
let dryRun = false;
|
|
179
235
|
let agentName = DEFAULT_AGENT_NAME;
|
|
180
236
|
let invite;
|
|
237
|
+
let email;
|
|
181
238
|
// Track SUPPLIED-ness separately from the values: `--invite` is legitimately
|
|
182
239
|
// valueless (prompt path) and `--api-key ""` is still a contradiction with it.
|
|
183
240
|
let inviteSupplied = false;
|
|
184
241
|
let apiKeySupplied = false;
|
|
242
|
+
let emailSupplied = false;
|
|
185
243
|
/** Read a `--flag value` or `--flag=value` value, advancing the index. */
|
|
186
244
|
const readValue = (arg, flag, i) => {
|
|
187
245
|
if (arg.startsWith(`${flag}=`)) {
|
|
@@ -228,6 +286,21 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
228
286
|
}
|
|
229
287
|
continue;
|
|
230
288
|
}
|
|
289
|
+
if (arg === "--email" || arg.startsWith("--email=")) {
|
|
290
|
+
// REQUIRED-value flag (unlike --invite): the email is what selects and drives
|
|
291
|
+
// the self-serve mint, so a bare or blank `--email` is a hard error rather
|
|
292
|
+
// than a prompt trigger. The value is NOT echoed in any error message.
|
|
293
|
+
const r = readValue(arg, "--email", i);
|
|
294
|
+
if ("error" in r)
|
|
295
|
+
return { status: "error", message: r.error };
|
|
296
|
+
if (r.value.trim().length === 0) {
|
|
297
|
+
return { status: "error", message: "--email requires a non-empty value." };
|
|
298
|
+
}
|
|
299
|
+
email = r.value.trim();
|
|
300
|
+
emailSupplied = true;
|
|
301
|
+
i = r.nextIndex;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
231
304
|
if (arg === "--repo" || arg.startsWith("--repo=")) {
|
|
232
305
|
const r = readValue(arg, "--repo", i);
|
|
233
306
|
if ("error" in r)
|
|
@@ -267,9 +340,26 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
267
340
|
"it does not consume an existing one.",
|
|
268
341
|
};
|
|
269
342
|
}
|
|
343
|
+
// --email selects self-serve mode, which CREATES the key — so it conflicts with
|
|
344
|
+
// both consuming an existing one (--api-key) and redeeming a pre-issued invite
|
|
345
|
+
// (--invite). Names the flags only — never the email or token values (BAPI-618).
|
|
346
|
+
if (emailSupplied && apiKeySupplied) {
|
|
347
|
+
return {
|
|
348
|
+
status: "error",
|
|
349
|
+
message: "--email and --api-key are mutually exclusive: self-serve signup creates your API key, " +
|
|
350
|
+
"it does not consume an existing one.",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (emailSupplied && inviteSupplied) {
|
|
354
|
+
return {
|
|
355
|
+
status: "error",
|
|
356
|
+
message: "--email and --invite are mutually exclusive: use --email for self-serve signup (no " +
|
|
357
|
+
"pre-issued invite), or --invite to redeem an invite you already have.",
|
|
358
|
+
};
|
|
359
|
+
}
|
|
270
360
|
return {
|
|
271
361
|
status: "ok",
|
|
272
|
-
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied },
|
|
362
|
+
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email },
|
|
273
363
|
};
|
|
274
364
|
}
|
|
275
365
|
/**
|
|
@@ -358,13 +448,25 @@ function promptLineViaReadline(promptText) {
|
|
|
358
448
|
* Always resolves (never rejects): a spawn error / non-zero exit / timeout becomes
|
|
359
449
|
* `{ ok: false, warning }` with secret-free warning text.
|
|
360
450
|
*/
|
|
451
|
+
/**
|
|
452
|
+
* The env clone handed to the pre-warm child, with every credential/PII variable
|
|
453
|
+
* removed: `BAPI_API_KEY` and the bootstrap-invite token (`BAPI_INVITE`) are
|
|
454
|
+
* secrets that must never reach an npm lifecycle script, and the self-serve signup
|
|
455
|
+
* email (`BAPI_SIGNUP_EMAIL`, BAPI-618) is invitee PII with no business in a
|
|
456
|
+
* `--version` probe. Extracted as a pure, exported helper so the strip is directly
|
|
457
|
+
* unit-testable (the default spawner below uses the real `spawn`, which is not
|
|
458
|
+
* injectable).
|
|
459
|
+
*/
|
|
460
|
+
export function sanitizePrewarmEnv(env) {
|
|
461
|
+
const sanitized = { ...env };
|
|
462
|
+
delete sanitized.BAPI_API_KEY;
|
|
463
|
+
delete sanitized.BAPI_INVITE;
|
|
464
|
+
delete sanitized.BAPI_SIGNUP_EMAIL;
|
|
465
|
+
return sanitized;
|
|
466
|
+
}
|
|
361
467
|
function spawnPrewarmDefault(command, args, env) {
|
|
362
468
|
return new Promise((resolve) => {
|
|
363
|
-
const sanitizedEnv =
|
|
364
|
-
delete sanitizedEnv.BAPI_API_KEY;
|
|
365
|
-
// The bootstrap-invite token is likewise never needed by a `--version` probe
|
|
366
|
-
// and must not reach an npm lifecycle script.
|
|
367
|
-
delete sanitizedEnv.BAPI_INVITE;
|
|
469
|
+
const sanitizedEnv = sanitizePrewarmEnv(env);
|
|
368
470
|
try {
|
|
369
471
|
const child = spawn(command, args, {
|
|
370
472
|
shell: false,
|
|
@@ -393,6 +495,11 @@ function spawnPrewarmDefault(command, args, env) {
|
|
|
393
495
|
/** Build default deps from the live process. */
|
|
394
496
|
export function createDefaultInstallBridgeDeps() {
|
|
395
497
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
498
|
+
// One production fetch, reused for both the `fetch` seam and the default
|
|
499
|
+
// resolver, so a direct caller of this factory gets a resolver bound to the
|
|
500
|
+
// same client. (runInstallBridgeCli re-binds the resolver to the FINAL merged
|
|
501
|
+
// fetch when the caller did not override the resolver — see below.)
|
|
502
|
+
const productionFetch = (...args) => fetch(...args);
|
|
396
503
|
return {
|
|
397
504
|
env: process.env,
|
|
398
505
|
cwd: process.cwd(),
|
|
@@ -417,7 +524,8 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
417
524
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
418
525
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
419
526
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
420
|
-
fetch:
|
|
527
|
+
fetch: productionFetch,
|
|
528
|
+
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
421
529
|
spawnPrewarm: spawnPrewarmDefault,
|
|
422
530
|
runInit,
|
|
423
531
|
upsertCredential: upsertBapiCredential,
|
|
@@ -496,20 +604,89 @@ export async function resolveInviteToken(options, deps) {
|
|
|
496
604
|
};
|
|
497
605
|
}
|
|
498
606
|
/**
|
|
499
|
-
* Resolve the
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
607
|
+
* Resolve the SELF-SERVE signup email: `--email <addr>` → `BAPI_SIGNUP_EMAIL` env →
|
|
608
|
+
* interactive VISIBLE prompt (BAPI-618). Fails (secret-free) when none is available
|
|
609
|
+
* and stdin is non-interactive.
|
|
610
|
+
*
|
|
611
|
+
* The precedence mirrors {@link resolveInviteToken}, but the input is NOT a secret:
|
|
612
|
+
* an email is not a credential, so it uses the ECHOED {@link InstallBridgeDeps.promptLine}
|
|
613
|
+
* — never `promptSecret`. (It IS invitee PII, so like every value in this module it
|
|
614
|
+
* is never written to a log line; it is only ever echoed as the user's own keystrokes
|
|
615
|
+
* and placed in the mint request body.)
|
|
616
|
+
*
|
|
617
|
+
* No email-format validation is performed here — only surrounding whitespace is
|
|
618
|
+
* trimmed, matching the other CLI input seams and the server's deliberately
|
|
619
|
+
* permissive "stored unvalidated" contract (D-2). The entered value is never echoed
|
|
620
|
+
* back in an error message.
|
|
504
621
|
*/
|
|
505
|
-
export async function
|
|
506
|
-
if (typeof options.
|
|
507
|
-
return { ok: true, value: options.
|
|
622
|
+
export async function resolveSignupEmail(options, deps) {
|
|
623
|
+
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
624
|
+
return { ok: true, value: options.email.trim() };
|
|
508
625
|
}
|
|
509
|
-
const fromEnv = deps.env.
|
|
626
|
+
const fromEnv = deps.env.BAPI_SIGNUP_EMAIL;
|
|
510
627
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
511
628
|
return { ok: true, value: fromEnv.trim() };
|
|
512
629
|
}
|
|
630
|
+
if (deps.isTTY && deps.promptLine) {
|
|
631
|
+
const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
|
|
632
|
+
if (entered.length > 0) {
|
|
633
|
+
return { ok: true, value: entered };
|
|
634
|
+
}
|
|
635
|
+
return { ok: false, error: "No email entered." };
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
ok: false,
|
|
639
|
+
error: "An email is required to create a Bridge workspace. Pass --email <addr> or set the " +
|
|
640
|
+
"BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt " +
|
|
641
|
+
"for it).",
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Select the onboarding branch as pure logic (deterministic, no I/O, no prompt):
|
|
646
|
+
*
|
|
647
|
+
* - `--invite` (with or without a value) or a non-empty `BAPI_INVITE` selects
|
|
648
|
+
* the `bootstrap-invite` need-key branch.
|
|
649
|
+
* - otherwise, `--email` or a non-empty `BAPI_SIGNUP_EMAIL` selects the
|
|
650
|
+
* `self-serve` need-key branch (BAPI-618).
|
|
651
|
+
* - every other invocation selects `have-key`.
|
|
652
|
+
*
|
|
653
|
+
* Precedence is explicit: an invite input wins over an email input (both are
|
|
654
|
+
* need-key, but a pre-issued invite is the more specific intent), and any
|
|
655
|
+
* need-key input wins over the have-key default. The parser already rejects
|
|
656
|
+
* `--email` alongside `--api-key`/`--invite`, so a flag-level conflict never
|
|
657
|
+
* reaches here; the precedence only disambiguates env-var combinations.
|
|
658
|
+
*/
|
|
659
|
+
export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
660
|
+
const inviteMode = options.inviteMode === true || (env.BAPI_INVITE ?? "").trim().length > 0;
|
|
661
|
+
if (inviteMode)
|
|
662
|
+
return { kind: "need-key", method: "bootstrap-invite" };
|
|
663
|
+
const emailMode = (options.email ?? "").trim().length > 0 ||
|
|
664
|
+
(env.BAPI_SIGNUP_EMAIL ?? "").trim().length > 0;
|
|
665
|
+
if (emailMode)
|
|
666
|
+
return { kind: "need-key", method: "self-serve" };
|
|
667
|
+
return { kind: "have-key" };
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Return the explicitly configured repository name (`--repo`, then
|
|
671
|
+
* `BAPI_REPO_NAME`), trimmed, or `undefined` when neither is supplied. Pure: no
|
|
672
|
+
* prompting, no inference, no I/O. `--repo` takes priority over the environment.
|
|
673
|
+
* This is the deterministic short-circuit that runs BEFORE any server resolution.
|
|
674
|
+
*/
|
|
675
|
+
export function resolveConfiguredRepoName(options, env) {
|
|
676
|
+
if (typeof options.repo === "string" && options.repo.trim().length > 0) {
|
|
677
|
+
return options.repo.trim();
|
|
678
|
+
}
|
|
679
|
+
const fromEnv = env.BAPI_REPO_NAME;
|
|
680
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
681
|
+
return fromEnv.trim();
|
|
682
|
+
}
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
export async function resolveRepoName(options, deps) {
|
|
686
|
+
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
687
|
+
if (configured !== undefined) {
|
|
688
|
+
return { ok: true, value: configured };
|
|
689
|
+
}
|
|
513
690
|
// Non-interactive: fail fast and require --repo. Inference is an interactive
|
|
514
691
|
// convenience only (the user must confirm it).
|
|
515
692
|
if (!deps.isTTY || !deps.promptLine) {
|
|
@@ -668,11 +845,13 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
668
845
|
signal: AbortSignal.timeout(10_000),
|
|
669
846
|
});
|
|
670
847
|
}
|
|
671
|
-
catch
|
|
672
|
-
|
|
848
|
+
catch {
|
|
849
|
+
// The caught exception message is NOT interpolated: some fetch/undici errors
|
|
850
|
+
// echo request detail (which carries the X-API-Key header) into the message,
|
|
851
|
+
// so a fixed, secret-free string is used instead (BAPI-616).
|
|
673
852
|
return {
|
|
674
853
|
ok: false,
|
|
675
|
-
message: `Could not reach the Bridge API at ${baseUrl}
|
|
854
|
+
message: `Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`,
|
|
676
855
|
};
|
|
677
856
|
}
|
|
678
857
|
if (resp.ok)
|
|
@@ -697,6 +876,54 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
697
876
|
message: `Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`,
|
|
698
877
|
};
|
|
699
878
|
}
|
|
879
|
+
/** The resolve endpoint: `<base>/setup/resolve-repo`. No query string — ever. */
|
|
880
|
+
export function buildResolveRepoUrl(baseUrl) {
|
|
881
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/resolve-repo`;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Resolve the repository name server-side from an existing API key. The key
|
|
885
|
+
* travels ONLY in the `X-API-Key` header (never the URL, never the body), bounded
|
|
886
|
+
* by the same 10s timeout as {@link verifyConnectivity}.
|
|
887
|
+
*
|
|
888
|
+
* Status mapping is the whole compatibility story: `404` → `not-deployed` (old
|
|
889
|
+
* server), `409` → `unresolved` (ambiguous/shared/client-scoped/legacy), every
|
|
890
|
+
* other non-OK (`401`, `403`, `5xx`, unexpected) → `error`, and any
|
|
891
|
+
* network/timeout/malformed-body failure → `error`. A caught exception or a
|
|
892
|
+
* response body is NEVER read into the result.
|
|
893
|
+
*/
|
|
894
|
+
export async function resolveRepoViaServer(fetchImpl, baseUrl, apiKey) {
|
|
895
|
+
const url = buildResolveRepoUrl(baseUrl);
|
|
896
|
+
let resp;
|
|
897
|
+
try {
|
|
898
|
+
resp = await fetchImpl(url, {
|
|
899
|
+
headers: { "X-API-Key": apiKey },
|
|
900
|
+
signal: AbortSignal.timeout(10_000),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
// Network failure / timeout. The exception may echo the request (and thus the
|
|
905
|
+
// X-API-Key header), so it is never inspected or propagated.
|
|
906
|
+
return { status: "error" };
|
|
907
|
+
}
|
|
908
|
+
if (resp.status === 404)
|
|
909
|
+
return { status: "not-deployed" };
|
|
910
|
+
if (resp.status === 409)
|
|
911
|
+
return { status: "unresolved" };
|
|
912
|
+
if (!resp.ok)
|
|
913
|
+
return { status: "error" };
|
|
914
|
+
let body;
|
|
915
|
+
try {
|
|
916
|
+
body = await resp.json();
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return { status: "error" };
|
|
920
|
+
}
|
|
921
|
+
const repoName = body?.repo_name;
|
|
922
|
+
const validated = validateRepoName(repoName);
|
|
923
|
+
if (!validated.ok)
|
|
924
|
+
return { status: "error" };
|
|
925
|
+
return { status: "resolved", repoName: validated.value };
|
|
926
|
+
}
|
|
700
927
|
// ---------------------------------------------------------------------------
|
|
701
928
|
// Bootstrap-invite exchange (BAPI-606) — replaces the pre-flight ping in invite mode
|
|
702
929
|
// ---------------------------------------------------------------------------
|
|
@@ -796,6 +1023,76 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
796
1023
|
message: `The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`,
|
|
797
1024
|
};
|
|
798
1025
|
}
|
|
1026
|
+
// ---------------------------------------------------------------------------
|
|
1027
|
+
// Self-serve mint (BAPI-618): email -> invite token
|
|
1028
|
+
// ---------------------------------------------------------------------------
|
|
1029
|
+
/**
|
|
1030
|
+
* The self-serve invite-token prefix, mirroring the server's
|
|
1031
|
+
* ``BOOTSTRAP_INVITE_TOKEN_PREFIX`` (``bapi_inv_``). A well-formed token
|
|
1032
|
+
* self-identifies with this prefix; a success response whose token does not is
|
|
1033
|
+
* treated as a `failed` mint rather than fed into the redemption path.
|
|
1034
|
+
*/
|
|
1035
|
+
export const BOOTSTRAP_INVITE_TOKEN_PREFIX = "bapi_inv_";
|
|
1036
|
+
/** The self-serve mint endpoint: `<base>/setup/bootstrap/self-serve`. No query string — ever. */
|
|
1037
|
+
export function buildSelfServeMintUrl(baseUrl) {
|
|
1038
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* POST the self-serve mint: an email in, an invite token out. Mirrors
|
|
1042
|
+
* {@link exchangeBootstrapInvite}'s fetch/timeout/secret-free-result discipline.
|
|
1043
|
+
*
|
|
1044
|
+
* The email travels ONLY in the JSON request body (never a query string, never a
|
|
1045
|
+
* header, never a log line). Bounded by the same 10s timeout as the exchange and
|
|
1046
|
+
* the connectivity ping. On any non-success status the failure body is NOT read —
|
|
1047
|
+
* the status alone decides the category — because an untrusted upstream body could
|
|
1048
|
+
* echo the email or carry internals. A caught fetch exception is likewise never
|
|
1049
|
+
* inspected or interpolated; some fetch impls embed the request (and thus the
|
|
1050
|
+
* email) in the message.
|
|
1051
|
+
*/
|
|
1052
|
+
export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
1053
|
+
const url = buildSelfServeMintUrl(baseUrl);
|
|
1054
|
+
let resp;
|
|
1055
|
+
try {
|
|
1056
|
+
resp = await deps.fetch(url, {
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
headers: { "Content-Type": "application/json" },
|
|
1059
|
+
body: JSON.stringify({ invitee_email: email }),
|
|
1060
|
+
signal: AbortSignal.timeout(10_000),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (err) {
|
|
1064
|
+
// The message may contain the request (some fetch impls echo it), so it is
|
|
1065
|
+
// deliberately NOT inspected or interpolated here.
|
|
1066
|
+
void err;
|
|
1067
|
+
return { ok: false, category: "failed" };
|
|
1068
|
+
}
|
|
1069
|
+
if (resp.ok) {
|
|
1070
|
+
let token;
|
|
1071
|
+
try {
|
|
1072
|
+
const body = (await resp.json());
|
|
1073
|
+
token = body?.token;
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
// An unreadable success body is not actionable and must not be exposed.
|
|
1077
|
+
return { ok: false, category: "failed" };
|
|
1078
|
+
}
|
|
1079
|
+
if (typeof token !== "string" ||
|
|
1080
|
+
token.trim().length === 0 ||
|
|
1081
|
+
!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)) {
|
|
1082
|
+
return { ok: false, category: "failed" };
|
|
1083
|
+
}
|
|
1084
|
+
return { ok: true, token };
|
|
1085
|
+
}
|
|
1086
|
+
// Non-success: the status alone decides the category. The failure body is NEVER
|
|
1087
|
+
// read — it is untrusted and could echo the email or leak internals.
|
|
1088
|
+
if (resp.status === 429) {
|
|
1089
|
+
return { ok: false, category: "rate-limited" };
|
|
1090
|
+
}
|
|
1091
|
+
if (resp.status === 400 || resp.status === 422) {
|
|
1092
|
+
return { ok: false, category: "invalid" };
|
|
1093
|
+
}
|
|
1094
|
+
return { ok: false, category: "failed" };
|
|
1095
|
+
}
|
|
799
1096
|
/**
|
|
800
1097
|
* The 401 message for a run that had to GENERATE a fresh secret — i.e. no local
|
|
801
1098
|
* pending record existed. Besides a simply-wrong token, this is exactly what a lost
|
|
@@ -844,8 +1141,10 @@ export function buildDryRunPreview(plan) {
|
|
|
844
1141
|
if (plan.bootstrapInvite)
|
|
845
1142
|
return buildBootstrapDryRunPreview(plan);
|
|
846
1143
|
return [
|
|
847
|
-
|
|
848
|
-
|
|
1144
|
+
plan.attemptedServerResolution
|
|
1145
|
+
? "install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
|
|
1146
|
+
: "install-bridge --dry-run (no writes, no network, no spawns)",
|
|
1147
|
+
`Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
|
|
849
1148
|
`Base URL (ping): ${plan.baseUrl}`,
|
|
850
1149
|
`Docs dir: ${plan.docsDir}`,
|
|
851
1150
|
`Agent: ${plan.agentName}`,
|
|
@@ -872,14 +1171,35 @@ export function buildDryRunPreview(plan) {
|
|
|
872
1171
|
*/
|
|
873
1172
|
function buildBootstrapDryRunPreview(plan) {
|
|
874
1173
|
const pendingTarget = `bootstrap-pending:${plan.repoName}`;
|
|
1174
|
+
// Self-serve signup (BAPI-618): the ONLY structural difference from a pre-issued
|
|
1175
|
+
// invite is a mint-from-email step (Step 2·pre) that turns an email into the
|
|
1176
|
+
// invite token before the unchanged redemption below. The preview states plainly
|
|
1177
|
+
// that in --dry-run that signup/mint is previewed-and-skipped: no account is
|
|
1178
|
+
// created, no mint request is sent, no email leaves the machine. The email and
|
|
1179
|
+
// any synthetic token are deliberately absent from this output.
|
|
1180
|
+
const header = plan.selfServeSignup
|
|
1181
|
+
? "install-bridge --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
|
|
1182
|
+
: "install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)";
|
|
1183
|
+
const repoLine = plan.selfServeSignup
|
|
1184
|
+
? `Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`
|
|
1185
|
+
: `Repo name: ${plan.repoName} (created by the exchange; globally unique)`;
|
|
1186
|
+
const selfServeStep = plan.selfServeSignup
|
|
1187
|
+
? [
|
|
1188
|
+
"Step 2·pre — self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace",
|
|
1189
|
+
" signup is requested, no mint call is made, and no email is sent or transmitted;",
|
|
1190
|
+
" a real run would request a fresh workspace for your email and receive an invite",
|
|
1191
|
+
" token, which then feeds the SAME redemption protocol below.",
|
|
1192
|
+
]
|
|
1193
|
+
: [];
|
|
875
1194
|
return [
|
|
876
|
-
|
|
877
|
-
|
|
1195
|
+
header,
|
|
1196
|
+
repoLine,
|
|
878
1197
|
`Base URL: ${plan.baseUrl}`,
|
|
879
1198
|
`Docs dir: ${plan.docsDir}`,
|
|
880
1199
|
`Agent: ${plan.agentName}`,
|
|
881
1200
|
"",
|
|
882
1201
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1202
|
+
...selfServeStep,
|
|
883
1203
|
`Step 2a — generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${plan.credentialStorePath}`,
|
|
884
1204
|
" BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.",
|
|
885
1205
|
`Step 2b — redeem the bootstrap invite (replaces the pre-flight ping — there is no key yet):`,
|
|
@@ -962,6 +1282,12 @@ function buildManualHostInstructions(entry, editors) {
|
|
|
962
1282
|
*/
|
|
963
1283
|
export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
964
1284
|
const deps = { ...createDefaultInstallBridgeDeps(), ...overrides };
|
|
1285
|
+
// When the caller overrode only `fetch` (the common test seam), bind the
|
|
1286
|
+
// default resolver to the FINAL merged fetch so server resolution still routes
|
|
1287
|
+
// through the override. An explicit `resolveRepoViaServer` override is honored.
|
|
1288
|
+
if (!overrides.resolveRepoViaServer) {
|
|
1289
|
+
deps.resolveRepoViaServer = (baseUrl, apiKey) => resolveRepoViaServer(deps.fetch, baseUrl, apiKey);
|
|
1290
|
+
}
|
|
965
1291
|
const { log, errorLog } = deps;
|
|
966
1292
|
const parsed = parseInstallBridgeArgs(argv);
|
|
967
1293
|
if (parsed.status === "help") {
|
|
@@ -975,18 +1301,33 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
975
1301
|
return 1;
|
|
976
1302
|
}
|
|
977
1303
|
const options = parsed.options;
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
|
|
1304
|
+
// Onboarding branch (pure, deterministic): have-key vs. a need-key method
|
|
1305
|
+
// (`bootstrap-invite` = redeem a pre-issued invite, `self-serve` = mint one from
|
|
1306
|
+
// an email, BAPI-618). BOTH need-key methods share the downstream redemption
|
|
1307
|
+
// protocol, so `bootstrapInviteMode` is true for both; `selfServeSignupMode`
|
|
1308
|
+
// discriminates the one extra step (mint-from-email) the self-serve path adds.
|
|
1309
|
+
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
1310
|
+
const bootstrapInviteMode = branch.kind === "need-key";
|
|
1311
|
+
const selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
|
|
982
1312
|
// ---- Resolve inputs (may prompt when interactive) ----
|
|
983
|
-
// Resolve the credential input first so a fully-empty non-interactive
|
|
984
|
-
// fails with the (more
|
|
985
|
-
// one. In
|
|
986
|
-
// MINTS it
|
|
1313
|
+
// Resolve the credential/entry input first so a fully-empty non-interactive
|
|
1314
|
+
// invocation fails with the (more relevant) missing-input message before the repo
|
|
1315
|
+
// one. In either need-key mode there is no API key to resolve — the exchange
|
|
1316
|
+
// MINTS it. Self-serve resolves the email here (up-front, before the dry-run
|
|
1317
|
+
// guard) so a missing email fails fast; the actual MINT happens later, strictly
|
|
1318
|
+
// after the dry-run return, so a preview neither mints nor sends anything.
|
|
987
1319
|
let apiKey = "";
|
|
988
1320
|
let inviteToken = "";
|
|
989
|
-
|
|
1321
|
+
let signupEmail = "";
|
|
1322
|
+
if (selfServeSignupMode) {
|
|
1323
|
+
const emailResult = await resolveSignupEmail(options, deps);
|
|
1324
|
+
if (!emailResult.ok) {
|
|
1325
|
+
errorLog(`Error: ${emailResult.error}`);
|
|
1326
|
+
return 1;
|
|
1327
|
+
}
|
|
1328
|
+
signupEmail = emailResult.value;
|
|
1329
|
+
}
|
|
1330
|
+
else if (bootstrapInviteMode) {
|
|
990
1331
|
const inviteResult = await resolveInviteToken(options, deps);
|
|
991
1332
|
if (!inviteResult.ok) {
|
|
992
1333
|
errorLog(`Error: ${inviteResult.error}`);
|
|
@@ -1002,25 +1343,59 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1002
1343
|
}
|
|
1003
1344
|
apiKey = keyResult.value;
|
|
1004
1345
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1346
|
+
// baseUrl is resolved BEFORE repository input because the have-key branch needs
|
|
1347
|
+
// it for the server-resolution request.
|
|
1348
|
+
const baseUrl = deps.env.BAPI_BASE_URL ?? DEFAULT_BAPI_BASE_URL;
|
|
1349
|
+
const docsDir = deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR;
|
|
1350
|
+
// ---- Resolve the repository name ----
|
|
1351
|
+
// Bootstrap-invite: unchanged (choose-a-name for the new project). Have-key:
|
|
1352
|
+
// `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve
|
|
1353
|
+
// it server-side from the API key (BAPI-616), and on ANY non-resolution outcome
|
|
1354
|
+
// (unresolved / not-deployed / error) fall back to the existing local
|
|
1355
|
+
// prompt/inference — never a hard failure.
|
|
1356
|
+
let repoName;
|
|
1357
|
+
let attemptedServerResolution = false;
|
|
1014
1358
|
if (bootstrapInviteMode) {
|
|
1015
|
-
const
|
|
1359
|
+
const repoResult = await resolveRepoName(options, deps);
|
|
1360
|
+
if (!repoResult.ok) {
|
|
1361
|
+
errorLog(`Error: ${repoResult.error}`);
|
|
1362
|
+
return 1;
|
|
1363
|
+
}
|
|
1364
|
+
// Invite mode creates the project, so the name must be valid BEFORE any
|
|
1365
|
+
// pending state or exchange.
|
|
1366
|
+
const validated = validateRepoName(repoResult.value);
|
|
1016
1367
|
if (!validated.ok) {
|
|
1017
1368
|
errorLog(`Error: invalid repo name — ${validated.error}.`);
|
|
1018
1369
|
return 1;
|
|
1019
1370
|
}
|
|
1020
1371
|
repoName = validated.value;
|
|
1021
1372
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1373
|
+
else {
|
|
1374
|
+
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
1375
|
+
if (configured !== undefined) {
|
|
1376
|
+
// Deterministic short-circuit: no server round-trip when the name is known.
|
|
1377
|
+
repoName = configured;
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
attemptedServerResolution = true;
|
|
1381
|
+
log("Resolving repository…");
|
|
1382
|
+
const resolution = await deps.resolveRepoViaServer(baseUrl, apiKey);
|
|
1383
|
+
if (resolution.status === "resolved") {
|
|
1384
|
+
repoName = resolution.repoName;
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
// Feature-detect + degrade: 404 (old server), 409 (unresolved/ambiguous/
|
|
1388
|
+
// client-scoped), and network/other errors all fall back to the existing
|
|
1389
|
+
// local resolution WITHOUT a cause-specific message or leaked detail.
|
|
1390
|
+
const repoResult = await resolveRepoName(options, deps);
|
|
1391
|
+
if (!repoResult.ok) {
|
|
1392
|
+
errorLog(`Error: ${repoResult.error}`);
|
|
1393
|
+
return 1;
|
|
1394
|
+
}
|
|
1395
|
+
repoName = repoResult.value;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1024
1399
|
const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
|
|
1025
1400
|
const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
1026
1401
|
const credentialStorePath = getPrimaryCredentialStorePath({
|
|
@@ -1045,6 +1420,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1045
1420
|
...(bootstrapInviteMode
|
|
1046
1421
|
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
1047
1422
|
: {}),
|
|
1423
|
+
...(selfServeSignupMode ? { selfServeSignup: true } : {}),
|
|
1424
|
+
...(attemptedServerResolution ? { attemptedServerResolution: true } : {}),
|
|
1048
1425
|
};
|
|
1049
1426
|
// ---- --dry-run: preview every step, strictly no side effects ----
|
|
1050
1427
|
// Positioned BEFORE the CSPRNG, the pending write, the exchange, the scaffold,
|
|
@@ -1091,6 +1468,32 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1091
1468
|
// ---- Step 2 — mint (invite mode) or verify (normal mode) the credential ----
|
|
1092
1469
|
let inviteFingerprint = "";
|
|
1093
1470
|
if (bootstrapInviteMode) {
|
|
1471
|
+
// ---- Self-serve signup (BAPI-618): mint the invite token from the email ----
|
|
1472
|
+
// This is the ONE step that precedes the unchanged BAPI-606 redemption below.
|
|
1473
|
+
// It sits AFTER the --dry-run return above, so a preview never mints, never
|
|
1474
|
+
// sends the email, and never writes a pending secret. On success the minted
|
|
1475
|
+
// token is assigned to `inviteToken` and the flow falls into the existing
|
|
1476
|
+
// persist-before-exchange block VERBATIM — identical to a --invite token from
|
|
1477
|
+
// here on. On failure we return BEFORE any CSPRNG/pending write, so nothing is
|
|
1478
|
+
// persisted. The email and the token are never logged.
|
|
1479
|
+
if (selfServeSignupMode) {
|
|
1480
|
+
log("Step 2/5 — requesting Bridge self-serve setup…");
|
|
1481
|
+
const mint = await mintSelfServeInvite(deps, baseUrl, signupEmail);
|
|
1482
|
+
if (!mint.ok) {
|
|
1483
|
+
if (mint.category === "rate-limited") {
|
|
1484
|
+
errorLog("Error: Self-serve setup is temporarily rate limited. Try again later.");
|
|
1485
|
+
}
|
|
1486
|
+
else if (mint.category === "invalid") {
|
|
1487
|
+
errorLog("Error: Self-serve setup could not be requested. Check the email value and try again.");
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
errorLog("Error: Unable to complete self-serve setup. Check connectivity and retry.");
|
|
1491
|
+
}
|
|
1492
|
+
return 1;
|
|
1493
|
+
}
|
|
1494
|
+
// Treated identically to a manually supplied invite token from here on.
|
|
1495
|
+
inviteToken = mint.token;
|
|
1496
|
+
}
|
|
1094
1497
|
// ===================================================================
|
|
1095
1498
|
// LOAD-BEARING ORDERING — NOT DEFENSIVE POLISH. DO NOT "TIDY" THIS.
|
|
1096
1499
|
//
|
|
@@ -1319,7 +1722,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1319
1722
|
}
|
|
1320
1723
|
}
|
|
1321
1724
|
// ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
|
|
1322
|
-
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge +
|
|
1725
|
+
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge configuration + capability report…`);
|
|
1323
1726
|
const terminal = detectTerminal(undefined, deps.env);
|
|
1324
1727
|
const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, spawnCommand, {
|
|
1325
1728
|
key: "install",
|
|
@@ -1332,16 +1735,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1332
1735
|
// user can run the agentic remainder by hand. Mirrors start-tickets treating a
|
|
1333
1736
|
// spawn failure as non-fatal to the run.
|
|
1334
1737
|
errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}). ` +
|
|
1335
|
-
"The project is NOT configured yet — run /install-bridge
|
|
1336
|
-
"
|
|
1738
|
+
"The project is NOT configured yet — run /install-bridge manually in this project to derive " +
|
|
1739
|
+
"and apply the config fields and see the capability report, then choose whether to run " +
|
|
1740
|
+
"/parse-repository to index the repository.");
|
|
1337
1741
|
return 0;
|
|
1338
1742
|
}
|
|
1339
1743
|
log("");
|
|
1340
|
-
log(`install-bridge setup steps complete. A fresh ${agent.name} session is now
|
|
1341
|
-
"
|
|
1744
|
+
log(`install-bridge setup steps complete. A fresh ${agent.name} session is now applying ` +
|
|
1745
|
+
"configuration, presenting the capability report, and ending with one indexing-consent question.");
|
|
1342
1746
|
log("NOTE: the install is not finished until that session's apply reports applied fields — " +
|
|
1343
|
-
"it will pause to ask you to approve the project description
|
|
1344
|
-
"
|
|
1345
|
-
"'Applied N of M' summary.");
|
|
1747
|
+
"it will pause to ask you to approve the project description, and it will close by asking " +
|
|
1748
|
+
"'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
|
|
1749
|
+
"(install status panel) or via the session's 'Applied N of M' summary.");
|
|
1346
1750
|
return 0;
|
|
1347
1751
|
}
|