@bridge_gpt/mcp-server 0.2.34 → 0.2.37
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 +490 -366
- 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/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-review-workflow.js +264 -0
- package/build/claude-user-config-doctor.js +317 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +393 -4
- 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 +37 -15
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +473 -81
- package/build/executor/cli.js +83 -6
- package/build/executor/credentials.js +39 -11
- package/build/executor/deps.js +32 -1
- package/build/executor/env.js +71 -28
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/index.js +4 -0
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +530 -20
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +114 -8
- package/build/executor/preflight.js +127 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +23 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +75 -3
- package/build/executor/types.js +4 -18
- package/build/executor/worker-command.js +73 -12
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +1039 -237
- package/build/init.js +503 -111
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +1188 -247
- package/build/install-reexec.js +233 -0
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +52 -13
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +32 -13
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +259 -0
- package/build/setup-epic.js +772 -28
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +412 -110
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +132 -6
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +19 -4
- 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,15 +143,20 @@ 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
|
-
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
159
|
+
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
130
160
|
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
131
161
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
132
162
|
import { fetchGithubConfigurationState } from "./connect-github-api.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,28 +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
|
-
"
|
|
260
|
-
"the
|
|
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.",
|
|
261
332
|
"",
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
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",
|
|
265
343
|
" confirmed on a terminal before it is redeemed; a non-interactive run keeps",
|
|
266
344
|
" today's automatic switch but prints the notice.",
|
|
267
|
-
" - an API-key-shaped value
|
|
345
|
+
" - an API-key-shaped value passed as an invite is announced and may switch in",
|
|
268
346
|
" place only after you confirm on a terminal — non-interactive runs exit with",
|
|
269
|
-
" guidance instead, so a script is never silently redirected.
|
|
270
|
-
"
|
|
271
|
-
"
|
|
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.",
|
|
272
350
|
"",
|
|
273
351
|
"Inputs (the only two irreducible ones):",
|
|
274
352
|
" --api-key <key> Bridge API key OR bootstrap invite. Falls back to the",
|
|
@@ -280,7 +358,10 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
280
358
|
" CREATE a new project and its first admin key — the same as",
|
|
281
359
|
" --invite — so it skips repository lookup entirely (--email",
|
|
282
360
|
" and --invite remain the explicit, preferred entry points for",
|
|
283
|
-
" a new project).
|
|
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.",
|
|
284
365
|
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
285
366
|
" priority and short-circuit before any network call. When",
|
|
286
367
|
" neither is set, a compatible server resolves the unique",
|
|
@@ -294,8 +375,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
294
375
|
" --repo is required when stdin is non-interactive). In the",
|
|
295
376
|
" existing-project flow it MUST match the server-side repo",
|
|
296
377
|
" registration (it keys the credential store as bapi:<repo>).",
|
|
297
|
-
" In either new-project flow (--email, --invite,
|
|
298
|
-
"
|
|
378
|
+
" In either new-project flow (--email, --invite, answering 2",
|
|
379
|
+
" above, or a token that turns out to be an invite) it NAMES",
|
|
299
380
|
" creates, so you are asked to name a new project — with a",
|
|
300
381
|
" suggested default — rather than match an existing one; the",
|
|
301
382
|
" name must be globally unique.",
|
|
@@ -303,24 +384,34 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
303
384
|
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
304
385
|
" --email <addr> Create a brand-new Bridge workspace from just an email —",
|
|
305
386
|
" the primary path for a first-time user with nothing yet.",
|
|
306
|
-
" It requests a fresh workspace for that email, then creates",
|
|
307
|
-
" the project and mints your own admin API key in one command.",
|
|
308
387
|
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
309
|
-
" interactive prompt — which is also what
|
|
310
|
-
" bare run reaches. The email is NOT a",
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
" exclusive with --api-key and --invite. No email verification",
|
|
314
|
-
" is performed and no message is sent to the address — it only",
|
|
315
|
-
" labels the new workspace.",
|
|
388
|
+
" interactive prompt — which is also what answering 2 on a",
|
|
389
|
+
" bare run reaches. The email is NOT a secret (it is shown as",
|
|
390
|
+
" you type), but it is never printed to a log. Mutually",
|
|
391
|
+
" exclusive with --api-key and --invite.",
|
|
316
392
|
"",
|
|
317
|
-
"
|
|
393
|
+
" VERIFIED: Bridge emails a 6-digit code to that address and",
|
|
394
|
+
" this command prompts you for it. Nothing is created until",
|
|
395
|
+
" the code is entered — no workspace, no project, no API key.",
|
|
396
|
+
" Delivery is asynchronous, so the message arrives shortly",
|
|
397
|
+
" after you are prompted rather than instantly. The code",
|
|
398
|
+
" expires 60 minutes after it is requested, and you can ask",
|
|
399
|
+
" for a new one if it does not arrive; earlier codes keep",
|
|
400
|
+
" working until one is accepted.",
|
|
401
|
+
"",
|
|
402
|
+
" REQUIRES A TERMINAL: there is no flag or environment",
|
|
403
|
+
" variable for the code — either would put a live credential",
|
|
404
|
+
" into your shell history and process list. For unattended",
|
|
405
|
+
" setup use --api-key <key> or --invite <token> instead.",
|
|
406
|
+
"",
|
|
407
|
+
" RESUMABLE: a self-serve run that is interrupted saves its",
|
|
318
408
|
" signup state under bootstrap-pending:<repo> in the credential",
|
|
319
|
-
" store. Re-running
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
409
|
+
" store. Re-running RESUMES that attempt — at the code prompt",
|
|
410
|
+
" if the code has not been entered yet, or at the exchange if",
|
|
411
|
+
" it has — so a retry never sends a second code and never",
|
|
412
|
+
" creates a second workspace. The code itself is never saved.",
|
|
413
|
+
" Never copy, display, or hand-remove that record; if the saved",
|
|
414
|
+
" invite has genuinely expired the CLI asks before discarding it.",
|
|
324
415
|
"",
|
|
325
416
|
"Bootstrap-invite onboarding (no web UI, no pre-existing key):",
|
|
326
417
|
" --invite [token] Redeem a bootstrap invite you were already given: creates",
|
|
@@ -328,7 +419,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
328
419
|
" Mutually exclusive with --api-key and --email (in this mode",
|
|
329
420
|
" the key is created, not consumed).",
|
|
330
421
|
"",
|
|
331
|
-
" Run it WITHOUT a value — `install
|
|
422
|
+
" Run it WITHOUT a value — `install --invite` — and the",
|
|
332
423
|
" token is read from an interactive prompt with echo",
|
|
333
424
|
" suppressed, then sent only in the request body. This is the",
|
|
334
425
|
" default and the recommended path: 'a copy/paste one-liner'",
|
|
@@ -385,6 +476,35 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
385
476
|
" --agent cursor-agent still force-spawns it.",
|
|
386
477
|
" -h, --help Show this help.",
|
|
387
478
|
"",
|
|
479
|
+
"Subcommands:",
|
|
480
|
+
" install conductor Bootstrap Epic Conductor v2 for this repository",
|
|
481
|
+
" (BAPI-679). Runs a unified READ-ONLY doctor that",
|
|
482
|
+
" composes the conductor, executor-preflight,",
|
|
483
|
+
" install-status, and executor service-unit",
|
|
484
|
+
" diagnostics with server-side",
|
|
485
|
+
" readiness (supervisor defaults, empty required-",
|
|
486
|
+
" check detection, GitHub App credentials and the",
|
|
487
|
+
" `actions` permission, reconciler tick liveness).",
|
|
488
|
+
" The resolved BAPI_BASE_URL is confirmed BEFORE",
|
|
489
|
+
" any network call — the doctor's readiness GET",
|
|
490
|
+
" carries the repo API key, and the URL silently",
|
|
491
|
+
" defaults to production when unset. A fatal",
|
|
492
|
+
" finding aborts BEFORE any write. A separate",
|
|
493
|
+
" consent immediately precedes the bootstrap POST,",
|
|
494
|
+
" which writes the safe project-default supervisor",
|
|
495
|
+
" posture in one transaction; it optionally",
|
|
496
|
+
" scaffolds a parameterized claude-review workflow",
|
|
497
|
+
" (its own separate consent), prints the capability",
|
|
498
|
+
" matrix, and re-runs the doctor. `--dry-run`",
|
|
499
|
+
" performs NO write of any kind and no write-consent",
|
|
500
|
+
" prompt (the base-URL confirmation still applies).",
|
|
501
|
+
" Executor service units are generated by",
|
|
502
|
+
" `executor install-service`; starting one stays",
|
|
503
|
+
" operator-managed, so a run reports",
|
|
504
|
+
" `complete-with-manual-steps` until a live",
|
|
505
|
+
" executor is actually observed. See",
|
|
506
|
+
" `install conductor --help`.",
|
|
507
|
+
"",
|
|
388
508
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
389
509
|
"(default docs/tmp) are read from the environment with the shown fallbacks.",
|
|
390
510
|
"BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is",
|
|
@@ -519,7 +639,7 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
519
639
|
}
|
|
520
640
|
return {
|
|
521
641
|
status: "error",
|
|
522
|
-
message: `Unexpected positional argument: '${arg}'. install
|
|
642
|
+
message: `Unexpected positional argument: '${arg}'. install does not accept positional arguments.`,
|
|
523
643
|
};
|
|
524
644
|
}
|
|
525
645
|
// Mutually exclusive by construction: bootstrap-invite mode CREATES the key, so
|
|
@@ -651,7 +771,7 @@ export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = "npx -y @bridge_gpt/mcp-ser
|
|
|
651
771
|
export const INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT = [
|
|
652
772
|
"",
|
|
653
773
|
"Step 4b — optional: connect GitHub.",
|
|
654
|
-
" This installs the Bridge GitHub App so
|
|
774
|
+
" This installs the Bridge GitHub App so you can implement and review tickets with Bridge. It opens",
|
|
655
775
|
" github.com in your browser; no GitHub credential is shared with Bridge.",
|
|
656
776
|
` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
|
|
657
777
|
];
|
|
@@ -841,6 +961,8 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
841
961
|
repointBootstrapPending: repointBootstrapPendingCredential,
|
|
842
962
|
promoteBootstrapPending: promoteBootstrapPendingCredential,
|
|
843
963
|
lookupSelfServeBootstrapPending: lookupSelfServeBootstrapPendingCredential,
|
|
964
|
+
prepareSelfServeBootstrapChallenge: prepareSelfServeBootstrapChallengeCredential,
|
|
965
|
+
now: () => Date.now(),
|
|
844
966
|
discardBootstrapPending: discardBootstrapPendingCredential,
|
|
845
967
|
buildShellCommand: buildGenericAgentShellCommand,
|
|
846
968
|
spawnTerminalTab: getDefaultSpawnTerminalTabForPlatform(process.platform),
|
|
@@ -854,6 +976,55 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
854
976
|
if (process.env.BAPI_INSTALL_DEBUG)
|
|
855
977
|
console.error(m);
|
|
856
978
|
},
|
|
979
|
+
// ---- Nested `install-bridge conductor` seams (BAPI-679) ----
|
|
980
|
+
conductorRunDoctor: (params) => runConductorInstallDoctor({
|
|
981
|
+
access: params.access,
|
|
982
|
+
accessError: params.accessError,
|
|
983
|
+
fetch: params.fetch,
|
|
984
|
+
reviewPolicySource: params.reviewPolicySource,
|
|
985
|
+
readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
|
|
986
|
+
installDoctorDeps: {
|
|
987
|
+
env: params.env,
|
|
988
|
+
cwd: params.cwd,
|
|
989
|
+
platform: process.platform,
|
|
990
|
+
homedir: os.homedir,
|
|
991
|
+
readFile: params.readFile,
|
|
992
|
+
stat: (p) => stat(p),
|
|
993
|
+
fetch: (...args) => fetch(...args),
|
|
994
|
+
},
|
|
995
|
+
}),
|
|
996
|
+
conductorResolveAccess: async (repoName, baseUrl, mergedDeps) => {
|
|
997
|
+
const result = await resolveConductorBridgeApiAccess({
|
|
998
|
+
env: { ...mergedDeps.env, BAPI_BASE_URL: baseUrl },
|
|
999
|
+
cwd: mergedDeps.cwd,
|
|
1000
|
+
homedir: mergedDeps.homedir,
|
|
1001
|
+
platform: mergedDeps.platform,
|
|
1002
|
+
readFile: mergedDeps.readFile,
|
|
1003
|
+
stat: (p) => mergedDeps.stat(p),
|
|
1004
|
+
repoName,
|
|
1005
|
+
});
|
|
1006
|
+
// Never surface the resolver's secret-bearing internals — it already
|
|
1007
|
+
// guarantees a secret-free `error`, which is forwarded verbatim.
|
|
1008
|
+
return result.ok ? { ok: true, access: result.access } : { ok: false, error: result.error };
|
|
1009
|
+
},
|
|
1010
|
+
conductorWriteWorkflow: (cwd, content, options, mergedDeps) => writeClaudeReviewWorkflow(cwd, content, {
|
|
1011
|
+
readFile: mergedDeps.readFile,
|
|
1012
|
+
mkdir: (p, o) => mergedDeps.mkdir(p, o),
|
|
1013
|
+
writeFile: (p, data) => mergedDeps.writeFile(p, data),
|
|
1014
|
+
}, options),
|
|
1015
|
+
conductorRecordArtifact: async (cwd, relPath, mergedDeps) => {
|
|
1016
|
+
const result = await recordInstalledProjectArtifact(cwd, relPath, {
|
|
1017
|
+
readFile: mergedDeps.readFile,
|
|
1018
|
+
writeFile: (p, data) => mergedDeps.writeFile(p, data),
|
|
1019
|
+
rename: mergedDeps.rename,
|
|
1020
|
+
mkdir: (p, o) => mergedDeps.mkdir(p, o),
|
|
1021
|
+
unlink: mergedDeps.unlink,
|
|
1022
|
+
});
|
|
1023
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
1024
|
+
},
|
|
1025
|
+
conductorRunSetupEpicDryRun: async (argv) => ({
|
|
1026
|
+
exitCode: await runSetupEpicCli(argv),
|
|
1027
|
+
}),
|
|
857
1028
|
};
|
|
858
1029
|
}
|
|
859
1030
|
/**
|
|
@@ -881,7 +1052,11 @@ export async function resolveApiKey(options, deps) {
|
|
|
881
1052
|
return { ok: true, value: fromEnv.trim(), source: "env" };
|
|
882
1053
|
}
|
|
883
1054
|
if (deps.isTTY && deps.promptSecret) {
|
|
884
|
-
|
|
1055
|
+
// BAPI-708 (A-3): the prompt no longer asks the user to pre-classify their own
|
|
1056
|
+
// credential ("Bridge API key or invite"). Classification is done from SHAPE
|
|
1057
|
+
// downstream with no probe request, so the only thing worth asking for is the
|
|
1058
|
+
// token itself.
|
|
1059
|
+
const entered = (await deps.promptSecret("Paste your Bridge token (input hidden): ")).trim();
|
|
885
1060
|
if (entered.length > 0) {
|
|
886
1061
|
return { ok: true, value: entered, source: "prompt" };
|
|
887
1062
|
}
|
|
@@ -890,12 +1065,12 @@ export async function resolveApiKey(options, deps) {
|
|
|
890
1065
|
// and it named no route usable by someone who has no key at all.
|
|
891
1066
|
return {
|
|
892
1067
|
ok: false,
|
|
893
|
-
error: `No Bridge
|
|
1068
|
+
error: `No Bridge token entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
894
1069
|
};
|
|
895
1070
|
}
|
|
896
1071
|
return {
|
|
897
1072
|
ok: false,
|
|
898
|
-
error: "A Bridge
|
|
1073
|
+
error: "A Bridge token is required (no interactive terminal is available to prompt " +
|
|
899
1074
|
`for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
900
1075
|
};
|
|
901
1076
|
}
|
|
@@ -935,7 +1110,7 @@ export async function resolveInviteToken(options, deps) {
|
|
|
935
1110
|
error: "A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE " +
|
|
936
1111
|
"environment variable (no interactive terminal is available to prompt for it). Note that " +
|
|
937
1112
|
"both forms expose the token to your shell history and process list — prefer running " +
|
|
938
|
-
"'install
|
|
1113
|
+
"'install --invite' interactively.",
|
|
939
1114
|
};
|
|
940
1115
|
}
|
|
941
1116
|
/**
|
|
@@ -955,6 +1130,16 @@ export async function resolveInviteToken(options, deps) {
|
|
|
955
1130
|
* back in an error message.
|
|
956
1131
|
*/
|
|
957
1132
|
export async function resolveSignupEmail(options, deps) {
|
|
1133
|
+
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
1134
|
+
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
1135
|
+
// so an unattended run fails before any request is made, no verification email
|
|
1136
|
+
// is sent, and no challenge row is created. Supplying the address was never the
|
|
1137
|
+
// hard part; entering a code that only arrives by email is, and there is
|
|
1138
|
+
// deliberately no `--code` flag and no environment variable to supply it (a live
|
|
1139
|
+
// code must never reach shell history or a process list).
|
|
1140
|
+
if (!deps.isTTY || !deps.promptLine) {
|
|
1141
|
+
return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
|
|
1142
|
+
}
|
|
958
1143
|
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
959
1144
|
return { ok: true, value: options.email.trim() };
|
|
960
1145
|
}
|
|
@@ -962,19 +1147,11 @@ export async function resolveSignupEmail(options, deps) {
|
|
|
962
1147
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
963
1148
|
return { ok: true, value: fromEnv.trim() };
|
|
964
1149
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
return { ok: true, value: entered };
|
|
969
|
-
}
|
|
970
|
-
return { ok: false, error: "No email entered." };
|
|
1150
|
+
const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
|
|
1151
|
+
if (entered.length > 0) {
|
|
1152
|
+
return { ok: true, value: entered };
|
|
971
1153
|
}
|
|
972
|
-
return {
|
|
973
|
-
ok: false,
|
|
974
|
-
error: "An email is required to create a Bridge workspace. Pass --email <addr> or set the " +
|
|
975
|
-
"BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt " +
|
|
976
|
-
"for it).",
|
|
977
|
-
};
|
|
1154
|
+
return { ok: false, error: "No email entered." };
|
|
978
1155
|
}
|
|
979
1156
|
/**
|
|
980
1157
|
* Select the onboarding branch as pure logic (deterministic, no I/O, no prompt):
|
|
@@ -1002,40 +1179,38 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
1002
1179
|
return { kind: "have-key" };
|
|
1003
1180
|
}
|
|
1004
1181
|
/**
|
|
1005
|
-
* The bare-TTY onboarding
|
|
1006
|
-
*
|
|
1007
|
-
* users — the ones with no key, no invite, and no account — into a hidden API-key
|
|
1008
|
-
* prompt they could not answer, then exited. The self-serve path built for exactly
|
|
1009
|
-
* that user was never named. The chooser names all three routes and has NO default,
|
|
1010
|
-
* so Enter can no longer route anyone into a prompt they cannot satisfy.
|
|
1182
|
+
* The bare-TTY onboarding question (BAPI-708, Part A), replacing the BAPI-667/687
|
|
1183
|
+
* three-option chooser.
|
|
1011
1184
|
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1185
|
+
* The three-option form asked the user to make a distinction the CODE can already
|
|
1186
|
+
* make for itself: whether the string in their clipboard is an invite or an API
|
|
1187
|
+
* key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
|
|
1188
|
+
* SHAPE alone, with no probe request — so the only thing a first-time user
|
|
1189
|
+
* genuinely knows, and the only thing worth asking, is whether they have a token
|
|
1190
|
+
* at all. Hence a yes/no question, with the route inferred afterwards.
|
|
1191
|
+
*
|
|
1192
|
+
* Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
|
|
1193
|
+
* render the heading and the numbered options exactly once, accept ONE strict
|
|
1194
|
+
* line, no default (a bare Enter selects nothing), bounded to a single re-prompt
|
|
1195
|
+
* after one hint, and EOF resolves as an empty answer so it can never hang.
|
|
1014
1196
|
*/
|
|
1015
|
-
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "
|
|
1197
|
+
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
|
|
1016
1198
|
/**
|
|
1017
|
-
* The
|
|
1018
|
-
*
|
|
1019
|
-
* BAPI-687 (A-1) put the two PROJECT-CREATING routes first. The previous order led
|
|
1020
|
-
* with the existing-project key path, labelled only "I have a Bridge API key" — which
|
|
1021
|
-
* reads as the default for anyone holding any credential, so a user with a valid
|
|
1022
|
-
* invite picked it and dead-ended at a repo-not-registered 403 with no route to the
|
|
1023
|
-
* path that would have created their project. Position was doing the opposite of the
|
|
1024
|
-
* intended routing.
|
|
1199
|
+
* The question's numbered options, in render order. Index + 1 is the accepted
|
|
1200
|
+
* token, exactly as before — only the option SET narrowed from three to two.
|
|
1025
1201
|
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1202
|
+
* "Token" is deliberately credential-agnostic: an invite and an API key are both
|
|
1203
|
+
* tokens as far as this question is concerned, and answering "Yes" leads to ONE
|
|
1204
|
+
* hidden prompt whose value selects the route by shape.
|
|
1029
1205
|
*/
|
|
1030
1206
|
export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
|
|
1031
|
-
"1. I have
|
|
1032
|
-
"2. I
|
|
1033
|
-
"3. I have an API key for an existing project",
|
|
1207
|
+
"1. Yes, I have received a token",
|
|
1208
|
+
"2. No, I need one",
|
|
1034
1209
|
];
|
|
1035
1210
|
/** The single-line answer prompt drawn after the options (no bracketed default). */
|
|
1036
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1
|
|
1211
|
+
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
|
|
1037
1212
|
/** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
|
|
1038
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1
|
|
1213
|
+
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
|
|
1039
1214
|
/**
|
|
1040
1215
|
* The one place the "no usable credential" copy lives (BAPI-667, U1-3).
|
|
1041
1216
|
*
|
|
@@ -1044,10 +1219,10 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1, 2, or 3.";
|
|
|
1044
1219
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
1045
1220
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
1046
1221
|
*/
|
|
1047
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install
|
|
1048
|
-
"
|
|
1049
|
-
"
|
|
1050
|
-
"
|
|
1222
|
+
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 if you already have a token " +
|
|
1223
|
+
"(an invite or an API key — either one is accepted at the hidden prompt) or 2 to sign up " +
|
|
1224
|
+
"with just an email. Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
1225
|
+
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
1051
1226
|
/**
|
|
1052
1227
|
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
1053
1228
|
*
|
|
@@ -1076,21 +1251,32 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install-bridge and cho
|
|
|
1076
1251
|
* bounded to ONE re-prompt — an unanswerable prompt that loops is the same
|
|
1077
1252
|
* dead-end in a different costume.
|
|
1078
1253
|
*
|
|
1079
|
-
* BAPI-
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1254
|
+
* BAPI-708 (Part A) narrowed those three options to a token yes/no question. The
|
|
1255
|
+
* chooser was asking the user to pre-classify their own credential, which the
|
|
1256
|
+
* shape checks downstream already do without a probe request. Every interaction
|
|
1257
|
+
* property above survives verbatim — heading + options rendered once, one strict
|
|
1258
|
+
* line, no default, one hint, one re-prompt, EOF-as-empty — and so does every
|
|
1259
|
+
* bypass. Only the option SET and the dispatch changed.
|
|
1260
|
+
*
|
|
1261
|
+
* PROVENANCE. This function is the SOLE authority on `credentialIntent`, and that
|
|
1262
|
+
* is deliberate: the route-switch arms downstream must not re-derive provenance
|
|
1263
|
+
* from `argv`, because a second derivation is exactly the drift BAPI-708 exists to
|
|
1264
|
+
* fix. `"inferred"` is returned ONLY when the question was actually answered — a
|
|
1265
|
+
* state reachable on a bare interactive TTY run and nowhere else. Every other exit
|
|
1266
|
+
* is `"declared"`, so 100% of non-interactive runs stay on declared paths and the
|
|
1267
|
+
* documented directional asymmetry (invite-on-key auto-switches on a non-TTY;
|
|
1268
|
+
* key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
|
|
1269
|
+
* two.
|
|
1084
1270
|
*/
|
|
1085
1271
|
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
1086
1272
|
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
1087
1273
|
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
1088
1274
|
if (branch.kind === "need-key")
|
|
1089
|
-
return { ok: true, branch };
|
|
1275
|
+
return { ok: true, branch, credentialIntent: "declared" };
|
|
1090
1276
|
const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
|
|
1091
1277
|
const isBareInvocation = argv.length === 0;
|
|
1092
1278
|
if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
|
|
1093
|
-
return { ok: true, branch };
|
|
1279
|
+
return { ok: true, branch, credentialIntent: "declared" };
|
|
1094
1280
|
}
|
|
1095
1281
|
const promptLine = deps.promptLine;
|
|
1096
1282
|
try {
|
|
@@ -1105,18 +1291,20 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
1105
1291
|
// it can never hang or silently select a branch.
|
|
1106
1292
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1107
1293
|
const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
|
|
1108
|
-
// BAPI-
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
if (answer === "1")
|
|
1114
|
-
return { ok: true, branch: { kind: "
|
|
1294
|
+
// BAPI-708 (A-1): the accepted tokens are now exactly 1/2 (no letters, no
|
|
1295
|
+
// aliases), preserving the one-strict-line contract. "Yes" routes to the
|
|
1296
|
+
// have-key branch's credential-agnostic prompt, whose value is classified by
|
|
1297
|
+
// SHAPE — so a user holding an invite still lands in the redemption protocol
|
|
1298
|
+
// without ever having been asked which kind of token they hold.
|
|
1299
|
+
if (answer === "1")
|
|
1300
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
1301
|
+
if (answer === "2") {
|
|
1302
|
+
return {
|
|
1303
|
+
ok: true,
|
|
1304
|
+
branch: { kind: "need-key", method: "self-serve" },
|
|
1305
|
+
credentialIntent: "inferred",
|
|
1306
|
+
};
|
|
1115
1307
|
}
|
|
1116
|
-
if (answer === "2")
|
|
1117
|
-
return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
|
|
1118
|
-
if (answer === "3")
|
|
1119
|
-
return { ok: true, branch: { kind: "have-key" } };
|
|
1120
1308
|
if (attempt === 0)
|
|
1121
1309
|
deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
|
|
1122
1310
|
}
|
|
@@ -1465,13 +1653,21 @@ export function buildManualInstallBridgeContinuation(kind, toolLabels) {
|
|
|
1465
1653
|
if (kind === "empty-selection") {
|
|
1466
1654
|
return [
|
|
1467
1655
|
"No AI coding tools were configured, so nothing was set up for this project.",
|
|
1468
|
-
"Re-run install
|
|
1656
|
+
"Re-run install and select at least one tool to configure it.",
|
|
1469
1657
|
].join("\n");
|
|
1470
1658
|
}
|
|
1471
1659
|
const toolPhrase = formatToolLabelPhrase(toolLabels);
|
|
1660
|
+
// BAPI-708 (B-c): the reload clause lives here so the requirement survives on
|
|
1661
|
+
// every NO-SPAWN terminal path (declined consent, non-TTY suppression, no
|
|
1662
|
+
// launchable tool, chooser failure, failed spawn). A written project MCP config
|
|
1663
|
+
// is not a live connection until the host reloads and its trust prompt is
|
|
1664
|
+
// approved — the single most common place a fresh install looks broken. The
|
|
1665
|
+
// spawn path states the same requirement in its own handoff.
|
|
1472
1666
|
return [
|
|
1473
1667
|
`To finish configuring this project, open it in ${toolPhrase} that has the`,
|
|
1474
1668
|
"Bridge MCP server configured, start a new session, and run /install-bridge.",
|
|
1669
|
+
"Restart or reload the tool first if it is already running — the newly written",
|
|
1670
|
+
"MCP server is not live until it reloads and you approve it if prompted.",
|
|
1475
1671
|
"Until the project is configured, your Bridge MCP tools stay limited.",
|
|
1476
1672
|
].join("\n");
|
|
1477
1673
|
}
|
|
@@ -1795,6 +1991,22 @@ async function requestTrackedConfigConsent(deps, relPath) {
|
|
|
1795
1991
|
const normalized = (answer ?? "").trim().toLowerCase();
|
|
1796
1992
|
return normalized === "y" || normalized === "yes";
|
|
1797
1993
|
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Overlay a freshly built Bridge entry onto whatever is already registered,
|
|
1996
|
+
* preserving the EXISTING launcher args composition (BAPI-714, Group C).
|
|
1997
|
+
*
|
|
1998
|
+
* Only the package-spec token is refreshed on an existing entry, so a legacy bare
|
|
1999
|
+
* launcher stays bare and an already-`serve`-bearing launcher keeps `serve`
|
|
2000
|
+
* exactly once. When there is no existing entry — or its args are a shape we
|
|
2001
|
+
* cannot recognize — the new template args (which DO include `serve`) are used
|
|
2002
|
+
* verbatim. `command` and `env` always come from the freshly built entry: this
|
|
2003
|
+
* helper governs launcher-token migration only, never credentials.
|
|
2004
|
+
*/
|
|
2005
|
+
export function preserveExistingLauncherArgs(existing, entry) {
|
|
2006
|
+
const existingArgs = asRecord(existing)?.args;
|
|
2007
|
+
const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
|
|
2008
|
+
return preserved ? { ...entry, args: preserved } : entry;
|
|
2009
|
+
}
|
|
1798
2010
|
/**
|
|
1799
2011
|
* Write the `bridge-api` entry into each host config via read-merge-write,
|
|
1800
2012
|
* preserving unrelated servers and top-level keys (BAPI-666). Per target:
|
|
@@ -1855,7 +2067,13 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
1855
2067
|
// never initialized before its tracked-state consent is resolved.
|
|
1856
2068
|
const config = read.state === "parsed" ? read.config : {};
|
|
1857
2069
|
const topLevel = asRecord(config[target.topLevelKey]) ?? {};
|
|
1858
|
-
|
|
2070
|
+
// BAPI-714 (Group C): a PRE-EXISTING Bridge entry keeps its own launcher args
|
|
2071
|
+
// composition — only the package-spec token is refreshed — so the installer
|
|
2072
|
+
// never migrates a legacy bare launcher to `serve`. A newly created entry gets
|
|
2073
|
+
// the full template args, `serve` included. The rule is identical for the
|
|
2074
|
+
// real-key and secret-free variants: credential safety must not decide whether
|
|
2075
|
+
// a launcher is migrated.
|
|
2076
|
+
topLevel["bridge-api"] = preserveExistingLauncherArgs(topLevel["bridge-api"], entry);
|
|
1859
2077
|
config[target.topLevelKey] = topLevel;
|
|
1860
2078
|
await deps.mkdir(path.dirname(fullPath), { recursive: true });
|
|
1861
2079
|
await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
|
|
@@ -2294,7 +2512,7 @@ export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT = "Redeem it as an invite
|
|
|
2294
2512
|
/** Non-TTY form: the switch still happens automatically, but it is announced (B-2). */
|
|
2295
2513
|
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE = "Redeeming it as a bootstrap invite (non-interactive — no confirmation is possible here).";
|
|
2296
2514
|
/** Printed on decline. Names the other route rather than leaving the user stranded. */
|
|
2297
|
-
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED = "Error: cancelled — nothing was created. Re-run install
|
|
2515
|
+
export const INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED = "Error: cancelled — nothing was created. Re-run install with an API key for an " +
|
|
2298
2516
|
"existing project (--api-key <key>, or BAPI_API_KEY), or re-run and confirm to redeem the " +
|
|
2299
2517
|
"invite and create a new project.";
|
|
2300
2518
|
/**
|
|
@@ -2320,7 +2538,7 @@ export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT = "Use it to connect to yo
|
|
|
2320
2538
|
* direction would silently redirect a script away from the project-creating run it
|
|
2321
2539
|
* asked for. A script gets a non-zero exit and the exact flag it should have passed.
|
|
2322
2540
|
*/
|
|
2323
|
-
export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED = "Error: cancelled — nothing was created and no invite was spent. Re-run install
|
|
2541
|
+
export const INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED = "Error: cancelled — nothing was created and no invite was spent. Re-run install with " +
|
|
2324
2542
|
"--api-key <key> (or BAPI_API_KEY) to connect to your existing project, or supply a bootstrap " +
|
|
2325
2543
|
"invite token (bapi_inv_…) to create a new one.";
|
|
2326
2544
|
/**
|
|
@@ -2347,23 +2565,234 @@ export async function requestCredentialRouteSwitchConfirmation(deps, promptText)
|
|
|
2347
2565
|
export function buildSelfServeMintUrl(baseUrl) {
|
|
2348
2566
|
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
2349
2567
|
}
|
|
2568
|
+
// ===================================================================
|
|
2569
|
+
// BAPI-757 — the verification checkpoint's user-facing copy.
|
|
2570
|
+
//
|
|
2571
|
+
// Every string below is a module constant rather than an inline literal, for two
|
|
2572
|
+
// reasons: the static-contract suite pins several of them, and a uniform outcome
|
|
2573
|
+
// stated in one place cannot drift into four subtly different sentences that
|
|
2574
|
+
// together disclose which failure occurred.
|
|
2575
|
+
// ===================================================================
|
|
2576
|
+
/** Digits in a verification code. Mirrors the server's protocol constant. */
|
|
2577
|
+
export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
|
|
2578
|
+
/** The one prompt label. The E2E harness and the static contract both pin it. */
|
|
2579
|
+
export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
|
|
2580
|
+
/** Shown when a stored, unexpired challenge is picked up on a later run. */
|
|
2581
|
+
export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
|
|
2582
|
+
/**
|
|
2583
|
+
* The non-interactive refusal (AC-9 / R-3).
|
|
2584
|
+
*
|
|
2585
|
+
* Names BOTH unattended alternatives, because the whole point of the message is
|
|
2586
|
+
* that the user is running in a context where no prompt can be answered — telling
|
|
2587
|
+
* them only what does not work would leave them stuck.
|
|
2588
|
+
*/
|
|
2589
|
+
export const INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY = [
|
|
2590
|
+
"Creating a Bridge workspace requires verifying your email address, and that needs an",
|
|
2591
|
+
"interactive terminal to enter the emailed code. There is deliberately no flag or",
|
|
2592
|
+
"environment variable for the code — either would put a live credential into your shell",
|
|
2593
|
+
"history and process list.",
|
|
2594
|
+
"",
|
|
2595
|
+
"Two onboarding paths still work unattended:",
|
|
2596
|
+
" • An existing API key: install --api-key <key> (or set BAPI_API_KEY)",
|
|
2597
|
+
" • A pre-issued invite: install --invite <token> (or set BAPI_INVITE)",
|
|
2598
|
+
"",
|
|
2599
|
+
"To sign up from scratch, re-run this command in an interactive terminal.",
|
|
2600
|
+
].join("\n");
|
|
2601
|
+
/**
|
|
2602
|
+
* The UNIFORM recovery message for a rejected verification.
|
|
2603
|
+
*
|
|
2604
|
+
* The server answers a wrong code, an expired code, an unknown handle, and an
|
|
2605
|
+
* exhausted challenge with one identical response — and this message is the client
|
|
2606
|
+
* half of that property. It must never be specialized per case, and it must never
|
|
2607
|
+
* speculate ("maybe it expired?"): a message that narrowed the possibilities would
|
|
2608
|
+
* hand back exactly the signal the uniform response withholds.
|
|
2609
|
+
*/
|
|
2610
|
+
export const INSTALL_BRIDGE_VERIFICATION_REJECTED = [
|
|
2611
|
+
"That code was not accepted.",
|
|
2612
|
+
"",
|
|
2613
|
+
"Check the most recent verification email and try again, or request a new code. If the",
|
|
2614
|
+
"signup attempt is too old, re-run this command to start a fresh one.",
|
|
2615
|
+
].join("\n");
|
|
2616
|
+
/** Resend success. States plainly that earlier codes still work — see R-5. */
|
|
2617
|
+
export const INSTALL_BRIDGE_RESEND_SENT = "A new code is on its way. Any code from an earlier email for this signup still works too.";
|
|
2618
|
+
/** Cooldown. Non-accusatory, and silent about which limiter applied. */
|
|
2619
|
+
export const INSTALL_BRIDGE_RESEND_COOLDOWN = "A verification email was sent moments ago — give it a minute to arrive before requesting another.";
|
|
2620
|
+
/** Send ceiling. Names no bucket: not the address, not the source, not the global cap. */
|
|
2621
|
+
export const INSTALL_BRIDGE_RESEND_THROTTLED = "Bridge is not sending another verification email for this signup right now. Try again later, or use the code from an email you already received.";
|
|
2622
|
+
/**
|
|
2623
|
+
* A throttle that arrives from the VERIFY call, i.e. after a code was submitted.
|
|
2624
|
+
*
|
|
2625
|
+
* Deliberately NOT {@link INSTALL_BRIDGE_RESEND_THROTTLED}. That message tells the
|
|
2626
|
+
* user to fall back on a code they already have — which is backwards here: the
|
|
2627
|
+
* server throttles verification at the invite-mint ceiling, so their code may have
|
|
2628
|
+
* been perfectly correct and there is nothing to fall back to. Re-running is the
|
|
2629
|
+
* remedy, and the code stays valid until it expires.
|
|
2630
|
+
*/
|
|
2631
|
+
export const INSTALL_BRIDGE_VERIFY_THROTTLED = "Bridge is at capacity and could not finish creating your workspace just now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
|
|
2632
|
+
/**
|
|
2633
|
+
* A transient server failure during verification.
|
|
2634
|
+
*
|
|
2635
|
+
* Separate from {@link INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED} because a 5xx means
|
|
2636
|
+
* the server understood this CLI perfectly and simply failed. Telling the user to
|
|
2637
|
+
* upgrade would send them to the one recovery step that cannot help, and would
|
|
2638
|
+
* hide a real outage behind a version-skew story.
|
|
2639
|
+
*/
|
|
2640
|
+
export const INSTALL_BRIDGE_VERIFY_SERVER_ERROR = "Error: Bridge could not complete verification right now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
|
|
2641
|
+
/**
|
|
2642
|
+
* Local cap on rejected verification attempts before the CLI stops re-prompting.
|
|
2643
|
+
*
|
|
2644
|
+
* AC-4 makes "wrong code" and "this challenge is dead" byte-identical on purpose,
|
|
2645
|
+
* so the CLI genuinely cannot tell them apart. Without a cap, a user who exhausts
|
|
2646
|
+
* the server's attempt budget is re-prompted forever against a challenge that can
|
|
2647
|
+
* never succeed, with Ctrl-C as the only exit.
|
|
2648
|
+
*
|
|
2649
|
+
* Set ABOVE the server's default attempt budget (5) so a legitimately mistyping
|
|
2650
|
+
* user is never cut off before the server itself would refuse — the cap exists to
|
|
2651
|
+
* end a hopeless loop, not to add a second, tighter budget the user cannot see.
|
|
2652
|
+
*/
|
|
2653
|
+
export const INSTALL_BRIDGE_MAX_CODE_REJECTIONS = 6;
|
|
2654
|
+
/** Printed when the rejection cap is reached. Still state-neutral (AC-4). */
|
|
2655
|
+
export const INSTALL_BRIDGE_VERIFICATION_GIVING_UP = [
|
|
2656
|
+
"Error: that signup could not be verified.",
|
|
2657
|
+
"",
|
|
2658
|
+
"Nothing has been created. Re-run this command to start a fresh signup — it will email",
|
|
2659
|
+
"a new code to the same address.",
|
|
2660
|
+
].join("\n");
|
|
2661
|
+
/** The fail-closed enqueue failure (R-4). No suggestion that verification is skippable. */
|
|
2662
|
+
export const INSTALL_BRIDGE_SIGNUP_UNAVAILABLE = "Error: Bridge could not send a verification email, so no workspace was created. Nothing has been set up — try again in a few minutes.";
|
|
2663
|
+
/**
|
|
2664
|
+
* R-2's hard break, rendered for a server that no longer speaks this client's
|
|
2665
|
+
* protocol — or a client that no longer speaks the server's.
|
|
2666
|
+
*
|
|
2667
|
+
* Deliberately NOT the old generic "Check connectivity and retry": connectivity is
|
|
2668
|
+
* fine, and sending a user to debug their network for a protocol change is the
|
|
2669
|
+
* failure mode this constant exists to prevent.
|
|
2670
|
+
*/
|
|
2671
|
+
export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
|
|
2672
|
+
"Error: this Bridge server's signup flow does not match this CLI.",
|
|
2673
|
+
"",
|
|
2674
|
+
"Signup now verifies your email address with a code before creating a workspace. Upgrade",
|
|
2675
|
+
"and re-run:",
|
|
2676
|
+
"",
|
|
2677
|
+
" npx -y @bridge_gpt/mcp-server@latest install --email <addr>",
|
|
2678
|
+
].join("\n");
|
|
2679
|
+
/**
|
|
2680
|
+
* Mask an address for display: first character, then the domain.
|
|
2681
|
+
*
|
|
2682
|
+
* `ada@example.com` renders as `a***@example.com`. Enough for the user to confirm
|
|
2683
|
+
* they typed the right mailbox, without putting a full address into a terminal
|
|
2684
|
+
* transcript that may be pasted into an issue or captured by CI. Anything that
|
|
2685
|
+
* does not look like a mailbox is reduced to a fixed placeholder rather than
|
|
2686
|
+
* echoed — an unparseable value is exactly the one most likely to be a typo worth
|
|
2687
|
+
* not repeating.
|
|
2688
|
+
*/
|
|
2689
|
+
export function maskSignupEmail(email) {
|
|
2690
|
+
const trimmed = (email ?? "").trim();
|
|
2691
|
+
const at = trimmed.lastIndexOf("@");
|
|
2692
|
+
if (at <= 0 || at === trimmed.length - 1)
|
|
2693
|
+
return "your email address";
|
|
2694
|
+
return `${trimmed[0]}***${trimmed.slice(at)}`;
|
|
2695
|
+
}
|
|
2350
2696
|
/**
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2697
|
+
* Render a challenge's remaining lifetime in calm, approximate language.
|
|
2698
|
+
*
|
|
2699
|
+
* Whole minutes, rounded down, and never a countdown: the exact number does not
|
|
2700
|
+
* change what the user does, and a precise timer turns a routine wait into a
|
|
2701
|
+
* deadline. A challenge already past its expiry says so plainly rather than
|
|
2702
|
+
* rendering a negative number.
|
|
2703
|
+
*/
|
|
2704
|
+
export function formatChallengeRemaining(expiresAtIso, nowMs) {
|
|
2705
|
+
const expiresMs = Date.parse(expiresAtIso);
|
|
2706
|
+
if (Number.isNaN(expiresMs))
|
|
2707
|
+
return "";
|
|
2708
|
+
const minutes = Math.floor((expiresMs - nowMs) / 60_000);
|
|
2709
|
+
if (minutes <= 0)
|
|
2710
|
+
return "This code has expired.";
|
|
2711
|
+
if (minutes === 1)
|
|
2712
|
+
return "This code is valid for about another minute.";
|
|
2713
|
+
return `This code is valid for about another ${minutes} minutes.`;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Prompt for the verification code, validating its shape LOCALLY before any
|
|
2717
|
+
* network call.
|
|
2718
|
+
*
|
|
2719
|
+
* Local validation is a courtesy, not a security control — the server re-validates
|
|
2720
|
+
* everything — but it keeps a mistyped code from consuming one of the five attempts
|
|
2721
|
+
* that permanently kill a challenge. The user gets the prompt back instead.
|
|
2722
|
+
*
|
|
2723
|
+
* Pasted input is trimmed: copying a code out of a mail client routinely brings
|
|
2724
|
+
* whitespace with it, and refusing that would be an avoidable dead end.
|
|
2725
|
+
*
|
|
2726
|
+
* The entered value is returned and never echoed, never logged, and never written
|
|
2727
|
+
* to disk. `null` means the user gave up (an empty line, or an unreadable prompt).
|
|
2728
|
+
*/
|
|
2729
|
+
export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
|
|
2730
|
+
if (!deps.promptLine)
|
|
2731
|
+
return null;
|
|
2732
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2733
|
+
let entered;
|
|
2734
|
+
try {
|
|
2735
|
+
entered = (await deps.promptLine(INSTALL_BRIDGE_CODE_PROMPT)).trim();
|
|
2736
|
+
}
|
|
2737
|
+
catch {
|
|
2738
|
+
// Secret-free by construction: the caught value is never surfaced or
|
|
2739
|
+
// inspected, because a readline failure can carry the partial input.
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
if (entered.length === 0)
|
|
2743
|
+
return null;
|
|
2744
|
+
if (new RegExp(`^[0-9]{${SIGNUP_VERIFICATION_CODE_LENGTH}}$`).test(entered)) {
|
|
2745
|
+
return entered;
|
|
2746
|
+
}
|
|
2747
|
+
// Describes the SHAPE, never the value the user typed.
|
|
2748
|
+
log(` That does not look like a code — it is ${SIGNUP_VERIFICATION_CODE_LENGTH} digits.`);
|
|
2749
|
+
}
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
/*
|
|
2753
|
+
* `mintSelfServeInvite` and `SelfServeMintResult` were REMOVED in BAPI-757.
|
|
2353
2754
|
*
|
|
2354
|
-
*
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2358
|
-
* echo the email or carry internals. A caught fetch exception is likewise never
|
|
2359
|
-
* inspected or interpolated; some fetch impls embed the request (and thus the
|
|
2360
|
-
* email) in the message.
|
|
2755
|
+
* They read `token` from the request phase's success body. Under R-2 that field
|
|
2756
|
+
* no longer exists for any client version, and a helper that still looked for it
|
|
2757
|
+
* would be a live path back to an unverified signup — so it is deleted rather than
|
|
2758
|
+
* deprecated. The replacement is the three-phase client immediately below.
|
|
2361
2759
|
*/
|
|
2362
|
-
|
|
2363
|
-
|
|
2760
|
+
// ===================================================================
|
|
2761
|
+
// BAPI-757 — the three-phase self-serve signup client.
|
|
2762
|
+
//
|
|
2763
|
+
// The server no longer returns a token from the request phase, for ANY client
|
|
2764
|
+
// version (R-2). These three helpers replace the single-shot mint above:
|
|
2765
|
+
//
|
|
2766
|
+
// requestSelfServeChallenge → POST /setup/bootstrap/self-serve
|
|
2767
|
+
// verifySelfServeChallenge → POST /setup/bootstrap/self-serve/verify
|
|
2768
|
+
// resendSelfServeChallenge → POST /setup/bootstrap/self-serve/resend
|
|
2769
|
+
//
|
|
2770
|
+
// All three keep the discipline the deleted single-shot mint established, and that
|
|
2771
|
+
// `exchangeBootstrapInvite` still follows: the same 10s timeout, a COARSE result
|
|
2772
|
+
// category on failure, and a failure body that is never read — an untrusted
|
|
2773
|
+
// upstream body could echo the email or the code, and some fetch implementations
|
|
2774
|
+
// embed the request (and therefore the code) in a thrown error's message, so caught
|
|
2775
|
+
// exceptions are never inspected either. The one exception is the resend 429, which
|
|
2776
|
+
// reads a FIXED server-controlled detail string carrying no request value.
|
|
2777
|
+
// ===================================================================
|
|
2778
|
+
/** The challenge REQUEST endpoint: `<base>/setup/bootstrap/self-serve`. */
|
|
2779
|
+
export function buildSelfServeVerifyUrl(baseUrl) {
|
|
2780
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve/verify`;
|
|
2781
|
+
}
|
|
2782
|
+
/** The challenge RESEND endpoint: `<base>/setup/bootstrap/self-serve/resend`. */
|
|
2783
|
+
export function buildSelfServeResendUrl(baseUrl) {
|
|
2784
|
+
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve/resend`;
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* POST the challenge REQUEST: an email in, a handle out. Never a token.
|
|
2788
|
+
*
|
|
2789
|
+
* The email travels ONLY in the JSON body — never a query string, never a header,
|
|
2790
|
+
* never a log line.
|
|
2791
|
+
*/
|
|
2792
|
+
export async function requestSelfServeChallenge(deps, baseUrl, email) {
|
|
2364
2793
|
let resp;
|
|
2365
2794
|
try {
|
|
2366
|
-
resp = await deps.fetch(
|
|
2795
|
+
resp = await deps.fetch(buildSelfServeMintUrl(baseUrl), {
|
|
2367
2796
|
method: "POST",
|
|
2368
2797
|
headers: { "Content-Type": "application/json" },
|
|
2369
2798
|
body: JSON.stringify({ invitee_email: email }),
|
|
@@ -2371,8 +2800,57 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2371
2800
|
});
|
|
2372
2801
|
}
|
|
2373
2802
|
catch (err) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2803
|
+
void err;
|
|
2804
|
+
return { ok: false, category: "failed" };
|
|
2805
|
+
}
|
|
2806
|
+
if (resp.ok) {
|
|
2807
|
+
let handle;
|
|
2808
|
+
let expiresAt;
|
|
2809
|
+
try {
|
|
2810
|
+
const body = (await resp.json());
|
|
2811
|
+
handle = body?.handle;
|
|
2812
|
+
expiresAt = body?.expires_at;
|
|
2813
|
+
}
|
|
2814
|
+
catch {
|
|
2815
|
+
return { ok: false, category: "failed" };
|
|
2816
|
+
}
|
|
2817
|
+
if (typeof handle !== "string" ||
|
|
2818
|
+
handle.trim().length === 0 ||
|
|
2819
|
+
typeof expiresAt !== "string" ||
|
|
2820
|
+
expiresAt.trim().length === 0) {
|
|
2821
|
+
// A success body without challenge metadata is not something this client can
|
|
2822
|
+
// act on. It is also what an OLD server (pre-BAPI-757, still returning a
|
|
2823
|
+
// token) looks like — and continuing from one would mean skipping
|
|
2824
|
+
// verification entirely, so it is refused rather than tolerated.
|
|
2825
|
+
return { ok: false, category: "failed" };
|
|
2826
|
+
}
|
|
2827
|
+
return { ok: true, handle: handle.trim(), expiresAt: expiresAt.trim() };
|
|
2828
|
+
}
|
|
2829
|
+
if (resp.status === 429)
|
|
2830
|
+
return { ok: false, category: "rate-limited" };
|
|
2831
|
+
if (resp.status === 503)
|
|
2832
|
+
return { ok: false, category: "unavailable" };
|
|
2833
|
+
if (resp.status === 400 || resp.status === 422)
|
|
2834
|
+
return { ok: false, category: "invalid" };
|
|
2835
|
+
return { ok: false, category: "failed" };
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* POST the VERIFY phase: a handle and a code in, the invite token out.
|
|
2839
|
+
*
|
|
2840
|
+
* The code is sent once, in the body, and is never echoed back into terminal
|
|
2841
|
+
* output or into any error this returns.
|
|
2842
|
+
*/
|
|
2843
|
+
export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
|
|
2844
|
+
let resp;
|
|
2845
|
+
try {
|
|
2846
|
+
resp = await deps.fetch(buildSelfServeVerifyUrl(baseUrl), {
|
|
2847
|
+
method: "POST",
|
|
2848
|
+
headers: { "Content-Type": "application/json" },
|
|
2849
|
+
body: JSON.stringify({ handle, code }),
|
|
2850
|
+
signal: AbortSignal.timeout(10_000),
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
catch (err) {
|
|
2376
2854
|
void err;
|
|
2377
2855
|
return { ok: false, category: "failed" };
|
|
2378
2856
|
}
|
|
@@ -2383,7 +2861,6 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2383
2861
|
token = body?.token;
|
|
2384
2862
|
}
|
|
2385
2863
|
catch {
|
|
2386
|
-
// An unreadable success body is not actionable and must not be exposed.
|
|
2387
2864
|
return { ok: false, category: "failed" };
|
|
2388
2865
|
}
|
|
2389
2866
|
if (typeof token !== "string" ||
|
|
@@ -2393,14 +2870,83 @@ export async function mintSelfServeInvite(deps, baseUrl, email) {
|
|
|
2393
2870
|
}
|
|
2394
2871
|
return { ok: true, token };
|
|
2395
2872
|
}
|
|
2396
|
-
//
|
|
2397
|
-
//
|
|
2398
|
-
|
|
2873
|
+
// 401 is the server's uniform invalid-challenge outcome. It covers a wrong code,
|
|
2874
|
+
// an expired code, an unknown handle, and an exhausted challenge, and the client
|
|
2875
|
+
// deliberately cannot tell them apart — so neither can a caller watching the
|
|
2876
|
+
// client's output.
|
|
2877
|
+
if (resp.status === 401)
|
|
2878
|
+
return { ok: false, category: "rejected" };
|
|
2879
|
+
if (resp.status === 429)
|
|
2399
2880
|
return { ok: false, category: "rate-limited" };
|
|
2881
|
+
// A 5xx is the server failing, not the protocol disagreeing. Kept distinct so the
|
|
2882
|
+
// caller can say "try again" instead of "upgrade the CLI".
|
|
2883
|
+
if (resp.status >= 500)
|
|
2884
|
+
return { ok: false, category: "server-error" };
|
|
2885
|
+
return { ok: false, category: "failed" };
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* POST the RESEND phase: the handle alone, timing metadata out.
|
|
2889
|
+
*
|
|
2890
|
+
* No email field — accepting one would let a caller request mail for an address
|
|
2891
|
+
* they hold no challenge for, which is the abuse the per-email budget bounds.
|
|
2892
|
+
*/
|
|
2893
|
+
export async function resendSelfServeChallenge(deps, baseUrl, handle) {
|
|
2894
|
+
let resp;
|
|
2895
|
+
try {
|
|
2896
|
+
resp = await deps.fetch(buildSelfServeResendUrl(baseUrl), {
|
|
2897
|
+
method: "POST",
|
|
2898
|
+
headers: { "Content-Type": "application/json" },
|
|
2899
|
+
body: JSON.stringify({ handle }),
|
|
2900
|
+
signal: AbortSignal.timeout(10_000),
|
|
2901
|
+
});
|
|
2400
2902
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2903
|
+
catch (err) {
|
|
2904
|
+
void err;
|
|
2905
|
+
return { ok: false, category: "failed" };
|
|
2403
2906
|
}
|
|
2907
|
+
if (resp.ok) {
|
|
2908
|
+
let expiresAt;
|
|
2909
|
+
let resendsRemaining;
|
|
2910
|
+
try {
|
|
2911
|
+
const body = (await resp.json());
|
|
2912
|
+
expiresAt = body?.expires_at;
|
|
2913
|
+
resendsRemaining = body?.resends_remaining;
|
|
2914
|
+
}
|
|
2915
|
+
catch {
|
|
2916
|
+
return { ok: false, category: "failed" };
|
|
2917
|
+
}
|
|
2918
|
+
if (typeof expiresAt !== "string" || expiresAt.trim().length === 0) {
|
|
2919
|
+
return { ok: false, category: "failed" };
|
|
2920
|
+
}
|
|
2921
|
+
return {
|
|
2922
|
+
ok: true,
|
|
2923
|
+
expiresAt: expiresAt.trim(),
|
|
2924
|
+
resendsRemaining: typeof resendsRemaining === "number" && Number.isFinite(resendsRemaining)
|
|
2925
|
+
? resendsRemaining
|
|
2926
|
+
: 0,
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
// Cooldown and the send ceilings are BOTH 429 and are distinguished only by the
|
|
2930
|
+
// fixed detail string. Reading it is safe here precisely because it is fixed and
|
|
2931
|
+
// server-controlled — it carries no request value — and the distinction lets the
|
|
2932
|
+
// CLI say "wait a moment" instead of "try again later". Any parse problem falls
|
|
2933
|
+
// through to the more conservative `rate-limited`.
|
|
2934
|
+
if (resp.status === 429) {
|
|
2935
|
+
let detail = "";
|
|
2936
|
+
try {
|
|
2937
|
+
const body = (await resp.json());
|
|
2938
|
+
detail = typeof body?.detail === "string" ? body.detail : "";
|
|
2939
|
+
}
|
|
2940
|
+
catch {
|
|
2941
|
+
detail = "";
|
|
2942
|
+
}
|
|
2943
|
+
return {
|
|
2944
|
+
ok: false,
|
|
2945
|
+
category: detail.toLowerCase().includes("very recently") ? "cooldown" : "rate-limited",
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
if (resp.status === 401)
|
|
2949
|
+
return { ok: false, category: "rejected" };
|
|
2404
2950
|
return { ok: false, category: "failed" };
|
|
2405
2951
|
}
|
|
2406
2952
|
/**
|
|
@@ -2430,7 +2976,7 @@ export const BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE = [
|
|
|
2430
2976
|
"",
|
|
2431
2977
|
"Ask your Bridge API operator to recover it for you: they revoke the orphaned key",
|
|
2432
2978
|
"(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project",
|
|
2433
|
-
"(POST /setup/keys), then send you that key. Run install
|
|
2979
|
+
"(POST /setup/keys), then send you that key. Run install with --api-key <that key>.",
|
|
2434
2980
|
].join("\n");
|
|
2435
2981
|
/**
|
|
2436
2982
|
* The 401 message for a run that REUSED an existing pending secret — the replay
|
|
@@ -2459,8 +3005,8 @@ export const BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE = "The Bridge API rejected the
|
|
|
2459
3005
|
*/
|
|
2460
3006
|
export function buildBootstrapRetryAdvice(selfServeSignupMode) {
|
|
2461
3007
|
return selfServeSignupMode
|
|
2462
|
-
? "Re-run install
|
|
2463
|
-
: "Re-run install
|
|
3008
|
+
? "Re-run install and choose the email option — your previous attempt will resume."
|
|
3009
|
+
: "Re-run install with the same bootstrap invite — the redemption will replay and " +
|
|
2464
3010
|
"return the same key.";
|
|
2465
3011
|
}
|
|
2466
3012
|
/**
|
|
@@ -2509,6 +3055,69 @@ export async function resolveCredentialConflictConsent(prepared, ctx) {
|
|
|
2509
3055
|
ctx.grantConsent();
|
|
2510
3056
|
return ctx.retry();
|
|
2511
3057
|
}
|
|
3058
|
+
/**
|
|
3059
|
+
* The secret-free install-state file. Gitignored before it is written, so it is
|
|
3060
|
+
* named here once and fed to BOTH the gitignore call and the commit-notice
|
|
3061
|
+
* exclusion set — a single definition site keeps the two from drifting apart.
|
|
3062
|
+
*/
|
|
3063
|
+
export const INSTALL_BRIDGE_INSTALL_STATE_PATH = ".bridge/install-state.json";
|
|
3064
|
+
/** Heading printed above the commit-your-assets list. Shared with the tests. */
|
|
3065
|
+
export const INSTALL_BRIDGE_COMMIT_ASSETS_HEADING = "Commit these generated files so your team inherits the same Bridge setup:";
|
|
3066
|
+
/**
|
|
3067
|
+
* Format the commit-your-assets notice (BAPI-708, Part C).
|
|
3068
|
+
*
|
|
3069
|
+
* THIS IS A CORRECTNESS BOUNDARY, NOT COPY. Several files this install writes are
|
|
3070
|
+
* deliberately gitignored because they can carry a real `BAPI_API_KEY` — the
|
|
3071
|
+
* per-host MCP configs above all — and `.bridge/install-state.json` is gitignored
|
|
3072
|
+
* too. A generic "commit your Bridge assets" is exactly how a live key reaches a
|
|
3073
|
+
* public remote. So the notice is built by SUBTRACTION as well as selection: every
|
|
3074
|
+
* path this run handed to `ensureGitignoredShared` is removed from the candidate
|
|
3075
|
+
* list, which means the safe set cannot silently acquire a gitignored target if
|
|
3076
|
+
* the host-target registry grows a new entry later.
|
|
3077
|
+
*
|
|
3078
|
+
* SECRET DISCIPLINE: parameters are project-relative scaffold paths only. The
|
|
3079
|
+
* credential-store path is never passed in — it lives outside the repository and
|
|
3080
|
+
* holds the key, so it is excluded by never being a candidate at all, not by
|
|
3081
|
+
* filtering.
|
|
3082
|
+
*
|
|
3083
|
+
* Returns `""` when nothing survives, so a caller can skip an empty notice.
|
|
3084
|
+
*/
|
|
3085
|
+
export function buildCommitGeneratedAssetsNotice(safePaths, gitignoredPaths) {
|
|
3086
|
+
// POSIX-normalized, trailing slash preserved (it marks a directory), so
|
|
3087
|
+
// "./a/b" and "a//b" compare equal to "a/b" on both sides of the exclusion.
|
|
3088
|
+
const normalize = (value) => {
|
|
3089
|
+
const trailingSlash = /\/+$/.test(value.trim());
|
|
3090
|
+
const collapsed = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
3091
|
+
return trailingSlash ? `${collapsed}/` : collapsed;
|
|
3092
|
+
};
|
|
3093
|
+
const excluded = new Set();
|
|
3094
|
+
for (const raw of gitignoredPaths) {
|
|
3095
|
+
const normalized = normalize(raw);
|
|
3096
|
+
if (normalized.length === 0)
|
|
3097
|
+
continue;
|
|
3098
|
+
// Match a gitignored file whether the candidate names it with or without a
|
|
3099
|
+
// trailing slash, so a registry entry can never slip through on formatting.
|
|
3100
|
+
excluded.add(normalized);
|
|
3101
|
+
excluded.add(normalized.replace(/\/+$/, ""));
|
|
3102
|
+
excluded.add(`${normalized.replace(/\/+$/, "")}/`);
|
|
3103
|
+
}
|
|
3104
|
+
const seen = new Set();
|
|
3105
|
+
const lines = [];
|
|
3106
|
+
for (const raw of safePaths) {
|
|
3107
|
+
const normalized = normalize(raw);
|
|
3108
|
+
if (normalized.length === 0)
|
|
3109
|
+
continue;
|
|
3110
|
+
if (excluded.has(normalized))
|
|
3111
|
+
continue;
|
|
3112
|
+
if (seen.has(normalized))
|
|
3113
|
+
continue;
|
|
3114
|
+
seen.add(normalized);
|
|
3115
|
+
lines.push(` ${normalized}`);
|
|
3116
|
+
}
|
|
3117
|
+
if (lines.length === 0)
|
|
3118
|
+
return "";
|
|
3119
|
+
return [INSTALL_BRIDGE_COMMIT_ASSETS_HEADING, ...lines].join("\n");
|
|
3120
|
+
}
|
|
2512
3121
|
/**
|
|
2513
3122
|
* Render the --dry-run preview lines. Every secret is ALWAYS redacted — the
|
|
2514
3123
|
* spawnCommand, config preview, and (in bootstrap-invite mode) the exchange body
|
|
@@ -2525,8 +3134,8 @@ export function buildDryRunPreview(plan) {
|
|
|
2525
3134
|
// truthful for every outcome — but ONLY `resolved` may claim the name came back
|
|
2526
3135
|
// from the server (BAPI-687, C-4).
|
|
2527
3136
|
plan.serverRepoResolutionStatus !== undefined
|
|
2528
|
-
? "install
|
|
2529
|
-
: "install
|
|
3137
|
+
? "install --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)"
|
|
3138
|
+
: "install --dry-run (no writes, no network, no spawns)",
|
|
2530
3139
|
`Repo name: ${plan.repoName}${plan.serverRepoResolutionStatus === "resolved"
|
|
2531
3140
|
? " (resolved server-side from your API key)"
|
|
2532
3141
|
: ""}`,
|
|
@@ -2546,9 +3155,25 @@ export function buildDryRunPreview(plan) {
|
|
|
2546
3155
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
|
|
2547
3156
|
MCP_TIMEOUT_GUIDANCE,
|
|
2548
3157
|
`Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
|
|
3158
|
+
...buildCommitAssetsPreviewLines(plan),
|
|
2549
3159
|
...buildLaunchStepPreview(plan),
|
|
2550
3160
|
];
|
|
2551
3161
|
}
|
|
3162
|
+
/**
|
|
3163
|
+
* The commit-assets notice as preview lines (BAPI-708, Part C). A `--dry-run`
|
|
3164
|
+
* returns before the live emission point, and this module's convention is that a
|
|
3165
|
+
* preview never advertises different behavior — so the same notice, built from the
|
|
3166
|
+
* same resolved manifest, appears here too. The gitignored exclusion set is the
|
|
3167
|
+
* plan's own `configTargets` plus the install-state file, exactly as the live path
|
|
3168
|
+
* computes it.
|
|
3169
|
+
*/
|
|
3170
|
+
function buildCommitAssetsPreviewLines(plan) {
|
|
3171
|
+
const notice = buildCommitGeneratedAssetsNotice(plan.commitSafeAssets ?? [], [
|
|
3172
|
+
...plan.configTargets,
|
|
3173
|
+
INSTALL_BRIDGE_INSTALL_STATE_PATH,
|
|
3174
|
+
]);
|
|
3175
|
+
return notice.length > 0 ? ["", notice] : [];
|
|
3176
|
+
}
|
|
2552
3177
|
/**
|
|
2553
3178
|
* One-line description of the planned launch agent for the dry-run header (BAPI-657).
|
|
2554
3179
|
* Never claims an unconditional Claude spawn: it reflects the discriminated launch
|
|
@@ -2589,8 +3214,10 @@ function buildLaunchStepPreview(plan) {
|
|
|
2589
3214
|
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
2590
3215
|
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
2591
3216
|
` interactive, offer '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/, "")}' before the agent session starts.`,
|
|
2592
|
-
|
|
2593
|
-
|
|
3217
|
+
// BAPI-708: the purpose sentence is kept on ONE line in both copies so the
|
|
3218
|
+
// no-drift test can compare the whole phrase rather than a wrapped fragment.
|
|
3219
|
+
" It installs the Bridge GitHub App so you can implement and review tickets with Bridge.",
|
|
3220
|
+
" It opens github.com in your browser, and shares no GitHub credential with Bridge; it",
|
|
2594
3221
|
` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
|
|
2595
3222
|
];
|
|
2596
3223
|
if (plan.launch.kind === "spawn") {
|
|
@@ -2634,8 +3261,8 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
2634
3261
|
// created, no mint request is sent, no email leaves the machine. The email and
|
|
2635
3262
|
// any synthetic token are deliberately absent from this output.
|
|
2636
3263
|
const header = plan.selfServeSignup
|
|
2637
|
-
? "install
|
|
2638
|
-
: "install
|
|
3264
|
+
? "install --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
|
|
3265
|
+
: "install --invite --dry-run (no writes, no network, no spawns, no secret generated)";
|
|
2639
3266
|
const repoLine = plan.selfServeSignup
|
|
2640
3267
|
? `Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`
|
|
2641
3268
|
: `Repo name: ${plan.repoName} (created by the exchange; globally unique)`;
|
|
@@ -2675,6 +3302,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
2675
3302
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
|
|
2676
3303
|
MCP_TIMEOUT_GUIDANCE,
|
|
2677
3304
|
`Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
|
|
3305
|
+
...buildCommitAssetsPreviewLines(plan),
|
|
2678
3306
|
...buildLaunchStepPreview(plan),
|
|
2679
3307
|
];
|
|
2680
3308
|
}
|
|
@@ -2724,6 +3352,28 @@ export function manualEditorNames(editors) {
|
|
|
2724
3352
|
export function formatNeedKeyCredentialStoreLine(repoName, credentialStorePath) {
|
|
2725
3353
|
return `Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" — copy BAPI_API_KEY from there.`;
|
|
2726
3354
|
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Where THIS run's routing credential landed (BAPI-708, B-b) — the single Step 4
|
|
3357
|
+
* result line, printed on the have-key and need-key arms alike.
|
|
3358
|
+
*
|
|
3359
|
+
* It replaces both `stored routing credential for … at …` lines, and it is also
|
|
3360
|
+
* what makes Part B-a honest: `.mcp.json` is an MCP SERVER REGISTRATION, not a
|
|
3361
|
+
* credentials file, so the one place a credential is reported must name the
|
|
3362
|
+
* credential store instead.
|
|
3363
|
+
*
|
|
3364
|
+
* Distinct from {@link formatNeedKeyCredentialStoreLine}, which stays for the
|
|
3365
|
+
* MANUAL-EDITOR context — its "copy BAPI_API_KEY from there" instruction only
|
|
3366
|
+
* makes sense when the user has a global config to paste into.
|
|
3367
|
+
*
|
|
3368
|
+
* SECRET DISCIPLINE (hard constraint, same as its sibling): this formatter accepts
|
|
3369
|
+
* NO key material. Its parameters are a repo name and a filesystem path, both
|
|
3370
|
+
* printable; it must never be handed `apiKey`, `inviteToken`, `keySecret`, a signup
|
|
3371
|
+
* email, or a credential record. The arity is pinned by test precisely so a third
|
|
3372
|
+
* parameter cannot be added without that test failing.
|
|
3373
|
+
*/
|
|
3374
|
+
export function formatProjectCredentialStoreLine(repoName, credentialStorePath) {
|
|
3375
|
+
return `We wrote your project credentials to: ${credentialStorePath} (target "bapi:${repoName}")`;
|
|
3376
|
+
}
|
|
2727
3377
|
/**
|
|
2728
3378
|
* Build the manual-instruction block for the DETECTED global-config editors only
|
|
2729
3379
|
* (key always redacted). Returns `null` when neither Windsurf nor Codex is
|
|
@@ -2868,7 +3518,7 @@ function readInstallBridgeDebugValue(read) {
|
|
|
2868
3518
|
*/
|
|
2869
3519
|
export function buildInstallBridgeFailureLines(error, context) {
|
|
2870
3520
|
const lines = [
|
|
2871
|
-
`Error: install
|
|
3521
|
+
`Error: install failed at: ${context.step}`,
|
|
2872
3522
|
` cause: ${classifyInstallBridgeFailure(error)}`,
|
|
2873
3523
|
` ${INSTALL_BRIDGE_DOCTOR_POINTER}`,
|
|
2874
3524
|
];
|
|
@@ -2935,6 +3585,14 @@ export function resolveInstallBridgeBaseUrl(env) {
|
|
|
2935
3585
|
* Every printed Step 1–5 label, defined once. `runInstallBridgeCli` uses each
|
|
2936
3586
|
* constant BOTH when logging the step AND when recording the current step, so a
|
|
2937
3587
|
* failure can never be attributed to a label the user did not just see.
|
|
3588
|
+
*
|
|
3589
|
+
* BAPI-708 (Part D) removed the separate `promoteCredential` entry: promotion is
|
|
3590
|
+
* not a step the user does anything about, and printing it as its own numbered
|
|
3591
|
+
* step told them the bootstrap two-phase commit exists without giving them a way
|
|
3592
|
+
* to act on it. Both credential arms now log `persistCredential`. Attribution is
|
|
3593
|
+
* NOT lost — the promotion-failure fatal is highly specific about what failed and
|
|
3594
|
+
* what state the user is left in. The step COUNT is unchanged at 5; nothing is
|
|
3595
|
+
* renumbered.
|
|
2938
3596
|
*/
|
|
2939
3597
|
export const INSTALL_BRIDGE_STEP_LABELS = {
|
|
2940
3598
|
scaffold: "Step 1/5 — scaffolding project (commands, agents, pipelines, config placeholders)…",
|
|
@@ -2942,7 +3600,6 @@ export const INSTALL_BRIDGE_STEP_LABELS = {
|
|
|
2942
3600
|
inviteRedeem: "Step 2/5 — redeeming the bootstrap invite…",
|
|
2943
3601
|
verifyConnectivity: "Step 2/5 — verifying connectivity…",
|
|
2944
3602
|
writeHostConfigs: "Step 3/5 — writing per-host MCP config…",
|
|
2945
|
-
promoteCredential: "Step 4/5 — promoting the bootstrap credential…",
|
|
2946
3603
|
persistCredential: "Step 4/5 — persisting routing credential…",
|
|
2947
3604
|
};
|
|
2948
3605
|
/**
|
|
@@ -2974,7 +3631,7 @@ export const INSTALL_BRIDGE_NO_GIT_WARNING = "This doesn't look like a project r
|
|
|
2974
3631
|
/** Default-No confirmation shown only on a TTY when `.git` is absent. */
|
|
2975
3632
|
export const INSTALL_BRIDGE_NO_GIT_PROMPT = `${INSTALL_BRIDGE_NO_GIT_WARNING} — continue? [y/N]: `;
|
|
2976
3633
|
/** Printed on decline. Paired with exit code 0 — a cancel is not an error. */
|
|
2977
|
-
export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install
|
|
3634
|
+
export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install from your project root, or re-run and confirm to continue.";
|
|
2978
3635
|
/** The non-TTY form: same finding, stated as a warning, then the run continues. */
|
|
2979
3636
|
export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} — continuing (non-interactive).`;
|
|
2980
3637
|
/**
|
|
@@ -3040,6 +3697,63 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3040
3697
|
// A rejected value cannot be interpolated into guidance, so usage falls back to
|
|
3041
3698
|
// the documented default — the run is about to fail on it anyway.
|
|
3042
3699
|
const usageBaseUrl = baseUrlResult.ok ? baseUrlResult.value : DEFAULT_BAPI_BASE_URL;
|
|
3700
|
+
// ---- BAPI-679: nested `install-bridge conductor` -----------------------
|
|
3701
|
+
// Intercepted HERE, before `parseInstallBridgeArgs`, because that parser
|
|
3702
|
+
// rejects ALL positional arguments — a bare `conductor` token would otherwise
|
|
3703
|
+
// be an argument error. Deliberately after the deps merge and the base-URL
|
|
3704
|
+
// resolution above, so the nested command inherits the same final merged deps
|
|
3705
|
+
// (including a test's `fetch` override) and the same normalized base URL.
|
|
3706
|
+
// Note this is `install-bridge conductor`; the bare top-level `conductor`
|
|
3707
|
+
// subcommand still routes to `runConductorCli` (the local event ledger) and is
|
|
3708
|
+
// untouched.
|
|
3709
|
+
if (argv[0] === "conductor") {
|
|
3710
|
+
if (!baseUrlResult.ok) {
|
|
3711
|
+
errorLog(baseUrlResult.error);
|
|
3712
|
+
return 1;
|
|
3713
|
+
}
|
|
3714
|
+
const conductorBaseUrl = baseUrlResult.value;
|
|
3715
|
+
const conductorDeps = {
|
|
3716
|
+
env: deps.env,
|
|
3717
|
+
cwd: deps.cwd,
|
|
3718
|
+
log: deps.log,
|
|
3719
|
+
errorLog: deps.errorLog,
|
|
3720
|
+
promptLine: deps.promptLine,
|
|
3721
|
+
resolveBaseUrl: () => ({ ok: true, value: conductorBaseUrl }),
|
|
3722
|
+
resolveAccess: (repoName, resolvedBaseUrl) => (deps.conductorResolveAccess ??
|
|
3723
|
+
(async () => ({ ok: false, error: "conductor access seam unavailable" })))(repoName, resolvedBaseUrl, deps),
|
|
3724
|
+
fetchJson: deps.fetch,
|
|
3725
|
+
postJson: deps.fetch,
|
|
3726
|
+
runDoctor: (params) => (deps.conductorRunDoctor ??
|
|
3727
|
+
(async () => ({
|
|
3728
|
+
legacyConductor: null,
|
|
3729
|
+
legacyExecutorPreflight: null,
|
|
3730
|
+
legacyInstallChecks: null,
|
|
3731
|
+
executorService: null,
|
|
3732
|
+
readiness: null,
|
|
3733
|
+
sections: [
|
|
3734
|
+
{
|
|
3735
|
+
id: "conductor-doctor",
|
|
3736
|
+
label: "Conductor install doctor",
|
|
3737
|
+
status: "fatal",
|
|
3738
|
+
detail: "the unified doctor seam is unavailable",
|
|
3739
|
+
},
|
|
3740
|
+
],
|
|
3741
|
+
})))({
|
|
3742
|
+
...params,
|
|
3743
|
+
fetch: deps.fetch,
|
|
3744
|
+
cwd: deps.cwd,
|
|
3745
|
+
env: deps.env,
|
|
3746
|
+
readFile: deps.readFile,
|
|
3747
|
+
}),
|
|
3748
|
+
writeWorkflow: (cwd, content, options) => (deps.conductorWriteWorkflow ??
|
|
3749
|
+
(async () => ({ ok: false, error: "workflow seam unavailable" })))(cwd, content, options, deps),
|
|
3750
|
+
recordArtifact: (cwd, relPath) => (deps.conductorRecordArtifact ??
|
|
3751
|
+
(async () => ({ ok: false, error: "install-state seam unavailable" })))(cwd, relPath, deps),
|
|
3752
|
+
runSetupEpicDryRun: (setupArgv) => (deps.conductorRunSetupEpicDryRun ??
|
|
3753
|
+
(async () => ({ exitCode: 1 })))(setupArgv),
|
|
3754
|
+
};
|
|
3755
|
+
return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
|
|
3756
|
+
}
|
|
3043
3757
|
const parsed = parseInstallBridgeArgs(argv);
|
|
3044
3758
|
if (parsed.status === "help") {
|
|
3045
3759
|
log(getInstallBridgeUsage(usageBaseUrl));
|
|
@@ -3109,6 +3823,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3109
3823
|
return 1;
|
|
3110
3824
|
}
|
|
3111
3825
|
const branch = branchResult.branch;
|
|
3826
|
+
// BAPI-708 (A-5): provenance comes from the branch resolver and NOWHERE else. It
|
|
3827
|
+
// is deliberately NOT re-derived from `argv`/`env` here — the resolver owns that
|
|
3828
|
+
// logic, and a second derivation is exactly the drift this ticket removes.
|
|
3829
|
+
const credentialIntent = branchResult.credentialIntent;
|
|
3112
3830
|
// Mutable (BAPI-661): an ordinary have-key credential that classifies as a
|
|
3113
3831
|
// bootstrap invite (see the have-key arm below) flips `bootstrapInviteMode` to
|
|
3114
3832
|
// true AFTER branch selection, before repository-name resolution — everything
|
|
@@ -3170,13 +3888,23 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3170
3888
|
if (classifyEnteredCredential(inviteToken) !== "invite" &&
|
|
3171
3889
|
isHighEntropyApiKeyShape(inviteToken)) {
|
|
3172
3890
|
log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3891
|
+
// BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES. On an INFERRED route the
|
|
3892
|
+
// announcement above is purely informational — the user never declared the
|
|
3893
|
+
// invite path, so there is no stated intent for the value to contradict and a
|
|
3894
|
+
// confirmation would ask them to ratify a decision they never made. On a
|
|
3895
|
+
// DECLARED route the confirmation is byte-for-byte BAPI-687 behavior,
|
|
3896
|
+
// including the intentional non-TTY asymmetry: an API-key-shaped value on the
|
|
3897
|
+
// declared invite path exits non-zero rather than silently redirecting a
|
|
3898
|
+
// script away from the project-creating run it asked for.
|
|
3899
|
+
if (credentialIntent === "declared") {
|
|
3900
|
+
const switchToKeyPath = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_KEY_ON_INVITE_PATH_PROMPT);
|
|
3901
|
+
if (!switchToKeyPath) {
|
|
3902
|
+
// Decline, an unreadable prompt, and a non-TTY run all land here — nothing
|
|
3903
|
+
// has been prompted for, written, or sent, so the exit costs the user only
|
|
3904
|
+
// the re-run named in the guidance.
|
|
3905
|
+
errorLog(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_DECLINED);
|
|
3906
|
+
return 1;
|
|
3907
|
+
}
|
|
3180
3908
|
}
|
|
3181
3909
|
apiKey = inviteToken;
|
|
3182
3910
|
inviteToken = "";
|
|
@@ -3200,20 +3928,26 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3200
3928
|
// bootstrap-invite protocol verbatim.
|
|
3201
3929
|
if (classifyEnteredCredential(keyResult.value) === "invite") {
|
|
3202
3930
|
// BAPI-687 (B-2): the switch survives, the SILENCE does not. The user is told
|
|
3203
|
-
// which credential type was detected and what it will do
|
|
3204
|
-
// asked to confirm before the branch changes. A non-TTY run keeps today's
|
|
3205
|
-
// automatic switch — a script must not gain a prompt — but still prints the
|
|
3206
|
-
// notice, so the transcript explains itself.
|
|
3931
|
+
// which credential type was detected and what it will do.
|
|
3207
3932
|
log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_NOTICE);
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3933
|
+
// BAPI-708 (A-4): DO NOT UNIFY THESE TWO BRANCHES — see the matching comment
|
|
3934
|
+
// on the invite-path arm above. INFERRED means the user answered "yes, I have
|
|
3935
|
+
// a token" and pasted it; they never claimed it was an API key, so the notice
|
|
3936
|
+
// is an announcement and the run simply proceeds. DECLARED keeps BAPI-687
|
|
3937
|
+
// verbatim, asymmetry included: a TTY gets the default-No confirmation, while
|
|
3938
|
+
// a non-TTY still auto-switches (a script that worked yesterday must not gain
|
|
3939
|
+
// a prompt) and prints the notice so the transcript explains itself.
|
|
3940
|
+
if (credentialIntent === "declared") {
|
|
3941
|
+
if (deps.isTTY && deps.promptLine) {
|
|
3942
|
+
const switchToInvite = await requestCredentialRouteSwitchConfirmation(deps, INSTALL_BRIDGE_INVITE_ON_KEY_PATH_PROMPT);
|
|
3943
|
+
if (!switchToInvite) {
|
|
3944
|
+
errorLog(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_DECLINED);
|
|
3945
|
+
return 1;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
else {
|
|
3949
|
+
log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);
|
|
3213
3950
|
}
|
|
3214
|
-
}
|
|
3215
|
-
else {
|
|
3216
|
-
log(INSTALL_BRIDGE_INVITE_ON_KEY_PATH_AUTO_NOTICE);
|
|
3217
3951
|
}
|
|
3218
3952
|
inviteToken = keyResult.value.trim();
|
|
3219
3953
|
apiKey = "";
|
|
@@ -3224,6 +3958,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3224
3958
|
apiKeySource = undefined;
|
|
3225
3959
|
}
|
|
3226
3960
|
else {
|
|
3961
|
+
// BAPI-708 (A-4): on the INFERRED path the route decision is announced even
|
|
3962
|
+
// when nothing switches, so a bare run always says which credential type it
|
|
3963
|
+
// detected and where that leads. Reuses the existing notice because it names
|
|
3964
|
+
// only the credential TYPE and the destination ROUTE — never the value.
|
|
3965
|
+
// Guarded on the POSITIVE shape test: a value matching neither shape is not
|
|
3966
|
+
// reclassified AND not announced; it stays on this route and fails here
|
|
3967
|
+
// exactly as it does today. Declared paths announce nothing new.
|
|
3968
|
+
if (credentialIntent === "inferred" && isHighEntropyApiKeyShape(keyResult.value.trim())) {
|
|
3969
|
+
log(INSTALL_BRIDGE_KEY_ON_INVITE_PATH_NOTICE);
|
|
3970
|
+
}
|
|
3227
3971
|
apiKey = keyResult.value;
|
|
3228
3972
|
apiKeySource = keyResult.source;
|
|
3229
3973
|
}
|
|
@@ -3343,12 +4087,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3343
4087
|
else {
|
|
3344
4088
|
planLaunch = { kind: "manual", reason: launchDecision.reason };
|
|
3345
4089
|
}
|
|
4090
|
+
// BAPI-708 (Part C): resolved from THIS invocation's cwd + env, so the notice
|
|
4091
|
+
// names only what this run actually scaffolds — `.cursor/commands/` only when
|
|
4092
|
+
// Cursor was detected, `.github/agents/` only when VS Code was, and the pipelines
|
|
4093
|
+
// / instructions entries under whatever `BAPI_PIPELINES_DIR` resolved to. It is
|
|
4094
|
+
// deliberately independent of which MCP HOSTS were selected: a scaffolded command
|
|
4095
|
+
// directory is committed whether or not its editor got an MCP registration.
|
|
4096
|
+
const scaffoldAssets = await resolveInitScaffoldAssets({
|
|
4097
|
+
cwd: deps.cwd,
|
|
4098
|
+
env: deps.env,
|
|
4099
|
+
stat: deps.stat,
|
|
4100
|
+
});
|
|
3346
4101
|
const plan = {
|
|
3347
4102
|
repoName,
|
|
3348
4103
|
baseUrl,
|
|
3349
4104
|
docsDir,
|
|
3350
4105
|
launch: planLaunch,
|
|
3351
4106
|
configTargets: targets.map((t) => t.relPath),
|
|
4107
|
+
commitSafeAssets: scaffoldAssets.commitSafePaths,
|
|
3352
4108
|
manualEditors: manualEditorNames(manualEditors),
|
|
3353
4109
|
credentialTarget: `bapi:${repoName}`,
|
|
3354
4110
|
credentialStorePath,
|
|
@@ -3486,7 +4242,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3486
4242
|
// ---- Step 1 — scaffold (secret-free placeholders only) ----
|
|
3487
4243
|
currentStep = INSTALL_BRIDGE_STEP_LABELS.scaffold;
|
|
3488
4244
|
log(currentStep);
|
|
3489
|
-
|
|
4245
|
+
// BAPI-728: hand init the identity this run already resolved and validated.
|
|
4246
|
+
// `credentialAvailable: true` is a statement about THIS flow, not a lookup:
|
|
4247
|
+
// Step 2 mints/verifies the key and Step 4 persists it, so a placeholder
|
|
4248
|
+
// warning here would be a false alarm about a config the installer is
|
|
4249
|
+
// actively configuring. The key value itself never crosses this boundary.
|
|
4250
|
+
await deps.runInit(deps.cwd, {
|
|
4251
|
+
creationContext: { repoName, baseUrl, credentialAvailable: true },
|
|
4252
|
+
});
|
|
3490
4253
|
// ---- Step 2 — mint (invite mode) or verify (normal mode) the credential ----
|
|
3491
4254
|
let inviteFingerprint = "";
|
|
3492
4255
|
if (bootstrapInviteMode) {
|
|
@@ -3504,13 +4267,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3504
4267
|
let resumedSelfServeReplay = false;
|
|
3505
4268
|
let keySecret = "";
|
|
3506
4269
|
let reusedPendingSecret = false;
|
|
4270
|
+
// BAPI-757: a LIVE challenge found by the lookup below. When set, the run
|
|
4271
|
+
// resumes at the code prompt and MUST NOT request or resend — the emailed
|
|
4272
|
+
// code is still valid, and asking again would spend one of the user's own
|
|
4273
|
+
// per-address sends for nothing.
|
|
4274
|
+
let liveChallenge = null;
|
|
3507
4275
|
if (selfServeSignupMode) {
|
|
3508
|
-
const lookup = await deps.lookupSelfServeBootstrapPending({ repoName }, credentialWriteDeps);
|
|
4276
|
+
const lookup = await deps.lookupSelfServeBootstrapPending({ repoName, now: deps.now() }, credentialWriteDeps);
|
|
3509
4277
|
if (!lookup.ok) {
|
|
3510
|
-
// Fail BEFORE
|
|
3511
|
-
// untouched — an ordinary-invite record, a malformed record,
|
|
3512
|
-
// unreadable store are all states where
|
|
3513
|
-
// problem rather than recover from it.
|
|
4278
|
+
// Fail BEFORE any endpoint is called and leave the stored record
|
|
4279
|
+
// untouched — an ordinary-invite record, a malformed record, a partial
|
|
4280
|
+
// challenge record, and an unreadable store are all states where
|
|
4281
|
+
// starting over would compound the problem rather than recover from it.
|
|
3514
4282
|
return fatal(`Error: ${lookup.error}`);
|
|
3515
4283
|
}
|
|
3516
4284
|
if (lookup.state === "resumable") {
|
|
@@ -3524,37 +4292,176 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3524
4292
|
reusedPendingSecret = true;
|
|
3525
4293
|
log(` resuming your previous signup attempt for ${repoName}`);
|
|
3526
4294
|
}
|
|
4295
|
+
else if (lookup.state === "challenge") {
|
|
4296
|
+
liveChallenge = {
|
|
4297
|
+
handle: lookup.challengeHandle,
|
|
4298
|
+
expiresAt: lookup.challengeExpiresAt,
|
|
4299
|
+
email: lookup.signupEmail,
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
// `challenge-expired` and `none` both fall through to a fresh request.
|
|
3527
4303
|
}
|
|
3528
4304
|
/**
|
|
3529
|
-
*
|
|
4305
|
+
* Run the self-serve signup through request → verify, then durably store the
|
|
4306
|
+
* resulting invite token WITH a fresh `key_secret` (BAPI-757).
|
|
4307
|
+
*
|
|
4308
|
+
* Three phases, in this order, and the order is the protocol:
|
|
4309
|
+
*
|
|
4310
|
+
* 1. **Request** (skipped when resuming a live challenge). The server
|
|
4311
|
+
* creates a challenge and queues an email. No tenant, no invitation, and
|
|
4312
|
+
* nothing billable exists yet.
|
|
4313
|
+
* 2. **Persist the challenge, THEN prompt.** The email has already been
|
|
4314
|
+
* sent and the user's per-address send budget already charged by this
|
|
4315
|
+
* point, so the handle reaches disk before anything can interrupt the
|
|
4316
|
+
* run. A terminal closed at the prompt resumes the SAME challenge with
|
|
4317
|
+
* the SAME code.
|
|
4318
|
+
* 3. **Verify**, which is what actually mints — and hands back the very
|
|
4319
|
+
* `inviteToken` the untouched persist-before-exchange region below
|
|
4320
|
+
* already expected.
|
|
4321
|
+
*
|
|
4322
|
+
* The verification code is prompted for, submitted once, and never written to
|
|
4323
|
+
* disk, never logged, and never echoed.
|
|
3530
4324
|
*
|
|
3531
|
-
*
|
|
3532
|
-
*
|
|
3533
|
-
* load-bearing protocol described below: mint the token, durably store the
|
|
3534
|
-
* token plus the `key_secret`, and only THEN exchange. Logs its own
|
|
3535
|
-
* (secret-free) failure and returns false; the caller exits non-zero.
|
|
4325
|
+
* Logs its own (secret-free) failure and returns false; the caller exits
|
|
4326
|
+
* non-zero.
|
|
3536
4327
|
*/
|
|
3537
4328
|
const mintAndPrepareSelfServe = async () => {
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
4329
|
+
let challengeHandle;
|
|
4330
|
+
let challengeExpiresAt;
|
|
4331
|
+
let challengeEmail;
|
|
4332
|
+
if (liveChallenge) {
|
|
4333
|
+
// Phase 1 SKIPPED. Resuming an unexpired challenge: no request call, no
|
|
4334
|
+
// resend call, and no second email.
|
|
4335
|
+
challengeHandle = liveChallenge.handle;
|
|
4336
|
+
challengeExpiresAt = liveChallenge.expiresAt;
|
|
4337
|
+
challengeEmail = liveChallenge.email;
|
|
4338
|
+
log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
|
|
4339
|
+
log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
4340
|
+
}
|
|
4341
|
+
else {
|
|
4342
|
+
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
4343
|
+
if (!requested.ok) {
|
|
4344
|
+
if (requested.category === "rate-limited") {
|
|
4345
|
+
// Deliberately does NOT name a limiter bucket. The server refuses to
|
|
4346
|
+
// say which ceiling was reached, and repeating a guess here would
|
|
4347
|
+
// undo that.
|
|
4348
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4349
|
+
}
|
|
4350
|
+
else if (requested.category === "unavailable") {
|
|
4351
|
+
// R-4's fail-closed branch: nothing was created, and there is no
|
|
4352
|
+
// suggestion anywhere that verification can be skipped.
|
|
4353
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
|
|
4354
|
+
}
|
|
4355
|
+
else if (requested.category === "invalid") {
|
|
4356
|
+
reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
|
|
4357
|
+
}
|
|
4358
|
+
else {
|
|
4359
|
+
// R-2: a malformed or tokenless-unexpected response is a PROTOCOL
|
|
4360
|
+
// mismatch, not a network problem. Saying "check connectivity" here
|
|
4361
|
+
// was the old behavior and is exactly what this ticket replaces.
|
|
4362
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
4363
|
+
}
|
|
4364
|
+
return false;
|
|
3547
4365
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
4366
|
+
challengeHandle = requested.handle;
|
|
4367
|
+
challengeExpiresAt = requested.expiresAt;
|
|
4368
|
+
challengeEmail = signupEmail;
|
|
4369
|
+
// Phase 2, first half — durable BEFORE the prompt. A failure here stops
|
|
4370
|
+
// the run: continuing would prompt for a code whose challenge could not
|
|
4371
|
+
// be resumed if anything interrupted the terminal.
|
|
4372
|
+
const recorded = await deps.prepareSelfServeBootstrapChallenge({
|
|
4373
|
+
repoName,
|
|
4374
|
+
signupEmail: challengeEmail,
|
|
4375
|
+
challengeHandle,
|
|
4376
|
+
challengeExpiresAt,
|
|
4377
|
+
}, credentialWriteDeps);
|
|
4378
|
+
if (!recorded.ok) {
|
|
4379
|
+
reportFatal(`Error: could not record the signup attempt (${recorded.kind}). ${recorded.error} ` +
|
|
4380
|
+
"No workspace has been set up — fix the problem and re-run.");
|
|
4381
|
+
return false;
|
|
3550
4382
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
4383
|
+
log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
4384
|
+
}
|
|
4385
|
+
log(` Enter the ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to finish setting up your workspace.`);
|
|
4386
|
+
const remaining = formatChallengeRemaining(challengeExpiresAt, deps.now());
|
|
4387
|
+
if (remaining)
|
|
4388
|
+
log(` ${remaining}`);
|
|
4389
|
+
// Phase 2, second half, and phase 3. One resend is offered on the recovery
|
|
4390
|
+
// path only — never proactively — so the budget is spent on the user's
|
|
4391
|
+
// explicit request rather than by the CLI's own retry logic.
|
|
4392
|
+
let resendOffered = false;
|
|
4393
|
+
// BOUNDED, not `for (;;)`. AC-4 makes a wrong code and a dead challenge
|
|
4394
|
+
// byte-identical, so the CLI cannot detect that a challenge has been killed
|
|
4395
|
+
// — an unbounded loop would re-prompt forever against something that can
|
|
4396
|
+
// never succeed, with Ctrl-C as the only way out.
|
|
4397
|
+
let rejections = 0;
|
|
4398
|
+
for (;;) {
|
|
4399
|
+
const code = await promptForVerificationCode(deps, log);
|
|
4400
|
+
if (code === null) {
|
|
4401
|
+
reportFatal("Error: No verification code entered — nothing has been set up. Re-run this command " +
|
|
4402
|
+
"to continue; the code already emailed to you stays valid until it expires.");
|
|
4403
|
+
return false;
|
|
4404
|
+
}
|
|
4405
|
+
const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
|
|
4406
|
+
if (verified.ok) {
|
|
4407
|
+
inviteToken = verified.token;
|
|
4408
|
+
break;
|
|
4409
|
+
}
|
|
4410
|
+
if (verified.category === "rate-limited") {
|
|
4411
|
+
// NOT the resend wording: a throttle here comes from the invite-mint
|
|
4412
|
+
// ceiling and can fire AFTER a perfectly correct code, so telling the
|
|
4413
|
+
// user to fall back on an email they already have is backwards.
|
|
4414
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`);
|
|
4415
|
+
return false;
|
|
4416
|
+
}
|
|
4417
|
+
if (verified.category === "server-error") {
|
|
4418
|
+
reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
|
|
4419
|
+
return false;
|
|
4420
|
+
}
|
|
4421
|
+
if (verified.category === "failed") {
|
|
4422
|
+
// Only a genuine protocol mismatch reaches here now: a malformed body,
|
|
4423
|
+
// a missing token, a network error, or a 4xx this client does not
|
|
4424
|
+
// expect. R-2's upgrade instruction is the right answer for those.
|
|
4425
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
4426
|
+
return false;
|
|
4427
|
+
}
|
|
4428
|
+
// `rejected` — the server's UNIFORM outcome. One message, no speculation
|
|
4429
|
+
// about which of the four states it was.
|
|
4430
|
+
rejections += 1;
|
|
4431
|
+
if (rejections >= INSTALL_BRIDGE_MAX_CODE_REJECTIONS) {
|
|
4432
|
+
// Still state-neutral: this says the attempt is over, never WHY.
|
|
4433
|
+
reportFatal(INSTALL_BRIDGE_VERIFICATION_GIVING_UP);
|
|
4434
|
+
return false;
|
|
4435
|
+
}
|
|
4436
|
+
errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED);
|
|
4437
|
+
if (!resendOffered && deps.isTTY && deps.promptLine) {
|
|
4438
|
+
resendOffered = true;
|
|
4439
|
+
const answer = await requestCredentialRouteSwitchConfirmation(deps, "Send a new code? [y/N]: ");
|
|
4440
|
+
if (answer) {
|
|
4441
|
+
const resent = await resendSelfServeChallenge(deps, baseUrl, challengeHandle);
|
|
4442
|
+
if (resent.ok) {
|
|
4443
|
+
log(` ${INSTALL_BRIDGE_RESEND_SENT}`);
|
|
4444
|
+
}
|
|
4445
|
+
else if (resent.category === "cooldown") {
|
|
4446
|
+
log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);
|
|
4447
|
+
}
|
|
4448
|
+
else if (resent.category === "rate-limited") {
|
|
4449
|
+
log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4450
|
+
}
|
|
4451
|
+
else if (resent.category === "rejected") {
|
|
4452
|
+
// The challenge is no longer eligible — the same uniform outcome
|
|
4453
|
+
// verification reports. Stop rather than loop on a dead challenge.
|
|
4454
|
+
reportFatal(`Error: ${INSTALL_BRIDGE_VERIFICATION_REJECTED}`);
|
|
4455
|
+
return false;
|
|
4456
|
+
}
|
|
4457
|
+
else {
|
|
4458
|
+
log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
3553
4461
|
}
|
|
3554
|
-
return false;
|
|
3555
4462
|
}
|
|
3556
|
-
//
|
|
3557
|
-
|
|
4463
|
+
// From here on the token is treated identically to a manually supplied
|
|
4464
|
+
// invite — the load-bearing region below is completely unchanged.
|
|
3558
4465
|
inviteFingerprint = fingerprintBootstrapInvite(inviteToken);
|
|
3559
4466
|
const prepareSelfServePending = (allowOverwrite) => deps.prepareBootstrapPending({
|
|
3560
4467
|
repoName,
|
|
@@ -3585,7 +4492,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3585
4492
|
}
|
|
3586
4493
|
keySecret = prep.keySecret;
|
|
3587
4494
|
reusedPendingSecret = prep.reused;
|
|
3588
|
-
|
|
4495
|
+
// BAPI-708 (Part D): the pending write is NOT narrated on the happy path.
|
|
4496
|
+
// The target name and the fsync ordering are implementation detail the user
|
|
4497
|
+
// cannot act on; they remain fully described in --dry-run and in the failure
|
|
4498
|
+
// messages, which name the pending record and what it means for a retry. The
|
|
4499
|
+
// MECHANISM above is untouched — see the LOAD-BEARING ORDERING block below.
|
|
3589
4500
|
return true;
|
|
3590
4501
|
};
|
|
3591
4502
|
// ---- Self-serve signup (BAPI-618): mint the invite token from the email ----
|
|
@@ -3670,7 +4581,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3670
4581
|
}
|
|
3671
4582
|
keySecret = prepared.keySecret;
|
|
3672
4583
|
reusedPendingSecret = prepared.reused;
|
|
3673
|
-
|
|
4584
|
+
// BAPI-708 (Part D): happy-path narration removed; the mechanism above is
|
|
4585
|
+
// unchanged. See the matching comment on the self-serve arm.
|
|
3674
4586
|
}
|
|
3675
4587
|
// The exchange REPLACES the pre-flight ping: in invite mode there is no key to
|
|
3676
4588
|
// ping with — this call is what mints it.
|
|
@@ -3727,7 +4639,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3727
4639
|
!selfServeRefreshed) {
|
|
3728
4640
|
if (!deps.isTTY || !deps.promptLine) {
|
|
3729
4641
|
return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE, `The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} ` +
|
|
3730
|
-
"has been left untouched. Re-run install
|
|
4642
|
+
"has been left untouched. Re-run install on an interactive terminal to " +
|
|
3731
4643
|
"discard it and start a fresh signup.");
|
|
3732
4644
|
}
|
|
3733
4645
|
errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);
|
|
@@ -3860,8 +4772,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3860
4772
|
trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
|
|
3861
4773
|
}
|
|
3862
4774
|
const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
|
|
4775
|
+
// BAPI-708 (B-a): these files are the MCP SERVER REGISTRATION (BAPI_REPO_NAME,
|
|
4776
|
+
// BAPI_BASE_URL, BAPI_DOCS_DIR, BAPI_PROJECT_ROOT, and the version-pinned npx
|
|
4777
|
+
// launcher) — calling them a credentials file was simply inaccurate. Where the
|
|
4778
|
+
// credential actually landed is reported once, at Step 4.
|
|
3863
4779
|
for (const { relPath, mode } of writeResult.written) {
|
|
3864
|
-
log(mode === "secret-free"
|
|
4780
|
+
log(mode === "secret-free"
|
|
4781
|
+
? ` registered the Bridge MCP server in ${relPath} (secret-free — key resolved at runtime)`
|
|
4782
|
+
: ` registered the Bridge MCP server in ${relPath}`);
|
|
3865
4783
|
}
|
|
3866
4784
|
for (const { relPath } of writeResult.skipped) {
|
|
3867
4785
|
log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
|
|
@@ -3897,16 +4815,16 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3897
4815
|
log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
|
|
3898
4816
|
"global ~/.copilot/mcp-config.json — the two are configured separately.");
|
|
3899
4817
|
}
|
|
3900
|
-
// BAPI-
|
|
3901
|
-
// not
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
4818
|
+
// BAPI-708 (B-c): the Claude trust reminder that used to print here was
|
|
4819
|
+
// RELOCATED, not dropped — the reload requirement is genuinely actionable (the
|
|
4820
|
+
// server does not work until the host reloads), but at Step 3 it duplicated the
|
|
4821
|
+
// spawn handoff. It now survives on EVERY terminal path: the spawn handoff
|
|
4822
|
+
// already carries it, and `buildManualInstallBridgeContinuation("configured")`
|
|
4823
|
+
// carries it for the declined, non-TTY, and failed-spawn paths.
|
|
3906
4824
|
// BAPI-635 (Step 8 + Step 3): persist the secret-free install state, ignoring
|
|
3907
4825
|
// it before the write. Project-local paths only; global paths are never in it.
|
|
3908
4826
|
try {
|
|
3909
|
-
await ensureGitignoredShared(deps.cwd,
|
|
4827
|
+
await ensureGitignoredShared(deps.cwd, INSTALL_BRIDGE_INSTALL_STATE_PATH, gitignoreDeps);
|
|
3910
4828
|
// writeMcpInstallState catches its own I/O errors and returns { ok: false }
|
|
3911
4829
|
// (it does NOT throw), so the failure warning must inspect the return value —
|
|
3912
4830
|
// a try/catch alone would silently swallow a real persistence failure.
|
|
@@ -3939,7 +4857,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3939
4857
|
// resolve. Promotion removes the pending record and writes the credential in one
|
|
3940
4858
|
// durable replacement; on failure the pending record survives so a re-run replays
|
|
3941
4859
|
// the same redemption. The agent session is NOT spawned until this succeeds.
|
|
3942
|
-
|
|
4860
|
+
// BAPI-708 (Part D): the SHARED Step 4 label — identical on both arms, so a
|
|
4861
|
+
// successful run prints exactly one Step 4 line and never narrates promotion
|
|
4862
|
+
// as a step of its own. Still assigned before the log, preserving the
|
|
4863
|
+
// log == record invariant the failure attribution depends on.
|
|
4864
|
+
currentStep = INSTALL_BRIDGE_STEP_LABELS.persistCredential;
|
|
3943
4865
|
log(currentStep);
|
|
3944
4866
|
const promoted = await deps.promoteBootstrapPending({ repoName, inviteFingerprint, allowOverwriteExistingCredential: overwriteConsent }, credentialWriteDeps);
|
|
3945
4867
|
if (!promoted.ok) {
|
|
@@ -3949,7 +4871,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3949
4871
|
`(${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending ` +
|
|
3950
4872
|
`record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);
|
|
3951
4873
|
}
|
|
3952
|
-
log(
|
|
4874
|
+
log(formatProjectCredentialStoreLine(repoName, promoted.path));
|
|
3953
4875
|
}
|
|
3954
4876
|
else {
|
|
3955
4877
|
// ---- persist routing credential (non-blocking / fail-open) ----
|
|
@@ -3958,13 +4880,13 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3958
4880
|
try {
|
|
3959
4881
|
const result = await deps.upsertCredential(repoName, apiKey, credentialWriteDeps);
|
|
3960
4882
|
if (result.ok) {
|
|
3961
|
-
log(
|
|
4883
|
+
log(formatProjectCredentialStoreLine(repoName, result.path));
|
|
3962
4884
|
}
|
|
3963
4885
|
else {
|
|
3964
4886
|
log(` warning: could not persist the routing credential (${result.kind}). ` +
|
|
3965
4887
|
`start-tickets model routing may not resolve the key for bapi:${repoName} ` +
|
|
3966
4888
|
"and will fail open to the premium/Opus tier (the most expensive) — " +
|
|
3967
|
-
"set BAPI_API_KEY in the shell or re-run install
|
|
4889
|
+
"set BAPI_API_KEY in the shell or re-run install, then verify with " +
|
|
3968
4890
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
3969
4891
|
}
|
|
3970
4892
|
}
|
|
@@ -3977,6 +4899,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3977
4899
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
3978
4900
|
}
|
|
3979
4901
|
}
|
|
4902
|
+
// ---- commit-your-assets notice (BAPI-708, Part C) ----
|
|
4903
|
+
// Emitted AFTER the credential step and BEFORE the GitHub offer and the Step 5
|
|
4904
|
+
// consent/spawn block, so every terminal path — spawn, decline, non-TTY, and
|
|
4905
|
+
// spawn-failure alike — receives it exactly once. The exclusion set is computed
|
|
4906
|
+
// from what this run actually gitignored (the selected project host targets and
|
|
4907
|
+
// the install-state file), not a hand-copied literal, so a future host-registry
|
|
4908
|
+
// entry is excluded automatically. `credentialStorePath` is never a candidate.
|
|
4909
|
+
const commitAssetsNotice = buildCommitGeneratedAssetsNotice(scaffoldAssets.commitSafePaths, [
|
|
4910
|
+
...targets.map((t) => t.relPath),
|
|
4911
|
+
INSTALL_BRIDGE_INSTALL_STATE_PATH,
|
|
4912
|
+
]);
|
|
4913
|
+
if (commitAssetsNotice.length > 0) {
|
|
4914
|
+
log("");
|
|
4915
|
+
log(commitAssetsNotice);
|
|
4916
|
+
}
|
|
3980
4917
|
// ---- optional GitHub connect offer (BAPI-631) ----
|
|
3981
4918
|
// Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
|
|
3982
4919
|
// the agent spawn, for two reasons: the spawned session's capability report should
|
|
@@ -3992,8 +4929,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3992
4929
|
if (prewarmPromise) {
|
|
3993
4930
|
const prewarm = await prewarmPromise;
|
|
3994
4931
|
if (prewarm.ok) {
|
|
3995
|
-
|
|
3996
|
-
|
|
4932
|
+
// BAPI-708 (Part B): a SUCCESSFUL pre-warm is silent. Both lines described
|
|
4933
|
+
// an internal optimization that worked and a remedy for a problem that did
|
|
4934
|
+
// not occur. The failure branch below — where the remedy is real — keeps
|
|
4935
|
+
// the MCP_TIMEOUT guidance verbatim, and both dry-run previews still carry
|
|
4936
|
+
// it, so it stays discoverable exactly where it is actionable.
|
|
3997
4937
|
}
|
|
3998
4938
|
else {
|
|
3999
4939
|
// Escalate to a stronger, still secret-free warning when pre-warm didn't
|
|
@@ -4035,8 +4975,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4035
4975
|
// raw agent binary name — computed once so all three sentences agree.
|
|
4036
4976
|
const handoffToolLabel = toolLabelForLaunchAgent(finalAgentName);
|
|
4037
4977
|
log("");
|
|
4038
|
-
log(`install
|
|
4039
|
-
"configuration, presenting the concise capability report, and
|
|
4978
|
+
log(`install setup steps complete. A fresh ${handoffToolLabel} session is now applying ` +
|
|
4979
|
+
"configuration, presenting the concise capability report, and showing any next step the " +
|
|
4980
|
+
"server provides.");
|
|
4040
4981
|
// The trust dialog is the single most common place a spawned session stalls:
|
|
4041
4982
|
// the tab opens, nothing is approved, and the install looks hung.
|
|
4042
4983
|
log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} ` +
|