@bridge_gpt/mcp-server 0.2.23 → 0.2.25
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 +96 -24
- package/build/commands.generated.js +4 -4
- package/build/conductor/epic-reconcile.js +7 -1
- package/build/conductor/epic-runtime.js +5 -0
- package/build/connect-github-api.js +365 -0
- package/build/connect-github.js +415 -0
- package/build/decision-page-schema.js +34 -5
- package/build/decision-page-template.js +126 -39
- package/build/docs.generated.js +5 -0
- package/build/index.js +1872 -633
- package/build/init.js +29 -0
- package/build/install-bridge.js +739 -91
- package/build/install-doctor.js +64 -0
- package/build/pipelines.generated.js +122 -128
- 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/start-tickets.js +48 -13
- package/build/version.generated.js +1 -1
- package/{CONDUCTOR.md → docs/CONDUCTOR.md} +2 -2
- package/docs/install/github-app.md +252 -0
- package/docs/install/mcp-tool-integrations.md +305 -0
- package/docs/install/sfcc-integration.md +140 -0
- package/package.json +5 -5
- package/pipelines/learn-repository.json +111 -119
- package/public/css/main.min.css +258 -65
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +233 -74
- 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,14 +28,34 @@
|
|
|
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.
|
|
30
38
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* That spawn command embeds the entire agent prompt and runs to multiple KB, which
|
|
40
|
+
* no macOS terminal will accept as one typed line. So it is never typed: the full
|
|
41
|
+
* command is written to a restricted (0600) launch script and only a short
|
|
42
|
+
* `. '<path>'` runner is spawned (BAPI-626, via `materializeWorkerLaunchCommand`).
|
|
43
|
+
* Materialization happens BEFORE any install side effect, because a command that
|
|
44
|
+
* cannot be launched is a whole-run failure, not a Step 5 warning.
|
|
45
|
+
*
|
|
46
|
+
* ONBOARDING BRANCHES — `have-key` (consume an existing key) vs. `need-key` (this
|
|
47
|
+
* command creates the project AND its first admin key). There are two need-key
|
|
48
|
+
* methods, differing only in how the first token is obtained: `bootstrap-invite`
|
|
49
|
+
* (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
|
|
50
|
+
* email. Both then feed the SAME redemption protocol. Selection is pure and
|
|
51
|
+
* deterministic from flags/env, except that a BARE interactive run is asked which
|
|
52
|
+
* branch it wants (BAPI-626) — without that question a first-time user cannot
|
|
53
|
+
* discover self-serve at all. Either need-key method NAMES a new project, so the
|
|
54
|
+
* repository prompt asks for a new project name rather than an existing
|
|
55
|
+
* registration (see `RepoNamePromptMode`).
|
|
56
|
+
*
|
|
57
|
+
* BOOTSTRAP-INVITE MODE (BAPI-606) — one of the two exceptions to "this command
|
|
58
|
+
* consumes a key, it does not create one". With `--invite` (or `BAPI_INVITE`) there is no
|
|
33
59
|
* API key yet, so the pre-flight ping of Step 2 CANNOT be made: the exchange is
|
|
34
60
|
* what mints the key, and it REPLACES that ping. The order becomes:
|
|
35
61
|
*
|
|
@@ -63,9 +89,13 @@ import { runInit, buildBridgeApiEntry } from "./init.js";
|
|
|
63
89
|
import { VERSION } from "./version.generated.js";
|
|
64
90
|
import { validateRepoName } from "./bridge-config.js";
|
|
65
91
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
66
|
-
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, } from "./credential-store.js";
|
|
92
|
+
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, resolveBapiCredentials, } from "./credential-store.js";
|
|
93
|
+
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
94
|
+
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
95
|
+
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
96
|
+
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
67
97
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
68
|
-
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "./start-tickets.js";
|
|
98
|
+
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
|
|
69
99
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
70
100
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
71
101
|
/**
|
|
@@ -85,15 +115,46 @@ export function buildPrewarmArgs() {
|
|
|
85
115
|
export function buildPrewarmCommandPreview() {
|
|
86
116
|
return `npx ${buildPrewarmArgs().join(" ")}`;
|
|
87
117
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The natural-language prompt handed to the spawned agent session. It is
|
|
120
|
+
* CONFIGURE-ONLY: it derives and applies configuration, presents the capability
|
|
121
|
+
* report, and ends with exactly one indexing-consent question. It never chains
|
|
122
|
+
* /learn-repository and never indexes before explicit consent.
|
|
123
|
+
*
|
|
124
|
+
* The index-consent beat is AGENT-OWNED (asked inside this spawned session), not
|
|
125
|
+
* parent-CLI-owned, because `spawnTerminalTab()` opens an asynchronous terminal
|
|
126
|
+
* session: the parent CLI returns immediately and cannot reliably ask a question
|
|
127
|
+
* that must appear AFTER the spawned session's capability report. Asking it via
|
|
128
|
+
* the parent's `deps.promptLine` would display the question before the report
|
|
129
|
+
* even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
|
|
130
|
+
* prompts (repository / overwrite / invite confirmation) and is intentionally
|
|
131
|
+
* NOT used for this post-report question.
|
|
132
|
+
*/
|
|
133
|
+
export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
|
|
134
|
+
"in the install-spawn context so it SKIPS its Stage 8 and Stage 9 offers — this session's only " +
|
|
135
|
+
"closing interaction is the single indexing question below). Do NOT run /learn-repository. Do NOT " +
|
|
136
|
+
"call parse_repository (or otherwise start indexing) before the capability report and explicit " +
|
|
137
|
+
"consent below. " +
|
|
138
|
+
"Complete the command's read-after-write five-section capability report first: 'Connected ✓', " +
|
|
139
|
+
"'Not yet connected ✗', 'Tools you can use now', 'Tools you'll unlock', and 'Recommended next " +
|
|
140
|
+
"step + why'. " +
|
|
141
|
+
"Only AFTER that report is fully presented, ask exactly one question using this visible prompt: " +
|
|
142
|
+
"'[Y/n] Index repository now?'. Only an explicit affirmative answer (e.g. 'y'/'yes') starts " +
|
|
143
|
+
"indexing; a blank answer, a negative answer, EOF, an unavailable interaction, and any " +
|
|
144
|
+
"non-interactive/headless run all resolve to NO. " +
|
|
145
|
+
"On an affirmative answer: call the parse_repository MCP tool exactly once, describe the accepted " +
|
|
146
|
+
"job as QUEUED, and direct later progress checks to get_parse_status or /check-parse-status " +
|
|
147
|
+
"(do NOT poll it to completion). If parse_repository returns a blocking refusal or error, do NOT " +
|
|
148
|
+
"claim the job was queued — report the sanitized result and leave indexing pending. " +
|
|
149
|
+
"On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
|
|
150
|
+
"continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
|
|
151
|
+
"Never request, echo, or transport any credential — only ever direct the human to that " +
|
|
152
|
+
"integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
|
|
153
|
+
"always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
|
|
154
|
+
"Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
|
|
155
|
+
"End with an explicit summary line stating how many config fields the apply_install_manifest call " +
|
|
156
|
+
"applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending — " +
|
|
157
|
+
"if 0 fields were applied, say so loudly and explain what is still pending.";
|
|
97
158
|
/** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
|
|
98
159
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
99
160
|
/** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
|
|
@@ -107,27 +168,56 @@ export function getInstallBridgeUsage() {
|
|
|
107
168
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
108
169
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
109
170
|
"routing credential, then opens a fresh agent session to derive the remaining",
|
|
110
|
-
"config and
|
|
171
|
+
"config, present a capability report, and offer optional repository indexing.",
|
|
172
|
+
"",
|
|
173
|
+
"Run it bare — `install-bridge` with no flags — in a terminal and it asks",
|
|
174
|
+
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
|
|
175
|
+
"existing-key flow below; answer no and it asks for an email and creates a new",
|
|
176
|
+
"Bridge workspace for you (the self-serve flow). That question is asked ONLY for a",
|
|
177
|
+
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without",
|
|
178
|
+
"an interactive terminal keeps the existing deterministic behavior and no prompt.",
|
|
111
179
|
"",
|
|
112
180
|
"Inputs (the only two irreducible ones):",
|
|
113
181
|
" --api-key <key> Bridge API key. Falls back to the BAPI_API_KEY env var,",
|
|
114
182
|
" then an interactive (no-echo) prompt. Generate one in the",
|
|
115
183
|
" Bridge API web UI Security page — this command consumes a",
|
|
116
|
-
" key, it does not create one (--invite
|
|
117
|
-
"
|
|
118
|
-
" printed or logged.",
|
|
119
|
-
" --repo <name> Repository name.
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
" is
|
|
124
|
-
"
|
|
184
|
+
" key, it does not create one (--email and --invite are the",
|
|
185
|
+
" exceptions: they CREATE the project and its first admin key).",
|
|
186
|
+
" NEVER printed or logged.",
|
|
187
|
+
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
188
|
+
" priority and short-circuit before any network call. When",
|
|
189
|
+
" neither is set, a compatible server resolves the unique",
|
|
190
|
+
" repository from your existing API key automatically; if the",
|
|
191
|
+
" server is older, the key is unresolvable, or resolution",
|
|
192
|
+
" fails, it falls back to an inferred default you confirm",
|
|
193
|
+
" interactively (and to a required --repo when stdin is",
|
|
194
|
+
" non-interactive). In the existing-key flow it MUST match the",
|
|
195
|
+
" server-side repo registration (it keys the credential store",
|
|
196
|
+
" as bapi:<repo>). In either new-project flow (--email,",
|
|
197
|
+
" --invite, or a negative answer to the key question above) it",
|
|
198
|
+
" instead NAMES the project this run creates, so you are asked",
|
|
199
|
+
" to name a new project rather than match an existing one; the",
|
|
200
|
+
" name must be globally unique.",
|
|
201
|
+
"",
|
|
202
|
+
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
203
|
+
" --email <addr> Create a brand-new Bridge workspace from just an email —",
|
|
204
|
+
" the primary path for a first-time user with nothing yet.",
|
|
205
|
+
" It requests a fresh workspace for that email, then creates",
|
|
206
|
+
" the project and mints your own admin API key in one command.",
|
|
207
|
+
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
208
|
+
" interactive prompt — which is also what a negative answer to",
|
|
209
|
+
" the bare-run key question above reaches. The email is NOT a",
|
|
210
|
+
" secret (it is shown as you type), but it is never printed to",
|
|
211
|
+
" a log. Mutually",
|
|
212
|
+
" exclusive with --api-key and --invite. No email verification",
|
|
213
|
+
" is performed and no message is sent to the address — it only",
|
|
214
|
+
" labels the new workspace.",
|
|
125
215
|
"",
|
|
126
216
|
"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).",
|
|
217
|
+
" --invite [token] Redeem a bootstrap invite you were already given: creates",
|
|
218
|
+
" the project and mints your own admin API key in one command.",
|
|
219
|
+
" Mutually exclusive with --api-key and --email (in this mode",
|
|
220
|
+
" the key is created, not consumed).",
|
|
131
221
|
"",
|
|
132
222
|
" Run it WITHOUT a value — `install-bridge --invite` — and the",
|
|
133
223
|
" token is read from an interactive prompt with echo",
|
|
@@ -157,8 +247,10 @@ export function getInstallBridgeUsage() {
|
|
|
157
247
|
"",
|
|
158
248
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
159
249
|
"(default docs/tmp) are read from the environment with the shown fallbacks.",
|
|
160
|
-
"
|
|
161
|
-
"
|
|
250
|
+
"BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
|
|
251
|
+
"visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token",
|
|
252
|
+
"non-interactively (scripting only — it is exposed to shell history; prefer the",
|
|
253
|
+
"prompt).",
|
|
162
254
|
].join("\n");
|
|
163
255
|
}
|
|
164
256
|
/**
|
|
@@ -178,10 +270,12 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
178
270
|
let dryRun = false;
|
|
179
271
|
let agentName = DEFAULT_AGENT_NAME;
|
|
180
272
|
let invite;
|
|
273
|
+
let email;
|
|
181
274
|
// Track SUPPLIED-ness separately from the values: `--invite` is legitimately
|
|
182
275
|
// valueless (prompt path) and `--api-key ""` is still a contradiction with it.
|
|
183
276
|
let inviteSupplied = false;
|
|
184
277
|
let apiKeySupplied = false;
|
|
278
|
+
let emailSupplied = false;
|
|
185
279
|
/** Read a `--flag value` or `--flag=value` value, advancing the index. */
|
|
186
280
|
const readValue = (arg, flag, i) => {
|
|
187
281
|
if (arg.startsWith(`${flag}=`)) {
|
|
@@ -228,6 +322,21 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
228
322
|
}
|
|
229
323
|
continue;
|
|
230
324
|
}
|
|
325
|
+
if (arg === "--email" || arg.startsWith("--email=")) {
|
|
326
|
+
// REQUIRED-value flag (unlike --invite): the email is what selects and drives
|
|
327
|
+
// the self-serve mint, so a bare or blank `--email` is a hard error rather
|
|
328
|
+
// than a prompt trigger. The value is NOT echoed in any error message.
|
|
329
|
+
const r = readValue(arg, "--email", i);
|
|
330
|
+
if ("error" in r)
|
|
331
|
+
return { status: "error", message: r.error };
|
|
332
|
+
if (r.value.trim().length === 0) {
|
|
333
|
+
return { status: "error", message: "--email requires a non-empty value." };
|
|
334
|
+
}
|
|
335
|
+
email = r.value.trim();
|
|
336
|
+
emailSupplied = true;
|
|
337
|
+
i = r.nextIndex;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
231
340
|
if (arg === "--repo" || arg.startsWith("--repo=")) {
|
|
232
341
|
const r = readValue(arg, "--repo", i);
|
|
233
342
|
if ("error" in r)
|
|
@@ -267,9 +376,26 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
267
376
|
"it does not consume an existing one.",
|
|
268
377
|
};
|
|
269
378
|
}
|
|
379
|
+
// --email selects self-serve mode, which CREATES the key — so it conflicts with
|
|
380
|
+
// both consuming an existing one (--api-key) and redeeming a pre-issued invite
|
|
381
|
+
// (--invite). Names the flags only — never the email or token values (BAPI-618).
|
|
382
|
+
if (emailSupplied && apiKeySupplied) {
|
|
383
|
+
return {
|
|
384
|
+
status: "error",
|
|
385
|
+
message: "--email and --api-key are mutually exclusive: self-serve signup creates your API key, " +
|
|
386
|
+
"it does not consume an existing one.",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (emailSupplied && inviteSupplied) {
|
|
390
|
+
return {
|
|
391
|
+
status: "error",
|
|
392
|
+
message: "--email and --invite are mutually exclusive: use --email for self-serve signup (no " +
|
|
393
|
+
"pre-issued invite), or --invite to redeem an invite you already have.",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
270
396
|
return {
|
|
271
397
|
status: "ok",
|
|
272
|
-
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied },
|
|
398
|
+
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email },
|
|
273
399
|
};
|
|
274
400
|
}
|
|
275
401
|
/**
|
|
@@ -330,6 +456,66 @@ export function promptSecretViaReadline(promptText, input = process.stdin, outpu
|
|
|
330
456
|
muted = true;
|
|
331
457
|
});
|
|
332
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Offer to connect GitHub, if it is not already connected (BAPI-631).
|
|
461
|
+
*
|
|
462
|
+
* Entirely best-effort and non-destructive: by the time this runs the install itself is
|
|
463
|
+
* already complete and durable, so nothing here may fail the run. Every branch that is
|
|
464
|
+
* not "the user said yes and it worked" simply proceeds to the agent session.
|
|
465
|
+
*
|
|
466
|
+
* Reuses the shared connect-github flow rather than duplicating the API, polling,
|
|
467
|
+
* browser, or picker logic — there is exactly one implementation of that handshake.
|
|
468
|
+
*/
|
|
469
|
+
async function offerGithubConnection(repoName, deps, log) {
|
|
470
|
+
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
471
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
472
|
+
return;
|
|
473
|
+
try {
|
|
474
|
+
const credDeps = {
|
|
475
|
+
env: deps.env,
|
|
476
|
+
homedir: deps.homedir,
|
|
477
|
+
platform: deps.platform,
|
|
478
|
+
readFile: deps.readFile,
|
|
479
|
+
stat: deps.stat,
|
|
480
|
+
stderr: () => { },
|
|
481
|
+
};
|
|
482
|
+
// Resolve through the shared resolver rather than reusing an in-memory key from
|
|
483
|
+
// this run: the offered flow is the same shell-spawned surface the standalone
|
|
484
|
+
// command uses, and it must resolve credentials the same way (project MCP config
|
|
485
|
+
// env is NOT visible to a spawned shell).
|
|
486
|
+
const cred = await resolveBapiCredentials(repoName, credDeps);
|
|
487
|
+
if (!cred.ok)
|
|
488
|
+
return;
|
|
489
|
+
const api = {
|
|
490
|
+
fetch: deps.fetch,
|
|
491
|
+
baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
|
|
492
|
+
apiKey: cred.credentials.apiKey,
|
|
493
|
+
};
|
|
494
|
+
const state = await fetchGithubConfigurationState(api, repoName);
|
|
495
|
+
if (state === "configured")
|
|
496
|
+
return;
|
|
497
|
+
if (state === "unavailable") {
|
|
498
|
+
// Do NOT fabricate "unconfigured" from a probe that simply failed — offering to
|
|
499
|
+
// connect an already-connected repo is worse than staying quiet.
|
|
500
|
+
log(" note: could not read GitHub configuration status; skipping the GitHub offer.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const answer = (await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();
|
|
504
|
+
if (answer === "n" || answer === "no")
|
|
505
|
+
return; // declining is a normal outcome
|
|
506
|
+
const connectDeps = createDefaultConnectGithubDeps();
|
|
507
|
+
const code = await runGithubConnectionFlow(connectDeps, api, repoName);
|
|
508
|
+
if (code !== 0) {
|
|
509
|
+
log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
|
|
510
|
+
`'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// The install is already durable; a failure here is never fatal to it.
|
|
515
|
+
log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
|
|
516
|
+
`GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
333
519
|
/** Echoed single-line prompt on stderr (used for repo confirmation / value). */
|
|
334
520
|
function promptLineViaReadline(promptText) {
|
|
335
521
|
return new Promise((resolve) => {
|
|
@@ -358,13 +544,25 @@ function promptLineViaReadline(promptText) {
|
|
|
358
544
|
* Always resolves (never rejects): a spawn error / non-zero exit / timeout becomes
|
|
359
545
|
* `{ ok: false, warning }` with secret-free warning text.
|
|
360
546
|
*/
|
|
547
|
+
/**
|
|
548
|
+
* The env clone handed to the pre-warm child, with every credential/PII variable
|
|
549
|
+
* removed: `BAPI_API_KEY` and the bootstrap-invite token (`BAPI_INVITE`) are
|
|
550
|
+
* secrets that must never reach an npm lifecycle script, and the self-serve signup
|
|
551
|
+
* email (`BAPI_SIGNUP_EMAIL`, BAPI-618) is invitee PII with no business in a
|
|
552
|
+
* `--version` probe. Extracted as a pure, exported helper so the strip is directly
|
|
553
|
+
* unit-testable (the default spawner below uses the real `spawn`, which is not
|
|
554
|
+
* injectable).
|
|
555
|
+
*/
|
|
556
|
+
export function sanitizePrewarmEnv(env) {
|
|
557
|
+
const sanitized = { ...env };
|
|
558
|
+
delete sanitized.BAPI_API_KEY;
|
|
559
|
+
delete sanitized.BAPI_INVITE;
|
|
560
|
+
delete sanitized.BAPI_SIGNUP_EMAIL;
|
|
561
|
+
return sanitized;
|
|
562
|
+
}
|
|
361
563
|
function spawnPrewarmDefault(command, args, env) {
|
|
362
564
|
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;
|
|
565
|
+
const sanitizedEnv = sanitizePrewarmEnv(env);
|
|
368
566
|
try {
|
|
369
567
|
const child = spawn(command, args, {
|
|
370
568
|
shell: false,
|
|
@@ -393,6 +591,11 @@ function spawnPrewarmDefault(command, args, env) {
|
|
|
393
591
|
/** Build default deps from the live process. */
|
|
394
592
|
export function createDefaultInstallBridgeDeps() {
|
|
395
593
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
594
|
+
// One production fetch, reused for both the `fetch` seam and the default
|
|
595
|
+
// resolver, so a direct caller of this factory gets a resolver bound to the
|
|
596
|
+
// same client. (runInstallBridgeCli re-binds the resolver to the FINAL merged
|
|
597
|
+
// fetch when the caller did not override the resolver — see below.)
|
|
598
|
+
const productionFetch = (...args) => fetch(...args);
|
|
396
599
|
return {
|
|
397
600
|
env: process.env,
|
|
398
601
|
cwd: process.cwd(),
|
|
@@ -417,7 +620,8 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
417
620
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
418
621
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
419
622
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
420
|
-
fetch:
|
|
623
|
+
fetch: productionFetch,
|
|
624
|
+
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
421
625
|
spawnPrewarm: spawnPrewarmDefault,
|
|
422
626
|
runInit,
|
|
423
627
|
upsertCredential: upsertBapiCredential,
|
|
@@ -496,28 +700,174 @@ export async function resolveInviteToken(options, deps) {
|
|
|
496
700
|
};
|
|
497
701
|
}
|
|
498
702
|
/**
|
|
499
|
-
* Resolve the
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
703
|
+
* Resolve the SELF-SERVE signup email: `--email <addr>` → `BAPI_SIGNUP_EMAIL` env →
|
|
704
|
+
* interactive VISIBLE prompt (BAPI-618). Fails (secret-free) when none is available
|
|
705
|
+
* and stdin is non-interactive.
|
|
706
|
+
*
|
|
707
|
+
* The precedence mirrors {@link resolveInviteToken}, but the input is NOT a secret:
|
|
708
|
+
* an email is not a credential, so it uses the ECHOED {@link InstallBridgeDeps.promptLine}
|
|
709
|
+
* — never `promptSecret`. (It IS invitee PII, so like every value in this module it
|
|
710
|
+
* is never written to a log line; it is only ever echoed as the user's own keystrokes
|
|
711
|
+
* and placed in the mint request body.)
|
|
712
|
+
*
|
|
713
|
+
* No email-format validation is performed here — only surrounding whitespace is
|
|
714
|
+
* trimmed, matching the other CLI input seams and the server's deliberately
|
|
715
|
+
* permissive "stored unvalidated" contract (D-2). The entered value is never echoed
|
|
716
|
+
* back in an error message.
|
|
504
717
|
*/
|
|
505
|
-
export async function
|
|
506
|
-
if (typeof options.
|
|
507
|
-
return { ok: true, value: options.
|
|
718
|
+
export async function resolveSignupEmail(options, deps) {
|
|
719
|
+
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
720
|
+
return { ok: true, value: options.email.trim() };
|
|
508
721
|
}
|
|
509
|
-
const fromEnv = deps.env.
|
|
722
|
+
const fromEnv = deps.env.BAPI_SIGNUP_EMAIL;
|
|
510
723
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
511
724
|
return { ok: true, value: fromEnv.trim() };
|
|
512
725
|
}
|
|
726
|
+
if (deps.isTTY && deps.promptLine) {
|
|
727
|
+
const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
|
|
728
|
+
if (entered.length > 0) {
|
|
729
|
+
return { ok: true, value: entered };
|
|
730
|
+
}
|
|
731
|
+
return { ok: false, error: "No email entered." };
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
ok: false,
|
|
735
|
+
error: "An email is required to create a Bridge workspace. Pass --email <addr> or set the " +
|
|
736
|
+
"BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt " +
|
|
737
|
+
"for it).",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Select the onboarding branch as pure logic (deterministic, no I/O, no prompt):
|
|
742
|
+
*
|
|
743
|
+
* - `--invite` (with or without a value) or a non-empty `BAPI_INVITE` selects
|
|
744
|
+
* the `bootstrap-invite` need-key branch.
|
|
745
|
+
* - otherwise, `--email` or a non-empty `BAPI_SIGNUP_EMAIL` selects the
|
|
746
|
+
* `self-serve` need-key branch (BAPI-618).
|
|
747
|
+
* - every other invocation selects `have-key`.
|
|
748
|
+
*
|
|
749
|
+
* Precedence is explicit: an invite input wins over an email input (both are
|
|
750
|
+
* need-key, but a pre-issued invite is the more specific intent), and any
|
|
751
|
+
* need-key input wins over the have-key default. The parser already rejects
|
|
752
|
+
* `--email` alongside `--api-key`/`--invite`, so a flag-level conflict never
|
|
753
|
+
* reaches here; the precedence only disambiguates env-var combinations.
|
|
754
|
+
*/
|
|
755
|
+
export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
756
|
+
const inviteMode = options.inviteMode === true || (env.BAPI_INVITE ?? "").trim().length > 0;
|
|
757
|
+
if (inviteMode)
|
|
758
|
+
return { kind: "need-key", method: "bootstrap-invite" };
|
|
759
|
+
const emailMode = (options.email ?? "").trim().length > 0 ||
|
|
760
|
+
(env.BAPI_SIGNUP_EMAIL ?? "").trim().length > 0;
|
|
761
|
+
if (emailMode)
|
|
762
|
+
return { kind: "need-key", method: "self-serve" };
|
|
763
|
+
return { kind: "have-key" };
|
|
764
|
+
}
|
|
765
|
+
/** The exact visible text of the bare-TTY onboarding selector (BAPI-626). */
|
|
766
|
+
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Do you have a Bridge API key? [Y/n] ";
|
|
767
|
+
/**
|
|
768
|
+
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
769
|
+
*
|
|
770
|
+
* The pure resolver defaults every un-signalled invocation to `have-key`, which is
|
|
771
|
+
* correct for a script but wrong for a human: a first-time user with nothing yet
|
|
772
|
+
* runs a bare `install-bridge`, gets the hidden API-key prompt, and has no way to
|
|
773
|
+
* discover that the self-serve email path exists. So a BARE INTERACTIVE run — and
|
|
774
|
+
* only that — is asked which branch it wants.
|
|
775
|
+
*
|
|
776
|
+
* "Bare" is deliberately strict. Any of these keeps the existing deterministic
|
|
777
|
+
* behaviour with NO prompt:
|
|
778
|
+
*
|
|
779
|
+
* - stdin is not a TTY, or no `promptLine` seam is available (scripts, CI);
|
|
780
|
+
* - any CLI argument was supplied (the user already stated an intent);
|
|
781
|
+
* - a non-blank `BAPI_API_KEY` is present (that IS the existing-key intent);
|
|
782
|
+
* - the pure resolver already chose a need-key branch explicitly.
|
|
783
|
+
*
|
|
784
|
+
* A prompt failure becomes a typed, secret-free failure rather than an exception:
|
|
785
|
+
* the caller turns it into an exit code, and terminal/internal error text never
|
|
786
|
+
* reaches the user.
|
|
787
|
+
*/
|
|
788
|
+
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
789
|
+
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
790
|
+
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
791
|
+
if (branch.kind === "need-key")
|
|
792
|
+
return { ok: true, branch };
|
|
793
|
+
const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
|
|
794
|
+
const isBareInvocation = argv.length === 0;
|
|
795
|
+
if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
|
|
796
|
+
return { ok: true, branch };
|
|
797
|
+
}
|
|
798
|
+
const promptLine = deps.promptLine;
|
|
799
|
+
try {
|
|
800
|
+
// Bounded so a prompt seam that returns the same invalid value forever (a
|
|
801
|
+
// misbehaving pipe that passes the TTY check) cannot spin indefinitely.
|
|
802
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
803
|
+
const answer = (await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();
|
|
804
|
+
// Blank = accept the bracketed default (Y), matching the prompt's own contract.
|
|
805
|
+
if (answer.length === 0 || answer === "y" || answer === "yes") {
|
|
806
|
+
return { ok: true, branch: { kind: "have-key" } };
|
|
807
|
+
}
|
|
808
|
+
if (answer === "n" || answer === "no") {
|
|
809
|
+
return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
|
|
810
|
+
}
|
|
811
|
+
deps.log("Please answer y or n (press Enter for yes).");
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
error: "No valid answer to the Bridge API key question. Re-run and answer y or n.",
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// Secret-free by construction: the caught value is never surfaced.
|
|
820
|
+
return {
|
|
821
|
+
ok: false,
|
|
822
|
+
error: "Could not read your answer from the terminal. Re-run with --api-key <key> if you have a " +
|
|
823
|
+
"Bridge API key, or --email <addr> to create a new Bridge workspace.",
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Return the explicitly configured repository name (`--repo`, then
|
|
829
|
+
* `BAPI_REPO_NAME`), trimmed, or `undefined` when neither is supplied. Pure: no
|
|
830
|
+
* prompting, no inference, no I/O. `--repo` takes priority over the environment.
|
|
831
|
+
* This is the deterministic short-circuit that runs BEFORE any server resolution.
|
|
832
|
+
*/
|
|
833
|
+
export function resolveConfiguredRepoName(options, env) {
|
|
834
|
+
if (typeof options.repo === "string" && options.repo.trim().length > 0) {
|
|
835
|
+
return options.repo.trim();
|
|
836
|
+
}
|
|
837
|
+
const fromEnv = env.BAPI_REPO_NAME;
|
|
838
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
839
|
+
return fromEnv.trim();
|
|
840
|
+
}
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Resolve the repo name: `--repo` → `BAPI_REPO_NAME` env → inferred default
|
|
845
|
+
* (from .bridge/config, else the cwd basename) confirmed interactively. Fails
|
|
846
|
+
* fast (no inference) when neither is supplied and stdin is non-interactive —
|
|
847
|
+
* the repo identity keys the credential store, so it is never silently inferred
|
|
848
|
+
* non-interactively.
|
|
849
|
+
*
|
|
850
|
+
* `mode` selects only the WORDING (prompt and non-interactive error); resolution
|
|
851
|
+
* order, inference, and validation are identical in both. It defaults to
|
|
852
|
+
* `existing-registration` to preserve the behaviour of pre-BAPI-626 callers.
|
|
853
|
+
*/
|
|
854
|
+
export async function resolveRepoName(options, deps, mode = "existing-registration") {
|
|
855
|
+
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
856
|
+
if (configured !== undefined) {
|
|
857
|
+
return { ok: true, value: configured };
|
|
858
|
+
}
|
|
513
859
|
// Non-interactive: fail fast and require --repo. Inference is an interactive
|
|
514
860
|
// convenience only (the user must confirm it).
|
|
515
861
|
if (!deps.isTTY || !deps.promptLine) {
|
|
516
862
|
return {
|
|
517
863
|
ok: false,
|
|
518
|
-
error:
|
|
519
|
-
"
|
|
520
|
-
|
|
864
|
+
error: mode === "new-project"
|
|
865
|
+
? "A project name is required. Pass --repo or set the BAPI_REPO_NAME environment " +
|
|
866
|
+
"variable (no interactive terminal is available to confirm an inferred name). It " +
|
|
867
|
+
"names the new Bridge project this run creates and must be globally unique."
|
|
868
|
+
: "A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable " +
|
|
869
|
+
"(no interactive terminal is available to confirm an inferred name). It must match the " +
|
|
870
|
+
"server-side repository registration.",
|
|
521
871
|
};
|
|
522
872
|
}
|
|
523
873
|
// Infer a sensible default: existing .bridge/config, else the cwd basename.
|
|
@@ -532,13 +882,19 @@ export async function resolveRepoName(options, deps) {
|
|
|
532
882
|
inferred = validated.value;
|
|
533
883
|
}
|
|
534
884
|
if (inferred) {
|
|
535
|
-
const
|
|
885
|
+
const promptText = mode === "new-project"
|
|
886
|
+
? `Name your new Bridge project [${inferred}]: `
|
|
887
|
+
: `Repo name [${inferred}] (must match server-side registration): `;
|
|
888
|
+
const answer = (await deps.promptLine(promptText)).trim();
|
|
536
889
|
const chosen = answer.length > 0 ? answer : inferred;
|
|
537
890
|
if (chosen.length > 0)
|
|
538
891
|
return { ok: true, value: chosen };
|
|
539
892
|
}
|
|
540
893
|
else {
|
|
541
|
-
const
|
|
894
|
+
const promptText = mode === "new-project"
|
|
895
|
+
? "Name your new Bridge project: "
|
|
896
|
+
: "Repo name (must match server-side registration): ";
|
|
897
|
+
const answer = (await deps.promptLine(promptText)).trim();
|
|
542
898
|
if (answer.length > 0)
|
|
543
899
|
return { ok: true, value: answer };
|
|
544
900
|
}
|
|
@@ -668,11 +1024,13 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
668
1024
|
signal: AbortSignal.timeout(10_000),
|
|
669
1025
|
});
|
|
670
1026
|
}
|
|
671
|
-
catch
|
|
672
|
-
|
|
1027
|
+
catch {
|
|
1028
|
+
// The caught exception message is NOT interpolated: some fetch/undici errors
|
|
1029
|
+
// echo request detail (which carries the X-API-Key header) into the message,
|
|
1030
|
+
// so a fixed, secret-free string is used instead (BAPI-616).
|
|
673
1031
|
return {
|
|
674
1032
|
ok: false,
|
|
675
|
-
message: `Could not reach the Bridge API at ${baseUrl}
|
|
1033
|
+
message: `Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`,
|
|
676
1034
|
};
|
|
677
1035
|
}
|
|
678
1036
|
if (resp.ok)
|
|
@@ -697,6 +1055,54 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
697
1055
|
message: `Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`,
|
|
698
1056
|
};
|
|
699
1057
|
}
|
|
1058
|
+
/** The resolve endpoint: `<base>/setup/resolve-repo`. No query string — ever. */
|
|
1059
|
+
export function buildResolveRepoUrl(baseUrl) {
|
|
1060
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/resolve-repo`;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Resolve the repository name server-side from an existing API key. The key
|
|
1064
|
+
* travels ONLY in the `X-API-Key` header (never the URL, never the body), bounded
|
|
1065
|
+
* by the same 10s timeout as {@link verifyConnectivity}.
|
|
1066
|
+
*
|
|
1067
|
+
* Status mapping is the whole compatibility story: `404` → `not-deployed` (old
|
|
1068
|
+
* server), `409` → `unresolved` (ambiguous/shared/client-scoped/legacy), every
|
|
1069
|
+
* other non-OK (`401`, `403`, `5xx`, unexpected) → `error`, and any
|
|
1070
|
+
* network/timeout/malformed-body failure → `error`. A caught exception or a
|
|
1071
|
+
* response body is NEVER read into the result.
|
|
1072
|
+
*/
|
|
1073
|
+
export async function resolveRepoViaServer(fetchImpl, baseUrl, apiKey) {
|
|
1074
|
+
const url = buildResolveRepoUrl(baseUrl);
|
|
1075
|
+
let resp;
|
|
1076
|
+
try {
|
|
1077
|
+
resp = await fetchImpl(url, {
|
|
1078
|
+
headers: { "X-API-Key": apiKey },
|
|
1079
|
+
signal: AbortSignal.timeout(10_000),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
// Network failure / timeout. The exception may echo the request (and thus the
|
|
1084
|
+
// X-API-Key header), so it is never inspected or propagated.
|
|
1085
|
+
return { status: "error" };
|
|
1086
|
+
}
|
|
1087
|
+
if (resp.status === 404)
|
|
1088
|
+
return { status: "not-deployed" };
|
|
1089
|
+
if (resp.status === 409)
|
|
1090
|
+
return { status: "unresolved" };
|
|
1091
|
+
if (!resp.ok)
|
|
1092
|
+
return { status: "error" };
|
|
1093
|
+
let body;
|
|
1094
|
+
try {
|
|
1095
|
+
body = await resp.json();
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
return { status: "error" };
|
|
1099
|
+
}
|
|
1100
|
+
const repoName = body?.repo_name;
|
|
1101
|
+
const validated = validateRepoName(repoName);
|
|
1102
|
+
if (!validated.ok)
|
|
1103
|
+
return { status: "error" };
|
|
1104
|
+
return { status: "resolved", repoName: validated.value };
|
|
1105
|
+
}
|
|
700
1106
|
// ---------------------------------------------------------------------------
|
|
701
1107
|
// Bootstrap-invite exchange (BAPI-606) — replaces the pre-flight ping in invite mode
|
|
702
1108
|
// ---------------------------------------------------------------------------
|
|
@@ -796,6 +1202,76 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
796
1202
|
message: `The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`,
|
|
797
1203
|
};
|
|
798
1204
|
}
|
|
1205
|
+
// ---------------------------------------------------------------------------
|
|
1206
|
+
// Self-serve mint (BAPI-618): email -> invite token
|
|
1207
|
+
// ---------------------------------------------------------------------------
|
|
1208
|
+
/**
|
|
1209
|
+
* The self-serve invite-token prefix, mirroring the server's
|
|
1210
|
+
* ``BOOTSTRAP_INVITE_TOKEN_PREFIX`` (``bapi_inv_``). A well-formed token
|
|
1211
|
+
* self-identifies with this prefix; a success response whose token does not is
|
|
1212
|
+
* treated as a `failed` mint rather than fed into the redemption path.
|
|
1213
|
+
*/
|
|
1214
|
+
export const BOOTSTRAP_INVITE_TOKEN_PREFIX = "bapi_inv_";
|
|
1215
|
+
/** The self-serve mint endpoint: `<base>/setup/bootstrap/self-serve`. No query string — ever. */
|
|
1216
|
+
export function buildSelfServeMintUrl(baseUrl) {
|
|
1217
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* POST the self-serve mint: an email in, an invite token out. Mirrors
|
|
1221
|
+
* {@link exchangeBootstrapInvite}'s fetch/timeout/secret-free-result discipline.
|
|
1222
|
+
*
|
|
1223
|
+
* The email travels ONLY in the JSON request body (never a query string, never a
|
|
1224
|
+
* header, never a log line). Bounded by the same 10s timeout as the exchange and
|
|
1225
|
+
* the connectivity ping. On any non-success status the failure body is NOT read —
|
|
1226
|
+
* the status alone decides the category — because an untrusted upstream body could
|
|
1227
|
+
* echo the email or carry internals. A caught fetch exception is likewise never
|
|
1228
|
+
* inspected or interpolated; some fetch impls embed the request (and thus the
|
|
1229
|
+
* email) in the message.
|
|
1230
|
+
*/
|
|
1231
|
+
export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
1232
|
+
const url = buildSelfServeMintUrl(baseUrl);
|
|
1233
|
+
let resp;
|
|
1234
|
+
try {
|
|
1235
|
+
resp = await deps.fetch(url, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
headers: { "Content-Type": "application/json" },
|
|
1238
|
+
body: JSON.stringify({ invitee_email: email }),
|
|
1239
|
+
signal: AbortSignal.timeout(10_000),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
catch (err) {
|
|
1243
|
+
// The message may contain the request (some fetch impls echo it), so it is
|
|
1244
|
+
// deliberately NOT inspected or interpolated here.
|
|
1245
|
+
void err;
|
|
1246
|
+
return { ok: false, category: "failed" };
|
|
1247
|
+
}
|
|
1248
|
+
if (resp.ok) {
|
|
1249
|
+
let token;
|
|
1250
|
+
try {
|
|
1251
|
+
const body = (await resp.json());
|
|
1252
|
+
token = body?.token;
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
// An unreadable success body is not actionable and must not be exposed.
|
|
1256
|
+
return { ok: false, category: "failed" };
|
|
1257
|
+
}
|
|
1258
|
+
if (typeof token !== "string" ||
|
|
1259
|
+
token.trim().length === 0 ||
|
|
1260
|
+
!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)) {
|
|
1261
|
+
return { ok: false, category: "failed" };
|
|
1262
|
+
}
|
|
1263
|
+
return { ok: true, token };
|
|
1264
|
+
}
|
|
1265
|
+
// Non-success: the status alone decides the category. The failure body is NEVER
|
|
1266
|
+
// read — it is untrusted and could echo the email or leak internals.
|
|
1267
|
+
if (resp.status === 429) {
|
|
1268
|
+
return { ok: false, category: "rate-limited" };
|
|
1269
|
+
}
|
|
1270
|
+
if (resp.status === 400 || resp.status === 422) {
|
|
1271
|
+
return { ok: false, category: "invalid" };
|
|
1272
|
+
}
|
|
1273
|
+
return { ok: false, category: "failed" };
|
|
1274
|
+
}
|
|
799
1275
|
/**
|
|
800
1276
|
* The 401 message for a run that had to GENERATE a fresh secret — i.e. no local
|
|
801
1277
|
* pending record existed. Besides a simply-wrong token, this is exactly what a lost
|
|
@@ -844,8 +1320,10 @@ export function buildDryRunPreview(plan) {
|
|
|
844
1320
|
if (plan.bootstrapInvite)
|
|
845
1321
|
return buildBootstrapDryRunPreview(plan);
|
|
846
1322
|
return [
|
|
847
|
-
|
|
848
|
-
|
|
1323
|
+
plan.attemptedServerResolution
|
|
1324
|
+
? "install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
|
|
1325
|
+
: "install-bridge --dry-run (no writes, no network, no spawns)",
|
|
1326
|
+
`Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
|
|
849
1327
|
`Base URL (ping): ${plan.baseUrl}`,
|
|
850
1328
|
`Docs dir: ${plan.docsDir}`,
|
|
851
1329
|
`Agent: ${plan.agentName}`,
|
|
@@ -860,7 +1338,29 @@ export function buildDryRunPreview(plan) {
|
|
|
860
1338
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
|
|
861
1339
|
MCP_TIMEOUT_GUIDANCE,
|
|
862
1340
|
`Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
|
|
863
|
-
|
|
1341
|
+
...buildLaunchStepPreview(plan),
|
|
1342
|
+
];
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* The Step 5 preview lines, shared by both previews so the launch description
|
|
1346
|
+
* cannot drift between the have-key and need-key flows.
|
|
1347
|
+
*
|
|
1348
|
+
* A --dry-run never writes the launch script, so the preview describes the
|
|
1349
|
+
* materialization rather than performing it — but it still shows the full command,
|
|
1350
|
+
* because the command is what the user is previewing and it is secret-free.
|
|
1351
|
+
*/
|
|
1352
|
+
function buildLaunchStepPreview(plan) {
|
|
1353
|
+
return [
|
|
1354
|
+
// BAPI-631: described, never performed in --dry-run — a preview must not open a
|
|
1355
|
+
// browser or reach the network. It is also strictly optional, so it carries no step
|
|
1356
|
+
// number of its own and never changes the 5-step count.
|
|
1357
|
+
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
1358
|
+
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
1359
|
+
" interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
|
|
1360
|
+
"Step 5 — spawn agent session: the full command below is stored in a restricted launch script",
|
|
1361
|
+
" (mode 0600, under the system temp dir) and only a short sourced runner is spawned",
|
|
1362
|
+
" (the script itself is NOT written in --dry-run):",
|
|
1363
|
+
` ${plan.spawnCommand}`,
|
|
864
1364
|
];
|
|
865
1365
|
}
|
|
866
1366
|
/**
|
|
@@ -872,14 +1372,35 @@ export function buildDryRunPreview(plan) {
|
|
|
872
1372
|
*/
|
|
873
1373
|
function buildBootstrapDryRunPreview(plan) {
|
|
874
1374
|
const pendingTarget = `bootstrap-pending:${plan.repoName}`;
|
|
1375
|
+
// Self-serve signup (BAPI-618): the ONLY structural difference from a pre-issued
|
|
1376
|
+
// invite is a mint-from-email step (Step 2·pre) that turns an email into the
|
|
1377
|
+
// invite token before the unchanged redemption below. The preview states plainly
|
|
1378
|
+
// that in --dry-run that signup/mint is previewed-and-skipped: no account is
|
|
1379
|
+
// created, no mint request is sent, no email leaves the machine. The email and
|
|
1380
|
+
// any synthetic token are deliberately absent from this output.
|
|
1381
|
+
const header = plan.selfServeSignup
|
|
1382
|
+
? "install-bridge --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
|
|
1383
|
+
: "install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)";
|
|
1384
|
+
const repoLine = plan.selfServeSignup
|
|
1385
|
+
? `Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`
|
|
1386
|
+
: `Repo name: ${plan.repoName} (created by the exchange; globally unique)`;
|
|
1387
|
+
const selfServeStep = plan.selfServeSignup
|
|
1388
|
+
? [
|
|
1389
|
+
"Step 2·pre — self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace",
|
|
1390
|
+
" signup is requested, no mint call is made, and no email is sent or transmitted;",
|
|
1391
|
+
" a real run would request a fresh workspace for your email and receive an invite",
|
|
1392
|
+
" token, which then feeds the SAME redemption protocol below.",
|
|
1393
|
+
]
|
|
1394
|
+
: [];
|
|
875
1395
|
return [
|
|
876
|
-
|
|
877
|
-
|
|
1396
|
+
header,
|
|
1397
|
+
repoLine,
|
|
878
1398
|
`Base URL: ${plan.baseUrl}`,
|
|
879
1399
|
`Docs dir: ${plan.docsDir}`,
|
|
880
1400
|
`Agent: ${plan.agentName}`,
|
|
881
1401
|
"",
|
|
882
1402
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1403
|
+
...selfServeStep,
|
|
883
1404
|
`Step 2a — generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${plan.credentialStorePath}`,
|
|
884
1405
|
" BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.",
|
|
885
1406
|
`Step 2b — redeem the bootstrap invite (replaces the pre-flight ping — there is no key yet):`,
|
|
@@ -894,7 +1415,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
894
1415
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
|
|
895
1416
|
MCP_TIMEOUT_GUIDANCE,
|
|
896
1417
|
`Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
|
|
897
|
-
|
|
1418
|
+
...buildLaunchStepPreview(plan),
|
|
898
1419
|
];
|
|
899
1420
|
}
|
|
900
1421
|
/**
|
|
@@ -962,6 +1483,12 @@ function buildManualHostInstructions(entry, editors) {
|
|
|
962
1483
|
*/
|
|
963
1484
|
export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
964
1485
|
const deps = { ...createDefaultInstallBridgeDeps(), ...overrides };
|
|
1486
|
+
// When the caller overrode only `fetch` (the common test seam), bind the
|
|
1487
|
+
// default resolver to the FINAL merged fetch so server resolution still routes
|
|
1488
|
+
// through the override. An explicit `resolveRepoViaServer` override is honored.
|
|
1489
|
+
if (!overrides.resolveRepoViaServer) {
|
|
1490
|
+
deps.resolveRepoViaServer = (baseUrl, apiKey) => resolveRepoViaServer(deps.fetch, baseUrl, apiKey);
|
|
1491
|
+
}
|
|
965
1492
|
const { log, errorLog } = deps;
|
|
966
1493
|
const parsed = parseInstallBridgeArgs(argv);
|
|
967
1494
|
if (parsed.status === "help") {
|
|
@@ -975,18 +1502,42 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
975
1502
|
return 1;
|
|
976
1503
|
}
|
|
977
1504
|
const options = parsed.options;
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
981
|
-
|
|
1505
|
+
// Onboarding branch: have-key vs. a need-key method (`bootstrap-invite` = redeem
|
|
1506
|
+
// a pre-issued invite, `self-serve` = mint one from an email, BAPI-618). BOTH
|
|
1507
|
+
// need-key methods share the downstream redemption protocol, so
|
|
1508
|
+
// `bootstrapInviteMode` is true for both; `selfServeSignupMode` discriminates the
|
|
1509
|
+
// one extra step (mint-from-email) the self-serve path adds.
|
|
1510
|
+
//
|
|
1511
|
+
// The selection is pure and deterministic for every explicit, env-driven, and
|
|
1512
|
+
// non-TTY invocation; only a BARE interactive run is asked which branch it wants
|
|
1513
|
+
// (BAPI-626 — otherwise a first-time user can never reach self-serve).
|
|
1514
|
+
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
|
|
1515
|
+
if (!branchResult.ok) {
|
|
1516
|
+
errorLog(`Error: ${branchResult.error}`);
|
|
1517
|
+
return 1;
|
|
1518
|
+
}
|
|
1519
|
+
const branch = branchResult.branch;
|
|
1520
|
+
const bootstrapInviteMode = branch.kind === "need-key";
|
|
1521
|
+
const selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
|
|
982
1522
|
// ---- 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
|
|
1523
|
+
// Resolve the credential/entry input first so a fully-empty non-interactive
|
|
1524
|
+
// invocation fails with the (more relevant) missing-input message before the repo
|
|
1525
|
+
// one. In either need-key mode there is no API key to resolve — the exchange
|
|
1526
|
+
// MINTS it. Self-serve resolves the email here (up-front, before the dry-run
|
|
1527
|
+
// guard) so a missing email fails fast; the actual MINT happens later, strictly
|
|
1528
|
+
// after the dry-run return, so a preview neither mints nor sends anything.
|
|
987
1529
|
let apiKey = "";
|
|
988
1530
|
let inviteToken = "";
|
|
989
|
-
|
|
1531
|
+
let signupEmail = "";
|
|
1532
|
+
if (selfServeSignupMode) {
|
|
1533
|
+
const emailResult = await resolveSignupEmail(options, deps);
|
|
1534
|
+
if (!emailResult.ok) {
|
|
1535
|
+
errorLog(`Error: ${emailResult.error}`);
|
|
1536
|
+
return 1;
|
|
1537
|
+
}
|
|
1538
|
+
signupEmail = emailResult.value;
|
|
1539
|
+
}
|
|
1540
|
+
else if (bootstrapInviteMode) {
|
|
990
1541
|
const inviteResult = await resolveInviteToken(options, deps);
|
|
991
1542
|
if (!inviteResult.ok) {
|
|
992
1543
|
errorLog(`Error: ${inviteResult.error}`);
|
|
@@ -1002,25 +1553,61 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1002
1553
|
}
|
|
1003
1554
|
apiKey = keyResult.value;
|
|
1004
1555
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1556
|
+
// baseUrl is resolved BEFORE repository input because the have-key branch needs
|
|
1557
|
+
// it for the server-resolution request.
|
|
1558
|
+
const baseUrl = deps.env.BAPI_BASE_URL ?? DEFAULT_BAPI_BASE_URL;
|
|
1559
|
+
const docsDir = deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR;
|
|
1560
|
+
// ---- Resolve the repository name ----
|
|
1561
|
+
// Need-key (invite or self-serve): choose-a-name for the project this run is
|
|
1562
|
+
// about to create, so the prompt says exactly that. Have-key:
|
|
1563
|
+
// `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve
|
|
1564
|
+
// it server-side from the API key (BAPI-616), and on ANY non-resolution outcome
|
|
1565
|
+
// (unresolved / not-deployed / error) fall back to the existing local
|
|
1566
|
+
// prompt/inference — never a hard failure. That fallback keeps the
|
|
1567
|
+
// existing-registration wording: there the name must match a real project.
|
|
1568
|
+
let repoName;
|
|
1569
|
+
let attemptedServerResolution = false;
|
|
1014
1570
|
if (bootstrapInviteMode) {
|
|
1015
|
-
const
|
|
1571
|
+
const repoResult = await resolveRepoName(options, deps, "new-project");
|
|
1572
|
+
if (!repoResult.ok) {
|
|
1573
|
+
errorLog(`Error: ${repoResult.error}`);
|
|
1574
|
+
return 1;
|
|
1575
|
+
}
|
|
1576
|
+
// Invite mode creates the project, so the name must be valid BEFORE any
|
|
1577
|
+
// pending state or exchange.
|
|
1578
|
+
const validated = validateRepoName(repoResult.value);
|
|
1016
1579
|
if (!validated.ok) {
|
|
1017
1580
|
errorLog(`Error: invalid repo name — ${validated.error}.`);
|
|
1018
1581
|
return 1;
|
|
1019
1582
|
}
|
|
1020
1583
|
repoName = validated.value;
|
|
1021
1584
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1585
|
+
else {
|
|
1586
|
+
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
1587
|
+
if (configured !== undefined) {
|
|
1588
|
+
// Deterministic short-circuit: no server round-trip when the name is known.
|
|
1589
|
+
repoName = configured;
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
attemptedServerResolution = true;
|
|
1593
|
+
log("Resolving repository…");
|
|
1594
|
+
const resolution = await deps.resolveRepoViaServer(baseUrl, apiKey);
|
|
1595
|
+
if (resolution.status === "resolved") {
|
|
1596
|
+
repoName = resolution.repoName;
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
// Feature-detect + degrade: 404 (old server), 409 (unresolved/ambiguous/
|
|
1600
|
+
// client-scoped), and network/other errors all fall back to the existing
|
|
1601
|
+
// local resolution WITHOUT a cause-specific message or leaked detail.
|
|
1602
|
+
const repoResult = await resolveRepoName(options, deps, "existing-registration");
|
|
1603
|
+
if (!repoResult.ok) {
|
|
1604
|
+
errorLog(`Error: ${repoResult.error}`);
|
|
1605
|
+
return 1;
|
|
1606
|
+
}
|
|
1607
|
+
repoName = repoResult.value;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1024
1611
|
const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
|
|
1025
1612
|
const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
1026
1613
|
const credentialStorePath = getPrimaryCredentialStorePath({
|
|
@@ -1045,6 +1632,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1045
1632
|
...(bootstrapInviteMode
|
|
1046
1633
|
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
1047
1634
|
: {}),
|
|
1635
|
+
...(selfServeSignupMode ? { selfServeSignup: true } : {}),
|
|
1636
|
+
...(attemptedServerResolution ? { attemptedServerResolution: true } : {}),
|
|
1048
1637
|
};
|
|
1049
1638
|
// ---- --dry-run: preview every step, strictly no side effects ----
|
|
1050
1639
|
// Positioned BEFORE the CSPRNG, the pending write, the exchange, the scaffold,
|
|
@@ -1055,6 +1644,31 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1055
1644
|
log(line);
|
|
1056
1645
|
return 0;
|
|
1057
1646
|
}
|
|
1647
|
+
// ---- Materialize the Step 5 launch command BEFORE any install side effect ----
|
|
1648
|
+
// `spawnCommand` embeds the whole INSTALL_BRIDGE_AGENT_PROMPT and is multiple KB
|
|
1649
|
+
// — far past what osascript can type into a Terminal/iTerm tab, which is why the
|
|
1650
|
+
// spawn silently delivered a truncated line. It must travel via a launch script.
|
|
1651
|
+
//
|
|
1652
|
+
// This runs HERE, before the scaffold/mint/credential/config writes, because a
|
|
1653
|
+
// command that cannot be launched safely is a whole-run failure, not a Step 5
|
|
1654
|
+
// warning: the deterministic setup would otherwise complete and leave the user
|
|
1655
|
+
// in the "configured but never configured by the agent" state the warning below
|
|
1656
|
+
// explicitly calls out. Failing first means nothing is half-done.
|
|
1657
|
+
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
|
|
1658
|
+
if (!materialized.ok) {
|
|
1659
|
+
errorLog(`Error: ${materialized.error}`);
|
|
1660
|
+
return 1;
|
|
1661
|
+
}
|
|
1662
|
+
const launchCommand = materialized.command;
|
|
1663
|
+
// Independent final guard on the line ACTUALLY handed to the terminal — not on
|
|
1664
|
+
// the original command. It catches the two ways a "successful" materialization
|
|
1665
|
+
// can still be unlaunchable: no writer seam (inline command preserved verbatim),
|
|
1666
|
+
// and a runner whose script path is unexpectedly long.
|
|
1667
|
+
if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
|
|
1668
|
+
errorLog("Error: the agent session command is too long to send to the terminal safely. " +
|
|
1669
|
+
"Check that the system temporary directory is writable so the launch script can be used.");
|
|
1670
|
+
return 1;
|
|
1671
|
+
}
|
|
1058
1672
|
const credentialWriteDeps = {
|
|
1059
1673
|
env: deps.env,
|
|
1060
1674
|
homedir: deps.homedir,
|
|
@@ -1091,6 +1705,32 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1091
1705
|
// ---- Step 2 — mint (invite mode) or verify (normal mode) the credential ----
|
|
1092
1706
|
let inviteFingerprint = "";
|
|
1093
1707
|
if (bootstrapInviteMode) {
|
|
1708
|
+
// ---- Self-serve signup (BAPI-618): mint the invite token from the email ----
|
|
1709
|
+
// This is the ONE step that precedes the unchanged BAPI-606 redemption below.
|
|
1710
|
+
// It sits AFTER the --dry-run return above, so a preview never mints, never
|
|
1711
|
+
// sends the email, and never writes a pending secret. On success the minted
|
|
1712
|
+
// token is assigned to `inviteToken` and the flow falls into the existing
|
|
1713
|
+
// persist-before-exchange block VERBATIM — identical to a --invite token from
|
|
1714
|
+
// here on. On failure we return BEFORE any CSPRNG/pending write, so nothing is
|
|
1715
|
+
// persisted. The email and the token are never logged.
|
|
1716
|
+
if (selfServeSignupMode) {
|
|
1717
|
+
log("Step 2/5 — requesting Bridge self-serve setup…");
|
|
1718
|
+
const mint = await mintSelfServeInvite(deps, baseUrl, signupEmail);
|
|
1719
|
+
if (!mint.ok) {
|
|
1720
|
+
if (mint.category === "rate-limited") {
|
|
1721
|
+
errorLog("Error: Self-serve setup is temporarily rate limited. Try again later.");
|
|
1722
|
+
}
|
|
1723
|
+
else if (mint.category === "invalid") {
|
|
1724
|
+
errorLog("Error: Self-serve setup could not be requested. Check the email value and try again.");
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
errorLog("Error: Unable to complete self-serve setup. Check connectivity and retry.");
|
|
1728
|
+
}
|
|
1729
|
+
return 1;
|
|
1730
|
+
}
|
|
1731
|
+
// Treated identically to a manually supplied invite token from here on.
|
|
1732
|
+
inviteToken = mint.token;
|
|
1733
|
+
}
|
|
1094
1734
|
// ===================================================================
|
|
1095
1735
|
// LOAD-BEARING ORDERING — NOT DEFENSIVE POLISH. DO NOT "TIDY" THIS.
|
|
1096
1736
|
//
|
|
@@ -1318,10 +1958,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1318
1958
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
1319
1959
|
}
|
|
1320
1960
|
}
|
|
1961
|
+
// ---- optional GitHub connect offer (BAPI-631) ----
|
|
1962
|
+
// Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
|
|
1963
|
+
// the agent spawn, for two reasons: the spawned session's capability report should
|
|
1964
|
+
// observe GitHub as configured if the user connects it here, and running it after the
|
|
1965
|
+
// spawn would put two prompts on the same terminal at once.
|
|
1966
|
+
await offerGithubConnection(repoName, deps, log);
|
|
1321
1967
|
// ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
|
|
1322
|
-
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge +
|
|
1968
|
+
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge configuration + capability report…`);
|
|
1323
1969
|
const terminal = detectTerminal(undefined, deps.env);
|
|
1324
|
-
|
|
1970
|
+
// Only the validated short runner reaches the terminal — never the inline prompt.
|
|
1971
|
+
const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
|
|
1325
1972
|
key: "install",
|
|
1326
1973
|
worktreePath: deps.cwd,
|
|
1327
1974
|
});
|
|
@@ -1332,16 +1979,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1332
1979
|
// user can run the agentic remainder by hand. Mirrors start-tickets treating a
|
|
1333
1980
|
// spawn failure as non-fatal to the run.
|
|
1334
1981
|
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
|
-
"
|
|
1982
|
+
"The project is NOT configured yet — run /install-bridge manually in this project to derive " +
|
|
1983
|
+
"and apply the config fields and see the capability report, then choose whether to run " +
|
|
1984
|
+
"/parse-repository to index the repository.");
|
|
1337
1985
|
return 0;
|
|
1338
1986
|
}
|
|
1339
1987
|
log("");
|
|
1340
|
-
log(`install-bridge setup steps complete. A fresh ${agent.name} session is now
|
|
1341
|
-
"
|
|
1988
|
+
log(`install-bridge setup steps complete. A fresh ${agent.name} session is now applying ` +
|
|
1989
|
+
"configuration, presenting the capability report, and ending with one indexing-consent question.");
|
|
1342
1990
|
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.");
|
|
1991
|
+
"it will pause to ask you to approve the project description, and it will close by asking " +
|
|
1992
|
+
"'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
|
|
1993
|
+
"(install status panel) or via the session's 'Applied N of M' summary.");
|
|
1346
1994
|
return 0;
|
|
1347
1995
|
}
|