@bridge_gpt/mcp-server 0.2.33 → 0.2.36
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 +456 -340
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/claude-review-workflow.js +264 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +252 -3
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +30 -9
- package/build/doctor.js +234 -1
- package/build/executor/cli.js +32 -5
- package/build/executor/credentials.js +45 -11
- package/build/executor/deps.js +14 -0
- package/build/executor/env.js +23 -6
- package/build/executor/index.js +4 -0
- package/build/executor/job-runner.js +119 -9
- package/build/executor/permissions.js +12 -2
- package/build/executor/preflight.js +95 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +15 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +0 -17
- package/build/executor/worker-command.js +62 -9
- package/build/index.js +575 -143
- package/build/init.js +153 -51
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +884 -176
- package/build/install-reexec.js +233 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-provisioning.js +22 -6
- package/build/pipelines.generated.js +14 -8
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +257 -0
- package/build/setup-epic.js +117 -8
- package/build/upgrade-cli.js +1 -15
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +115 -4
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +6 -1
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
package/build/install-bridge.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
|
|
3
3
|
*
|
|
4
|
-
* npx -y @bridge_gpt/mcp-server
|
|
4
|
+
* npx -y @bridge_gpt/mcp-server install [--repo <name>] [--api-key <key>] [--force] [--dry-run] [--agent <name>]
|
|
5
|
+
*
|
|
6
|
+
* The documented spelling is `install` (BAPI-714, Group E). The legacy
|
|
7
|
+
* `install-bridge` token remains a silent, permanent alias — every string this
|
|
8
|
+
* module prints names `install`, because that is the only spelling the README,
|
|
9
|
+
* the invite email, and the operator-provisioned command teach.
|
|
5
10
|
*
|
|
6
11
|
* Collapses the previously five-step manual setup into a single CLI subcommand.
|
|
7
12
|
* It does the DETERMINISTIC work in the shell:
|
|
@@ -36,8 +41,9 @@
|
|
|
36
41
|
*
|
|
37
42
|
* then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
|
|
38
43
|
* remainder: `/install-bridge` config-field derivation → the read-after-write
|
|
39
|
-
* concise capability report →
|
|
40
|
-
*
|
|
44
|
+
* concise capability report → whatever next step the manifest's `next_step.command`
|
|
45
|
+
* names, rendered only when it is non-empty (BAPI-658, BAPI-720).
|
|
46
|
+
* It does NOT chain into running that command itself — that stays the
|
|
41
47
|
* human's next explicit invocation — and it never asks about, decides, or
|
|
42
48
|
* claims anything about repository indexing: indexing is entirely automatic,
|
|
43
49
|
* gated server-side by the existing readiness funnel, with no prompt on any
|
|
@@ -57,22 +63,41 @@
|
|
|
57
63
|
* methods, differing only in how the first token is obtained: `bootstrap-invite`
|
|
58
64
|
* (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
|
|
59
65
|
* email. Both then feed the SAME redemption protocol. Selection is pure and
|
|
60
|
-
* deterministic from flags/env, except that a BARE interactive run is asked
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
66
|
+
* deterministic from flags/env, except that a BARE interactive run is asked ONE
|
|
67
|
+
* question (BAPI-626/708): `Welcome to Bridge - do you have a token yet?`, answered
|
|
68
|
+
* `1` (yes) or `2` (no). Answering `2` is the self-serve route, which a first-time
|
|
69
|
+
* user could not otherwise discover. Answering `1` opens a single hidden,
|
|
70
|
+
* CREDENTIAL-AGNOSTIC token prompt and the route is then inferred from the pasted
|
|
71
|
+
* value's SHAPE — the user is never asked to pre-classify their own credential,
|
|
72
|
+
* because the shape checks below do that with no probe request. Either need-key
|
|
73
|
+
* method NAMES a new project, so the repository prompt asks for a new project name
|
|
74
|
+
* rather than an existing registration (see `RepoNamePromptMode`).
|
|
75
|
+
*
|
|
76
|
+
* SHAPE-BASED ROUTING, AND THE INFERRED/DECLARED SPLIT (BAPI-661/687/708) — the
|
|
77
|
+
* have-key branch's credential input (`--api-key`, `BAPI_API_KEY`, or its hidden
|
|
78
|
+
* prompt) is CREDENTIAL-AGNOSTIC: it accepts a full API key OR a bootstrap-invite
|
|
79
|
+
* token. `classifyEnteredCredential` (prefix) and `isHighEntropyApiKeyShape`
|
|
80
|
+
* (positive 43-char canonical base64url test) inspect the resolved value
|
|
81
|
+
* immediately after input resolution, before repository-name resolution; a
|
|
82
|
+
* `bapi_inv_…` value flips `bootstrapInviteMode`/`inviteToken` and the run falls
|
|
83
|
+
* into the SAME bootstrap-invite protocol described below, verbatim.
|
|
84
|
+
*
|
|
85
|
+
* What differs is whether the user DECLARED a route:
|
|
86
|
+
*
|
|
87
|
+
* - INFERRED — the bare interactive question was answered. This is the PRIMARY
|
|
88
|
+
* path, not mis-paste recovery: nothing was declared, so the detected type and
|
|
89
|
+
* destination route are ANNOUNCED and the run proceeds. There is no
|
|
90
|
+
* confirmation prompt and no decline exit, because there is no stated intent
|
|
91
|
+
* for the value to contradict.
|
|
92
|
+
* - DECLARED — `--api-key`/`BAPI_API_KEY`, `--invite`/`BAPI_INVITE`, or
|
|
93
|
+
* `--email`/`BAPI_SIGNUP_EMAIL`. BAPI-687 behavior is unchanged, asymmetry
|
|
94
|
+
* included: an invite-shaped value on the key path auto-switches on a non-TTY
|
|
95
|
+
* (with a notice), while an API-key-shaped value on the invite path exits
|
|
96
|
+
* non-zero rather than silently redirecting a script.
|
|
97
|
+
*
|
|
98
|
+
* A value matching NEITHER shape is never reclassified in either direction; it
|
|
99
|
+
* stays on its route and fails there. Because the question is reachable only on a
|
|
100
|
+
* bare interactive TTY run, every non-interactive run is declared by construction.
|
|
76
101
|
*
|
|
77
102
|
* BOOTSTRAP-INVITE MODE (BAPI-606) — one of the exceptions to "this command
|
|
78
103
|
* consumes a key, it does not create one" (the ordinary-entry reclassification
|
|
@@ -118,12 +143,17 @@ import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
|
|
|
118
143
|
import os from "os";
|
|
119
144
|
import path from "path";
|
|
120
145
|
import readline from "readline";
|
|
121
|
-
import { runInit, buildBridgeApiEntry } from "./init.js";
|
|
146
|
+
import { runInit, buildBridgeApiEntry, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
|
|
122
147
|
import { VERSION } from "./version.generated.js";
|
|
123
148
|
import { validateRepoName } from "./bridge-config.js";
|
|
124
149
|
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
125
150
|
import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
|
|
126
|
-
import { writeMcpInstallState } from "./mcp-install-state.js";
|
|
151
|
+
import { recordInstalledProjectArtifact, writeMcpInstallState } from "./mcp-install-state.js";
|
|
152
|
+
import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
153
|
+
import { runConductorInstallDoctor, } from "./conductor/install-doctor.js";
|
|
154
|
+
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
155
|
+
import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
156
|
+
import { runSetupEpicCli } from "./setup-epic.js";
|
|
127
157
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
128
158
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
129
159
|
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
@@ -136,11 +166,17 @@ import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, d
|
|
|
136
166
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
137
167
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
138
168
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
169
|
+
* Fail-soft backstop note (BAPI-451 W3, E-3). The pinned launcher's FIRST cold
|
|
170
|
+
* launch can resolve/download the package inline; if the MCP client's connect
|
|
171
|
+
* deadline is very short it may time out before the handshake.
|
|
172
|
+
*
|
|
173
|
+
* BAPI-708 (Part B) narrowed WHERE this is surfaced. It is no longer printed after
|
|
174
|
+
* a SUCCESSFUL pre-warm — there, the cold install the note warns about has already
|
|
175
|
+
* been paid, so the remedy addressed a problem that did not occur. It is still
|
|
176
|
+
* emitted by the pre-warm FAILURE branch, where an incomplete warm makes the
|
|
177
|
+
* remedy real, and by both `--dry-run` previews, which describe the run rather
|
|
178
|
+
* than perform it. `mcp_server/src/doctor.ts` carries an independently-worded
|
|
179
|
+
* near-duplicate for the after-the-fact diagnosis case.
|
|
144
180
|
*/
|
|
145
181
|
export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch " +
|
|
146
182
|
"(the initial npx package resolution/download can exceed a short connect timeout).";
|
|
@@ -159,8 +195,12 @@ export function buildPrewarmCommandPreview() {
|
|
|
159
195
|
/**
|
|
160
196
|
* The natural-language prompt handed to the spawned agent session. It is
|
|
161
197
|
* CONFIGURE-ONLY: it derives and applies configuration, presents the concise
|
|
162
|
-
* capability report
|
|
163
|
-
* (BAPI-
|
|
198
|
+
* capability report — the availability-framed 'Available now' / 'Needs setup'
|
|
199
|
+
* pair the server authors (BAPI-719), rendered verbatim with zero local
|
|
200
|
+
* curation — and closes with the server-provided `next_step.command`
|
|
201
|
+
* when the manifest returned a non-empty one — and with nothing at all when it
|
|
202
|
+
* did not (BAPI-658, BAPI-720). The eligibility decision is the server's, not
|
|
203
|
+
* this prompt's. It never chains straight into running that command itself —
|
|
164
204
|
* that remains the human's next explicit invocation — and it never asks about,
|
|
165
205
|
* decides, or claims anything about repository indexing: indexing is entirely
|
|
166
206
|
* automatic, gated by the existing readiness funnel
|
|
@@ -171,13 +211,32 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
|
|
|
171
211
|
"The command chooses its own mode from the install manifest's `configured` flag — do NOT force a " +
|
|
172
212
|
"mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is " +
|
|
173
213
|
"true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any " +
|
|
174
|
-
"configuration and applies zero fields for any caller; a joining member's
|
|
175
|
-
"is the welcome plus the concise capability report
|
|
214
|
+
"configuration and applies zero fields for any caller; a joining member's closing interaction " +
|
|
215
|
+
"is the welcome plus the concise capability report. " +
|
|
176
216
|
"Complete the command's concise capability report: the single 'What Bridge " +
|
|
177
|
-
"can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it " +
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
217
|
+
"can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it. " +
|
|
218
|
+
"That field is exactly two availability sections in server order — 'Available now' first, then " +
|
|
219
|
+
"'Needs setup' — and each item in them carries display_name, description, how_to_use, and (in the " +
|
|
220
|
+
"'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
|
|
221
|
+
"and its how_to_use verbatim when that value is a non-empty string; render availability_text " +
|
|
222
|
+
"verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, " +
|
|
223
|
+
"asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
|
|
224
|
+
"contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
|
|
225
|
+
"and never branch on it. Keep an empty section visible with a short neutral line rather than " +
|
|
226
|
+
"dropping it, and after both sections close with a pointer to the Bridge MCP server README at " +
|
|
227
|
+
"https://www.npmjs.com/package/@bridge_gpt/mcp-server for the complete tool documentation, " +
|
|
228
|
+
"including when either or both sections are empty. Do not render the obsolete five-section report, " +
|
|
229
|
+
"and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall " +
|
|
230
|
+
"back to the complete tool_capabilities catalog or the workflows collection. If " +
|
|
231
|
+
"concise_tool_capabilities is missing or malformed, take the command's single fallback line and " +
|
|
232
|
+
"render nothing else in its place. " +
|
|
233
|
+
"In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT " +
|
|
234
|
+
"print or summarize its SUCCESSFUL result anywhere — no 'Stored routing credential for …' line, " +
|
|
235
|
+
"and no restatement of that target or path in any later summary or Return section — because the " +
|
|
236
|
+
"install-bridge CLI has already reported where the credential landed and a second sentence is a " +
|
|
237
|
+
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
238
|
+
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
239
|
+
"invocation Stage 6 reports its success exactly as it does today. " +
|
|
181
240
|
"The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 " +
|
|
182
241
|
"(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
|
|
183
242
|
"caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
|
|
@@ -186,11 +245,19 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
|
|
|
186
245
|
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
187
246
|
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
188
247
|
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"recommend
|
|
248
|
+
"Close with the server's own next step, which you must read at runtime rather than assume. Inspect " +
|
|
249
|
+
"the `next_step` object on the manifest response the flow already used: on the fresh-configuration " +
|
|
250
|
+
"path that is the Stage 7 post-apply read-after-write manifest, and in JOIN MODE it is the Stage 2 " +
|
|
251
|
+
"manifest (JOIN MODE performs no read-after-write). Look at `next_step.command`: when it is a " +
|
|
252
|
+
"NON-EMPTY string, recommend exactly that command, verbatim, in inline code — do not rename it or " +
|
|
253
|
+
"substitute a command you happen to know. When it is an empty string, make NO next-command " +
|
|
254
|
+
"recommendation at all and say nothing in its place: no heading, no placeholder, no 'nothing to " +
|
|
255
|
+
"do' line. The server decides eligibility (repository state AND whether this caller can run the " +
|
|
256
|
+
"command), so do NOT apply a role rule or a learned-state rule of your own. Do NOT run the " +
|
|
257
|
+
"returned command yourself — only recommend it; running it is the human's next explicit " +
|
|
258
|
+
"invocation. For supporting context around a recommended command, use the manifest's own " +
|
|
259
|
+
"next_step.post_install_indexing and next_step.post_install_scheduler text rather than inventing " +
|
|
260
|
+
"an explanation for it. " +
|
|
194
261
|
"Never request, echo, or transport any credential — only ever direct the human to that " +
|
|
195
262
|
"integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
|
|
196
263
|
"always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
|
|
@@ -227,16 +294,18 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
227
294
|
const setupUrl = buildInstallBridgeSetupUrl(baseUrl);
|
|
228
295
|
return [
|
|
229
296
|
"Usage:",
|
|
230
|
-
" npx -y @bridge_gpt/mcp-server
|
|
297
|
+
" npx -y @bridge_gpt/mcp-server install [flags]",
|
|
231
298
|
"",
|
|
232
299
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
233
300
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
234
301
|
"routing credential, then — on a TTY, only after a Y/N consent prompt — opens a",
|
|
235
302
|
"fresh session in your selected tool. If the project is not yet configured it",
|
|
236
|
-
"derives the remaining config, presents a concise capability report, and",
|
|
237
|
-
"
|
|
303
|
+
"derives the remaining config, presents a concise capability report, and shows",
|
|
304
|
+
"the next step the server returned — when it returns one.",
|
|
305
|
+
"If the project is already configured it instead",
|
|
238
306
|
"joins you to it without proposing or applying any changes and just shows the",
|
|
239
|
-
"concise capability report (a b2b admin is
|
|
307
|
+
"concise capability report plus that same conditional next step (a b2b admin is",
|
|
308
|
+
"additionally offered a teammate-invite",
|
|
240
309
|
"step). So not every run applies config fields. Indexing is never asked about —",
|
|
241
310
|
"it starts automatically once the repository reaches full parse readiness.",
|
|
242
311
|
"",
|
|
@@ -247,16 +316,37 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
247
316
|
"resolves your key from the credential store at runtime. A config file that",
|
|
248
317
|
"cannot be parsed is left untouched, with manual-merge instructions printed.",
|
|
249
318
|
"",
|
|
250
|
-
"Run it bare — `install
|
|
251
|
-
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with
|
|
319
|
+
"Run it bare — `install` with no flags — in a terminal and it asks",
|
|
320
|
+
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with two numbered choices:`,
|
|
252
321
|
...INSTALL_BRIDGE_ONBOARDING_CHOICES.map((choice) => ` ${choice}`),
|
|
253
|
-
"There is NO default — pressing Enter selects nothing; you must type 1
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
322
|
+
"There is NO default — pressing Enter selects nothing; you must type 1 or 2 (one",
|
|
323
|
+
"blank or invalid answer re-prompts once, then exits with guidance). Answering 1",
|
|
324
|
+
"opens a single hidden token prompt: paste whatever you were given and its SHAPE",
|
|
325
|
+
"picks the route — a bootstrap invite (bapi_inv_…) is redeemed to CREATE a new",
|
|
326
|
+
"project and mint your first admin key, while a Bridge API key connects to the",
|
|
327
|
+
"project that already exists. You are never asked which kind of token you hold.",
|
|
328
|
+
"Answering 2 prompts for an email and creates a brand-new Bridge workspace for",
|
|
329
|
+
"you (the self-serve flow). That question is asked ONLY for a bare interactive",
|
|
330
|
+
"run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
331
|
+
"terminal keeps the existing deterministic behavior and no prompt.",
|
|
332
|
+
"",
|
|
333
|
+
"On that bare run the route is INFERRED, so it is only announced — the detected",
|
|
334
|
+
"credential type and where it leads are printed and the run proceeds. There is no",
|
|
335
|
+
"confirmation question and no way to decline, because you never declared a route",
|
|
336
|
+
"for the value to contradict.",
|
|
337
|
+
"",
|
|
338
|
+
"When you DO declare a route with a flag or env var, the confirmations stay, and",
|
|
339
|
+
"the two directions are deliberately asymmetric. Both are detected by SHAPE alone",
|
|
340
|
+
"— no probe request is sent — BEFORE any project-name prompt, credential write,",
|
|
341
|
+
"or network call:",
|
|
342
|
+
" - a bootstrap invite (bapi_inv_…) passed as an API key is announced, then",
|
|
343
|
+
" confirmed on a terminal before it is redeemed; a non-interactive run keeps",
|
|
344
|
+
" today's automatic switch but prints the notice.",
|
|
345
|
+
" - an API-key-shaped value passed as an invite is announced and may switch in",
|
|
346
|
+
" place only after you confirm on a terminal — non-interactive runs exit with",
|
|
347
|
+
" guidance instead, so a script is never silently redirected.",
|
|
348
|
+
"A value that is neither shape is left on the route you chose, and is not",
|
|
349
|
+
"announced.",
|
|
260
350
|
"",
|
|
261
351
|
"Inputs (the only two irreducible ones):",
|
|
262
352
|
" --api-key <key> Bridge API key OR bootstrap invite. Falls back to the",
|
|
@@ -264,24 +354,31 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
264
354
|
` Generate a key at ${setupUrl} (Security page) — this`,
|
|
265
355
|
" command consumes a key, it does not create one. A value",
|
|
266
356
|
" detected as a bootstrap invite (bapi_inv_…), from any of the",
|
|
267
|
-
" three sources above, is instead redeemed to
|
|
268
|
-
" project and its first admin key — the same as
|
|
269
|
-
" so it skips repository lookup entirely (--email
|
|
270
|
-
" remain the explicit, preferred entry points for
|
|
271
|
-
"
|
|
357
|
+
" three sources above, is announced and instead redeemed to",
|
|
358
|
+
" CREATE a new project and its first admin key — the same as",
|
|
359
|
+
" --invite — so it skips repository lookup entirely (--email",
|
|
360
|
+
" and --invite remain the explicit, preferred entry points for",
|
|
361
|
+
" a new project). Passing the flag or setting the env var",
|
|
362
|
+
" DECLARES the key route, so that switch is confirmed on a",
|
|
363
|
+
" terminal; the bare run's hidden token prompt infers instead",
|
|
364
|
+
" and only announces. NEVER printed or logged.",
|
|
272
365
|
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
273
366
|
" priority and short-circuit before any network call. When",
|
|
274
367
|
" neither is set, a compatible server resolves the unique",
|
|
275
|
-
" repository from your existing API key automatically
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
" --
|
|
283
|
-
"
|
|
284
|
-
"
|
|
368
|
+
" repository from your existing API key automatically. If the",
|
|
369
|
+
" key does not identify exactly one project, the run STOPS —",
|
|
370
|
+
" it never guesses — and asks you to re-run with --repo <name>,",
|
|
371
|
+
` or to rotate the key at ${setupUrl} (Security page).`,
|
|
372
|
+
" If instead the server is older or resolution fails",
|
|
373
|
+
" transiently, you are asked to type the registered name, with",
|
|
374
|
+
" NO pre-filled default (and",
|
|
375
|
+
" --repo is required when stdin is non-interactive). In the",
|
|
376
|
+
" existing-project flow it MUST match the server-side repo",
|
|
377
|
+
" registration (it keys the credential store as bapi:<repo>).",
|
|
378
|
+
" In either new-project flow (--email, --invite, answering 2",
|
|
379
|
+
" above, or a token that turns out to be an invite) it NAMES",
|
|
380
|
+
" creates, so you are asked to name a new project — with a",
|
|
381
|
+
" suggested default — rather than match an existing one; the",
|
|
285
382
|
" name must be globally unique.",
|
|
286
383
|
"",
|
|
287
384
|
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
@@ -290,13 +387,18 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
290
387
|
" It requests a fresh workspace for that email, then creates",
|
|
291
388
|
" the project and mints your own admin API key in one command.",
|
|
292
389
|
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
293
|
-
" interactive prompt — which is also what
|
|
390
|
+
" interactive prompt — which is also what answering 2 on a",
|
|
294
391
|
" bare run reaches. The email is NOT a",
|
|
295
392
|
" secret (it is shown as you type), but it is never printed to",
|
|
296
393
|
" a log. Mutually",
|
|
297
|
-
" exclusive with --api-key and --invite.
|
|
298
|
-
"
|
|
299
|
-
"
|
|
394
|
+
" exclusive with --api-key and --invite. The address labels the",
|
|
395
|
+
" new workspace AND is used for transactional invite/setup",
|
|
396
|
+
" delivery, so a setup message may be sent to it. Delivery is",
|
|
397
|
+
" asynchronous and best-effort: it is queued after signup rather",
|
|
398
|
+
" than awaited, so do not wait on the message to continue — this",
|
|
399
|
+
" command already prints everything you need. No email",
|
|
400
|
+
" verification is performed; the address is not confirmed before",
|
|
401
|
+
" the workspace is created.",
|
|
300
402
|
"",
|
|
301
403
|
" RESUMABLE: a self-serve run that fails mid-protocol saves its",
|
|
302
404
|
" signup state under bootstrap-pending:<repo> in the credential",
|
|
@@ -312,7 +414,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
312
414
|
" Mutually exclusive with --api-key and --email (in this mode",
|
|
313
415
|
" the key is created, not consumed).",
|
|
314
416
|
"",
|
|
315
|
-
" Run it WITHOUT a value — `install
|
|
417
|
+
" Run it WITHOUT a value — `install --invite` — and the",
|
|
316
418
|
" token is read from an interactive prompt with echo",
|
|
317
419
|
" suppressed, then sent only in the request body. This is the",
|
|
318
420
|
" default and the recommended path: 'a copy/paste one-liner'",
|
|
@@ -369,6 +471,35 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
369
471
|
" --agent cursor-agent still force-spawns it.",
|
|
370
472
|
" -h, --help Show this help.",
|
|
371
473
|
"",
|
|
474
|
+
"Subcommands:",
|
|
475
|
+
" install conductor Bootstrap Epic Conductor v2 for this repository",
|
|
476
|
+
" (BAPI-679). Runs a unified READ-ONLY doctor that",
|
|
477
|
+
" composes the conductor, executor-preflight,",
|
|
478
|
+
" install-status, and executor service-unit",
|
|
479
|
+
" diagnostics with server-side",
|
|
480
|
+
" readiness (supervisor defaults, empty required-",
|
|
481
|
+
" check detection, GitHub App credentials and the",
|
|
482
|
+
" `actions` permission, reconciler tick liveness).",
|
|
483
|
+
" The resolved BAPI_BASE_URL is confirmed BEFORE",
|
|
484
|
+
" any network call — the doctor's readiness GET",
|
|
485
|
+
" carries the repo API key, and the URL silently",
|
|
486
|
+
" defaults to production when unset. A fatal",
|
|
487
|
+
" finding aborts BEFORE any write. A separate",
|
|
488
|
+
" consent immediately precedes the bootstrap POST,",
|
|
489
|
+
" which writes the safe project-default supervisor",
|
|
490
|
+
" posture in one transaction; it optionally",
|
|
491
|
+
" scaffolds a parameterized claude-review workflow",
|
|
492
|
+
" (its own separate consent), prints the capability",
|
|
493
|
+
" matrix, and re-runs the doctor. `--dry-run`",
|
|
494
|
+
" performs NO write of any kind and no write-consent",
|
|
495
|
+
" prompt (the base-URL confirmation still applies).",
|
|
496
|
+
" Executor service units are generated by",
|
|
497
|
+
" `executor install-service`; starting one stays",
|
|
498
|
+
" operator-managed, so a run reports",
|
|
499
|
+
" `complete-with-manual-steps` until a live",
|
|
500
|
+
" executor is actually observed. See",
|
|
501
|
+
" `install conductor --help`.",
|
|
502
|
+
"",
|
|
372
503
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
373
504
|
"(default docs/tmp) are read from the environment with the shown fallbacks.",
|
|
374
505
|
"BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
|
|
@@ -503,7 +634,7 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
503
634
|
}
|
|
504
635
|
return {
|
|
505
636
|
status: "error",
|
|
506
|
-
message: `Unexpected positional argument: '${arg}'. install
|
|
637
|
+
message: `Unexpected positional argument: '${arg}'. install does not accept positional arguments.`,
|
|
507
638
|
};
|
|
508
639
|
}
|
|
509
640
|
// Mutually exclusive by construction: bootstrap-invite mode CREATES the key, so
|
|
@@ -635,7 +766,7 @@ export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = "npx -y @bridge_gpt/mcp-ser
|
|
|
635
766
|
export const INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT = [
|
|
636
767
|
"",
|
|
637
768
|
"Step 4b — optional: connect GitHub.",
|
|
638
|
-
" This installs the Bridge GitHub App so
|
|
769
|
+
" This installs the Bridge GitHub App so you can implement and review tickets with Bridge. It opens",
|
|
639
770
|
" github.com in your browser; no GitHub credential is shared with Bridge.",
|
|
640
771
|
` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
|
|
641
772
|
];
|
|
@@ -838,6 +969,55 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
838
969
|
if (process.env.BAPI_INSTALL_DEBUG)
|
|
839
970
|
console.error(m);
|
|
840
971
|
},
|
|
972
|
+
// ---- Nested `install-bridge conductor` seams (BAPI-679) ----
|
|
973
|
+
conductorRunDoctor: (params) => runConductorInstallDoctor({
|
|
974
|
+
access: params.access,
|
|
975
|
+
accessError: params.accessError,
|
|
976
|
+
fetch: params.fetch,
|
|
977
|
+
reviewPolicySource: params.reviewPolicySource,
|
|
978
|
+
readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
|
|
979
|
+
installDoctorDeps: {
|
|
980
|
+
env: params.env,
|
|
981
|
+
cwd: params.cwd,
|
|
982
|
+
platform: process.platform,
|
|
983
|
+
homedir: os.homedir,
|
|
984
|
+
readFile: params.readFile,
|
|
985
|
+
stat: (p) => stat(p),
|
|
986
|
+
fetch: (...args) => fetch(...args),
|
|
987
|
+
},
|
|
988
|
+
}),
|
|
989
|
+
conductorResolveAccess: async (repoName, baseUrl, mergedDeps) => {
|
|
990
|
+
const result = await resolveConductorBridgeApiAccess({
|
|
991
|
+
env: { ...mergedDeps.env, BAPI_BASE_URL: baseUrl },
|
|
992
|
+
cwd: mergedDeps.cwd,
|
|
993
|
+
homedir: mergedDeps.homedir,
|
|
994
|
+
platform: mergedDeps.platform,
|
|
995
|
+
readFile: mergedDeps.readFile,
|
|
996
|
+
stat: (p) => mergedDeps.stat(p),
|
|
997
|
+
repoName,
|
|
998
|
+
});
|
|
999
|
+
// Never surface the resolver's secret-bearing internals — it already
|
|
1000
|
+
// guarantees a secret-free `error`, which is forwarded verbatim.
|
|
1001
|
+
return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
|
|
1002
|
+
},
|
|
1003
|
+
conductorWriteWorkflow: (cwd, content, options, mergedDeps) => writeClaudeReviewWorkflow(cwd, content, {
|
|
1004
|
+
readFile: mergedDeps.readFile,
|
|
1005
|
+
mkdir: (p, o) => mergedDeps.mkdir(p, o),
|
|
1006
|
+
writeFile: (p, data) => mergedDeps.writeFile(p, data),
|
|
1007
|
+
}, options),
|
|
1008
|
+
conductorRecordArtifact: async (cwd, relPath, mergedDeps) => {
|
|
1009
|
+
const result = await recordInstalledProjectArtifact(cwd, relPath, {
|
|
1010
|
+
readFile: mergedDeps.readFile,
|
|
1011
|
+
writeFile: (p, data) => mergedDeps.writeFile(p, data),
|
|
1012
|
+
rename: mergedDeps.rename,
|
|
1013
|
+
mkdir: (p, o) => mergedDeps.mkdir(p, o),
|
|
1014
|
+
unlink: mergedDeps.unlink,
|
|
1015
|
+
});
|
|
1016
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
1017
|
+
},
|
|
1018
|
+
conductorRunSetupEpicDryRun: async (argv) => ({
|
|
1019
|
+
exitCode: await runSetupEpicCli(argv),
|
|
1020
|
+
}),
|
|
841
1021
|
};
|
|
842
1022
|
}
|
|
843
1023
|
/**
|
|
@@ -865,7 +1045,11 @@ export async function resolveApiKey(options, deps) {
|
|
|
865
1045
|
return { ok: true, value: fromEnv.trim(), source: "env" };
|
|
866
1046
|
}
|
|
867
1047
|
if (deps.isTTY && deps.promptSecret) {
|
|
868
|
-
|
|
1048
|
+
// BAPI-708 (A-3): the prompt no longer asks the user to pre-classify their own
|
|
1049
|
+
// credential ("Bridge API key or invite"). Classification is done from SHAPE
|
|
1050
|
+
// downstream with no probe request, so the only thing worth asking for is the
|
|
1051
|
+
// token itself.
|
|
1052
|
+
const entered = (await deps.promptSecret("Paste your Bridge token (input hidden): ")).trim();
|
|
869
1053
|
if (entered.length > 0) {
|
|
870
1054
|
return { ok: true, value: entered, source: "prompt" };
|
|
871
1055
|
}
|
|
@@ -874,12 +1058,12 @@ export async function resolveApiKey(options, deps) {
|
|
|
874
1058
|
// and it named no route usable by someone who has no key at all.
|
|
875
1059
|
return {
|
|
876
1060
|
ok: false,
|
|
877
|
-
error: `No Bridge
|
|
1061
|
+
error: `No Bridge token entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
878
1062
|
};
|
|
879
1063
|
}
|
|
880
1064
|
return {
|
|
881
1065
|
ok: false,
|
|
882
|
-
error: "A Bridge
|
|
1066
|
+
error: "A Bridge token is required (no interactive terminal is available to prompt " +
|
|
883
1067
|
`for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
884
1068
|
};
|
|
885
1069
|
}
|
|
@@ -919,7 +1103,7 @@ export async function resolveInviteToken(options, deps) {
|
|
|
919
1103
|
error: "A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE " +
|
|
920
1104
|
"environment variable (no interactive terminal is available to prompt for it). Note that " +
|
|
921
1105
|
"both forms expose the token to your shell history and process list — prefer running " +
|
|
922
|
-
"'install
|
|
1106
|
+
"'install --invite' interactively.",
|
|
923
1107
|
};
|
|
924
1108
|
}
|
|
925
1109
|
/**
|
|
@@ -986,27 +1170,38 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
986
1170
|
return { kind: "have-key" };
|
|
987
1171
|
}
|
|
988
1172
|
/**
|
|
989
|
-
* The bare-TTY onboarding
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
1173
|
+
* The bare-TTY onboarding question (BAPI-708, Part A), replacing the BAPI-667/687
|
|
1174
|
+
* three-option chooser.
|
|
1175
|
+
*
|
|
1176
|
+
* The three-option form asked the user to make a distinction the CODE can already
|
|
1177
|
+
* make for itself: whether the string in their clipboard is an invite or an API
|
|
1178
|
+
* key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
|
|
1179
|
+
* SHAPE alone, with no probe request — so the only thing a first-time user
|
|
1180
|
+
* genuinely knows, and the only thing worth asking, is whether they have a token
|
|
1181
|
+
* at all. Hence a yes/no question, with the route inferred afterwards.
|
|
1182
|
+
*
|
|
1183
|
+
* Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
|
|
1184
|
+
* render the heading and the numbered options exactly once, accept ONE strict
|
|
1185
|
+
* line, no default (a bare Enter selects nothing), bounded to a single re-prompt
|
|
1186
|
+
* after one hint, and EOF resolves as an empty answer so it can never hang.
|
|
1187
|
+
*/
|
|
1188
|
+
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
|
|
1189
|
+
/**
|
|
1190
|
+
* The question's numbered options, in render order. Index + 1 is the accepted
|
|
1191
|
+
* token, exactly as before — only the option SET narrowed from three to two.
|
|
995
1192
|
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
1193
|
+
* "Token" is deliberately credential-agnostic: an invite and an API key are both
|
|
1194
|
+
* tokens as far as this question is concerned, and answering "Yes" leads to ONE
|
|
1195
|
+
* hidden prompt whose value selects the route by shape.
|
|
998
1196
|
*/
|
|
999
|
-
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "How would you like to connect to Bridge API?";
|
|
1000
|
-
/** The chooser's numbered options, in render order. Index + 1 is the accepted token. */
|
|
1001
1197
|
export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
|
|
1002
|
-
"1. I have a
|
|
1003
|
-
"2. I
|
|
1004
|
-
"3. I'm new — set me up with just my email",
|
|
1198
|
+
"1. Yes, I have received a token",
|
|
1199
|
+
"2. No, I need one",
|
|
1005
1200
|
];
|
|
1006
1201
|
/** The single-line answer prompt drawn after the options (no bracketed default). */
|
|
1007
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1
|
|
1202
|
+
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
|
|
1008
1203
|
/** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
|
|
1009
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1
|
|
1204
|
+
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
|
|
1010
1205
|
/**
|
|
1011
1206
|
* The one place the "no usable credential" copy lives (BAPI-667, U1-3).
|
|
1012
1207
|
*
|
|
@@ -1015,10 +1210,10 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1, 2, or 3.";
|
|
|
1015
1210
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
1016
1211
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
1017
1212
|
*/
|
|
1018
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install
|
|
1019
|
-
"
|
|
1020
|
-
"
|
|
1021
|
-
"
|
|
1213
|
+
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 if you already have a token " +
|
|
1214
|
+
"(an invite or an API key — either one is accepted at the hidden prompt) or 2 to sign up " +
|
|
1215
|
+
"with just an email. Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
1216
|
+
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
1022
1217
|
/**
|
|
1023
1218
|
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
1024
1219
|
*
|
|
@@ -1042,20 +1237,37 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install-bridge and cho
|
|
|
1042
1237
|
*
|
|
1043
1238
|
* BAPI-667 replaced the Y/n question with a three-option chooser. Two properties
|
|
1044
1239
|
* are load-bearing: there is NO default (a bare Enter can no longer route a
|
|
1045
|
-
* brand-new user into the hidden key prompt), and
|
|
1046
|
-
*
|
|
1240
|
+
* brand-new user into the hidden key prompt), and the self-serve route is named
|
|
1241
|
+
* explicitly so it is discoverable without reading `--help`. Validation is
|
|
1047
1242
|
* bounded to ONE re-prompt — an unanswerable prompt that loops is the same
|
|
1048
1243
|
* dead-end in a different costume.
|
|
1244
|
+
*
|
|
1245
|
+
* BAPI-708 (Part A) narrowed those three options to a token yes/no question. The
|
|
1246
|
+
* chooser was asking the user to pre-classify their own credential, which the
|
|
1247
|
+
* shape checks downstream already do without a probe request. Every interaction
|
|
1248
|
+
* property above survives verbatim — heading + options rendered once, one strict
|
|
1249
|
+
* line, no default, one hint, one re-prompt, EOF-as-empty — and so does every
|
|
1250
|
+
* bypass. Only the option SET and the dispatch changed.
|
|
1251
|
+
*
|
|
1252
|
+
* PROVENANCE. This function is the SOLE authority on `credentialIntent`, and that
|
|
1253
|
+
* is deliberate: the route-switch arms downstream must not re-derive provenance
|
|
1254
|
+
* from `argv`, because a second derivation is exactly the drift BAPI-708 exists to
|
|
1255
|
+
* fix. `"inferred"` is returned ONLY when the question was actually answered — a
|
|
1256
|
+
* state reachable on a bare interactive TTY run and nowhere else. Every other exit
|
|
1257
|
+
* is `"declared"`, so 100% of non-interactive runs stay on declared paths and the
|
|
1258
|
+
* documented directional asymmetry (invite-on-key auto-switches on a non-TTY;
|
|
1259
|
+
* key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
|
|
1260
|
+
* two.
|
|
1049
1261
|
*/
|
|
1050
1262
|
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
1051
1263
|
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
1052
1264
|
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
1053
1265
|
if (branch.kind === "need-key")
|
|
1054
|
-
return { ok: true, branch };
|
|
1266
|
+
return { ok: true, branch, credentialIntent: "declared" };
|
|
1055
1267
|
const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
|
|
1056
1268
|
const isBareInvocation = argv.length === 0;
|
|
1057
1269
|
if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
|
|
1058
|
-
return { ok: true, branch };
|
|
1270
|
+
return { ok: true, branch, credentialIntent: "declared" };
|
|
1059
1271
|
}
|
|
1060
1272
|
const promptLine = deps.promptLine;
|
|
1061
1273
|
try {
|
|
@@ -1070,15 +1282,20 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
1070
1282
|
// it can never hang or silently select a branch.
|
|
1071
1283
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1072
1284
|
const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1285
|
+
// BAPI-708 (A-1): the accepted tokens are now exactly 1/2 (no letters, no
|
|
1286
|
+
// aliases), preserving the one-strict-line contract. "Yes" routes to the
|
|
1287
|
+
// have-key branch's credential-agnostic prompt, whose value is classified by
|
|
1288
|
+
// SHAPE — so a user holding an invite still lands in the redemption protocol
|
|
1289
|
+
// without ever having been asked which kind of token they hold.
|
|
1075
1290
|
if (answer === "1")
|
|
1076
|
-
return { ok: true, branch: { kind: "have-key" } };
|
|
1291
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
1077
1292
|
if (answer === "2") {
|
|
1078
|
-
return {
|
|
1293
|
+
return {
|
|
1294
|
+
ok: true,
|
|
1295
|
+
branch: { kind: "need-key", method: "self-serve" },
|
|
1296
|
+
credentialIntent: "inferred",
|
|
1297
|
+
};
|
|
1079
1298
|
}
|
|
1080
|
-
if (answer === "3")
|
|
1081
|
-
return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
|
|
1082
1299
|
if (attempt === 0)
|
|
1083
1300
|
deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
|
|
1084
1301
|
}
|
|
@@ -1112,23 +1329,30 @@ export function resolveConfiguredRepoName(options, env) {
|
|
|
1112
1329
|
return undefined;
|
|
1113
1330
|
}
|
|
1114
1331
|
/**
|
|
1115
|
-
* Resolve the repo name: `--repo` → `BAPI_REPO_NAME` env →
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* the repo identity keys the credential store, so it is never silently inferred
|
|
1332
|
+
* Resolve the repo name: `--repo` → `BAPI_REPO_NAME` env → an interactive prompt.
|
|
1333
|
+
* Fails fast when neither is supplied and stdin is non-interactive — the repo
|
|
1334
|
+
* identity keys the credential store, so it is never silently inferred
|
|
1119
1335
|
* non-interactively.
|
|
1120
1336
|
*
|
|
1121
|
-
* `mode` selects
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1337
|
+
* `mode` selects the WORDING **and, since BAPI-687 (C-3), whether a default is
|
|
1338
|
+
* inferred at all**:
|
|
1339
|
+
*
|
|
1340
|
+
* - `new-project` — unchanged. The project does not exist yet, so a suggested
|
|
1341
|
+
* name (from `.bridge/config`, else the cwd basename) is genuinely helpful and
|
|
1342
|
+
* a bare Enter accepts it.
|
|
1343
|
+
* - `existing-registration` — NO inference and NO bracketed default. The name
|
|
1344
|
+
* must match a registration the user already has server-side, where the cwd
|
|
1345
|
+
* basename is essentially never right; BAPI-661 already tried warning harder
|
|
1346
|
+
* and the 2026-07-21 run shows the user accepted the guess anyway, then took a
|
|
1347
|
+
* 403 two steps later. A pre-filled wrong answer is the defect, so the user now
|
|
1348
|
+
* types the registered name deliberately.
|
|
1124
1349
|
*/
|
|
1125
1350
|
export async function resolveRepoName(options, deps, mode = "existing-registration") {
|
|
1126
1351
|
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
1127
1352
|
if (configured !== undefined) {
|
|
1128
1353
|
return { ok: true, value: configured };
|
|
1129
1354
|
}
|
|
1130
|
-
// Non-interactive: fail fast and require --repo.
|
|
1131
|
-
// convenience only (the user must confirm it).
|
|
1355
|
+
// Non-interactive: fail fast and require --repo.
|
|
1132
1356
|
if (!deps.isTTY || !deps.promptLine) {
|
|
1133
1357
|
return {
|
|
1134
1358
|
ok: false,
|
|
@@ -1137,10 +1361,19 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
|
|
|
1137
1361
|
"variable (no interactive terminal is available to confirm an inferred name). It " +
|
|
1138
1362
|
"names the new Bridge project this run creates and must be globally unique."
|
|
1139
1363
|
: "A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable " +
|
|
1140
|
-
"(no interactive terminal is available to
|
|
1141
|
-
"server-side repository registration.",
|
|
1364
|
+
"(no interactive terminal is available to prompt for it). It must match the " +
|
|
1365
|
+
"server-side repository registration exactly.",
|
|
1142
1366
|
};
|
|
1143
1367
|
}
|
|
1368
|
+
// BAPI-687 (C-3): only the new-project mode infers. The existing-registration
|
|
1369
|
+
// prompt is rendered with NO bracketed default, so a bare Enter cannot commit a
|
|
1370
|
+
// guess the user never chose.
|
|
1371
|
+
if (mode !== "new-project") {
|
|
1372
|
+
const answer = (await deps.promptLine("Repo name (must match server-side registration): ")).trim();
|
|
1373
|
+
if (answer.length > 0)
|
|
1374
|
+
return { ok: true, value: answer };
|
|
1375
|
+
return { ok: false, error: "No repo name provided." };
|
|
1376
|
+
}
|
|
1144
1377
|
// Infer a sensible default: existing .bridge/config, else the cwd basename.
|
|
1145
1378
|
let inferred = await resolveStartTicketsRepoName({
|
|
1146
1379
|
env: deps.env,
|
|
@@ -1153,19 +1386,13 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
|
|
|
1153
1386
|
inferred = validated.value;
|
|
1154
1387
|
}
|
|
1155
1388
|
if (inferred) {
|
|
1156
|
-
const
|
|
1157
|
-
? `Name your new Bridge project [${inferred}]: `
|
|
1158
|
-
: `Repo name [${inferred}] (must match server-side registration): `;
|
|
1159
|
-
const answer = (await deps.promptLine(promptText)).trim();
|
|
1389
|
+
const answer = (await deps.promptLine(`Name your new Bridge project [${inferred}]: `)).trim();
|
|
1160
1390
|
const chosen = answer.length > 0 ? answer : inferred;
|
|
1161
1391
|
if (chosen.length > 0)
|
|
1162
1392
|
return { ok: true, value: chosen };
|
|
1163
1393
|
}
|
|
1164
1394
|
else {
|
|
1165
|
-
const
|
|
1166
|
-
? "Name your new Bridge project: "
|
|
1167
|
-
: "Repo name (must match server-side registration): ";
|
|
1168
|
-
const answer = (await deps.promptLine(promptText)).trim();
|
|
1395
|
+
const answer = (await deps.promptLine("Name your new Bridge project: ")).trim();
|
|
1169
1396
|
if (answer.length > 0)
|
|
1170
1397
|
return { ok: true, value: answer };
|
|
1171
1398
|
}
|
|
@@ -1417,13 +1644,21 @@ export function buildManualInstallBridgeContinuation(kind, toolLabels) {
|
|
|
1417
1644
|
if (kind === "empty-selection") {
|
|
1418
1645
|
return [
|
|
1419
1646
|
"No AI coding tools were configured, so nothing was set up for this project.",
|
|
1420
|
-
"Re-run install
|
|
1647
|
+
"Re-run install and select at least one tool to configure it.",
|
|
1421
1648
|
].join("\n");
|
|
1422
1649
|
}
|
|
1423
1650
|
const toolPhrase = formatToolLabelPhrase(toolLabels);
|
|
1651
|
+
// BAPI-708 (B-c): the reload clause lives here so the requirement survives on
|
|
1652
|
+
// every NO-SPAWN terminal path (declined consent, non-TTY suppression, no
|
|
1653
|
+
// launchable tool, chooser failure, failed spawn). A written project MCP config
|
|
1654
|
+
// is not a live connection until the host reloads and its trust prompt is
|
|
1655
|
+
// approved — the single most common place a fresh install looks broken. The
|
|
1656
|
+
// spawn path states the same requirement in its own handoff.
|
|
1424
1657
|
return [
|
|
1425
1658
|
`To finish configuring this project, open it in ${toolPhrase} that has the`,
|
|
1426
1659
|
"Bridge MCP server configured, start a new session, and run /install-bridge.",
|
|
1660
|
+
"Restart or reload the tool first if it is already running — the newly written",
|
|
1661
|
+
"MCP server is not live until it reloads and you approve it if prompted.",
|
|
1427
1662
|
"Until the project is configured, your Bridge MCP tools stay limited.",
|
|
1428
1663
|
].join("\n");
|
|
1429
1664
|
}
|
|
@@ -1747,6 +1982,22 @@ async function requestTrackedConfigConsent(deps, relPath) {
|
|
|
1747
1982
|
const normalized = (answer ?? "").trim().toLowerCase();
|
|
1748
1983
|
return normalized === "y" || normalized === "yes";
|
|
1749
1984
|
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Overlay a freshly built Bridge entry onto whatever is already registered,
|
|
1987
|
+
* preserving the EXISTING launcher args composition (BAPI-714, Group C).
|
|
1988
|
+
*
|
|
1989
|
+
* Only the package-spec token is refreshed on an existing entry, so a legacy bare
|
|
1990
|
+
* launcher stays bare and an already-`serve`-bearing launcher keeps `serve`
|
|
1991
|
+
* exactly once. When there is no existing entry — or its args are a shape we
|
|
1992
|
+
* cannot recognize — the new template args (which DO include `serve`) are used
|
|
1993
|
+
* verbatim. `command` and `env` always come from the freshly built entry: this
|
|
1994
|
+
* helper governs launcher-token migration only, never credentials.
|
|
1995
|
+
*/
|
|
1996
|
+
export function preserveExistingLauncherArgs(existing, entry) {
|
|
1997
|
+
const existingArgs = asRecord(existing)?.args;
|
|
1998
|
+
const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
|
|
1999
|
+
return preserved ? { ...entry, args: preserved } : entry;
|
|
2000
|
+
}
|
|
1750
2001
|
/**
|
|
1751
2002
|
* Write the `bridge-api` entry into each host config via read-merge-write,
|
|
1752
2003
|
* preserving unrelated servers and top-level keys (BAPI-666). Per target:
|
|
@@ -1807,7 +2058,13 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
1807
2058
|
// never initialized before its tracked-state consent is resolved.
|
|
1808
2059
|
const config = read.state === "parsed" ? read.config : {};
|
|
1809
2060
|
const topLevel = asRecord(config[target.topLevelKey]) ?? {};
|
|
1810
|
-
|
|
2061
|
+
// BAPI-714 (Group C): a PRE-EXISTING Bridge entry keeps its own launcher args
|
|
2062
|
+
// composition — only the package-spec token is refreshed — so the installer
|
|
2063
|
+
// never migrates a legacy bare launcher to `serve`. A newly created entry gets
|
|
2064
|
+
// the full template args, `serve` included. The rule is identical for the
|
|
2065
|
+
// real-key and secret-free variants: credential safety must not decide whether
|
|
2066
|
+
// a launcher is migrated.
|
|
2067
|
+
topLevel["bridge-api"] = preserveExistingLauncherArgs(topLevel["bridge-api"], entry);
|
|
1811
2068
|
config[target.topLevelKey] = topLevel;
|
|
1812
2069
|
await deps.mkdir(path.dirname(fullPath), { recursive: true });
|
|
1813
2070
|
await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
|
|
@@ -1986,6 +2243,23 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey, apiKey
|
|
|
1986
2243
|
message: `Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`,
|
|
1987
2244
|
};
|
|
1988
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* The `unresolved` (409) hard stop (BAPI-687, C-2). Names both recoveries; the
|
|
2248
|
+
* caller appends the address-bearing rotation line so this string stays a constant.
|
|
2249
|
+
* Secret-free: no key, no HTTP status, no server detail.
|
|
2250
|
+
*/
|
|
2251
|
+
export const INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR = "Error: that API key does not identify exactly one Bridge project, so the repository could " +
|
|
2252
|
+
"not be resolved from it. Re-run with --repo <name> (or set BAPI_REPO_NAME) to name the " +
|
|
2253
|
+
"registered project explicitly.";
|
|
2254
|
+
/**
|
|
2255
|
+
* The surviving `not-deployed` (404) / `error` fallback notice (BAPI-687, C-5).
|
|
2256
|
+
*
|
|
2257
|
+
* Re-worded for a path that no longer OFFERS a name: the old copy said the CLI was
|
|
2258
|
+
* "falling back to a guessed name" and told the user to confirm it against the setup
|
|
2259
|
+
* UI, which is now false in both halves — there is nothing pre-filled to confirm.
|
|
2260
|
+
*/
|
|
2261
|
+
export const INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE = "Automatic repository resolution isn't available for this run. Enter the name your project " +
|
|
2262
|
+
"is registered under (or re-run with --repo <name>).";
|
|
1989
2263
|
/** The resolve endpoint: `<base>/setup/resolve-repo`. No query string — ever. */
|
|
1990
2264
|
export function buildResolveRepoUrl(baseUrl) {
|
|
1991
2265
|
return `${baseUrl.replace(/\/+$/, "")}/setup/resolve-repo`;
|
|
@@ -2166,6 +2440,118 @@ export const BOOTSTRAP_INVITE_TOKEN_PREFIX = "bapi_inv_";
|
|
|
2166
2440
|
export function classifyEnteredCredential(value) {
|
|
2167
2441
|
return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX) ? "invite" : "api-key";
|
|
2168
2442
|
}
|
|
2443
|
+
// ---------------------------------------------------------------------------
|
|
2444
|
+
// Cross-credential detection (BAPI-687, Part B)
|
|
2445
|
+
// ---------------------------------------------------------------------------
|
|
2446
|
+
/** The exact encoded length of `secrets.token_urlsafe(32)`: 32 bytes, unpadded base64url. */
|
|
2447
|
+
export const USER_ACCESS_KEY_ENCODED_LENGTH = 43;
|
|
2448
|
+
/** The exact decoded length the encoded form must yield. */
|
|
2449
|
+
export const USER_ACCESS_KEY_DECODED_BYTES = 32;
|
|
2450
|
+
/** Strict base64url alphabet, anchored to the exact issuance length. */
|
|
2451
|
+
const USER_ACCESS_KEY_SHAPE_PATTERN = new RegExp(`^[A-Za-z0-9_-]{${USER_ACCESS_KEY_ENCODED_LENGTH}}$`);
|
|
2452
|
+
/**
|
|
2453
|
+
* POSITIVE test for the project's API-key shape — the TypeScript port of the
|
|
2454
|
+
* server's `is_high_entropy_user_access_key_shape`
|
|
2455
|
+
* (`src/python/library/auth/user_access_lookup.py`). Detection had to become
|
|
2456
|
+
* positive: "is an API key" was previously only the NEGATIVE of the `bapi_inv_`
|
|
2457
|
+
* prefix check, which would confidently mis-announce a typo'd invite as an API key.
|
|
2458
|
+
*
|
|
2459
|
+
* Admits ONLY `secrets.token_urlsafe(32)`: exactly 43 characters from the strict
|
|
2460
|
+
* base64url alphabet, decoding to exactly 32 bytes, AND canonical — the input must
|
|
2461
|
+
* equal the unpadded base64url re-encoding of those bytes. The canonical check is
|
|
2462
|
+
* not decoration: the final 6-bit group of a 32-byte value carries 2 unused low
|
|
2463
|
+
* bits, so several distinct 43-character strings decode to the same 32 bytes and
|
|
2464
|
+
* length-only validation would admit the aliases. It deliberately does NOT admit
|
|
2465
|
+
* shorter legacy keys — a legacy credential misses detection rather than being
|
|
2466
|
+
* re-routed on a guess.
|
|
2467
|
+
*
|
|
2468
|
+
* Pure and total: no logging, no trimming (the caller owns normalization), no
|
|
2469
|
+
* throw, and the input is never placed in a returned or thrown value. Every
|
|
2470
|
+
* failure path returns `false`.
|
|
2471
|
+
*/
|
|
2472
|
+
export function isHighEntropyApiKeyShape(value) {
|
|
2473
|
+
if (typeof value !== "string" || !USER_ACCESS_KEY_SHAPE_PATTERN.test(value))
|
|
2474
|
+
return false;
|
|
2475
|
+
let decoded;
|
|
2476
|
+
try {
|
|
2477
|
+
// The alphabet was already validated above, so this decode cannot silently
|
|
2478
|
+
// discard characters the way a lenient base64url decode otherwise would.
|
|
2479
|
+
decoded = Buffer.from(value, "base64url");
|
|
2480
|
+
}
|
|
2481
|
+
catch {
|
|
2482
|
+
return false;
|
|
2483
|
+
}
|
|
2484
|
+
if (decoded.length !== USER_ACCESS_KEY_DECODED_BYTES)
|
|
2485
|
+
return false;
|
|
2486
|
+
return decoded.toString("base64url") === value;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Notice printed when a bootstrap invite is detected on the API-key path (B-2).
|
|
2490
|
+
*
|
|
2491
|
+
* The reclassification itself is not new (BAPI-661) — the SILENCE was. A user who
|
|
2492
|
+
* called their value an "API key" was branch-switched with no indication, which is
|
|
2493
|
+
* half of why the reported run was opaque to everyone reading the transcript.
|
|
2494
|
+
*
|
|
2495
|
+
* Secret discipline: names the credential TYPE and the destination ROUTE only.
|
|
2496
|
+
* No part of the value — no prefix excerpt, no length, no fingerprint.
|
|
2497
|
+
*/
|
|
2498
|
+
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE = "That looks like a bootstrap invite token, not a Bridge API key. An invite CREATES a new " +
|
|
2499
|
+
"Bridge project and mints your first admin key, instead of connecting to a project that " +
|
|
2500
|
+
"already exists.";
|
|
2501
|
+
/** TTY confirmation for the invite-detected-on-the-key-path switch. Default No. */
|
|
2502
|
+
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT = "Redeem it as an invite and create a new project? [y/N]: ";
|
|
2503
|
+
/** Non-TTY form: the switch still happens automatically, but it is announced (B-2). */
|
|
2504
|
+
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE = "Redeeming it as a bootstrap invite (non-interactive — no confirmation is possible here).";
|
|
2505
|
+
/** Printed on decline. Names the other route rather than leaving the user stranded. */
|
|
2506
|
+
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED = "Error: cancelled — nothing was created. Re-run install with an API key for an " +
|
|
2507
|
+
"existing project (--api-key <key>, or BAPI_API_KEY), or re-run and confirm to redeem the " +
|
|
2508
|
+
"invite and create a new project.";
|
|
2509
|
+
/**
|
|
2510
|
+
* Notice printed when an API-key-shaped value is detected on the invite path (B-3).
|
|
2511
|
+
*
|
|
2512
|
+
* This direction had NO detection at all, and the cost of that was severe: the run
|
|
2513
|
+
* prompted for a new project name, fsynced a durable `bootstrap-pending:<repo>`
|
|
2514
|
+
* record, POSTed the exchange, took a 401, and printed the "you cannot recover it
|
|
2515
|
+
* yourself — ask your Bridge API operator" message. For a wrong-credential-type
|
|
2516
|
+
* paste. The orphaned pending record then blocked the next attempt.
|
|
2517
|
+
*
|
|
2518
|
+
* Secret discipline: credential TYPE and destination ROUTE only.
|
|
2519
|
+
*/
|
|
2520
|
+
export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE = "That looks like a Bridge API key, not a bootstrap invite token. An API key connects to a " +
|
|
2521
|
+
"project that already exists, instead of creating a new one.";
|
|
2522
|
+
/** TTY confirmation for the key-detected-on-the-invite-path switch. Default No. */
|
|
2523
|
+
export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT = "Use it to connect to your existing project instead? [y/N]: ";
|
|
2524
|
+
/**
|
|
2525
|
+
* Printed when the switch is declined, unconfirmable, or non-interactive.
|
|
2526
|
+
*
|
|
2527
|
+
* Non-TTY does NOT auto-switch here, and the asymmetry is deliberate: the invite
|
|
2528
|
+
* direction merely continues a path BAPI-661 already established, while this
|
|
2529
|
+
* direction would silently redirect a script away from the project-creating run it
|
|
2530
|
+
* asked for. A script gets a non-zero exit and the exact flag it should have passed.
|
|
2531
|
+
*/
|
|
2532
|
+
export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED = "Error: cancelled — nothing was created and no invite was spent. Re-run install with " +
|
|
2533
|
+
"--api-key <key> (or BAPI_API_KEY) to connect to your existing project, or supply a bootstrap " +
|
|
2534
|
+
"invite token (bapi_inv_…) to create a new one.";
|
|
2535
|
+
/**
|
|
2536
|
+
* Shared default-No confirmation for a cross-credential route switch (B-2 / B-3).
|
|
2537
|
+
*
|
|
2538
|
+
* Accepts only an explicit `y`/`yes` (case-insensitive, trimmed); EVERY other
|
|
2539
|
+
* answer — including a prompt that throws or an EOF-resolved empty line — is a
|
|
2540
|
+
* decline, because an unreadable prompt is not consent. The credential value is
|
|
2541
|
+
* never passed in, so neither the prompt text nor a caught exception can carry it.
|
|
2542
|
+
*/
|
|
2543
|
+
export async function requestCredentialRouteSwitchConfirmation(deps, promptText) {
|
|
2544
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
2545
|
+
return false;
|
|
2546
|
+
try {
|
|
2547
|
+
const answer = (await deps.promptLine(promptText)).trim().toLowerCase();
|
|
2548
|
+
return answer === "y" || answer === "yes";
|
|
2549
|
+
}
|
|
2550
|
+
catch {
|
|
2551
|
+
// Secret-free by construction: the caught value is never surfaced or inspected.
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2169
2555
|
/** The self-serve mint endpoint: `<base>/setup/bootstrap/self-serve`. No query string — ever. */
|
|
2170
2556
|
export function buildSelfServeMintUrl(baseUrl) {
|
|
2171
2557
|
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
@@ -2253,7 +2639,7 @@ export const BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE = [
|
|
|
2253
2639
|
"",
|
|
2254
2640
|
"Ask your Bridge API operator to recover it for you: they revoke the orphaned key",
|
|
2255
2641
|
"(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project",
|
|
2256
|
-
"(POST /setup/keys), then send you that key. Run install
|
|
2642
|
+
"(POST /setup/keys), then send you that key. Run install with --api-key <that key>.",
|
|
2257
2643
|
].join("\n");
|
|
2258
2644
|
/**
|
|
2259
2645
|
* The 401 message for a run that REUSED an existing pending secret — the replay
|
|
@@ -2282,8 +2668,8 @@ export const BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE = "The Bridge API rejected the
|
|
|
2282
2668
|
*/
|
|
2283
2669
|
export function buildBootstrapRetryAdvice(selfServeSignupMode) {
|
|
2284
2670
|
return selfServeSignupMode
|
|
2285
|
-
? "Re-run install
|
|
2286
|
-
: "Re-run install
|
|
2671
|
+
? "Re-run install and choose the email option — your previous attempt will resume."
|
|
2672
|
+
: "Re-run install with the same bootstrap invite — the redemption will replay and " +
|
|
2287
2673
|
"return the same key.";
|
|
2288
2674
|
}
|
|
2289
2675
|
/**
|
|
@@ -2332,6 +2718,69 @@ export async function resolveCredentialConflictConsent(prepared, ctx) {
|
|
|
2332
2718
|
ctx.grantConsent();
|
|
2333
2719
|
return ctx.retry();
|
|
2334
2720
|
}
|
|
2721
|
+
/**
|
|
2722
|
+
* The secret-free install-state file. Gitignored before it is written, so it is
|
|
2723
|
+
* named here once and fed to BOTH the gitignore call and the commit-notice
|
|
2724
|
+
* exclusion set — a single definition site keeps the two from drifting apart.
|
|
2725
|
+
*/
|
|
2726
|
+
export const INSTALL_BRIDGE_INSTALL_STATE_PATH = ".bridge/install-state.json";
|
|
2727
|
+
/** Heading printed above the commit-your-assets list. Shared with the tests. */
|
|
2728
|
+
export const INSTALL_BRIDGE_COMMIT_ASSETS_HEADING = "Commit these generated files so your team inherits the same Bridge setup:";
|
|
2729
|
+
/**
|
|
2730
|
+
* Format the commit-your-assets notice (BAPI-708, Part C).
|
|
2731
|
+
*
|
|
2732
|
+
* THIS IS A CORRECTNESS BOUNDARY, NOT COPY. Several files this install writes are
|
|
2733
|
+
* deliberately gitignored because they can carry a real `BAPI_API_KEY` — the
|
|
2734
|
+
* per-host MCP configs above all — and `.bridge/install-state.json` is gitignored
|
|
2735
|
+
* too. A generic "commit your Bridge assets" is exactly how a live key reaches a
|
|
2736
|
+
* public remote. So the notice is built by SUBTRACTION as well as selection: every
|
|
2737
|
+
* path this run handed to `ensureGitignoredShared` is removed from the candidate
|
|
2738
|
+
* list, which means the safe set cannot silently acquire a gitignored target if
|
|
2739
|
+
* the host-target registry grows a new entry later.
|
|
2740
|
+
*
|
|
2741
|
+
* SECRET DISCIPLINE: parameters are project-relative scaffold paths only. The
|
|
2742
|
+
* credential-store path is never passed in — it lives outside the repository and
|
|
2743
|
+
* holds the key, so it is excluded by never being a candidate at all, not by
|
|
2744
|
+
* filtering.
|
|
2745
|
+
*
|
|
2746
|
+
* Returns `""` when nothing survives, so a caller can skip an empty notice.
|
|
2747
|
+
*/
|
|
2748
|
+
export function buildCommitGeneratedAssetsNotice(safePaths, gitignoredPaths) {
|
|
2749
|
+
// POSIX-normalized, trailing slash preserved (it marks a directory), so
|
|
2750
|
+
// "./a/b" and "a//b" compare equal to "a/b" on both sides of the exclusion.
|
|
2751
|
+
const normalize = (value) => {
|
|
2752
|
+
const trailingSlash = /\/+$/.test(value.trim());
|
|
2753
|
+
const collapsed = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2754
|
+
return trailingSlash ? `${collapsed}/` : collapsed;
|
|
2755
|
+
};
|
|
2756
|
+
const excluded = new Set();
|
|
2757
|
+
for (const raw of gitignoredPaths) {
|
|
2758
|
+
const normalized = normalize(raw);
|
|
2759
|
+
if (normalized.length === 0)
|
|
2760
|
+
continue;
|
|
2761
|
+
// Match a gitignored file whether the candidate names it with or without a
|
|
2762
|
+
// trailing slash, so a registry entry can never slip through on formatting.
|
|
2763
|
+
excluded.add(normalized);
|
|
2764
|
+
excluded.add(normalized.replace(/\/+$/, ""));
|
|
2765
|
+
excluded.add(`${normalized.replace(/\/+$/, "")}/`);
|
|
2766
|
+
}
|
|
2767
|
+
const seen = new Set();
|
|
2768
|
+
const lines = [];
|
|
2769
|
+
for (const raw of safePaths) {
|
|
2770
|
+
const normalized = normalize(raw);
|
|
2771
|
+
if (normalized.length === 0)
|
|
2772
|
+
continue;
|
|
2773
|
+
if (excluded.has(normalized))
|
|
2774
|
+
continue;
|
|
2775
|
+
if (seen.has(normalized))
|
|
2776
|
+
continue;
|
|
2777
|
+
seen.add(normalized);
|
|
2778
|
+
lines.push(` ${normalized}`);
|
|
2779
|
+
}
|
|
2780
|
+
if (lines.length === 0)
|
|
2781
|
+
return "";
|
|
2782
|
+
return [INSTALL_BRIDGE_COMMIT_ASSETS_HEADING, ...lines].join("\n");
|
|
2783
|
+
}
|
|
2335
2784
|
/**
|
|
2336
2785
|
* Render the --dry-run preview lines. Every secret is ALWAYS redacted — the
|
|
2337
2786
|
* spawnCommand, config preview, and (in bootstrap-invite mode) the exchange body
|
|
@@ -2344,10 +2793,15 @@ export function buildDryRunPreview(plan) {
|
|
|
2344
2793
|
if (plan.bootstrapInvite)
|
|
2345
2794
|
return buildBootstrapDryRunPreview(plan);
|
|
2346
2795
|
return [
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2796
|
+
// Any defined status means the read-only GET really happened, so the header is
|
|
2797
|
+
// truthful for every outcome — but ONLY `resolved` may claim the name came back
|
|
2798
|
+
// from the server (BAPI-687, C-4).
|
|
2799
|
+
plan.serverRepoResolutionStatus !== undefined
|
|
2800
|
+
? "install --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
|
|
2801
|
+
: "install --dry-run (no writes, no network, no spawns)",
|
|
2802
|
+
`Repo name: ${plan.repoName}${plan.serverRepoResolutionStatus === "resolved"
|
|
2803
|
+
? " (resolved server-side from your API key)"
|
|
2804
|
+
: ""}`,
|
|
2351
2805
|
`Base URL (ping): ${plan.baseUrl}`,
|
|
2352
2806
|
`Docs dir: ${plan.docsDir}`,
|
|
2353
2807
|
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
@@ -2364,9 +2818,25 @@ export function buildDryRunPreview(plan) {
|
|
|
2364
2818
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
|
|
2365
2819
|
MCP_TIMEOUT_GUIDANCE,
|
|
2366
2820
|
`Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
|
|
2821
|
+
...buildCommitAssetsPreviewLines(plan),
|
|
2367
2822
|
...buildLaunchStepPreview(plan),
|
|
2368
2823
|
];
|
|
2369
2824
|
}
|
|
2825
|
+
/**
|
|
2826
|
+
* The commit-assets notice as preview lines (BAPI-708, Part C). A `--dry-run`
|
|
2827
|
+
* returns before the live emission point, and this module's convention is that a
|
|
2828
|
+
* preview never advertises different behavior — so the same notice, built from the
|
|
2829
|
+
* same resolved manifest, appears here too. The gitignored exclusion set is the
|
|
2830
|
+
* plan's own `configTargets` plus the install-state file, exactly as the live path
|
|
2831
|
+
* computes it.
|
|
2832
|
+
*/
|
|
2833
|
+
function buildCommitAssetsPreviewLines(plan) {
|
|
2834
|
+
const notice = buildCommitGeneratedAssetsNotice(plan.commitSafeAssets ?? [], [
|
|
2835
|
+
...plan.configTargets,
|
|
2836
|
+
INSTALL_BRIDGE_INSTALL_STATE_PATH,
|
|
2837
|
+
]);
|
|
2838
|
+
return notice.length > 0 ? ["", notice] : [];
|
|
2839
|
+
}
|
|
2370
2840
|
/**
|
|
2371
2841
|
* One-line description of the planned launch agent for the dry-run header (BAPI-657).
|
|
2372
2842
|
* Never claims an unconditional Claude spawn: it reflects the discriminated launch
|
|
@@ -2407,8 +2877,10 @@ function buildLaunchStepPreview(plan) {
|
|
|
2407
2877
|
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
2408
2878
|
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
2409
2879
|
` interactive, offer '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/, "")}' before the agent session starts.`,
|
|
2410
|
-
|
|
2411
|
-
|
|
2880
|
+
// BAPI-708: the purpose sentence is kept on ONE line in both copies so the
|
|
2881
|
+
// no-drift test can compare the whole phrase rather than a wrapped fragment.
|
|
2882
|
+
" It installs the Bridge GitHub App so you can implement and review tickets with Bridge.",
|
|
2883
|
+
" It opens github.com in your browser, and shares no GitHub credential with Bridge; it",
|
|
2412
2884
|
` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
|
|
2413
2885
|
];
|
|
2414
2886
|
if (plan.launch.kind === "spawn") {
|
|
@@ -2452,8 +2924,8 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
2452
2924
|
// created, no mint request is sent, no email leaves the machine. The email and
|
|
2453
2925
|
// any synthetic token are deliberately absent from this output.
|
|
2454
2926
|
const header = plan.selfServeSignup
|
|
2455
|
-
? "install
|
|
2456
|
-
: "install
|
|
2927
|
+
? "install --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
|
|
2928
|
+
: "install --invite --dry-run (no writes, no network, no spawns, no secret generated)";
|
|
2457
2929
|
const repoLine = plan.selfServeSignup
|
|
2458
2930
|
? `Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`
|
|
2459
2931
|
: `Repo name: ${plan.repoName} (created by the exchange; globally unique)`;
|
|
@@ -2493,6 +2965,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
2493
2965
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
|
|
2494
2966
|
MCP_TIMEOUT_GUIDANCE,
|
|
2495
2967
|
`Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
|
|
2968
|
+
...buildCommitAssetsPreviewLines(plan),
|
|
2496
2969
|
...buildLaunchStepPreview(plan),
|
|
2497
2970
|
];
|
|
2498
2971
|
}
|
|
@@ -2542,6 +3015,28 @@ export function manualEditorNames(editors) {
|
|
|
2542
3015
|
export function formatNeedKeyCredentialStoreLine(repoName, credentialStorePath) {
|
|
2543
3016
|
return `Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" — copy BAPI_API_KEY from there.`;
|
|
2544
3017
|
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Where THIS run's routing credential landed (BAPI-708, B-b) — the single Step 4
|
|
3020
|
+
* result line, printed on the have-key and need-key arms alike.
|
|
3021
|
+
*
|
|
3022
|
+
* It replaces both `stored routing credential for … at …` lines, and it is also
|
|
3023
|
+
* what makes Part B-a honest: `.mcp.json` is an MCP SERVER REGISTRATION, not a
|
|
3024
|
+
* credentials file, so the one place a credential is reported must name the
|
|
3025
|
+
* credential store instead.
|
|
3026
|
+
*
|
|
3027
|
+
* Distinct from {@link formatNeedKeyCredentialStoreLine}, which stays for the
|
|
3028
|
+
* MANUAL-EDITOR context — its "copy BAPI_API_KEY from there" instruction only
|
|
3029
|
+
* makes sense when the user has a global config to paste into.
|
|
3030
|
+
*
|
|
3031
|
+
* SECRET DISCIPLINE (hard constraint, same as its sibling): this formatter accepts
|
|
3032
|
+
* NO key material. Its parameters are a repo name and a filesystem path, both
|
|
3033
|
+
* printable; it must never be handed `apiKey`, `inviteToken`, `keySecret`, a signup
|
|
3034
|
+
* email, or a credential record. The arity is pinned by test precisely so a third
|
|
3035
|
+
* parameter cannot be added without that test failing.
|
|
3036
|
+
*/
|
|
3037
|
+
export function formatProjectCredentialStoreLine(repoName, credentialStorePath) {
|
|
3038
|
+
return `We wrote your project credentials to: ${credentialStorePath} (target "bapi:${repoName}")`;
|
|
3039
|
+
}
|
|
2545
3040
|
/**
|
|
2546
3041
|
* Build the manual-instruction block for the DETECTED global-config editors only
|
|
2547
3042
|
* (key always redacted). Returns `null` when neither Windsurf nor Codex is
|
|
@@ -2686,7 +3181,7 @@ function readInstallBridgeDebugValue(read) {
|
|
|
2686
3181
|
*/
|
|
2687
3182
|
export function buildInstallBridgeFailureLines(error, context) {
|
|
2688
3183
|
const lines = [
|
|
2689
|
-
`Error: install
|
|
3184
|
+
`Error: install failed at: ${context.step}`,
|
|
2690
3185
|
` cause: ${classifyInstallBridgeFailure(error)}`,
|
|
2691
3186
|
` ${INSTALL_BRIDGE_DOCTOR_POINTER}`,
|
|
2692
3187
|
];
|
|
@@ -2753,6 +3248,14 @@ export function resolveInstallBridgeBaseUrl(env) {
|
|
|
2753
3248
|
* Every printed Step 1–5 label, defined once. `runInstallBridgeCli` uses each
|
|
2754
3249
|
* constant BOTH when logging the step AND when recording the current step, so a
|
|
2755
3250
|
* failure can never be attributed to a label the user did not just see.
|
|
3251
|
+
*
|
|
3252
|
+
* BAPI-708 (Part D) removed the separate `promoteCredential` entry: promotion is
|
|
3253
|
+
* not a step the user does anything about, and printing it as its own numbered
|
|
3254
|
+
* step told them the bootstrap two-phase commit exists without giving them a way
|
|
3255
|
+
* to act on it. Both credential arms now log `persistCredential`. Attribution is
|
|
3256
|
+
* NOT lost — the promotion-failure fatal is highly specific about what failed and
|
|
3257
|
+
* what state the user is left in. The step COUNT is unchanged at 5; nothing is
|
|
3258
|
+
* renumbered.
|
|
2756
3259
|
*/
|
|
2757
3260
|
export const INSTALL_BRIDGE_STEP_LABELS = {
|
|
2758
3261
|
scaffold: "Step 1/5 — scaffolding project (commands, agents, pipelines, config placeholders)…",
|
|
@@ -2760,7 +3263,6 @@ export const INSTALL_BRIDGE_STEP_LABELS = {
|
|
|
2760
3263
|
inviteRedeem: "Step 2/5 — redeeming the bootstrap invite…",
|
|
2761
3264
|
verifyConnectivity: "Step 2/5 — verifying connectivity…",
|
|
2762
3265
|
writeHostConfigs: "Step 3/5 — writing per-host MCP config…",
|
|
2763
|
-
promoteCredential: "Step 4/5 — promoting the bootstrap credential…",
|
|
2764
3266
|
persistCredential: "Step 4/5 — persisting routing credential…",
|
|
2765
3267
|
};
|
|
2766
3268
|
/**
|
|
@@ -2792,7 +3294,7 @@ export const INSTALL_BRIDGE_NO_GIT_WARNING = "This doesn't look like a project r
|
|
|
2792
3294
|
/** Default-No confirmation shown only on a TTY when `.git` is absent. */
|
|
2793
3295
|
export const INSTALL_BRIDGE_NO_GIT_PROMPT = `${INSTALL_BRIDGE_NO_GIT_WARNING} — continue? [y/N]: `;
|
|
2794
3296
|
/** Printed on decline. Paired with exit code 0 — a cancel is not an error. */
|
|
2795
|
-
export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install
|
|
3297
|
+
export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install from your project root, or re-run and confirm to continue.";
|
|
2796
3298
|
/** The non-TTY form: same finding, stated as a warning, then the run continues. */
|
|
2797
3299
|
export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} — continuing (non-interactive).`;
|
|
2798
3300
|
/**
|
|
@@ -2858,6 +3360,63 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2858
3360
|
// A rejected value cannot be interpolated into guidance, so usage falls back to
|
|
2859
3361
|
// the documented default — the run is about to fail on it anyway.
|
|
2860
3362
|
const usageBaseUrl = baseUrlResult.ok ? baseUrlResult.value : DEFAULT_BAPI_BASE_URL;
|
|
3363
|
+
// ---- BAPI-679: nested `install-bridge conductor` -----------------------
|
|
3364
|
+
// Intercepted HERE, before `parseInstallBridgeArgs`, because that parser
|
|
3365
|
+
// rejects ALL positional arguments — a bare `conductor` token would otherwise
|
|
3366
|
+
// be an argument error. Deliberately after the deps merge and the base-URL
|
|
3367
|
+
// resolution above, so the nested command inherits the same final merged deps
|
|
3368
|
+
// (including a test's `fetch` override) and the same normalized base URL.
|
|
3369
|
+
// Note this is `install-bridge conductor`; the bare top-level `conductor`
|
|
3370
|
+
// subcommand still routes to `runConductorCli` (the local event ledger) and is
|
|
3371
|
+
// untouched.
|
|
3372
|
+
if (argv[0] === "conductor") {
|
|
3373
|
+
if (!baseUrlResult.ok) {
|
|
3374
|
+
errorLog(baseUrlResult.error);
|
|
3375
|
+
return 1;
|
|
3376
|
+
}
|
|
3377
|
+
const conductorBaseUrl = baseUrlResult.value;
|
|
3378
|
+
const conductorDeps = {
|
|
3379
|
+
env: deps.env,
|
|
3380
|
+
cwd: deps.cwd,
|
|
3381
|
+
log: deps.log,
|
|
3382
|
+
errorLog: deps.errorLog,
|
|
3383
|
+
promptLine: deps.promptLine,
|
|
3384
|
+
resolveBaseUrl: () => ({ ok: true, value: conductorBaseUrl }),
|
|
3385
|
+
resolveAccess: (repoName, resolvedBaseUrl) => (deps.conductorResolveAccess ??
|
|
3386
|
+
(async () => ({ ok: false, error: "conductor access seam unavailable" })))(repoName, resolvedBaseUrl, deps),
|
|
3387
|
+
fetchJson: deps.fetch,
|
|
3388
|
+
postJson: deps.fetch,
|
|
3389
|
+
runDoctor: (params) => (deps.conductorRunDoctor ??
|
|
3390
|
+
(async () => ({
|
|
3391
|
+
legacyConductor: null,
|
|
3392
|
+
legacyExecutorPreflight: null,
|
|
3393
|
+
legacyInstallChecks: null,
|
|
3394
|
+
executorService: null,
|
|
3395
|
+
readiness: null,
|
|
3396
|
+
sections: [
|
|
3397
|
+
{
|
|
3398
|
+
id: "conductor-doctor",
|
|
3399
|
+
label: "Conductor install doctor",
|
|
3400
|
+
status: "fatal",
|
|
3401
|
+
detail: "the unified doctor seam is unavailable",
|
|
3402
|
+
},
|
|
3403
|
+
],
|
|
3404
|
+
})))({
|
|
3405
|
+
...params,
|
|
3406
|
+
fetch: deps.fetch,
|
|
3407
|
+
cwd: deps.cwd,
|
|
3408
|
+
env: deps.env,
|
|
3409
|
+
readFile: deps.readFile,
|
|
3410
|
+
}),
|
|
3411
|
+
writeWorkflow: (cwd, content, options) => (deps.conductorWriteWorkflow ??
|
|
3412
|
+
(async () => ({ ok: false, error: "workflow seam unavailable" })))(cwd, content, options, deps),
|
|
3413
|
+
recordArtifact: (cwd, relPath) => (deps.conductorRecordArtifact ??
|
|
3414
|
+
(async () => ({ ok: false, error: "install-state seam unavailable" })))(cwd, relPath, deps),
|
|
3415
|
+
runSetupEpicDryRun: (setupArgv) => (deps.conductorRunSetupEpicDryRun ??
|
|
3416
|
+
(async () => ({ exitCode: 1 })))(setupArgv),
|
|
3417
|
+
};
|
|
3418
|
+
return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
|
|
3419
|
+
}
|
|
2861
3420
|
const parsed = parseInstallBridgeArgs(argv);
|
|
2862
3421
|
if (parsed.status === "help") {
|
|
2863
3422
|
log(getInstallBridgeUsage(usageBaseUrl));
|
|
@@ -2927,12 +3486,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2927
3486
|
return 1;
|
|
2928
3487
|
}
|
|
2929
3488
|
const branch = branchResult.branch;
|
|
3489
|
+
// BAPI-708 (A-5): provenance comes from the branch resolver and NOWHERE else. It
|
|
3490
|
+
// is deliberately NOT re-derived from `argv`/`env` here — the resolver owns that
|
|
3491
|
+
// logic, and a second derivation is exactly the drift this ticket removes.
|
|
3492
|
+
const credentialIntent = branchResult.credentialIntent;
|
|
2930
3493
|
// Mutable (BAPI-661): an ordinary have-key credential that classifies as a
|
|
2931
3494
|
// bootstrap invite (see the have-key arm below) flips `bootstrapInviteMode` to
|
|
2932
3495
|
// true AFTER branch selection, before repository-name resolution — everything
|
|
2933
3496
|
// downstream that branches on these two flags then follows the existing
|
|
2934
3497
|
// bootstrap-invite protocol automatically. Explicit `--invite`/`BAPI_INVITE` and
|
|
2935
3498
|
// `--email`/`BAPI_SIGNUP_EMAIL` never pass through this reclassification.
|
|
3499
|
+
//
|
|
3500
|
+
// BAPI-687 (Part B) makes the reclassification BIDIRECTIONAL and, in both
|
|
3501
|
+
// directions, ANNOUNCED. The two directions are deliberately asymmetric on a
|
|
3502
|
+
// non-TTY: an invite found on the key path still switches automatically (a script
|
|
3503
|
+
// that worked yesterday must not gain a prompt) but now prints a notice, whereas
|
|
3504
|
+
// an API key found on the invite path exits non-zero, because auto-switching there
|
|
3505
|
+
// would silently redirect a script away from the project-creating run it asked
|
|
3506
|
+
// for. Detection is by SHAPE ONLY — no probe request, no credential-store read.
|
|
2936
3507
|
let bootstrapInviteMode = branch.kind === "need-key";
|
|
2937
3508
|
let selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
|
|
2938
3509
|
// ---- Resolve inputs (may prompt when interactive) ----
|
|
@@ -2965,6 +3536,47 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2965
3536
|
return 1;
|
|
2966
3537
|
}
|
|
2967
3538
|
inviteToken = inviteResult.value;
|
|
3539
|
+
// BAPI-687 (B-3/B-4): an API-key-shaped value pasted onto the invite path is
|
|
3540
|
+
// caught HERE — before the project-name prompt, before `prepareBootstrapPending`
|
|
3541
|
+
// writes a durable `bootstrap-pending:<repo>` record, and before any fetch. That
|
|
3542
|
+
// ordering IS the fix: today's failure leaves exactly that debris behind, and the
|
|
3543
|
+
// orphaned pending record is what turns a wrong-credential paste into an
|
|
3544
|
+
// unrecoverable "ask your Bridge API operator" message plus a `pending-conflict`
|
|
3545
|
+
// on the next attempt.
|
|
3546
|
+
//
|
|
3547
|
+
// Guarded by BOTH conditions (B-5): not `bapi_inv_`-prefixed AND a positive
|
|
3548
|
+
// API-key shape. A value that is neither — a typo'd invite, say — is not
|
|
3549
|
+
// reclassified in either direction; it stays on the branch the user chose and
|
|
3550
|
+
// fails there exactly as it does today.
|
|
3551
|
+
if (classifyEnteredCredential(inviteToken) !== "invite" &&
|
|
3552
|
+
isHighEntropyApiKeyShape(inviteToken)) {
|
|
3553
|
+
log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
|
|
3554
|
+
// BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES. On an INFERRED route the
|
|
3555
|
+
// announcement above is purely informational — the user never declared the
|
|
3556
|
+
// invite path, so there is no stated intent for the value to contradict and a
|
|
3557
|
+
// confirmation would ask them to ratify a decision they never made. On a
|
|
3558
|
+
// DECLARED route the confirmation is byte-for-byte BAPI-687 behavior,
|
|
3559
|
+
// including the intentional non-TTY asymmetry: an API-key-shaped value on the
|
|
3560
|
+
// declared invite path exits non-zero rather than silently redirecting a
|
|
3561
|
+
// script away from the project-creating run it asked for.
|
|
3562
|
+
if (credentialIntent === "declared") {
|
|
3563
|
+
const switchToKeyPath = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT);
|
|
3564
|
+
if (!switchToKeyPath) {
|
|
3565
|
+
// Decline, an unreadable prompt, and a non-TTY run all land here — nothing
|
|
3566
|
+
// has been prompted for, written, or sent, so the exit costs the user only
|
|
3567
|
+
// the re-run named in the guidance.
|
|
3568
|
+
errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED);
|
|
3569
|
+
return 1;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
apiKey = inviteToken;
|
|
3573
|
+
inviteToken = "";
|
|
3574
|
+
bootstrapInviteMode = false;
|
|
3575
|
+
selfServeSignupMode = false;
|
|
3576
|
+
// Deliberately unattributed: the value came through the INVITE input seam, so
|
|
3577
|
+
// naming `--api-key` or `BAPI_API_KEY` on a later 401 would simply be false.
|
|
3578
|
+
apiKeySource = undefined;
|
|
3579
|
+
}
|
|
2968
3580
|
}
|
|
2969
3581
|
else {
|
|
2970
3582
|
const keyResult = await resolveApiKey(options, deps);
|
|
@@ -2978,6 +3590,28 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2978
3590
|
// resolution is what lets the rest of the function reuse the existing
|
|
2979
3591
|
// bootstrap-invite protocol verbatim.
|
|
2980
3592
|
if (classifyEnteredCredential(keyResult.value) === "invite") {
|
|
3593
|
+
// BAPI-687 (B-2): the switch survives, the SILENCE does not. The user is told
|
|
3594
|
+
// which credential type was detected and what it will do.
|
|
3595
|
+
log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE);
|
|
3596
|
+
// BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES — see the matching comment
|
|
3597
|
+
// on the invite-path arm above. INFERRED means the user answered "yes, I have
|
|
3598
|
+
// a token" and pasted it; they never claimed it was an API key, so the notice
|
|
3599
|
+
// is an announcement and the run simply proceeds. DECLARED keeps BAPI-687
|
|
3600
|
+
// verbatim, asymmetry included: a TTY gets the default-No confirmation, while
|
|
3601
|
+
// a non-TTY still auto-switches (a script that worked yesterday must not gain
|
|
3602
|
+
// a prompt) and prints the notice so the transcript explains itself.
|
|
3603
|
+
if (credentialIntent === "declared") {
|
|
3604
|
+
if (deps.isTTY && deps.promptLine) {
|
|
3605
|
+
const switchToInvite = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT);
|
|
3606
|
+
if (!switchToInvite) {
|
|
3607
|
+
errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED);
|
|
3608
|
+
return 1;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
else {
|
|
3612
|
+
log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
2981
3615
|
inviteToken = keyResult.value.trim();
|
|
2982
3616
|
apiKey = "";
|
|
2983
3617
|
bootstrapInviteMode = true;
|
|
@@ -2987,6 +3621,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2987
3621
|
apiKeySource = undefined;
|
|
2988
3622
|
}
|
|
2989
3623
|
else {
|
|
3624
|
+
// BAPI-708 (A-4): on the INFERRED path the route decision is announced even
|
|
3625
|
+
// when nothing switches, so a bare run always says which credential type it
|
|
3626
|
+
// detected and where that leads. Reuses the existing notice because it names
|
|
3627
|
+
// only the credential TYPE and the destination ROUTE — never the value.
|
|
3628
|
+
// Guarded on the POSITIVE shape test: a value matching neither shape is not
|
|
3629
|
+
// reclassified AND not announced; it stays on this route and fails here
|
|
3630
|
+
// exactly as it does today. Declared paths announce nothing new.
|
|
3631
|
+
if (credentialIntent === "inferred" && isHighEntropyApiKeyShape(keyResult.value.trim())) {
|
|
3632
|
+
log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
|
|
3633
|
+
}
|
|
2990
3634
|
apiKey = keyResult.value;
|
|
2991
3635
|
apiKeySource = keyResult.source;
|
|
2992
3636
|
}
|
|
@@ -2997,13 +3641,25 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2997
3641
|
// ---- Resolve the repository name ----
|
|
2998
3642
|
// Need-key (invite or self-serve): choose-a-name for the project this run is
|
|
2999
3643
|
// about to create, so the prompt says exactly that. Have-key:
|
|
3000
|
-
// `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve
|
|
3001
|
-
//
|
|
3002
|
-
//
|
|
3003
|
-
//
|
|
3004
|
-
//
|
|
3644
|
+
// `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve it
|
|
3645
|
+
// server-side from the API key (BAPI-616).
|
|
3646
|
+
//
|
|
3647
|
+
// BAPI-687 (C-2) TIERS the non-`resolved` outcomes instead of collapsing them:
|
|
3648
|
+
//
|
|
3649
|
+
// - `unresolved` (409) → HARD STOP. The key does not identify exactly one
|
|
3650
|
+
// project, and that answer is real rather than a deployment artifact:
|
|
3651
|
+
// `user_access.api_key_lookup` cannot be backfilled offline, but
|
|
3652
|
+
// `verify_repo_access` lazily populates it on any successful authentication,
|
|
3653
|
+
// so the permanently-unresolvable population is only pre-migration keys that
|
|
3654
|
+
// have never authenticated since — small and shrinking. Guessing a name here
|
|
3655
|
+
// buys nothing but a 403 two steps later.
|
|
3656
|
+
// - `not-deployed` (404) / `error` → still recoverable, and must stay that way:
|
|
3657
|
+
// 404 is the BAPI-616 feature-detect for an older or self-hosted server, and
|
|
3658
|
+
// `error` covers a transient network failure or a 401. Collapsing these into
|
|
3659
|
+
// the hard stop would make a blip unrecoverable.
|
|
3005
3660
|
let repoName;
|
|
3006
|
-
|
|
3661
|
+
/** The resolver's OUTCOME (C-4), or undefined when it was never called. */
|
|
3662
|
+
let serverRepoResolutionStatus;
|
|
3007
3663
|
if (bootstrapInviteMode) {
|
|
3008
3664
|
const repoResult = await resolveRepoName(options, deps, "new-project");
|
|
3009
3665
|
if (!repoResult.ok) {
|
|
@@ -3026,20 +3682,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3026
3682
|
repoName = configured;
|
|
3027
3683
|
}
|
|
3028
3684
|
else {
|
|
3029
|
-
attemptedServerResolution = true;
|
|
3030
3685
|
log("Resolving repository…");
|
|
3031
3686
|
const resolution = await deps.resolveRepoViaServer(baseUrl, apiKey);
|
|
3687
|
+
serverRepoResolutionStatus = resolution.status;
|
|
3032
3688
|
if (resolution.status === "resolved") {
|
|
3033
3689
|
repoName = resolution.repoName;
|
|
3034
3690
|
}
|
|
3691
|
+
else if (resolution.status === "unresolved") {
|
|
3692
|
+
// C-2: a 409 means the key does not identify exactly one project. Stop here,
|
|
3693
|
+
// before any prompt, ping, scaffold, persist, or spawn — and name BOTH
|
|
3694
|
+
// recoveries rather than inferring anything.
|
|
3695
|
+
return fatal(INSTALL_BRIDGE_UNRESOLVED_KEY_ERROR, ` Or rotate the key at ${setupUrl} (Security page) if it should identify exactly one project.`);
|
|
3696
|
+
}
|
|
3035
3697
|
else {
|
|
3036
|
-
// Feature-detect + degrade: 404 (old server)
|
|
3037
|
-
//
|
|
3038
|
-
//
|
|
3039
|
-
//
|
|
3040
|
-
|
|
3041
|
-
log("Couldn't auto-resolve your repo from the key; falling back to a guessed name — confirm " +
|
|
3042
|
-
`it matches ${setupUrl} (setup UI).`);
|
|
3698
|
+
// Feature-detect + degrade: 404 (old server) and network/other errors keep
|
|
3699
|
+
// the local fallback prompt — WITHOUT a cause-specific message or leaked
|
|
3700
|
+
// detail. C-3/C-5: it no longer offers a name, so it no longer says
|
|
3701
|
+
// "guessed" or asks the user to confirm a value against the setup UI.
|
|
3702
|
+
log(INSTALL_BRIDGE_RESOLUTION_UNAVAILABLE_NOTICE);
|
|
3043
3703
|
const repoResult = await resolveRepoName(options, deps, "existing-registration");
|
|
3044
3704
|
if (!repoResult.ok) {
|
|
3045
3705
|
errorLog(`Error: ${repoResult.error}`);
|
|
@@ -3090,12 +3750,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3090
3750
|
else {
|
|
3091
3751
|
planLaunch = { kind: "manual", reason: launchDecision.reason };
|
|
3092
3752
|
}
|
|
3753
|
+
// BAPI-708 (Part C): resolved from THIS invocation's cwd + env, so the notice
|
|
3754
|
+
// names only what this run actually scaffolds — `.cursor/commands/` only when
|
|
3755
|
+
// Cursor was detected, `.github/agents/` only when VS Code was, and the pipelines
|
|
3756
|
+
// / instructions entries under whatever `BAPI_PIPELINES_DIR` resolved to. It is
|
|
3757
|
+
// deliberately independent of which MCP HOSTS were selected: a scaffolded command
|
|
3758
|
+
// directory is committed whether or not its editor got an MCP registration.
|
|
3759
|
+
const scaffoldAssets = await resolveInitScaffoldAssets({
|
|
3760
|
+
cwd: deps.cwd,
|
|
3761
|
+
env: deps.env,
|
|
3762
|
+
stat: deps.stat,
|
|
3763
|
+
});
|
|
3093
3764
|
const plan = {
|
|
3094
3765
|
repoName,
|
|
3095
3766
|
baseUrl,
|
|
3096
3767
|
docsDir,
|
|
3097
3768
|
launch: planLaunch,
|
|
3098
3769
|
configTargets: targets.map((t) => t.relPath),
|
|
3770
|
+
commitSafeAssets: scaffoldAssets.commitSafePaths,
|
|
3099
3771
|
manualEditors: manualEditorNames(manualEditors),
|
|
3100
3772
|
credentialTarget: `bapi:${repoName}`,
|
|
3101
3773
|
credentialStorePath,
|
|
@@ -3105,7 +3777,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3105
3777
|
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
3106
3778
|
: {}),
|
|
3107
3779
|
...(selfServeSignupMode ? { selfServeSignup: true } : {}),
|
|
3108
|
-
...(
|
|
3780
|
+
...(serverRepoResolutionStatus !== undefined
|
|
3781
|
+
? { serverRepoResolutionStatus }
|
|
3782
|
+
: {}),
|
|
3109
3783
|
};
|
|
3110
3784
|
// ---- --dry-run: preview every step, strictly no side effects ----
|
|
3111
3785
|
// Positioned BEFORE the CSPRNG, the pending write, the exchange, the scaffold,
|
|
@@ -3330,7 +4004,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3330
4004
|
}
|
|
3331
4005
|
keySecret = prep.keySecret;
|
|
3332
4006
|
reusedPendingSecret = prep.reused;
|
|
3333
|
-
|
|
4007
|
+
// BAPI-708 (Part D): the pending write is NOT narrated on the happy path.
|
|
4008
|
+
// The target name and the fsync ordering are implementation detail the user
|
|
4009
|
+
// cannot act on; they remain fully described in --dry-run and in the failure
|
|
4010
|
+
// messages, which name the pending record and what it means for a retry. The
|
|
4011
|
+
// MECHANISM above is untouched — see the LOAD-BEARING ORDERING block below.
|
|
3334
4012
|
return true;
|
|
3335
4013
|
};
|
|
3336
4014
|
// ---- Self-serve signup (BAPI-618): mint the invite token from the email ----
|
|
@@ -3415,7 +4093,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3415
4093
|
}
|
|
3416
4094
|
keySecret = prepared.keySecret;
|
|
3417
4095
|
reusedPendingSecret = prepared.reused;
|
|
3418
|
-
|
|
4096
|
+
// BAPI-708 (Part D): happy-path narration removed; the mechanism above is
|
|
4097
|
+
// unchanged. See the matching comment on the self-serve arm.
|
|
3419
4098
|
}
|
|
3420
4099
|
// The exchange REPLACES the pre-flight ping: in invite mode there is no key to
|
|
3421
4100
|
// ping with — this call is what mints it.
|
|
@@ -3472,7 +4151,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3472
4151
|
!selfServeRefreshed) {
|
|
3473
4152
|
if (!deps.isTTY || !deps.promptLine) {
|
|
3474
4153
|
return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE, `The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} ` +
|
|
3475
|
-
"has been left untouched. Re-run install
|
|
4154
|
+
"has been left untouched. Re-run install on an interactive terminal to " +
|
|
3476
4155
|
"discard it and start a fresh signup.");
|
|
3477
4156
|
}
|
|
3478
4157
|
errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);
|
|
@@ -3605,8 +4284,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3605
4284
|
trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
|
|
3606
4285
|
}
|
|
3607
4286
|
const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
|
|
4287
|
+
// BAPI-708 (B-a): these files are the MCP SERVER REGISTRATION (BAPI_REPO_NAME,
|
|
4288
|
+
// BAPI_BASE_URL, BAPI_DOCS_DIR, BAPI_PROJECT_ROOT, and the version-pinned npx
|
|
4289
|
+
// launcher) — calling them a credentials file was simply inaccurate. Where the
|
|
4290
|
+
// credential actually landed is reported once, at Step 4.
|
|
3608
4291
|
for (const { relPath, mode } of writeResult.written) {
|
|
3609
|
-
log(mode === "secret-free"
|
|
4292
|
+
log(mode === "secret-free"
|
|
4293
|
+
? ` registered the Bridge MCP server in ${relPath} (secret-free — key resolved at runtime)`
|
|
4294
|
+
: ` registered the Bridge MCP server in ${relPath}`);
|
|
3610
4295
|
}
|
|
3611
4296
|
for (const { relPath } of writeResult.skipped) {
|
|
3612
4297
|
log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
|
|
@@ -3642,16 +4327,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3642
4327
|
log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
|
|
3643
4328
|
"global ~/.copilot/mcp-config.json — the two are configured separately.");
|
|
3644
4329
|
}
|
|
3645
|
-
// BAPI-
|
|
3646
|
-
// not
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
4330
|
+
// BAPI-708 (B-c): the Claude trust reminder that used to print here was
|
|
4331
|
+
// RELOCATED, not dropped — the reload requirement is genuinely actionable (the
|
|
4332
|
+
// server does not work until the host reloads), but at Step 3 it duplicated the
|
|
4333
|
+
// spawn handoff. It now survives on EVERY terminal path: the spawn handoff
|
|
4334
|
+
// already carries it, and `buildManualInstallBridgeContinuation("configured")`
|
|
4335
|
+
// carries it for the declined, non-TTY, and failed-spawn paths.
|
|
3651
4336
|
// BAPI-635 (Step 8 + Step 3): persist the secret-free install state, ignoring
|
|
3652
4337
|
// it before the write. Project-local paths only; global paths are never in it.
|
|
3653
4338
|
try {
|
|
3654
|
-
await ensureGitignoredShared(deps.cwd,
|
|
4339
|
+
await ensureGitignoredShared(deps.cwd, INSTALL_BRIDGE_INSTALL_STATE_PATH, gitignoreDeps);
|
|
3655
4340
|
// writeMcpInstallState catches its own I/O errors and returns { ok: false }
|
|
3656
4341
|
// (it does NOT throw), so the failure warning must inspect the return value —
|
|
3657
4342
|
// a try/catch alone would silently swallow a real persistence failure.
|
|
@@ -3684,7 +4369,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3684
4369
|
// resolve. Promotion removes the pending record and writes the credential in one
|
|
3685
4370
|
// durable replacement; on failure the pending record survives so a re-run replays
|
|
3686
4371
|
// the same redemption. The agent session is NOT spawned until this succeeds.
|
|
3687
|
-
|
|
4372
|
+
// BAPI-708 (Part D): the SHARED Step 4 label — identical on both arms, so a
|
|
4373
|
+
// successful run prints exactly one Step 4 line and never narrates promotion
|
|
4374
|
+
// as a step of its own. Still assigned before the log, preserving the
|
|
4375
|
+
// log == record invariant the failure attribution depends on.
|
|
4376
|
+
currentStep = INSTALL_BRIDGE_STEP_LABELS.persistCredential;
|
|
3688
4377
|
log(currentStep);
|
|
3689
4378
|
const promoted = await deps.promoteBootstrapPending({ repoName, inviteFingerprint, allowOverwriteExistingCredential: overwriteConsent }, credentialWriteDeps);
|
|
3690
4379
|
if (!promoted.ok) {
|
|
@@ -3694,7 +4383,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3694
4383
|
`(${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending ` +
|
|
3695
4384
|
`record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);
|
|
3696
4385
|
}
|
|
3697
|
-
log(
|
|
4386
|
+
log(formatProjectCredentialStoreLine(repoName, promoted.path));
|
|
3698
4387
|
}
|
|
3699
4388
|
else {
|
|
3700
4389
|
// ---- persist routing credential (non-blocking / fail-open) ----
|
|
@@ -3703,13 +4392,13 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3703
4392
|
try {
|
|
3704
4393
|
const result = await deps.upsertCredential(repoName, apiKey, credentialWriteDeps);
|
|
3705
4394
|
if (result.ok) {
|
|
3706
|
-
log(
|
|
4395
|
+
log(formatProjectCredentialStoreLine(repoName, result.path));
|
|
3707
4396
|
}
|
|
3708
4397
|
else {
|
|
3709
4398
|
log(` warning: could not persist the routing credential (${result.kind}). ` +
|
|
3710
4399
|
`start-tickets model routing may not resolve the key for bapi:${repoName} ` +
|
|
3711
4400
|
"and will fail open to the premium/Opus tier (the most expensive) — " +
|
|
3712
|
-
"set BAPI_API_KEY in the shell or re-run install
|
|
4401
|
+
"set BAPI_API_KEY in the shell or re-run install, then verify with " +
|
|
3713
4402
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
3714
4403
|
}
|
|
3715
4404
|
}
|
|
@@ -3722,6 +4411,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3722
4411
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
3723
4412
|
}
|
|
3724
4413
|
}
|
|
4414
|
+
// ---- commit-your-assets notice (BAPI-708, Part C) ----
|
|
4415
|
+
// Emitted AFTER the credential step and BEFORE the GitHub offer and the Step 5
|
|
4416
|
+
// consent/spawn block, so every terminal path — spawn, decline, non-TTY, and
|
|
4417
|
+
// spawn-failure alike — receives it exactly once. The exclusion set is computed
|
|
4418
|
+
// from what this run actually gitignored (the selected project host targets and
|
|
4419
|
+
// the install-state file), not a hand-copied literal, so a future host-registry
|
|
4420
|
+
// entry is excluded automatically. `credentialStorePath` is never a candidate.
|
|
4421
|
+
const commitAssetsNotice = buildCommitGeneratedAssetsNotice(scaffoldAssets.commitSafePaths, [
|
|
4422
|
+
...targets.map((t) => t.relPath),
|
|
4423
|
+
INSTALL_BRIDGE_INSTALL_STATE_PATH,
|
|
4424
|
+
]);
|
|
4425
|
+
if (commitAssetsNotice.length > 0) {
|
|
4426
|
+
log("");
|
|
4427
|
+
log(commitAssetsNotice);
|
|
4428
|
+
}
|
|
3725
4429
|
// ---- optional GitHub connect offer (BAPI-631) ----
|
|
3726
4430
|
// Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
|
|
3727
4431
|
// the agent spawn, for two reasons: the spawned session's capability report should
|
|
@@ -3737,8 +4441,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3737
4441
|
if (prewarmPromise) {
|
|
3738
4442
|
const prewarm = await prewarmPromise;
|
|
3739
4443
|
if (prewarm.ok) {
|
|
3740
|
-
|
|
3741
|
-
|
|
4444
|
+
// BAPI-708 (Part B): a SUCCESSFUL pre-warm is silent. Both lines described
|
|
4445
|
+
// an internal optimization that worked and a remedy for a problem that did
|
|
4446
|
+
// not occur. The failure branch below — where the remedy is real — keeps
|
|
4447
|
+
// the MCP_TIMEOUT guidance verbatim, and both dry-run previews still carry
|
|
4448
|
+
// it, so it stays discoverable exactly where it is actionable.
|
|
3742
4449
|
}
|
|
3743
4450
|
else {
|
|
3744
4451
|
// Escalate to a stronger, still secret-free warning when pre-warm didn't
|
|
@@ -3780,8 +4487,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3780
4487
|
// raw agent binary name — computed once so all three sentences agree.
|
|
3781
4488
|
const handoffToolLabel = toolLabelForLaunchAgent(finalAgentName);
|
|
3782
4489
|
log("");
|
|
3783
|
-
log(`install
|
|
3784
|
-
"configuration, presenting the concise capability report, and
|
|
4490
|
+
log(`install setup steps complete. A fresh ${handoffToolLabel} session is now applying ` +
|
|
4491
|
+
"configuration, presenting the concise capability report, and showing any next step the " +
|
|
4492
|
+
"server provides.");
|
|
3785
4493
|
// The trust dialog is the single most common place a spawned session stalls:
|
|
3786
4494
|
// the tab opens, nothing is approved, and the install looks hung.
|
|
3787
4495
|
log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} ` +
|