@bridge_gpt/mcp-server 0.2.26 → 0.2.28
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 +32 -24
- package/build/commands.generated.js +2 -2
- package/build/conductor/bridge-api-client.js +98 -1
- package/build/conductor/epic-reconcile.js +28 -0
- package/build/conductor/epic-runtime.js +28 -1
- package/build/conductor-bin.js +68 -9267
- package/build/conductor-claude-hook-bin.js +2 -172
- package/build/connect-github-api.js +9 -0
- package/build/connect-github.js +10 -0
- package/build/decision-page-template.js +2 -31
- package/build/doctor.js +2 -2
- package/build/env-flags.js +23 -0
- package/build/index.js +4699 -37043
- package/build/init.js +80 -56
- package/build/install-bridge.js +451 -153
- package/build/mcp-host-targets.js +12 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +198 -4
- package/build/start-tickets.js +25 -4
- package/build/tool-surface-gating.js +13 -2
- package/build/version.generated.js +1 -1
- package/package.json +3 -5
- package/design-assets/favicon/android-chrome-192x192.png +0 -0
- package/design-assets/favicon/android-chrome-512x512.png +0 -0
- package/design-assets/favicon/apple-touch-icon.png +0 -0
- package/design-assets/favicon/favicon-16x16.png +0 -0
- package/design-assets/favicon/favicon.ico +0 -0
- package/design-assets/favicon/site.webmanifest +0 -1
- package/public/css/main.min.css +0 -4971
- package/public/css/main.min.css.map +0 -1
- package/public/fonts/OFL.txt +0 -93
- package/public/fonts/SourceSansPro-Black.ttf +0 -0
- package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Bold.ttf +0 -0
- package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Italic.ttf +0 -0
- package/public/fonts/SourceSansPro-Light.ttf +0 -0
- package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Regular.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
- package/public/img/bridge-logo-160x51.webp +0 -0
- package/public/img/bridge-logo-300x92.webp +0 -0
- package/public/img/favicon/android-chrome-192x192.png +0 -0
- package/public/img/favicon/android-chrome-512x512.png +0 -0
- package/public/img/favicon/apple-touch-icon.png +0 -0
- package/public/img/favicon/favicon-16x16.png +0 -0
- package/public/img/favicon/favicon-32x32.png +0 -0
- package/public/img/favicon/favicon.ico +0 -0
- package/public/img/favicon/site.webmanifest +0 -1
- package/public/img/installation/bitbucket/app-password-1.png +0 -0
- package/public/img/installation/bitbucket/app-password-2.png +0 -0
- package/public/img/installation/bitbucket/create-token-1.png +0 -0
- package/public/img/installation/bitbucket/create-token-2.png +0 -0
- package/public/img/installation/bitbucket/webhook-1.png +0 -0
- package/public/img/installation/github/github-review-webhook.png +0 -0
- package/public/img/installation/jira/credentials/api-key.png +0 -0
- package/public/img/installation/jira/webhook/create-rule.png +0 -0
- package/public/img/installation/jira/webhook/project-settings.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
- package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
- package/public/img/installation/pinecone/pinecone-index.png +0 -0
- package/public/js/main.min.js +0 -10409
- package/public/js/main.min.js.map +0 -1
- package/smoke-test/SMOKE-TEST.md +0 -560
- package/smoke-test/smoke-test-mcp.md +0 -23
package/build/install-bridge.js
CHANGED
|
@@ -30,11 +30,14 @@
|
|
|
30
30
|
*
|
|
31
31
|
* then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
|
|
32
32
|
* remainder: `/install-bridge` config-field derivation → the read-after-write
|
|
33
|
-
* capability report → a
|
|
34
|
-
* chain `/learn-repository`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* concise capability report → a `/learn-repository` recommendation (BAPI-658).
|
|
34
|
+
* It does NOT chain into running `/learn-repository` itself — that stays the
|
|
35
|
+
* human's next explicit invocation — and it never asks about, decides, or
|
|
36
|
+
* claims anything about repository indexing: indexing is entirely automatic,
|
|
37
|
+
* gated server-side by the existing readiness funnel, with no prompt on any
|
|
38
|
+
* path. The fresh session is required because a CLI cannot force the running
|
|
39
|
+
* editor to reload the just-written `.mcp.json`, and field derivation needs an
|
|
40
|
+
* agent runtime the shell does not have.
|
|
38
41
|
*
|
|
39
42
|
* That spawn command embeds the entire agent prompt and runs to multiple KB, which
|
|
40
43
|
* no macOS terminal will accept as one typed line. So it is never typed: the full
|
|
@@ -88,7 +91,7 @@ import readline from "readline";
|
|
|
88
91
|
import { runInit, buildBridgeApiEntry } from "./init.js";
|
|
89
92
|
import { VERSION } from "./version.generated.js";
|
|
90
93
|
import { validateRepoName } from "./bridge-config.js";
|
|
91
|
-
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
94
|
+
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
92
95
|
import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
|
|
93
96
|
import { writeMcpInstallState } from "./mcp-install-state.js";
|
|
94
97
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
@@ -98,7 +101,7 @@ import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPe
|
|
|
98
101
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
99
102
|
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
100
103
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
101
|
-
import {
|
|
104
|
+
import { resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
102
105
|
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
|
|
103
106
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
104
107
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
@@ -113,7 +116,11 @@ export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short c
|
|
|
113
116
|
"(the initial npx package resolution/download can exceed a short connect timeout).";
|
|
114
117
|
/** The exact, version-pinned launcher spec the pre-warm spawns (fast-exiting --version). */
|
|
115
118
|
export function buildPrewarmArgs() {
|
|
116
|
-
|
|
119
|
+
// `--prefer-offline`: the initial `@latest` bootstrap already populated the npm
|
|
120
|
+
// cache with the exact tarball this `@${VERSION}`-pinned warm needs, so resolve
|
|
121
|
+
// from cache and skip the redundant registry-metadata round-trip (falls back to
|
|
122
|
+
// the network on a genuine miss).
|
|
123
|
+
return ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
|
|
117
124
|
}
|
|
118
125
|
/** Secret-free preview of the pre-warm command (no env, no key). */
|
|
119
126
|
export function buildPrewarmCommandPreview() {
|
|
@@ -121,44 +128,37 @@ export function buildPrewarmCommandPreview() {
|
|
|
121
128
|
}
|
|
122
129
|
/**
|
|
123
130
|
* The natural-language prompt handed to the spawned agent session. It is
|
|
124
|
-
* CONFIGURE-ONLY: it derives and applies configuration, presents the
|
|
125
|
-
* report, and
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* that must appear AFTER the spawned session's capability report. Asking it via
|
|
132
|
-
* the parent's `deps.promptLine` would display the question before the report
|
|
133
|
-
* even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
|
|
134
|
-
* prompts (repository / overwrite / invite confirmation) and is intentionally
|
|
135
|
-
* NOT used for this post-report question.
|
|
131
|
+
* CONFIGURE-ONLY: it derives and applies configuration, presents the concise
|
|
132
|
+
* capability report, and closes with a `/learn-repository` recommendation
|
|
133
|
+
* (BAPI-658). It never chains straight into running /learn-repository itself —
|
|
134
|
+
* that remains the human's next explicit invocation — and it never asks about,
|
|
135
|
+
* decides, or claims anything about repository indexing: indexing is entirely
|
|
136
|
+
* automatic, gated by the existing readiness funnel
|
|
137
|
+
* (`maybe_auto_parse_if_ready`), with no prompt on any path.
|
|
136
138
|
*/
|
|
137
139
|
export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
|
|
138
|
-
"in the install-spawn context so it SKIPS its Stage 8 and Stage
|
|
139
|
-
"closing interaction is the
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"indexing
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
|
|
154
|
-
"continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
|
|
140
|
+
"in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers — this " +
|
|
141
|
+
"session's only closing interaction is the concise capability report and learn recommendation " +
|
|
142
|
+
"below). " +
|
|
143
|
+
"Complete the command's read-after-write concise capability report first: the single 'What Bridge " +
|
|
144
|
+
"can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it " +
|
|
145
|
+
"(Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's " +
|
|
146
|
+
"server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do " +
|
|
147
|
+
"not locally filter, count, or fall back to the complete tool_capabilities catalog. " +
|
|
148
|
+
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
149
|
+
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
150
|
+
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
151
|
+
"After the capability report, recommend /learn-repository as the next step: explain briefly that it " +
|
|
152
|
+
"learns repository-specific architecture, review, testing, correctness, and validation-manual " +
|
|
153
|
+
"configuration by researching the actual codebase. Do NOT run /learn-repository yourself — only " +
|
|
154
|
+
"recommend it; running it is the human's next explicit invocation. " +
|
|
155
155
|
"Never request, echo, or transport any credential — only ever direct the human to that " +
|
|
156
156
|
"integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
|
|
157
157
|
"always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
|
|
158
158
|
"Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
|
|
159
159
|
"End with an explicit summary line stating how many config fields the apply_install_manifest call " +
|
|
160
|
-
"applied (e.g. 'Applied 8 of 9 derived fields')
|
|
161
|
-
"
|
|
160
|
+
"applied (e.g. 'Applied 8 of 9 derived fields') — if 0 fields were applied, say so loudly and " +
|
|
161
|
+
"explain what is still pending.";
|
|
162
162
|
/** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
|
|
163
163
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
164
164
|
/** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
|
|
@@ -171,8 +171,11 @@ export function getInstallBridgeUsage() {
|
|
|
171
171
|
"",
|
|
172
172
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
173
173
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
174
|
-
"routing credential, then
|
|
175
|
-
"
|
|
174
|
+
"routing credential, then — on a TTY, only after a Y/N consent prompt — opens a",
|
|
175
|
+
"fresh session in your selected tool to derive the remaining config, present a",
|
|
176
|
+
"concise capability report, and recommend /learn-repository. Indexing is never",
|
|
177
|
+
"asked about — it starts automatically once the repository reaches full parse",
|
|
178
|
+
"readiness.",
|
|
176
179
|
"",
|
|
177
180
|
"Run it bare — `install-bridge` with no flags — in a terminal and it asks",
|
|
178
181
|
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
|
|
@@ -241,24 +244,35 @@ export function getInstallBridgeUsage() {
|
|
|
241
244
|
` ${HOST_PLATFORM_ORDER.join(", ")}.`,
|
|
242
245
|
" Both --tools=claude-code,codex and",
|
|
243
246
|
" --tools claude-code,codex are accepted. On an",
|
|
244
|
-
" interactive terminal WITHOUT this flag
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
" automatic set (Claude
|
|
249
|
-
" Cursor / Copilot VS Code)
|
|
250
|
-
"
|
|
247
|
+
" interactive terminal WITHOUT this flag the",
|
|
248
|
+
" checklist starts EMPTY — no tool is pre-selected",
|
|
249
|
+
" (not even Claude Code) and you must select at",
|
|
250
|
+
" least one. A non-interactive (non-TTY) run without",
|
|
251
|
+
" --tools writes the legacy automatic set (Claude",
|
|
252
|
+
" Code plus any detected Cursor / Copilot VS Code)",
|
|
253
|
+
" and never opens an agent terminal (it prints",
|
|
254
|
+
" manual continuation instead). --tools= (empty) is",
|
|
255
|
+
" an explicit empty selection: it writes nothing and",
|
|
256
|
+
" launches nothing.",
|
|
251
257
|
" --force Overwrite an existing real BAPI_API_KEY in a",
|
|
252
258
|
" host config (or in the credential store) without",
|
|
253
259
|
" prompting.",
|
|
254
260
|
" --dry-run Preview every step (scaffold targets, config",
|
|
255
261
|
" files + keys with the key REDACTED, ping",
|
|
256
|
-
" target, credential target,
|
|
257
|
-
" without writing, pinging,
|
|
258
|
-
" With --invite it
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
+
" target, credential target, and the consent-gated",
|
|
263
|
+
" launch outcome) without writing, pinging,",
|
|
264
|
+
" prompting, or spawning anything. With --invite it",
|
|
265
|
+
" also never calls the exchange endpoint and never",
|
|
266
|
+
" generates or stores a secret.",
|
|
267
|
+
" --agent claude|cursor-agent Explicit launch override for the post-install",
|
|
268
|
+
" session — it always wins, and there is NO Claude",
|
|
269
|
+
" default. Without it the launch tool is derived",
|
|
270
|
+
" from your selection: Claude Code opens `claude`,",
|
|
271
|
+
" Cursor opens `cursor-agent`; if several launchable",
|
|
272
|
+
" tools are selected the wizard asks which single one",
|
|
273
|
+
" to open; and a selection whose tools have no",
|
|
274
|
+
" agentic CLI (e.g. Copilot) opens nothing and prints",
|
|
275
|
+
" how to finish configuring later.",
|
|
262
276
|
" -h, --help Show this help.",
|
|
263
277
|
"",
|
|
264
278
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
@@ -284,7 +298,10 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
284
298
|
let repo;
|
|
285
299
|
let force = false;
|
|
286
300
|
let dryRun = false;
|
|
287
|
-
|
|
301
|
+
// Undefined until `--agent` is explicitly supplied — install-bridge never falls
|
|
302
|
+
// back to DEFAULT_AGENT_NAME (BAPI-657). An omitted agent is derived from the tool
|
|
303
|
+
// selection downstream; only an explicit `--agent` value lands here.
|
|
304
|
+
let agentName;
|
|
288
305
|
let invite;
|
|
289
306
|
let email;
|
|
290
307
|
let tools;
|
|
@@ -964,22 +981,26 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
|
|
|
964
981
|
/** Stable wording of the interactive tool-selection prompt (BAPI-635). */
|
|
965
982
|
export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
|
|
966
983
|
/**
|
|
967
|
-
* Interactive numbered multi-select prompt on stderr (TTY only). Displays
|
|
968
|
-
* option with a checked/unchecked marker seeded
|
|
969
|
-
* comma-separated list of numbers
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
984
|
+
* Interactive numbered multi-select prompt on stderr (TTY only). Displays a
|
|
985
|
+
* legend and usage line plus each option with a checked/unchecked marker seeded
|
|
986
|
+
* from `defaults`, accepts a comma-separated list of numbers that TOGGLE the rows
|
|
987
|
+
* and RE-PROMPT (acceptance is a separate bare Enter), and accepts only a NON-EMPTY
|
|
988
|
+
* selection on a bare Enter — an empty one prints "Select at least one tool." and
|
|
989
|
+
* re-prompts (AC-1/AC-3). Reprints on an invalid token rather than corrupting
|
|
990
|
+
* state. Resolves safely (to the current set, including an empty one) on EOF /
|
|
991
|
+
* synchronous close so a readline regression can never hang or discard the answer.
|
|
973
992
|
*/
|
|
974
993
|
export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
|
|
975
994
|
return new Promise((resolve) => {
|
|
976
995
|
const selected = new Set(defaults);
|
|
977
996
|
const render = () => {
|
|
978
997
|
output.write(`\n${promptText}\n`);
|
|
998
|
+
output.write("[x] = selected · [ ] = not selected\n");
|
|
979
999
|
options.forEach((opt, idx) => {
|
|
980
1000
|
const mark = selected.has(opt.id) ? "[x]" : "[ ]";
|
|
981
1001
|
output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
|
|
982
1002
|
});
|
|
1003
|
+
output.write("Type numbers to toggle, e.g. 1,3 — then Enter. Enter with ≥1 selected accepts.\n");
|
|
983
1004
|
output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
|
|
984
1005
|
};
|
|
985
1006
|
const rl = readline.createInterface({ input, output });
|
|
@@ -1000,6 +1021,14 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1000
1021
|
rl.question("", (answer) => {
|
|
1001
1022
|
const trimmed = answer.trim();
|
|
1002
1023
|
if (trimmed.length === 0) {
|
|
1024
|
+
// AC-3: a bare Enter accepts ONLY a non-empty selection; an empty one
|
|
1025
|
+
// re-prompts rather than resolving to nothing. EOF/close (handled above)
|
|
1026
|
+
// still resolves the current — possibly empty — set so input can never hang.
|
|
1027
|
+
if (selected.size === 0) {
|
|
1028
|
+
output.write("Select at least one tool.\n");
|
|
1029
|
+
ask();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1003
1032
|
finish();
|
|
1004
1033
|
return;
|
|
1005
1034
|
}
|
|
@@ -1026,7 +1055,9 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1026
1055
|
else
|
|
1027
1056
|
selected.add(opt.id);
|
|
1028
1057
|
}
|
|
1029
|
-
|
|
1058
|
+
// Toggling re-renders the updated checklist and re-prompts; acceptance is a
|
|
1059
|
+
// separate, deliberate bare Enter (AC-1/AC-3) rather than an implicit accept.
|
|
1060
|
+
ask();
|
|
1030
1061
|
});
|
|
1031
1062
|
};
|
|
1032
1063
|
ask();
|
|
@@ -1036,8 +1067,8 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1036
1067
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
1037
1068
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
1038
1069
|
* back to detection),
|
|
1039
|
-
* 2. interactive multi-select on a TTY (
|
|
1040
|
-
*
|
|
1070
|
+
* 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
|
|
1071
|
+
* starts empty and requires an explicit ≥1 selection),
|
|
1041
1072
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
1042
1073
|
* detected Copilot VS Code and Cursor automatic targets (never Codex or
|
|
1043
1074
|
* Copilot CLI just because a global directory exists).
|
|
@@ -1049,11 +1080,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1049
1080
|
}
|
|
1050
1081
|
const ctx = await buildDetectionContext(deps);
|
|
1051
1082
|
const detected = new Set(detectDefaultPlatforms(ctx));
|
|
1052
|
-
// 2. Interactive multi-select on a TTY.
|
|
1083
|
+
// 2. Interactive multi-select on a TTY. AC-2: no row is pre-selected — the
|
|
1084
|
+
// picker seeds an EMPTY default set (dropping the former forced Claude Code
|
|
1085
|
+
// inclusion and detected-platform seeding) and requires an explicit selection.
|
|
1053
1086
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
1054
1087
|
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
1055
|
-
|
|
1056
|
-
const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
|
|
1088
|
+
const defaults = [];
|
|
1057
1089
|
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
|
|
1058
1090
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
1059
1091
|
}
|
|
@@ -1065,6 +1097,146 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1065
1097
|
legacy.push("copilot-vscode");
|
|
1066
1098
|
return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
|
|
1067
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Resolve the post-install launch decision from the selected host platforms and an
|
|
1102
|
+
* OPTIONAL explicit `--agent` override (BAPI-657). Pure and total:
|
|
1103
|
+
*
|
|
1104
|
+
* - An explicit `--agent` is an ABSOLUTE override (ratified AC-4): a valid explicit
|
|
1105
|
+
* agent always returns `spawn(agent)`, regardless of which tools were selected —
|
|
1106
|
+
* the flag is honored even for a host outside the selection.
|
|
1107
|
+
* - Otherwise the selection drives it: map each selected host through
|
|
1108
|
+
* {@link agentForPlatform}, drop non-launchable hosts, dedup in registry order,
|
|
1109
|
+
* then return `manual("empty-selection")` for an empty selection,
|
|
1110
|
+
* `manual("no-launchable-agent")` for zero launchable agents, `spawn` for one, or
|
|
1111
|
+
* `choose-one` for more than one.
|
|
1112
|
+
*
|
|
1113
|
+
* Never prompts, logs, resolves an {@link AgentSpec}, or touches the filesystem.
|
|
1114
|
+
*/
|
|
1115
|
+
export function resolveInstallBridgeLaunchDecision(selectedPlatforms, explicitAgent) {
|
|
1116
|
+
if (explicitAgent) {
|
|
1117
|
+
return { kind: "spawn", agent: explicitAgent };
|
|
1118
|
+
}
|
|
1119
|
+
if (selectedPlatforms.length === 0) {
|
|
1120
|
+
return { kind: "manual", reason: "empty-selection" };
|
|
1121
|
+
}
|
|
1122
|
+
const agents = [];
|
|
1123
|
+
for (const id of HOST_PLATFORM_ORDER) {
|
|
1124
|
+
if (!selectedPlatforms.includes(id))
|
|
1125
|
+
continue;
|
|
1126
|
+
const agent = agentForPlatform(id);
|
|
1127
|
+
if (agent && !agents.includes(agent))
|
|
1128
|
+
agents.push(agent);
|
|
1129
|
+
}
|
|
1130
|
+
if (agents.length === 0)
|
|
1131
|
+
return { kind: "manual", reason: "no-launchable-agent" };
|
|
1132
|
+
if (agents.length === 1)
|
|
1133
|
+
return { kind: "spawn", agent: agents[0] };
|
|
1134
|
+
return { kind: "choose-one", agents };
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* The human-facing tool label for a launch agent, derived by scanning the host
|
|
1138
|
+
* registry's {@link HostTargetDefinition.launchAgent} metadata rather than keeping a
|
|
1139
|
+
* duplicate agent→label map (BAPI-657). Returns e.g. `"Claude Code"` for `"claude"`
|
|
1140
|
+
* and `"Cursor"` for `"cursor-agent"`, falling back to the raw agent name if no host
|
|
1141
|
+
* advertises it (never expected for a registered launch agent).
|
|
1142
|
+
*/
|
|
1143
|
+
export function toolLabelForLaunchAgent(agent) {
|
|
1144
|
+
const target = allHostTargets().find((t) => t.launchAgent === agent);
|
|
1145
|
+
return target?.label ?? agent;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
|
|
1149
|
+
* (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
|
|
1150
|
+
* label, asks for an exact numeric choice with NO default, validates the range, and
|
|
1151
|
+
* returns the chosen agent. Returns `null` — never a silent first-candidate default —
|
|
1152
|
+
* for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
|
|
1153
|
+
* input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
|
|
1154
|
+
* it must run only AFTER the multi-select promise has resolved and closed its readline
|
|
1155
|
+
* interface so two readers never contend for stdin.
|
|
1156
|
+
*/
|
|
1157
|
+
export async function chooseInstallBridgeLaunchAgent(agents, deps) {
|
|
1158
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
1159
|
+
return null;
|
|
1160
|
+
const promptLine = deps.promptLine;
|
|
1161
|
+
try {
|
|
1162
|
+
deps.log("");
|
|
1163
|
+
deps.log("More than one selected tool can host the configuration session:");
|
|
1164
|
+
agents.forEach((agent, i) => {
|
|
1165
|
+
deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
|
|
1166
|
+
});
|
|
1167
|
+
// No default: a stray Enter must not silently pick a tool.
|
|
1168
|
+
const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
|
|
1169
|
+
const index = Number(answer);
|
|
1170
|
+
if (!/^\d+$/.test(answer) ||
|
|
1171
|
+
!Number.isInteger(index) ||
|
|
1172
|
+
index < 1 ||
|
|
1173
|
+
index > agents.length) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
return agents[index - 1];
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// Never surface caught exception text; an unavailable chooser is a no-spawn.
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Shared, copy-pasteable "we didn't open a session for you" guidance so every
|
|
1185
|
+
* no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
|
|
1186
|
+
* consent decline, terminal-spawn failure) emits ONE consistent remediation
|
|
1187
|
+
* (AC-5 / AC-7). Two variants:
|
|
1188
|
+
*
|
|
1189
|
+
* - `"empty-selection"` — no MCP host config was written at all, so it does NOT tell
|
|
1190
|
+
* the user to run `/install-bridge` (there is no configured host to run it in); it
|
|
1191
|
+
* tells them to re-run `install-bridge` and select at least one tool.
|
|
1192
|
+
* - `"configured"` — at least one host WAS configured, so it tells the user to open
|
|
1193
|
+
* the project in an applicable AI coding tool and run `/install-bridge`, plus the
|
|
1194
|
+
* required notice that their Bridge MCP tools stay limited until configuration
|
|
1195
|
+
* completes.
|
|
1196
|
+
*
|
|
1197
|
+
* Condition-specific warning prefixes are added by the caller, never inside here.
|
|
1198
|
+
*/
|
|
1199
|
+
export function buildManualInstallBridgeContinuation(kind) {
|
|
1200
|
+
if (kind === "empty-selection") {
|
|
1201
|
+
return [
|
|
1202
|
+
"No AI coding tools were configured, so nothing was set up for this project.",
|
|
1203
|
+
"Re-run install-bridge and select at least one tool to configure it.",
|
|
1204
|
+
].join("\n");
|
|
1205
|
+
}
|
|
1206
|
+
return [
|
|
1207
|
+
"To finish configuring this project, open it in an AI coding tool that has the",
|
|
1208
|
+
"Bridge MCP server configured and run /install-bridge.",
|
|
1209
|
+
"Until the project is configured, your Bridge MCP tools stay limited.",
|
|
1210
|
+
].join("\n");
|
|
1211
|
+
}
|
|
1212
|
+
/** The exact consent-prompt prefix, shared with tests so the copy cannot drift. */
|
|
1213
|
+
export const INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX = "Bridge can configure and set up this project for you automatically. Open a ";
|
|
1214
|
+
/**
|
|
1215
|
+
* Ask for explicit TTY consent immediately before opening the selected tool (AC-7).
|
|
1216
|
+
* Returns `"spawn"` only when a live terminal user affirmatively agrees; returns
|
|
1217
|
+
* `"no-spawn"` WITHOUT prompting on a non-TTY run or a missing `promptLine` seam, and
|
|
1218
|
+
* on an `n`/`no` answer. Mirrors {@link offerGithubConnection}'s Y/N semantics: a
|
|
1219
|
+
* blank line, `y`, or `yes` accept; `n` or `no` decline. A prompt that throws safely
|
|
1220
|
+
* declines without surfacing the caught exception text. Uses only the per-call
|
|
1221
|
+
* `promptLine` seam, so it must run AFTER the tool-selection, chooser, and GitHub
|
|
1222
|
+
* flows have closed their readline interfaces — never while another prompt is active.
|
|
1223
|
+
*/
|
|
1224
|
+
export async function requestInstallBridgeLaunchConsent(toolLabel, deps) {
|
|
1225
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
1226
|
+
return "no-spawn";
|
|
1227
|
+
try {
|
|
1228
|
+
const answer = (await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `))
|
|
1229
|
+
.trim()
|
|
1230
|
+
.toLowerCase();
|
|
1231
|
+
if (answer === "n" || answer === "no")
|
|
1232
|
+
return "no-spawn";
|
|
1233
|
+
return "spawn";
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
// Never surface caught exception text; treat a prompt failure as a decline.
|
|
1237
|
+
return "no-spawn";
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1068
1240
|
/**
|
|
1069
1241
|
* Build a registry detection context from install deps. install-bridge has only
|
|
1070
1242
|
* async `stat`, but the registry's `detect` callbacks are synchronous, so we
|
|
@@ -1593,7 +1765,7 @@ export function buildDryRunPreview(plan) {
|
|
|
1593
1765
|
`Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
|
|
1594
1766
|
`Base URL (ping): ${plan.baseUrl}`,
|
|
1595
1767
|
`Docs dir: ${plan.docsDir}`,
|
|
1596
|
-
`Agent: ${plan.
|
|
1768
|
+
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
1597
1769
|
"",
|
|
1598
1770
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1599
1771
|
`Step 2 — connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,
|
|
@@ -1608,26 +1780,66 @@ export function buildDryRunPreview(plan) {
|
|
|
1608
1780
|
...buildLaunchStepPreview(plan),
|
|
1609
1781
|
];
|
|
1610
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* One-line description of the planned launch agent for the dry-run header (BAPI-657).
|
|
1785
|
+
* Never claims an unconditional Claude spawn: it reflects the discriminated launch
|
|
1786
|
+
* outcome — a single consent-gated tool, a choose-one among launchable tools, or a
|
|
1787
|
+
* no-automatic-launch manual outcome.
|
|
1788
|
+
*/
|
|
1789
|
+
function describePlannedLaunchAgent(launch) {
|
|
1790
|
+
if (launch.kind === "spawn") {
|
|
1791
|
+
return `${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) — opens only after Y/N consent`;
|
|
1792
|
+
}
|
|
1793
|
+
if (launch.kind === "choose-one") {
|
|
1794
|
+
const labels = launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
|
|
1795
|
+
return `choose one of ${labels} — the chosen tool opens only after Y/N consent`;
|
|
1796
|
+
}
|
|
1797
|
+
return launch.reason === "empty-selection"
|
|
1798
|
+
? "none — no tools selected"
|
|
1799
|
+
: "none — no launchable tool selected; manual continuation is printed";
|
|
1800
|
+
}
|
|
1611
1801
|
/**
|
|
1612
1802
|
* The Step 5 preview lines, shared by both previews so the launch description
|
|
1613
|
-
* cannot drift between the have-key and need-key flows.
|
|
1803
|
+
* cannot drift between the have-key and need-key flows (BAPI-657). The Step 5 body
|
|
1804
|
+
* is dispatched on the discriminated launch outcome: a consent-gated single-tool
|
|
1805
|
+
* spawn (with the full secret-free command shown), a consent-gated choose-one, or a
|
|
1806
|
+
* no-automatic-launch manual continuation — never an unconditional Claude spawn.
|
|
1614
1807
|
*
|
|
1615
|
-
* A --dry-run never writes the launch script,
|
|
1616
|
-
*
|
|
1617
|
-
* because the command is what the user is previewing and it is secret-free.
|
|
1808
|
+
* A --dry-run never writes the launch script, prompts for consent, or spawns, so
|
|
1809
|
+
* these lines describe the outcome rather than performing it.
|
|
1618
1810
|
*/
|
|
1619
1811
|
function buildLaunchStepPreview(plan) {
|
|
1620
|
-
|
|
1812
|
+
const githubLines = [
|
|
1621
1813
|
// BAPI-631: described, never performed in --dry-run — a preview must not open a
|
|
1622
1814
|
// browser or reach the network. It is also strictly optional, so it carries no step
|
|
1623
1815
|
// number of its own and never changes the 5-step count.
|
|
1624
1816
|
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
1625
1817
|
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
1626
1818
|
" interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1819
|
+
];
|
|
1820
|
+
if (plan.launch.kind === "spawn") {
|
|
1821
|
+
return [
|
|
1822
|
+
...githubLines,
|
|
1823
|
+
`Step 5 — agent session (${toolLabelForLaunchAgent(plan.launch.agent)}): on a TTY the wizard first asks`,
|
|
1824
|
+
" 'Open a … session to do that now? (Y/n)'; only on consent is the full command below",
|
|
1825
|
+
" stored in a restricted launch script (mode 0600, under the system temp dir) and a short",
|
|
1826
|
+
" sourced runner spawned (the script itself is NOT written in --dry-run):",
|
|
1827
|
+
` ${plan.launch.spawnCommand}`,
|
|
1828
|
+
];
|
|
1829
|
+
}
|
|
1830
|
+
if (plan.launch.kind === "choose-one") {
|
|
1831
|
+
const labels = plan.launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
|
|
1832
|
+
return [
|
|
1833
|
+
...githubLines,
|
|
1834
|
+
`Step 5 — agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`,
|
|
1835
|
+
" asks which single tool to open, then asks Y/N consent before spawning that one session.",
|
|
1836
|
+
];
|
|
1837
|
+
}
|
|
1838
|
+
return [
|
|
1839
|
+
...githubLines,
|
|
1840
|
+
plan.launch.reason === "empty-selection"
|
|
1841
|
+
? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
|
|
1842
|
+
: "Step 5 — no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
|
|
1631
1843
|
];
|
|
1632
1844
|
}
|
|
1633
1845
|
/**
|
|
@@ -1664,7 +1876,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
1664
1876
|
repoLine,
|
|
1665
1877
|
`Base URL: ${plan.baseUrl}`,
|
|
1666
1878
|
`Docs dir: ${plan.docsDir}`,
|
|
1667
|
-
`Agent: ${plan.
|
|
1879
|
+
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
1668
1880
|
"",
|
|
1669
1881
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1670
1882
|
...selfServeStep,
|
|
@@ -1875,33 +2087,58 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1875
2087
|
}
|
|
1876
2088
|
}
|
|
1877
2089
|
}
|
|
1878
|
-
const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
|
|
1879
|
-
const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
1880
2090
|
const credentialStorePath = getPrimaryCredentialStorePath({
|
|
1881
2091
|
env: deps.env,
|
|
1882
2092
|
homedir: deps.homedir,
|
|
1883
2093
|
});
|
|
1884
|
-
// BAPI-635: resolve the AI-coding-tool selection
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
//
|
|
2094
|
+
// BAPI-635: resolve the AI-coding-tool selection FIRST — BAPI-657 makes this
|
|
2095
|
+
// selection (not a hidden --agent default) drive which post-install session opens.
|
|
2096
|
+
// Explicit --tools, then the TTY multi-select, then the legacy non-TTY automatic
|
|
2097
|
+
// set. The selected project JSON targets drive the read-merge-write path; selected
|
|
2098
|
+
// global (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
|
|
1888
2099
|
// registry-driven emitter after the connectivity check.
|
|
1889
2100
|
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
1890
2101
|
const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
|
|
1891
2102
|
// Read-only detection (safe in dry-run) of global-config editors we can't write.
|
|
1892
2103
|
const manualEditors = await detectManualEditors(deps);
|
|
2104
|
+
// BAPI-657: the pure, selection-driven launch decision. An explicit --agent is an
|
|
2105
|
+
// absolute override (ratified AC-4); otherwise the selected tools map to an agent
|
|
2106
|
+
// (Claude Code → claude, Cursor → cursor-agent), yielding spawn / choose-one /
|
|
2107
|
+
// manual. Performs no prompting, resolveAgentSpec, or I/O.
|
|
2108
|
+
const launchDecision = resolveInstallBridgeLaunchDecision(selectedPlatforms, options.agentName);
|
|
2109
|
+
// Derive the plan's launch data. For a single-agent spawn we resolve the spec and
|
|
2110
|
+
// build the (secret-free) command now so the dry-run preview can show it; a registry
|
|
2111
|
+
// miss here is a hard error — install-bridge NEVER falls back to DEFAULT_AGENT_NAME.
|
|
2112
|
+
let planLaunch;
|
|
2113
|
+
if (launchDecision.kind === "spawn") {
|
|
2114
|
+
const spec = resolveAgentSpec(launchDecision.agent);
|
|
2115
|
+
if (!spec) {
|
|
2116
|
+
errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
|
|
2117
|
+
return 1;
|
|
2118
|
+
}
|
|
2119
|
+
planLaunch = {
|
|
2120
|
+
kind: "spawn",
|
|
2121
|
+
agent: launchDecision.agent,
|
|
2122
|
+
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
else if (launchDecision.kind === "choose-one") {
|
|
2126
|
+
planLaunch = { kind: "choose-one", agents: launchDecision.agents };
|
|
2127
|
+
}
|
|
2128
|
+
else {
|
|
2129
|
+
planLaunch = { kind: "manual", reason: launchDecision.reason };
|
|
2130
|
+
}
|
|
1893
2131
|
const plan = {
|
|
1894
2132
|
repoName,
|
|
1895
2133
|
baseUrl,
|
|
1896
2134
|
docsDir,
|
|
1897
|
-
|
|
2135
|
+
launch: planLaunch,
|
|
1898
2136
|
configTargets: targets.map((t) => t.relPath),
|
|
1899
2137
|
manualEditors: manualEditorNames(manualEditors),
|
|
1900
2138
|
credentialTarget: `bapi:${repoName}`,
|
|
1901
2139
|
credentialStorePath,
|
|
1902
2140
|
pingUrl: buildPingUrl(baseUrl, repoName),
|
|
1903
2141
|
prewarmCommand: buildPrewarmCommandPreview(),
|
|
1904
|
-
spawnCommand,
|
|
1905
2142
|
...(bootstrapInviteMode
|
|
1906
2143
|
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
1907
2144
|
: {}),
|
|
@@ -1917,30 +2154,71 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1917
2154
|
log(line);
|
|
1918
2155
|
return 0;
|
|
1919
2156
|
}
|
|
1920
|
-
// ----
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
//
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
// warning: the deterministic setup would otherwise complete and leave the user
|
|
1928
|
-
// in the "configured but never configured by the agent" state the warning below
|
|
1929
|
-
// explicitly calls out. Failing first means nothing is half-done.
|
|
1930
|
-
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
|
|
1931
|
-
if (!materialized.ok) {
|
|
1932
|
-
errorLog(`Error: ${materialized.error}`);
|
|
1933
|
-
return 1;
|
|
2157
|
+
// ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
|
|
2158
|
+
// AC-3: with zero configured tools there is nothing to write and no session to open.
|
|
2159
|
+
// Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
|
|
2160
|
+
// bare `--tools=` (or an EOF-resolved picker) cleanly does nothing but guide a re-run.
|
|
2161
|
+
if (planLaunch.kind === "manual" && planLaunch.reason === "empty-selection") {
|
|
2162
|
+
log(buildManualInstallBridgeContinuation("empty-selection"));
|
|
2163
|
+
return 0;
|
|
1934
2164
|
}
|
|
1935
|
-
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2165
|
+
// ---- Finalize the launch agent for spawn-capable decisions ----
|
|
2166
|
+
// choose-one: pick exactly one via the numbered chooser (TTY only); a non-TTY run or
|
|
2167
|
+
// a failed/aborted chooser degrades to the manual continuation (finalAgentName stays
|
|
2168
|
+
// null). This runs AFTER the multi-select readline closed and BEFORE the GitHub /
|
|
2169
|
+
// consent flows, so no two stdin readers ever overlap.
|
|
2170
|
+
let finalAgentName = null;
|
|
2171
|
+
let finalSpawnCommand = null;
|
|
2172
|
+
if (planLaunch.kind === "spawn") {
|
|
2173
|
+
finalAgentName = planLaunch.agent;
|
|
2174
|
+
finalSpawnCommand = planLaunch.spawnCommand;
|
|
2175
|
+
}
|
|
2176
|
+
else if (planLaunch.kind === "choose-one") {
|
|
2177
|
+
const chosen = await chooseInstallBridgeLaunchAgent(planLaunch.agents, deps);
|
|
2178
|
+
if (chosen) {
|
|
2179
|
+
const spec = resolveAgentSpec(chosen);
|
|
2180
|
+
if (!spec) {
|
|
2181
|
+
errorLog(`Error: no launch agent is registered for '${chosen}'.`);
|
|
2182
|
+
return 1;
|
|
2183
|
+
}
|
|
2184
|
+
finalAgentName = chosen;
|
|
2185
|
+
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
2186
|
+
}
|
|
2187
|
+
// chosen === null → non-TTY / unavailable / invalid → manual continuation below.
|
|
2188
|
+
}
|
|
2189
|
+
// planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
|
|
2190
|
+
// ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
|
|
2191
|
+
// spawn-capable path. A manual/no-launchable/chooser-declined path opens no session,
|
|
2192
|
+
// so it neither materializes a launch script nor warms a bucket it will never use.
|
|
2193
|
+
//
|
|
2194
|
+
// Materialization runs HERE, before the scaffold/mint/credential/config writes,
|
|
2195
|
+
// because a command that cannot be launched safely is a whole-run failure, not a
|
|
2196
|
+
// Step 5 warning: the deterministic setup would otherwise complete and leave the user
|
|
2197
|
+
// in the "configured but never configured by the agent" state. Failing first means
|
|
2198
|
+
// nothing is half-done. The @VERSION-pinned launcher lives in a DIFFERENT _npx bucket
|
|
2199
|
+
// than the @latest bucket this invocation warmed, so the pre-warm (fire-and-forget,
|
|
2200
|
+
// strictly fail-open) overlaps the ping/exchange/scaffold/writes and is awaited just
|
|
2201
|
+
// before the consent-gated spawn.
|
|
2202
|
+
let launchCommand = null;
|
|
2203
|
+
let prewarmPromise = null;
|
|
2204
|
+
if (finalAgentName && finalSpawnCommand) {
|
|
2205
|
+
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", finalSpawnCommand);
|
|
2206
|
+
if (!materialized.ok) {
|
|
2207
|
+
errorLog(`Error: ${materialized.error}`);
|
|
2208
|
+
return 1;
|
|
2209
|
+
}
|
|
2210
|
+
launchCommand = materialized.command;
|
|
2211
|
+
// Independent final guard on the line ACTUALLY handed to the terminal — not on the
|
|
2212
|
+
// original command. It catches the two ways a "successful" materialization can still
|
|
2213
|
+
// be unlaunchable: no writer seam (inline command preserved verbatim), and a runner
|
|
2214
|
+
// whose script path is unexpectedly long.
|
|
2215
|
+
if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
|
|
2216
|
+
errorLog("Error: the agent session command is too long to send to the terminal safely. " +
|
|
2217
|
+
"Check that the system temporary directory is writable so the launch script can be used.");
|
|
2218
|
+
return 1;
|
|
2219
|
+
}
|
|
2220
|
+
log(" pre-warming the version-pinned launcher bucket (in the background)…");
|
|
2221
|
+
prewarmPromise = deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
|
|
1944
2222
|
}
|
|
1945
2223
|
const credentialWriteDeps = {
|
|
1946
2224
|
env: deps.env,
|
|
@@ -2238,25 +2516,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2238
2516
|
// ensureGitignored for the state file can still throw — also advisory.
|
|
2239
2517
|
errorLog("Warning: could not persist the install-state file (non-fatal).");
|
|
2240
2518
|
}
|
|
2241
|
-
// ---- Step 3b — pre-warm the @${VERSION}-pinned _npx bucket (BAPI-451 W3) ----
|
|
2242
|
-
// The launcher just written is pinned to @${VERSION}, a DIFFERENT _npx bucket
|
|
2243
|
-
// than the @latest bucket this `npx … install-bridge` invocation warmed. Spawn
|
|
2244
|
-
// the exact pinned launcher once with the fast-exiting `--version` flag so the
|
|
2245
|
-
// first real MCP launch resolves from a warm bucket instead of paying the cold
|
|
2246
|
-
// install inline (which can exceed the client's connect deadline). Strictly
|
|
2247
|
-
// fail-open: a failure warns (secret-free) but never fails the install.
|
|
2248
|
-
log(" pre-warming the version-pinned launcher bucket…");
|
|
2249
|
-
const prewarm = await deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
|
|
2250
|
-
if (prewarm.ok) {
|
|
2251
|
-
log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
|
|
2252
|
-
log(` ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2253
|
-
}
|
|
2254
|
-
else {
|
|
2255
|
-
// Escalate to a stronger, still secret-free warning when pre-warm didn't
|
|
2256
|
-
// complete cleanly — but the install itself still succeeds (exit unaffected).
|
|
2257
|
-
errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
|
|
2258
|
-
`The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2259
|
-
}
|
|
2260
2519
|
// ---- Step 4 — persist the credential ----
|
|
2261
2520
|
if (bootstrapInviteMode) {
|
|
2262
2521
|
// FAIL-CLOSED (unlike normal mode below). The pending secret IS the minted admin
|
|
@@ -2306,32 +2565,71 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2306
2565
|
// observe GitHub as configured if the user connects it here, and running it after the
|
|
2307
2566
|
// spawn would put two prompts on the same terminal at once.
|
|
2308
2567
|
await offerGithubConnection(repoName, deps, log);
|
|
2309
|
-
// ---- Step
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
//
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2568
|
+
// ---- Step 3b (await) — settle the background pre-warm before the session opens ----
|
|
2569
|
+
// The warm was kicked off (only on a spawn-capable path) right after the
|
|
2570
|
+
// launch-command guards; await it HERE so the bucket is settled before the spawned
|
|
2571
|
+
// session's first MCP launch, but AFTER all the work it overlapped with — and BEFORE
|
|
2572
|
+
// the consent prompt, so no spawn ever races an in-flight warm. Strictly fail-open —
|
|
2573
|
+
// the exit code is never affected. Skipped entirely when no session will be opened.
|
|
2574
|
+
if (prewarmPromise) {
|
|
2575
|
+
const prewarm = await prewarmPromise;
|
|
2576
|
+
if (prewarm.ok) {
|
|
2577
|
+
log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
|
|
2578
|
+
log(` ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
// Escalate to a stronger, still secret-free warning when pre-warm didn't
|
|
2582
|
+
// complete cleanly — but the install itself still succeeds (exit unaffected).
|
|
2583
|
+
errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
|
|
2584
|
+
`The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
// ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
|
|
2588
|
+
// AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
|
|
2589
|
+
// resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
|
|
2590
|
+
// selection, a non-TTY run, or a declined/aborted chooser all skip the spawn and fall
|
|
2591
|
+
// through to the shared /install-bridge continuation + limited-tools notice below — so
|
|
2592
|
+
// every no-session outcome gets identical, accurate remediation.
|
|
2593
|
+
if (finalAgentName && launchCommand) {
|
|
2594
|
+
const consent = await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName), deps);
|
|
2595
|
+
if (consent === "spawn") {
|
|
2596
|
+
log(`Step 5/5 — opening a ${finalAgentName} session for /install-bridge configuration + concise capability report…`);
|
|
2597
|
+
const terminal = detectTerminal(undefined, deps.env);
|
|
2598
|
+
// Only the validated short runner reaches the terminal — never the inline prompt.
|
|
2599
|
+
// The branded "Bridge Install" title labels the tab/badge instead of "install
|
|
2600
|
+
// Implementation" (AC-6).
|
|
2601
|
+
const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
|
|
2602
|
+
key: "install",
|
|
2603
|
+
worktreePath: deps.cwd,
|
|
2604
|
+
title: "Bridge Install",
|
|
2605
|
+
});
|
|
2606
|
+
if (!spawnResult.ok) {
|
|
2607
|
+
// Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
|
|
2608
|
+
// succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
|
|
2609
|
+
// deterministic setup is complete, so this is a non-fatal warning (exit 0), with
|
|
2610
|
+
// the same shared continuation so the remediation is consistent.
|
|
2611
|
+
errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`);
|
|
2612
|
+
log(buildManualInstallBridgeContinuation("configured"));
|
|
2613
|
+
return 0;
|
|
2614
|
+
}
|
|
2615
|
+
log("");
|
|
2616
|
+
log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying ` +
|
|
2617
|
+
"configuration, presenting the concise capability report, and recommending /learn-repository.");
|
|
2618
|
+
log("NOTE: the install is not finished until that session's apply reports applied fields — " +
|
|
2619
|
+
"it will pause to ask you to approve the project description. Indexing starts automatically " +
|
|
2620
|
+
"once the repository reaches full parse readiness — there is no indexing question to answer. " +
|
|
2621
|
+
"Verify afterwards on the project's Get Started page (install status panel) or via the " +
|
|
2622
|
+
"session's 'Applied N of M' summary.");
|
|
2623
|
+
return 0;
|
|
2624
|
+
}
|
|
2625
|
+
// Declined (or non-TTY suppression): the deterministic setup is complete and durable.
|
|
2626
|
+
log(buildManualInstallBridgeContinuation("configured"));
|
|
2327
2627
|
return 0;
|
|
2328
2628
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
"'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
|
|
2335
|
-
"(install status panel) or via the session's 'Applied N of M' summary.");
|
|
2629
|
+
// No launchable session (a no-launchable selection, or a non-TTY / declined chooser):
|
|
2630
|
+
// finalAgentName is null, so nothing was materialized. The configured hosts are
|
|
2631
|
+
// durable — print the shared continuation + limited-tools note (never the
|
|
2632
|
+
// empty-selection variant, which already returned above).
|
|
2633
|
+
log(buildManualInstallBridgeContinuation("configured"));
|
|
2336
2634
|
return 0;
|
|
2337
2635
|
}
|