@bridge_gpt/mcp-server 0.2.25 → 0.2.26
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 +59 -9
- package/build/agents.generated.js +1 -1
- package/build/bridge-api-urls.js +31 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor-bundle-artifacts.js +802 -0
- package/build/conductor-bundle-cli.js +256 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +148 -1
- package/build/env-flags.js +31 -0
- package/build/index.js +2509 -296
- package/build/init.js +7 -3
- package/build/install-bridge.js +346 -4
- package/build/mcp-host-config.js +521 -0
- package/build/mcp-host-targets.js +194 -0
- package/build/mcp-install-state.js +175 -0
- package/build/pipelines.generated.js +5 -4
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +118 -5
- package/build/tool-surface-gating.js +396 -0
- package/build/version.generated.js +1 -1
- package/docs/install/mcp-tool-integrations.md +2 -2
- package/package.json +5 -5
- package/public/js/main.min.js +1 -19
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +4 -4
package/build/init.js
CHANGED
|
@@ -303,9 +303,13 @@ export async function runInit(cwd) {
|
|
|
303
303
|
console.log(` Skipped (unchanged): ${skippedFiles.size}`);
|
|
304
304
|
console.log(` ${dirNames}`);
|
|
305
305
|
// ---- Phase 4b: Scaffold shipped documentation assets ----
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
306
|
+
// Every doc asset the /install-bridge capability report points a human at,
|
|
307
|
+
// so no pointer it prints dangles inside an installed project: the
|
|
308
|
+
// capability catalog (docs/mcp-tool-integrations.md) that explains each
|
|
309
|
+
// gate's human "why", and the SFCC integration guide
|
|
310
|
+
// (docs/install/sfcc-integration.md) that the report's SFCC configure_in
|
|
311
|
+
// pointer names by path. Keyed by target path RELATIVE to the consumer
|
|
312
|
+
// project root; content is never secret.
|
|
309
313
|
const docsWritten = new Set();
|
|
310
314
|
const docsSkipped = new Set();
|
|
311
315
|
for (const [relTarget, content] of Object.entries(DOCS)) {
|
package/build/install-bridge.js
CHANGED
|
@@ -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
|
|
@@ -232,6 +236,18 @@ export function getInstallBridgeUsage() {
|
|
|
232
236
|
" THE TOKEN to your shell history and to the process list.",
|
|
233
237
|
"",
|
|
234
238
|
"Flags:",
|
|
239
|
+
" --tools <ids> Comma-separated AI-coding tools to configure,",
|
|
240
|
+
" bypassing the interactive picker. Accepted ids:",
|
|
241
|
+
` ${HOST_PLATFORM_ORDER.join(", ")}.`,
|
|
242
|
+
" Both --tools=claude-code,codex and",
|
|
243
|
+
" --tools claude-code,codex are accepted. On an",
|
|
244
|
+
" interactive terminal WITHOUT this flag you are",
|
|
245
|
+
" asked which tools you use (Claude Code plus any",
|
|
246
|
+
" detected editors are pre-checked). A non-",
|
|
247
|
+
" interactive run without --tools writes the legacy",
|
|
248
|
+
" automatic set (Claude Code plus any detected",
|
|
249
|
+
" Cursor / Copilot VS Code). --tools= (empty) is an",
|
|
250
|
+
" explicit empty selection and writes nothing.",
|
|
235
251
|
" --force Overwrite an existing real BAPI_API_KEY in a",
|
|
236
252
|
" host config (or in the credential store) without",
|
|
237
253
|
" prompting.",
|
|
@@ -271,6 +287,7 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
271
287
|
let agentName = DEFAULT_AGENT_NAME;
|
|
272
288
|
let invite;
|
|
273
289
|
let email;
|
|
290
|
+
let tools;
|
|
274
291
|
// Track SUPPLIED-ness separately from the values: `--invite` is legitimately
|
|
275
292
|
// valueless (prompt path) and `--api-key ""` is still a contradiction with it.
|
|
276
293
|
let inviteSupplied = false;
|
|
@@ -345,6 +362,17 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
345
362
|
i = r.nextIndex;
|
|
346
363
|
continue;
|
|
347
364
|
}
|
|
365
|
+
if (arg === "--tools" || arg.startsWith("--tools=")) {
|
|
366
|
+
const r = readValue(arg, "--tools", i);
|
|
367
|
+
if ("error" in r)
|
|
368
|
+
return { status: "error", message: r.error };
|
|
369
|
+
const parsed = parseToolsSelection(r.value);
|
|
370
|
+
if ("error" in parsed)
|
|
371
|
+
return { status: "error", message: parsed.error };
|
|
372
|
+
tools = parsed.tools;
|
|
373
|
+
i = r.nextIndex;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
348
376
|
if (arg === "--agent" || arg.startsWith("--agent=")) {
|
|
349
377
|
const r = readValue(arg, "--agent", i);
|
|
350
378
|
if ("error" in r)
|
|
@@ -395,9 +423,36 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
395
423
|
}
|
|
396
424
|
return {
|
|
397
425
|
status: "ok",
|
|
398
|
-
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email },
|
|
426
|
+
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools },
|
|
399
427
|
};
|
|
400
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Parse a `--tools` value into a validated platform-ID selection. A blank value
|
|
431
|
+
* (`--tools=`) is an EXPLICIT empty selection (`[]`), not an error. Comma-split
|
|
432
|
+
* IDs are trimmed, deduped in registry order, and each is validated against the
|
|
433
|
+
* host registry allowlist so an unvalidated string can never choose a path or
|
|
434
|
+
* command. Unknown IDs are rejected by name (the value is safe to echo — it is
|
|
435
|
+
* a platform ID, never a secret).
|
|
436
|
+
*/
|
|
437
|
+
export function parseToolsSelection(value) {
|
|
438
|
+
const raw = value
|
|
439
|
+
.split(",")
|
|
440
|
+
.map((s) => s.trim())
|
|
441
|
+
.filter((s) => s.length > 0);
|
|
442
|
+
const seen = new Set();
|
|
443
|
+
for (const id of raw) {
|
|
444
|
+
if (!isHostPlatformId(id)) {
|
|
445
|
+
const allowed = HOST_PLATFORM_ORDER.join(", ");
|
|
446
|
+
return {
|
|
447
|
+
error: `Invalid --tools value: '${id}' (allowed tools: ${allowed}).`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
seen.add(id);
|
|
451
|
+
}
|
|
452
|
+
// Dedupe + deterministic registry order.
|
|
453
|
+
const tools = HOST_PLATFORM_ORDER.filter((id) => seen.has(id));
|
|
454
|
+
return { tools };
|
|
455
|
+
}
|
|
401
456
|
/**
|
|
402
457
|
* No-echo secret prompt on stderr (so it never lands in piped stdout).
|
|
403
458
|
*
|
|
@@ -620,6 +675,8 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
620
675
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
621
676
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
622
677
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
678
|
+
promptMultiSelect: isTTY ? promptMultiSelectViaReadline : undefined,
|
|
679
|
+
vendor: createDefaultVendorProcessDeps(spawn),
|
|
623
680
|
fetch: productionFetch,
|
|
624
681
|
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
625
682
|
spawnPrewarm: spawnPrewarmDefault,
|
|
@@ -900,6 +957,164 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
|
|
|
900
957
|
}
|
|
901
958
|
return { ok: false, error: "No repo name provided." };
|
|
902
959
|
}
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// Per-host config write (Step 2)
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
/** A per-host MCP config target (mirrors runInit's configTargets shape). */
|
|
964
|
+
/** Stable wording of the interactive tool-selection prompt (BAPI-635). */
|
|
965
|
+
export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
|
|
966
|
+
/**
|
|
967
|
+
* Interactive numbered multi-select prompt on stderr (TTY only). Displays each
|
|
968
|
+
* option with a checked/unchecked marker seeded from `defaults`, accepts a
|
|
969
|
+
* comma-separated list of numbers to TOGGLE, and accepts the current selection on
|
|
970
|
+
* a bare Enter. Reprints on an invalid token rather than corrupting state.
|
|
971
|
+
* Resolves safely (to the seeded defaults) on EOF / synchronous close so a
|
|
972
|
+
* readline regression can never hang or discard the answer.
|
|
973
|
+
*/
|
|
974
|
+
export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
|
|
975
|
+
return new Promise((resolve) => {
|
|
976
|
+
const selected = new Set(defaults);
|
|
977
|
+
const render = () => {
|
|
978
|
+
output.write(`\n${promptText}\n`);
|
|
979
|
+
options.forEach((opt, idx) => {
|
|
980
|
+
const mark = selected.has(opt.id) ? "[x]" : "[ ]";
|
|
981
|
+
output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
|
|
982
|
+
});
|
|
983
|
+
output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
|
|
984
|
+
};
|
|
985
|
+
const rl = readline.createInterface({ input, output });
|
|
986
|
+
let answered = false;
|
|
987
|
+
const finish = () => {
|
|
988
|
+
answered = true;
|
|
989
|
+
rl.close();
|
|
990
|
+
resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
|
|
991
|
+
};
|
|
992
|
+
// EOF / synchronous close must resolve rather than deadlock the top-level
|
|
993
|
+
// await; `answered` guards the synchronous close from discarding a real answer.
|
|
994
|
+
rl.on("close", () => {
|
|
995
|
+
if (!answered)
|
|
996
|
+
resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
|
|
997
|
+
});
|
|
998
|
+
const ask = () => {
|
|
999
|
+
render();
|
|
1000
|
+
rl.question("", (answer) => {
|
|
1001
|
+
const trimmed = answer.trim();
|
|
1002
|
+
if (trimmed.length === 0) {
|
|
1003
|
+
finish();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const tokens = trimmed.split(",").map((t) => t.trim());
|
|
1007
|
+
const nums = [];
|
|
1008
|
+
let bad = false;
|
|
1009
|
+
for (const tok of tokens) {
|
|
1010
|
+
const n = Number(tok);
|
|
1011
|
+
if (!Number.isInteger(n) || n < 1 || n > options.length) {
|
|
1012
|
+
bad = true;
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
nums.push(n);
|
|
1016
|
+
}
|
|
1017
|
+
if (bad) {
|
|
1018
|
+
output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.\n`);
|
|
1019
|
+
ask();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
for (const n of nums) {
|
|
1023
|
+
const opt = options[n - 1];
|
|
1024
|
+
if (selected.has(opt.id))
|
|
1025
|
+
selected.delete(opt.id);
|
|
1026
|
+
else
|
|
1027
|
+
selected.add(opt.id);
|
|
1028
|
+
}
|
|
1029
|
+
finish();
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
ask();
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
1037
|
+
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
1038
|
+
* back to detection),
|
|
1039
|
+
* 2. interactive multi-select on a TTY (seeded from registry detection, with
|
|
1040
|
+
* Claude Code always checked as a default),
|
|
1041
|
+
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
1042
|
+
* detected Copilot VS Code and Cursor automatic targets (never Codex or
|
|
1043
|
+
* Copilot CLI just because a global directory exists).
|
|
1044
|
+
*/
|
|
1045
|
+
export async function resolveSelectedHostPlatforms(deps, options) {
|
|
1046
|
+
// 1. Explicit --tools (empty array is an explicit empty selection).
|
|
1047
|
+
if (options.tools !== undefined) {
|
|
1048
|
+
return options.tools;
|
|
1049
|
+
}
|
|
1050
|
+
const ctx = await buildDetectionContext(deps);
|
|
1051
|
+
const detected = new Set(detectDefaultPlatforms(ctx));
|
|
1052
|
+
// 2. Interactive multi-select on a TTY.
|
|
1053
|
+
if (deps.isTTY && deps.promptMultiSelect) {
|
|
1054
|
+
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
1055
|
+
// Claude Code is always a checked default; add every detected platform.
|
|
1056
|
+
const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
|
|
1057
|
+
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
|
|
1058
|
+
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
1059
|
+
}
|
|
1060
|
+
// 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
|
|
1061
|
+
const legacy = ["claude-code"];
|
|
1062
|
+
if (detected.has("cursor"))
|
|
1063
|
+
legacy.push("cursor");
|
|
1064
|
+
if (detected.has("copilot-vscode"))
|
|
1065
|
+
legacy.push("copilot-vscode");
|
|
1066
|
+
return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Build a registry detection context from install deps. install-bridge has only
|
|
1070
|
+
* async `stat`, but the registry's `detect` callbacks are synchronous, so we
|
|
1071
|
+
* pre-probe the candidate marker paths (the same ones the registry consults) and
|
|
1072
|
+
* expose them through a synchronous `exists` set. Paths are built in the registry's
|
|
1073
|
+
* POSIX-join form so the lookup matches exactly what `detect` passes to `exists`.
|
|
1074
|
+
*/
|
|
1075
|
+
async function buildDetectionContext(deps) {
|
|
1076
|
+
const cwd = deps.cwd;
|
|
1077
|
+
const homedir = deps.homedir();
|
|
1078
|
+
const posixJoin = (base, rel) => `${base.endsWith("/") ? base.slice(0, -1) : base}/${rel}`;
|
|
1079
|
+
const candidates = [
|
|
1080
|
+
posixJoin(cwd, ".cursor"),
|
|
1081
|
+
posixJoin(cwd, ".vscode"),
|
|
1082
|
+
posixJoin(cwd, ".windsurf"),
|
|
1083
|
+
posixJoin(cwd, ".windsurfrules"),
|
|
1084
|
+
posixJoin(homedir, ".codex"),
|
|
1085
|
+
];
|
|
1086
|
+
const present = new Set();
|
|
1087
|
+
await Promise.all(candidates.map(async (p) => {
|
|
1088
|
+
try {
|
|
1089
|
+
await deps.stat(p);
|
|
1090
|
+
present.add(p);
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
// absent — leave out of the set.
|
|
1094
|
+
}
|
|
1095
|
+
}));
|
|
1096
|
+
return {
|
|
1097
|
+
cwd,
|
|
1098
|
+
homedir,
|
|
1099
|
+
env: deps.env,
|
|
1100
|
+
exists: (p) => present.has(p),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Resolve which project-local JSON host configs to write for the selected
|
|
1105
|
+
* platforms. Only the project-scoped JSON targets (Claude Code, Cursor, Copilot
|
|
1106
|
+
* VS Code) are returned here; global targets (Codex, Copilot CLI) and manual
|
|
1107
|
+
* targets (Windsurf) are handled by the registry-driven emitter in
|
|
1108
|
+
* `runInstallBridgeCli`. The returned shape is unchanged so the existing
|
|
1109
|
+
* read-merge-write path and overwrite-consent detection are preserved.
|
|
1110
|
+
*/
|
|
1111
|
+
function hostConfigTargetsForPlatforms(platforms) {
|
|
1112
|
+
const set = new Set(platforms);
|
|
1113
|
+
return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
|
|
1114
|
+
.map((id) => MCP_HOST_TARGETS[id])
|
|
1115
|
+
.filter((t) => t.scope === "project" && t.format === "json")
|
|
1116
|
+
.map((t) => ({ relPath: t.relPath, topLevelKey: t.topLevelKey }));
|
|
1117
|
+
}
|
|
903
1118
|
/** Resolve which project-local host configs to write, mirroring runInit detection. */
|
|
904
1119
|
async function resolveHostConfigTargets(deps) {
|
|
905
1120
|
const targets = [
|
|
@@ -1001,6 +1216,58 @@ async function writeHostConfigs(deps, targets, entry) {
|
|
|
1001
1216
|
}
|
|
1002
1217
|
return written;
|
|
1003
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
|
|
1221
|
+
* targets through the registry-driven emitter (BAPI-635). Project JSON targets
|
|
1222
|
+
* are handled by {@link writeHostConfigs}; this covers everything else. Global
|
|
1223
|
+
* config paths are never added to the repository .gitignore. Returns secret-free
|
|
1224
|
+
* log lines and whether Codex was auto-provisioned (so the legacy Codex manual
|
|
1225
|
+
* hint can be suppressed).
|
|
1226
|
+
*/
|
|
1227
|
+
async function provisionSelectedGlobalTargets(deps, platforms, entry) {
|
|
1228
|
+
const logLines = [];
|
|
1229
|
+
const provisionDeps = {
|
|
1230
|
+
fs: {
|
|
1231
|
+
readFile: deps.readFile,
|
|
1232
|
+
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
1233
|
+
mkdir: async (p, o) => {
|
|
1234
|
+
await deps.mkdir(p, o);
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
vendor: deps.vendor,
|
|
1238
|
+
cwd: deps.cwd,
|
|
1239
|
+
homedir: deps.homedir(),
|
|
1240
|
+
env: deps.env,
|
|
1241
|
+
};
|
|
1242
|
+
const set = new Set(platforms);
|
|
1243
|
+
for (const id of HOST_PLATFORM_ORDER) {
|
|
1244
|
+
if (!set.has(id))
|
|
1245
|
+
continue;
|
|
1246
|
+
const target = MCP_HOST_TARGETS[id];
|
|
1247
|
+
// Skip project JSON targets — those are handled by writeHostConfigs.
|
|
1248
|
+
if (target.scope === "project" && target.format === "json")
|
|
1249
|
+
continue;
|
|
1250
|
+
const outcome = await provisionHostTarget(target, entry, provisionDeps);
|
|
1251
|
+
switch (outcome.status) {
|
|
1252
|
+
case "vendor-written":
|
|
1253
|
+
case "direct-written":
|
|
1254
|
+
case "created":
|
|
1255
|
+
logLines.push(` configured ${target.label} (${outcome.displayPath})`);
|
|
1256
|
+
break;
|
|
1257
|
+
case "manual-required":
|
|
1258
|
+
logLines.push(` ${target.label}: add the bridge-api MCP server manually to ${outcome.displayPath} ` +
|
|
1259
|
+
"(the API key is redacted in printed instructions).");
|
|
1260
|
+
break;
|
|
1261
|
+
case "skipped-invalid":
|
|
1262
|
+
logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
|
|
1263
|
+
break;
|
|
1264
|
+
case "failed":
|
|
1265
|
+
logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return logLines;
|
|
1270
|
+
}
|
|
1004
1271
|
/** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
|
|
1005
1272
|
export function buildPingUrl(baseUrl, repoName) {
|
|
1006
1273
|
const url = new URL(`${baseUrl.replace(/\/+$/, "")}/jira/ping`);
|
|
@@ -1614,7 +1881,13 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1614
1881
|
env: deps.env,
|
|
1615
1882
|
homedir: deps.homedir,
|
|
1616
1883
|
});
|
|
1617
|
-
|
|
1884
|
+
// BAPI-635: resolve the AI-coding-tool selection (explicit --tools, then TTY
|
|
1885
|
+
// multi-select, then the legacy non-TTY automatic set). The selected project
|
|
1886
|
+
// JSON targets drive the existing read-merge-write path; selected global
|
|
1887
|
+
// (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
|
|
1888
|
+
// registry-driven emitter after the connectivity check.
|
|
1889
|
+
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
1890
|
+
const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
|
|
1618
1891
|
// Read-only detection (safe in dry-run) of global-config editors we can't write.
|
|
1619
1892
|
const manualEditors = await detectManualEditors(deps);
|
|
1620
1893
|
const plan = {
|
|
@@ -1889,13 +2162,82 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1889
2162
|
const entry = buildInstallBridgeServerEntry(deps.cwd, repoName, apiKey, baseUrl, docsDir);
|
|
1890
2163
|
// ---- Step 3 — write per-host MCP config with real values ----
|
|
1891
2164
|
log("Step 3/5 — writing per-host MCP config…");
|
|
2165
|
+
// BAPI-635 (Step 8): every project-local, secret-bearing config MUST be
|
|
2166
|
+
// gitignored BEFORE the real API key is written. A project-target gitignore
|
|
2167
|
+
// failure is FATAL before the secret write (fixed, secret-free message).
|
|
2168
|
+
const gitignoreDeps = {
|
|
2169
|
+
readFile: deps.readFile,
|
|
2170
|
+
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
2171
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
2172
|
+
};
|
|
2173
|
+
for (const target of targets) {
|
|
2174
|
+
try {
|
|
2175
|
+
await ensureGitignoredShared(deps.cwd, target.relPath, gitignoreDeps);
|
|
2176
|
+
}
|
|
2177
|
+
catch {
|
|
2178
|
+
errorLog("Error: could not add a project MCP config to .gitignore before writing your key. " +
|
|
2179
|
+
"Aborting so the API key is never written to an un-ignored file.");
|
|
2180
|
+
return 1;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
1892
2183
|
const written = await writeHostConfigs(deps, targets, entry);
|
|
1893
2184
|
for (const relPath of written)
|
|
1894
2185
|
log(` wrote ${relPath}`);
|
|
1895
|
-
//
|
|
1896
|
-
|
|
2186
|
+
// BAPI-635: provision selected GLOBAL targets (Codex, Copilot CLI) and MANUAL
|
|
2187
|
+
// targets (Windsurf) via the registry-driven emitter. Global paths are never
|
|
2188
|
+
// added to the repository .gitignore.
|
|
2189
|
+
const globalLogLines = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry);
|
|
2190
|
+
for (const line of globalLogLines)
|
|
2191
|
+
log(line);
|
|
2192
|
+
// Legacy manual-editor instructions cover editors that are DETECTED but were
|
|
2193
|
+
// NOT part of the selection (so the emitter above did not handle them). The two
|
|
2194
|
+
// editors are suppressed INDEPENDENTLY: Codex is dropped when it was selected
|
|
2195
|
+
// (auto-provisioned or emitted above), Windsurf is dropped only when it was
|
|
2196
|
+
// selected — auto-provisioning Codex must never hide the Windsurf snippet.
|
|
2197
|
+
const legacyManualEditors = {
|
|
2198
|
+
windsurf: manualEditors.windsurf && !selectedPlatforms.includes("windsurf"),
|
|
2199
|
+
codex: manualEditors.codex && !selectedPlatforms.includes("codex"),
|
|
2200
|
+
};
|
|
2201
|
+
const manualInstructions = buildManualHostInstructions(entry, legacyManualEditors);
|
|
1897
2202
|
if (manualInstructions)
|
|
1898
2203
|
log(manualInstructions);
|
|
2204
|
+
// BAPI-635 (Step 7): when both Claude Code and Copilot CLI are selected, warn
|
|
2205
|
+
// that they use different, non-shared config surfaces.
|
|
2206
|
+
if (selectedPlatforms.includes("claude-code") && selectedPlatforms.includes("copilot-cli")) {
|
|
2207
|
+
log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
|
|
2208
|
+
"global ~/.copilot/mcp-config.json — the two are configured separately.");
|
|
2209
|
+
}
|
|
2210
|
+
// BAPI-635 (Step 7): Claude trust reminder — a written project MCP config is
|
|
2211
|
+
// not a live connection until approved in Claude Code's trust dialog.
|
|
2212
|
+
if (selectedPlatforms.includes("claude-code")) {
|
|
2213
|
+
log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; " +
|
|
2214
|
+
"restart or reload an already-running session for it to take effect.");
|
|
2215
|
+
}
|
|
2216
|
+
// BAPI-635 (Step 8 + Step 3): persist the secret-free install state, ignoring
|
|
2217
|
+
// it before the write. Project-local paths only; global paths are never in it.
|
|
2218
|
+
try {
|
|
2219
|
+
await ensureGitignoredShared(deps.cwd, ".bridge/install-state.json", gitignoreDeps);
|
|
2220
|
+
// writeMcpInstallState catches its own I/O errors and returns { ok: false }
|
|
2221
|
+
// (it does NOT throw), so the failure warning must inspect the return value —
|
|
2222
|
+
// a try/catch alone would silently swallow a real persistence failure.
|
|
2223
|
+
const stateResult = await writeMcpInstallState(deps.cwd, { selectedPlatforms, projectConfigPaths: targets.map((t) => t.relPath) }, {
|
|
2224
|
+
readFile: deps.readFile,
|
|
2225
|
+
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
2226
|
+
rename: deps.rename,
|
|
2227
|
+
mkdir: async (p, o) => {
|
|
2228
|
+
await deps.mkdir(p, o);
|
|
2229
|
+
},
|
|
2230
|
+
unlink: deps.unlink,
|
|
2231
|
+
});
|
|
2232
|
+
if (!stateResult.ok) {
|
|
2233
|
+
// Install-state persistence is advisory — never fail the install over it.
|
|
2234
|
+
errorLog("Warning: could not persist the install-state file (non-fatal).");
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
catch {
|
|
2238
|
+
// ensureGitignored for the state file can still throw — also advisory.
|
|
2239
|
+
errorLog("Warning: could not persist the install-state file (non-fatal).");
|
|
2240
|
+
}
|
|
1899
2241
|
// ---- Step 3b — pre-warm the @${VERSION}-pinned _npx bucket (BAPI-451 W3) ----
|
|
1900
2242
|
// The launcher just written is pinned to @${VERSION}, a DIFFERENT _npx bucket
|
|
1901
2243
|
// than the @latest bucket this `npx … install-bridge` invocation warmed. Spawn
|