@bridge_gpt/mcp-server 0.2.25 → 0.2.27

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.
Files changed (73) hide show
  1. package/README.md +59 -9
  2. package/build/agents.generated.js +1 -1
  3. package/build/bridge-api-urls.js +31 -0
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor-bin.js +68 -9267
  6. package/build/conductor-bundle-artifacts.js +802 -0
  7. package/build/conductor-bundle-cli.js +256 -0
  8. package/build/conductor-claude-hook-bin.js +2 -172
  9. package/build/decision-page-template.js +2 -31
  10. package/build/docs.generated.js +2 -1
  11. package/build/doctor.js +148 -1
  12. package/build/env-flags.js +31 -0
  13. package/build/index.js +4659 -34836
  14. package/build/init.js +87 -59
  15. package/build/install-bridge.js +380 -22
  16. package/build/mcp-host-config.js +521 -0
  17. package/build/mcp-host-targets.js +194 -0
  18. package/build/mcp-install-state.js +175 -0
  19. package/build/pipelines.generated.js +5 -4
  20. package/build/readme.generated.js +1 -1
  21. package/build/start-tickets.js +118 -5
  22. package/build/tool-surface-gating.js +396 -0
  23. package/build/version.generated.js +1 -1
  24. package/docs/install/mcp-tool-integrations.md +2 -2
  25. package/package.json +5 -7
  26. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  27. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  28. package/design-assets/favicon/apple-touch-icon.png +0 -0
  29. package/design-assets/favicon/favicon-16x16.png +0 -0
  30. package/design-assets/favicon/favicon.ico +0 -0
  31. package/design-assets/favicon/site.webmanifest +0 -1
  32. package/public/css/main.min.css +0 -4971
  33. package/public/css/main.min.css.map +0 -1
  34. package/public/fonts/OFL.txt +0 -93
  35. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  36. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  37. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  38. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  39. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  40. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  41. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  42. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  43. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  44. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  45. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  46. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  47. package/public/img/bridge-logo-160x51.webp +0 -0
  48. package/public/img/bridge-logo-300x92.webp +0 -0
  49. package/public/img/favicon/android-chrome-192x192.png +0 -0
  50. package/public/img/favicon/android-chrome-512x512.png +0 -0
  51. package/public/img/favicon/apple-touch-icon.png +0 -0
  52. package/public/img/favicon/favicon-16x16.png +0 -0
  53. package/public/img/favicon/favicon-32x32.png +0 -0
  54. package/public/img/favicon/favicon.ico +0 -0
  55. package/public/img/favicon/site.webmanifest +0 -1
  56. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  57. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  58. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  59. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  60. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  61. package/public/img/installation/github/github-review-webhook.png +0 -0
  62. package/public/img/installation/jira/credentials/api-key.png +0 -0
  63. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  64. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  65. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  66. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  67. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  68. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  69. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  70. package/public/js/main.min.js +0 -10427
  71. package/public/js/main.min.js.map +0 -1
  72. package/smoke-test/SMOKE-TEST.md +0 -560
  73. package/smoke-test/smoke-test-mcp.md +0 -23
@@ -88,6 +88,10 @@ import readline from "readline";
88
88
  import { runInit, buildBridgeApiEntry } from "./init.js";
89
89
  import { VERSION } from "./version.generated.js";
90
90
  import { validateRepoName } from "./bridge-config.js";
91
+ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
92
+ import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
93
+ import { writeMcpInstallState } from "./mcp-install-state.js";
94
+ import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
91
95
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
92
96
  import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, resolveBapiCredentials, } from "./credential-store.js";
93
97
  // BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
@@ -109,7 +113,11 @@ export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short c
109
113
  "(the initial npx package resolution/download can exceed a short connect timeout).";
110
114
  /** The exact, version-pinned launcher spec the pre-warm spawns (fast-exiting --version). */
111
115
  export function buildPrewarmArgs() {
112
- return ["-y", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
116
+ // `--prefer-offline`: the initial `@latest` bootstrap already populated the npm
117
+ // cache with the exact tarball this `@${VERSION}`-pinned warm needs, so resolve
118
+ // from cache and skip the redundant registry-metadata round-trip (falls back to
119
+ // the network on a genuine miss).
120
+ return ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
113
121
  }
114
122
  /** Secret-free preview of the pre-warm command (no env, no key). */
115
123
  export function buildPrewarmCommandPreview() {
@@ -232,6 +240,18 @@ export function getInstallBridgeUsage() {
232
240
  " THE TOKEN to your shell history and to the process list.",
233
241
  "",
234
242
  "Flags:",
243
+ " --tools <ids> Comma-separated AI-coding tools to configure,",
244
+ " bypassing the interactive picker. Accepted ids:",
245
+ ` ${HOST_PLATFORM_ORDER.join(", ")}.`,
246
+ " Both --tools=claude-code,codex and",
247
+ " --tools claude-code,codex are accepted. On an",
248
+ " interactive terminal WITHOUT this flag you are",
249
+ " asked which tools you use (Claude Code plus any",
250
+ " detected editors are pre-checked). A non-",
251
+ " interactive run without --tools writes the legacy",
252
+ " automatic set (Claude Code plus any detected",
253
+ " Cursor / Copilot VS Code). --tools= (empty) is an",
254
+ " explicit empty selection and writes nothing.",
235
255
  " --force Overwrite an existing real BAPI_API_KEY in a",
236
256
  " host config (or in the credential store) without",
237
257
  " prompting.",
@@ -271,6 +291,7 @@ export function parseInstallBridgeArgs(argv) {
271
291
  let agentName = DEFAULT_AGENT_NAME;
272
292
  let invite;
273
293
  let email;
294
+ let tools;
274
295
  // Track SUPPLIED-ness separately from the values: `--invite` is legitimately
275
296
  // valueless (prompt path) and `--api-key ""` is still a contradiction with it.
276
297
  let inviteSupplied = false;
@@ -345,6 +366,17 @@ export function parseInstallBridgeArgs(argv) {
345
366
  i = r.nextIndex;
346
367
  continue;
347
368
  }
369
+ if (arg === "--tools" || arg.startsWith("--tools=")) {
370
+ const r = readValue(arg, "--tools", i);
371
+ if ("error" in r)
372
+ return { status: "error", message: r.error };
373
+ const parsed = parseToolsSelection(r.value);
374
+ if ("error" in parsed)
375
+ return { status: "error", message: parsed.error };
376
+ tools = parsed.tools;
377
+ i = r.nextIndex;
378
+ continue;
379
+ }
348
380
  if (arg === "--agent" || arg.startsWith("--agent=")) {
349
381
  const r = readValue(arg, "--agent", i);
350
382
  if ("error" in r)
@@ -395,9 +427,36 @@ export function parseInstallBridgeArgs(argv) {
395
427
  }
396
428
  return {
397
429
  status: "ok",
398
- options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email },
430
+ options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools },
399
431
  };
400
432
  }
433
+ /**
434
+ * Parse a `--tools` value into a validated platform-ID selection. A blank value
435
+ * (`--tools=`) is an EXPLICIT empty selection (`[]`), not an error. Comma-split
436
+ * IDs are trimmed, deduped in registry order, and each is validated against the
437
+ * host registry allowlist so an unvalidated string can never choose a path or
438
+ * command. Unknown IDs are rejected by name (the value is safe to echo — it is
439
+ * a platform ID, never a secret).
440
+ */
441
+ export function parseToolsSelection(value) {
442
+ const raw = value
443
+ .split(",")
444
+ .map((s) => s.trim())
445
+ .filter((s) => s.length > 0);
446
+ const seen = new Set();
447
+ for (const id of raw) {
448
+ if (!isHostPlatformId(id)) {
449
+ const allowed = HOST_PLATFORM_ORDER.join(", ");
450
+ return {
451
+ error: `Invalid --tools value: '${id}' (allowed tools: ${allowed}).`,
452
+ };
453
+ }
454
+ seen.add(id);
455
+ }
456
+ // Dedupe + deterministic registry order.
457
+ const tools = HOST_PLATFORM_ORDER.filter((id) => seen.has(id));
458
+ return { tools };
459
+ }
401
460
  /**
402
461
  * No-echo secret prompt on stderr (so it never lands in piped stdout).
403
462
  *
@@ -620,6 +679,8 @@ export function createDefaultInstallBridgeDeps() {
620
679
  randomBytes: (size) => cryptoRandomBytes(size),
621
680
  promptSecret: isTTY ? promptSecretViaReadline : undefined,
622
681
  promptLine: isTTY ? promptLineViaReadline : undefined,
682
+ promptMultiSelect: isTTY ? promptMultiSelectViaReadline : undefined,
683
+ vendor: createDefaultVendorProcessDeps(spawn),
623
684
  fetch: productionFetch,
624
685
  resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
625
686
  spawnPrewarm: spawnPrewarmDefault,
@@ -900,6 +961,164 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
900
961
  }
901
962
  return { ok: false, error: "No repo name provided." };
902
963
  }
964
+ // ---------------------------------------------------------------------------
965
+ // Per-host config write (Step 2)
966
+ // ---------------------------------------------------------------------------
967
+ /** A per-host MCP config target (mirrors runInit's configTargets shape). */
968
+ /** Stable wording of the interactive tool-selection prompt (BAPI-635). */
969
+ export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
970
+ /**
971
+ * Interactive numbered multi-select prompt on stderr (TTY only). Displays each
972
+ * option with a checked/unchecked marker seeded from `defaults`, accepts a
973
+ * comma-separated list of numbers to TOGGLE, and accepts the current selection on
974
+ * a bare Enter. Reprints on an invalid token rather than corrupting state.
975
+ * Resolves safely (to the seeded defaults) on EOF / synchronous close so a
976
+ * readline regression can never hang or discard the answer.
977
+ */
978
+ export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
979
+ return new Promise((resolve) => {
980
+ const selected = new Set(defaults);
981
+ const render = () => {
982
+ output.write(`\n${promptText}\n`);
983
+ options.forEach((opt, idx) => {
984
+ const mark = selected.has(opt.id) ? "[x]" : "[ ]";
985
+ output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
986
+ });
987
+ output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
988
+ };
989
+ const rl = readline.createInterface({ input, output });
990
+ let answered = false;
991
+ const finish = () => {
992
+ answered = true;
993
+ rl.close();
994
+ resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
995
+ };
996
+ // EOF / synchronous close must resolve rather than deadlock the top-level
997
+ // await; `answered` guards the synchronous close from discarding a real answer.
998
+ rl.on("close", () => {
999
+ if (!answered)
1000
+ resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
1001
+ });
1002
+ const ask = () => {
1003
+ render();
1004
+ rl.question("", (answer) => {
1005
+ const trimmed = answer.trim();
1006
+ if (trimmed.length === 0) {
1007
+ finish();
1008
+ return;
1009
+ }
1010
+ const tokens = trimmed.split(",").map((t) => t.trim());
1011
+ const nums = [];
1012
+ let bad = false;
1013
+ for (const tok of tokens) {
1014
+ const n = Number(tok);
1015
+ if (!Number.isInteger(n) || n < 1 || n > options.length) {
1016
+ bad = true;
1017
+ break;
1018
+ }
1019
+ nums.push(n);
1020
+ }
1021
+ if (bad) {
1022
+ output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.\n`);
1023
+ ask();
1024
+ return;
1025
+ }
1026
+ for (const n of nums) {
1027
+ const opt = options[n - 1];
1028
+ if (selected.has(opt.id))
1029
+ selected.delete(opt.id);
1030
+ else
1031
+ selected.add(opt.id);
1032
+ }
1033
+ finish();
1034
+ });
1035
+ };
1036
+ ask();
1037
+ });
1038
+ }
1039
+ /**
1040
+ * Resolve the selected host platforms with strict precedence (BAPI-635):
1041
+ * 1. explicit `--tools` (including an explicit EMPTY selection — never falls
1042
+ * back to detection),
1043
+ * 2. interactive multi-select on a TTY (seeded from registry detection, with
1044
+ * Claude Code always checked as a default),
1045
+ * 3. the legacy non-TTY automatic set: Claude Code plus only the currently
1046
+ * detected Copilot VS Code and Cursor automatic targets (never Codex or
1047
+ * Copilot CLI just because a global directory exists).
1048
+ */
1049
+ export async function resolveSelectedHostPlatforms(deps, options) {
1050
+ // 1. Explicit --tools (empty array is an explicit empty selection).
1051
+ if (options.tools !== undefined) {
1052
+ return options.tools;
1053
+ }
1054
+ const ctx = await buildDetectionContext(deps);
1055
+ const detected = new Set(detectDefaultPlatforms(ctx));
1056
+ // 2. Interactive multi-select on a TTY.
1057
+ if (deps.isTTY && deps.promptMultiSelect) {
1058
+ const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
1059
+ // Claude Code is always a checked default; add every detected platform.
1060
+ const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
1061
+ const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
1062
+ return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
1063
+ }
1064
+ // 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
1065
+ const legacy = ["claude-code"];
1066
+ if (detected.has("cursor"))
1067
+ legacy.push("cursor");
1068
+ if (detected.has("copilot-vscode"))
1069
+ legacy.push("copilot-vscode");
1070
+ return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
1071
+ }
1072
+ /**
1073
+ * Build a registry detection context from install deps. install-bridge has only
1074
+ * async `stat`, but the registry's `detect` callbacks are synchronous, so we
1075
+ * pre-probe the candidate marker paths (the same ones the registry consults) and
1076
+ * expose them through a synchronous `exists` set. Paths are built in the registry's
1077
+ * POSIX-join form so the lookup matches exactly what `detect` passes to `exists`.
1078
+ */
1079
+ async function buildDetectionContext(deps) {
1080
+ const cwd = deps.cwd;
1081
+ const homedir = deps.homedir();
1082
+ const posixJoin = (base, rel) => `${base.endsWith("/") ? base.slice(0, -1) : base}/${rel}`;
1083
+ const candidates = [
1084
+ posixJoin(cwd, ".cursor"),
1085
+ posixJoin(cwd, ".vscode"),
1086
+ posixJoin(cwd, ".windsurf"),
1087
+ posixJoin(cwd, ".windsurfrules"),
1088
+ posixJoin(homedir, ".codex"),
1089
+ ];
1090
+ const present = new Set();
1091
+ await Promise.all(candidates.map(async (p) => {
1092
+ try {
1093
+ await deps.stat(p);
1094
+ present.add(p);
1095
+ }
1096
+ catch {
1097
+ // absent — leave out of the set.
1098
+ }
1099
+ }));
1100
+ return {
1101
+ cwd,
1102
+ homedir,
1103
+ env: deps.env,
1104
+ exists: (p) => present.has(p),
1105
+ };
1106
+ }
1107
+ /**
1108
+ * Resolve which project-local JSON host configs to write for the selected
1109
+ * platforms. Only the project-scoped JSON targets (Claude Code, Cursor, Copilot
1110
+ * VS Code) are returned here; global targets (Codex, Copilot CLI) and manual
1111
+ * targets (Windsurf) are handled by the registry-driven emitter in
1112
+ * `runInstallBridgeCli`. The returned shape is unchanged so the existing
1113
+ * read-merge-write path and overwrite-consent detection are preserved.
1114
+ */
1115
+ function hostConfigTargetsForPlatforms(platforms) {
1116
+ const set = new Set(platforms);
1117
+ return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
1118
+ .map((id) => MCP_HOST_TARGETS[id])
1119
+ .filter((t) => t.scope === "project" && t.format === "json")
1120
+ .map((t) => ({ relPath: t.relPath, topLevelKey: t.topLevelKey }));
1121
+ }
903
1122
  /** Resolve which project-local host configs to write, mirroring runInit detection. */
904
1123
  async function resolveHostConfigTargets(deps) {
905
1124
  const targets = [
@@ -1001,6 +1220,58 @@ async function writeHostConfigs(deps, targets, entry) {
1001
1220
  }
1002
1221
  return written;
1003
1222
  }
1223
+ /**
1224
+ * Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
1225
+ * targets through the registry-driven emitter (BAPI-635). Project JSON targets
1226
+ * are handled by {@link writeHostConfigs}; this covers everything else. Global
1227
+ * config paths are never added to the repository .gitignore. Returns secret-free
1228
+ * log lines and whether Codex was auto-provisioned (so the legacy Codex manual
1229
+ * hint can be suppressed).
1230
+ */
1231
+ async function provisionSelectedGlobalTargets(deps, platforms, entry) {
1232
+ const logLines = [];
1233
+ const provisionDeps = {
1234
+ fs: {
1235
+ readFile: deps.readFile,
1236
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
1237
+ mkdir: async (p, o) => {
1238
+ await deps.mkdir(p, o);
1239
+ },
1240
+ },
1241
+ vendor: deps.vendor,
1242
+ cwd: deps.cwd,
1243
+ homedir: deps.homedir(),
1244
+ env: deps.env,
1245
+ };
1246
+ const set = new Set(platforms);
1247
+ for (const id of HOST_PLATFORM_ORDER) {
1248
+ if (!set.has(id))
1249
+ continue;
1250
+ const target = MCP_HOST_TARGETS[id];
1251
+ // Skip project JSON targets — those are handled by writeHostConfigs.
1252
+ if (target.scope === "project" && target.format === "json")
1253
+ continue;
1254
+ const outcome = await provisionHostTarget(target, entry, provisionDeps);
1255
+ switch (outcome.status) {
1256
+ case "vendor-written":
1257
+ case "direct-written":
1258
+ case "created":
1259
+ logLines.push(` configured ${target.label} (${outcome.displayPath})`);
1260
+ break;
1261
+ case "manual-required":
1262
+ logLines.push(` ${target.label}: add the bridge-api MCP server manually to ${outcome.displayPath} ` +
1263
+ "(the API key is redacted in printed instructions).");
1264
+ break;
1265
+ case "skipped-invalid":
1266
+ logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
1267
+ break;
1268
+ case "failed":
1269
+ logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
1270
+ break;
1271
+ }
1272
+ }
1273
+ return logLines;
1274
+ }
1004
1275
  /** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
1005
1276
  export function buildPingUrl(baseUrl, repoName) {
1006
1277
  const url = new URL(`${baseUrl.replace(/\/+$/, "")}/jira/ping`);
@@ -1614,7 +1885,13 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1614
1885
  env: deps.env,
1615
1886
  homedir: deps.homedir,
1616
1887
  });
1617
- const targets = await resolveHostConfigTargets(deps);
1888
+ // BAPI-635: resolve the AI-coding-tool selection (explicit --tools, then TTY
1889
+ // multi-select, then the legacy non-TTY automatic set). The selected project
1890
+ // JSON targets drive the existing read-merge-write path; selected global
1891
+ // (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
1892
+ // registry-driven emitter after the connectivity check.
1893
+ const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
1894
+ const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
1618
1895
  // Read-only detection (safe in dry-run) of global-config editors we can't write.
1619
1896
  const manualEditors = await detectManualEditors(deps);
1620
1897
  const plan = {
@@ -1669,6 +1946,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1669
1946
  "Check that the system temporary directory is writable so the launch script can be used.");
1670
1947
  return 1;
1671
1948
  }
1949
+ // ---- Step 3b (kickoff) — pre-warm the @${VERSION}-pinned _npx bucket ----
1950
+ // The @${VERSION}-pinned launcher written below lives in a DIFFERENT _npx bucket
1951
+ // than the @latest bucket this `npx … install-bridge` invocation warmed, so the
1952
+ // first real MCP launch would otherwise pay a cold install inline (which can
1953
+ // exceed the client's connect deadline). Nothing before the Step 5 terminal spawn
1954
+ // depends on the warmed bucket — only the spawned session's first MCP launch does
1955
+ // — so START the warm HERE (fire-and-forget) and AWAIT it just before that spawn.
1956
+ // Its multi-second `npx` resolve then overlaps the network round-trips (ping /
1957
+ // exchange), the scaffold, and the config/credential writes instead of adding to
1958
+ // them serially. Strictly fail-open: the default impl always RESOLVES (never
1959
+ // rejects), so a later abort-return that skips the await leaves no unhandled
1960
+ // rejection. Positioned AFTER the launch-command guards above so an aborted run
1961
+ // that never reaches an install side effect also never spawns the pre-warm.
1962
+ log(" pre-warming the version-pinned launcher bucket (in the background)…");
1963
+ const prewarmPromise = deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
1672
1964
  const credentialWriteDeps = {
1673
1965
  env: deps.env,
1674
1966
  homedir: deps.homedir,
@@ -1889,31 +2181,81 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1889
2181
  const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
1890
2182
  // ---- Step 3 — write per-host MCP config with real values ----
1891
2183
  log("Step 3/5 — writing per-host MCP config…");
2184
+ // BAPI-635 (Step 8): every project-local, secret-bearing config MUST be
2185
+ // gitignored BEFORE the real API key is written. A project-target gitignore
2186
+ // failure is FATAL before the secret write (fixed, secret-free message).
2187
+ const gitignoreDeps = {
2188
+ readFile: deps.readFile,
2189
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
2190
+ mkdir: (p, o) => deps.mkdir(p, o),
2191
+ };
2192
+ for (const target of targets) {
2193
+ try {
2194
+ await ensureGitignoredShared(deps.cwd, target.relPath, gitignoreDeps);
2195
+ }
2196
+ catch {
2197
+ errorLog("Error: could not add a project MCP config to .gitignore before writing your key. " +
2198
+ "Aborting so the API key is never written to an un-ignored file.");
2199
+ return 1;
2200
+ }
2201
+ }
1892
2202
  const written = await writeHostConfigs(deps, targets, entry);
1893
2203
  for (const relPath of written)
1894
2204
  log(` wrote ${relPath}`);
1895
- // Only print global-editor manual setup when Windsurf/Codex is actually detected.
1896
- const manualInstructions = buildManualHostInstructions(entry, manualEditors);
2205
+ // BAPI-635: provision selected GLOBAL targets (Codex, Copilot CLI) and MANUAL
2206
+ // targets (Windsurf) via the registry-driven emitter. Global paths are never
2207
+ // added to the repository .gitignore.
2208
+ const globalLogLines = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry);
2209
+ for (const line of globalLogLines)
2210
+ log(line);
2211
+ // Legacy manual-editor instructions cover editors that are DETECTED but were
2212
+ // NOT part of the selection (so the emitter above did not handle them). The two
2213
+ // editors are suppressed INDEPENDENTLY: Codex is dropped when it was selected
2214
+ // (auto-provisioned or emitted above), Windsurf is dropped only when it was
2215
+ // selected — auto-provisioning Codex must never hide the Windsurf snippet.
2216
+ const legacyManualEditors = {
2217
+ windsurf: manualEditors.windsurf && !selectedPlatforms.includes("windsurf"),
2218
+ codex: manualEditors.codex && !selectedPlatforms.includes("codex"),
2219
+ };
2220
+ const manualInstructions = buildManualHostInstructions(entry, legacyManualEditors);
1897
2221
  if (manualInstructions)
1898
2222
  log(manualInstructions);
1899
- // ---- Step 3b pre-warm the @${VERSION}-pinned _npx bucket (BAPI-451 W3) ----
1900
- // The launcher just written is pinned to @${VERSION}, a DIFFERENT _npx bucket
1901
- // than the @latest bucket this `npx … install-bridge` invocation warmed. Spawn
1902
- // the exact pinned launcher once with the fast-exiting `--version` flag so the
1903
- // first real MCP launch resolves from a warm bucket instead of paying the cold
1904
- // install inline (which can exceed the client's connect deadline). Strictly
1905
- // fail-open: a failure warns (secret-free) but never fails the install.
1906
- log(" pre-warming the version-pinned launcher bucket…");
1907
- const prewarm = await deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
1908
- if (prewarm.ok) {
1909
- log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
1910
- log(` ${MCP_TIMEOUT_GUIDANCE}`);
2223
+ // BAPI-635 (Step 7): when both Claude Code and Copilot CLI are selected, warn
2224
+ // that they use different, non-shared config surfaces.
2225
+ if (selectedPlatforms.includes("claude-code") && selectedPlatforms.includes("copilot-cli")) {
2226
+ log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
2227
+ "global ~/.copilot/mcp-config.json the two are configured separately.");
2228
+ }
2229
+ // BAPI-635 (Step 7): Claude trust reminder a written project MCP config is
2230
+ // not a live connection until approved in Claude Code's trust dialog.
2231
+ if (selectedPlatforms.includes("claude-code")) {
2232
+ log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; " +
2233
+ "restart or reload an already-running session for it to take effect.");
2234
+ }
2235
+ // BAPI-635 (Step 8 + Step 3): persist the secret-free install state, ignoring
2236
+ // it before the write. Project-local paths only; global paths are never in it.
2237
+ try {
2238
+ await ensureGitignoredShared(deps.cwd, ".bridge/install-state.json", gitignoreDeps);
2239
+ // writeMcpInstallState catches its own I/O errors and returns { ok: false }
2240
+ // (it does NOT throw), so the failure warning must inspect the return value —
2241
+ // a try/catch alone would silently swallow a real persistence failure.
2242
+ const stateResult = await writeMcpInstallState(deps.cwd, { selectedPlatforms, projectConfigPaths: targets.map((t) => t.relPath) }, {
2243
+ readFile: deps.readFile,
2244
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
2245
+ rename: deps.rename,
2246
+ mkdir: async (p, o) => {
2247
+ await deps.mkdir(p, o);
2248
+ },
2249
+ unlink: deps.unlink,
2250
+ });
2251
+ if (!stateResult.ok) {
2252
+ // Install-state persistence is advisory — never fail the install over it.
2253
+ errorLog("Warning: could not persist the install-state file (non-fatal).");
2254
+ }
1911
2255
  }
1912
- else {
1913
- // Escalate to a stronger, still secret-free warning when pre-warm didn't
1914
- // complete cleanly but the install itself still succeeds (exit unaffected).
1915
- errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
1916
- `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
2256
+ catch {
2257
+ // ensureGitignored for the state file can still throw also advisory.
2258
+ errorLog("Warning: could not persist the install-state file (non-fatal).");
1917
2259
  }
1918
2260
  // ---- Step 4 — persist the credential ----
1919
2261
  if (bootstrapInviteMode) {
@@ -1964,6 +2306,22 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1964
2306
  // observe GitHub as configured if the user connects it here, and running it after the
1965
2307
  // spawn would put two prompts on the same terminal at once.
1966
2308
  await offerGithubConnection(repoName, deps, log);
2309
+ // ---- Step 3b (await) — settle the background pre-warm before the session opens ----
2310
+ // The warm was kicked off right after the launch-command guards (above); await it
2311
+ // HERE so the bucket is settled before the spawned session's first MCP launch, but
2312
+ // AFTER all the work it overlapped with. Strictly fail-open — the exit code is
2313
+ // never affected, mirroring the original inline Step 3b behavior.
2314
+ const prewarm = await prewarmPromise;
2315
+ if (prewarm.ok) {
2316
+ log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
2317
+ log(` ${MCP_TIMEOUT_GUIDANCE}`);
2318
+ }
2319
+ else {
2320
+ // Escalate to a stronger, still secret-free warning when pre-warm didn't
2321
+ // complete cleanly — but the install itself still succeeds (exit unaffected).
2322
+ errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
2323
+ `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
2324
+ }
1967
2325
  // ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
1968
2326
  log(`Step 5/5 — opening a ${agent.name} session for /install-bridge configuration + capability report…`);
1969
2327
  const terminal = detectTerminal(undefined, deps.env);