@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.
- 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-bin.js +68 -9267
- package/build/conductor-bundle-artifacts.js +802 -0
- package/build/conductor-bundle-cli.js +256 -0
- package/build/conductor-claude-hook-bin.js +2 -172
- package/build/decision-page-template.js +2 -31
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +148 -1
- package/build/env-flags.js +31 -0
- package/build/index.js +4659 -34836
- package/build/init.js +87 -59
- package/build/install-bridge.js +380 -22
- 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 -7
- 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 -10427
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1896
|
-
|
|
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
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
//
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
1913
|
-
//
|
|
1914
|
-
|
|
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);
|