@bridge_gpt/mcp-server 0.2.34 → 0.2.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +490 -366
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-review-workflow.js +264 -0
- package/build/claude-user-config-doctor.js +317 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +393 -4
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +37 -15
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +473 -81
- package/build/executor/cli.js +83 -6
- package/build/executor/credentials.js +39 -11
- package/build/executor/deps.js +32 -1
- package/build/executor/env.js +71 -28
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/index.js +4 -0
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +530 -20
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +114 -8
- package/build/executor/preflight.js +127 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +23 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +75 -3
- package/build/executor/types.js +4 -18
- package/build/executor/worker-command.js +73 -12
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +1039 -237
- package/build/init.js +503 -111
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +1188 -247
- package/build/install-reexec.js +233 -0
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +52 -13
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +32 -13
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +259 -0
- package/build/setup-epic.js +772 -28
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +412 -110
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +132 -6
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +19 -4
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
package/build/init.js
CHANGED
|
@@ -11,17 +11,24 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
13
13
|
import path from "path";
|
|
14
|
+
import os from "os";
|
|
14
15
|
import { COMMANDS } from "./commands.generated.js";
|
|
15
16
|
import { AGENTS } from "./agents.generated.js";
|
|
16
17
|
import { DOCS } from "./docs.generated.js";
|
|
17
18
|
import { VERSION } from "./version.generated.js";
|
|
18
19
|
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
19
|
-
import { validateRepoName } from "./bridge-config.js";
|
|
20
|
+
import { validateRepoName, resolveRepoNameForProjectRoot } from "./bridge-config.js";
|
|
20
21
|
import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
|
|
22
|
+
import { resolveBapiCredentials } from "./credential-store.js";
|
|
23
|
+
import { BRIDGE_ENTRY_KEY, describeLauncherReason, inspectLauncherConfigs, isPrerequisiteFailure, launcherTargetFor, } from "./launcher-config-inspection.js";
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
22
25
|
// CLI: --init scaffolds slash commands and MCP configs into the current project
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
27
|
+
/** The production Bridge API endpoint a placeholder entry falls back to. */
|
|
28
|
+
export const DEFAULT_BRIDGE_BASE_URL = "https://bridgegpt-api.com";
|
|
29
|
+
/** The repo-name placeholder that marks an entry as never-configured. */
|
|
30
|
+
export const PLACEHOLDER_REPO_NAME = "YOUR_REPO_NAME";
|
|
31
|
+
export function buildBridgeApiEntry(cwd, metadata) {
|
|
25
32
|
// Secret-free by design: BAPI_API_KEY is NEVER scaffolded into generated MCP
|
|
26
33
|
// config. The server self-resolves the key from the environment or the
|
|
27
34
|
// home-dir credential store (~/.config/bridge/credentials.json) at runtime.
|
|
@@ -40,17 +47,65 @@ export function buildBridgeApiEntry(cwd) {
|
|
|
40
47
|
// `--offline`, it falls back automatically). It cannot resurrect a stale build
|
|
41
48
|
// — the BAPI-429 hazard above was specific to UNPINNED specs; here the exact
|
|
42
49
|
// `@${VERSION}` is immutable, so a cache hit is always the right build.
|
|
50
|
+
// BAPI-714 (Group C): NEWLY generated registrations name the server subcommand
|
|
51
|
+
// explicitly. `serve` is a no-op relative to bare invocation — it consumes its
|
|
52
|
+
// own token and falls through to the same startup path — but it makes a
|
|
53
|
+
// generated config self-describing, so a reader can tell a server launcher from
|
|
54
|
+
// an installer invocation. Bare invocation remains a permanent server-start
|
|
55
|
+
// contract; see `refreshBridgeApiPackageSpec` for why EXISTING configs are never
|
|
56
|
+
// migrated to this shape.
|
|
57
|
+
// BAPI-728: `repoName`/`baseUrl` are non-secret and always supplied by the
|
|
58
|
+
// caller when it has already resolved them. `BAPI_API_KEY` remains absent in
|
|
59
|
+
// EVERY branch — the server self-resolves it at runtime.
|
|
43
60
|
return {
|
|
44
61
|
command: "npx",
|
|
45
|
-
args: ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}
|
|
62
|
+
args: ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "serve"],
|
|
46
63
|
env: {
|
|
47
|
-
BAPI_BASE_URL:
|
|
48
|
-
BAPI_REPO_NAME:
|
|
64
|
+
BAPI_BASE_URL: metadata?.baseUrl ?? DEFAULT_BRIDGE_BASE_URL,
|
|
65
|
+
BAPI_REPO_NAME: metadata?.repoName ?? PLACEHOLDER_REPO_NAME,
|
|
49
66
|
BAPI_DOCS_DIR: "docs/tmp",
|
|
50
67
|
BAPI_PROJECT_ROOT: cwd,
|
|
51
68
|
},
|
|
52
69
|
};
|
|
53
70
|
}
|
|
71
|
+
/** The package-name prefix every Bridge launcher spec starts with. */
|
|
72
|
+
const BRIDGE_PACKAGE_PREFIX = "@bridge_gpt/mcp-server";
|
|
73
|
+
/**
|
|
74
|
+
* Refresh ONLY the package-spec token of an existing launcher args array
|
|
75
|
+
* (BAPI-714, Group C).
|
|
76
|
+
*
|
|
77
|
+
* Adding `serve` to {@link buildBridgeApiEntry} changes the bytes of every FRESH
|
|
78
|
+
* registration, and the previous update path assigned the whole template array
|
|
79
|
+
* over an existing entry — which would have silently migrated every legacy bare
|
|
80
|
+
* launcher on the next `--init`, `install`, or `upgrade`. That is explicitly out
|
|
81
|
+
* of scope: legacy bare launchers can never be declared fully migrated, so bare
|
|
82
|
+
* invocation must keep meaning "server" permanently and no config-rewrite pass may
|
|
83
|
+
* add `serve`.
|
|
84
|
+
*
|
|
85
|
+
* So this helper replaces the single element beginning with `@bridge_gpt/mcp-server`
|
|
86
|
+
* with `spec` and preserves EVERY other token and its position — including whether
|
|
87
|
+
* `serve` is present. An args array with no package-spec token is returned
|
|
88
|
+
* unchanged (a custom launcher shape this helper must not invent tokens for).
|
|
89
|
+
*
|
|
90
|
+
* Pure: it returns a new array and never mutates its input.
|
|
91
|
+
*/
|
|
92
|
+
export function refreshBridgeApiPackageSpec(args, spec) {
|
|
93
|
+
if (!Array.isArray(args))
|
|
94
|
+
return null;
|
|
95
|
+
const tokens = args.filter((a) => typeof a === "string");
|
|
96
|
+
if (tokens.length !== args.length)
|
|
97
|
+
return null;
|
|
98
|
+
const index = tokens.findIndex((token) => token.startsWith(BRIDGE_PACKAGE_PREFIX));
|
|
99
|
+
if (index === -1)
|
|
100
|
+
return null;
|
|
101
|
+
const next = [...tokens];
|
|
102
|
+
next[index] = spec;
|
|
103
|
+
return next;
|
|
104
|
+
}
|
|
105
|
+
/** The exact package spec a launcher generated by THIS build should carry. */
|
|
106
|
+
export function currentBridgePackageSpec() {
|
|
107
|
+
return `${BRIDGE_PACKAGE_PREFIX}@${VERSION}`;
|
|
108
|
+
}
|
|
54
109
|
/**
|
|
55
110
|
* Choose the `repo_name` written into a scaffolded `.bridge/config`. Uses the
|
|
56
111
|
* cwd basename when it passes manifest repo-name validation; otherwise falls
|
|
@@ -86,6 +141,89 @@ export function buildBridgeConfigManifest(repoName) {
|
|
|
86
141
|
"",
|
|
87
142
|
].join("\n");
|
|
88
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Detect the editors present in `cwd`. Extracted from `runInit`'s Phase 1 so the
|
|
146
|
+
* asset manifest and the scaffolder cannot disagree about which conditional
|
|
147
|
+
* targets (`.cursor/commands/`, `.github/agents/`) this run actually writes.
|
|
148
|
+
*/
|
|
149
|
+
export async function detectInitIdes(options) {
|
|
150
|
+
const { cwd } = options;
|
|
151
|
+
const env = options.env ?? process.env;
|
|
152
|
+
const probe = options.stat ?? stat;
|
|
153
|
+
const exists = async (p) => {
|
|
154
|
+
try {
|
|
155
|
+
await probe(p);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const detection = {
|
|
163
|
+
claude: true,
|
|
164
|
+
vscode: await exists(path.join(cwd, ".vscode")),
|
|
165
|
+
cursor: await exists(path.join(cwd, ".cursor")),
|
|
166
|
+
windsurf: await exists(path.join(cwd, ".windsurf")),
|
|
167
|
+
};
|
|
168
|
+
if (!detection.cursor && env.CURSOR_TRACE_DIR)
|
|
169
|
+
detection.cursor = true;
|
|
170
|
+
if (!detection.windsurf)
|
|
171
|
+
detection.windsurf = await exists(path.join(cwd, ".windsurfrules"));
|
|
172
|
+
return detection;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the pipelines directory exactly the way `runInit`'s Phase 6 does, so a
|
|
176
|
+
* `BAPI_PIPELINES_DIR` override moves the scaffold AND the manifest together.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveInitPipelinesDir(cwd, env = process.env) {
|
|
179
|
+
return path.resolve(cwd, env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines");
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Render an absolute scaffold target as the path we print. Targets inside the
|
|
183
|
+
* project stay project-relative (in POSIX form, so the notice reads the same on
|
|
184
|
+
* every platform); anything resolved outside it keeps its absolute path rather
|
|
185
|
+
* than growing a `../../` prefix nobody can act on.
|
|
186
|
+
*/
|
|
187
|
+
function displayScaffoldPath(cwd, target, isDirectory) {
|
|
188
|
+
const relative = path.relative(cwd, target);
|
|
189
|
+
const inProject = relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
190
|
+
const base = inProject ? relative.split(path.sep).join("/") : target;
|
|
191
|
+
return isDirectory ? `${base.replace(/\/+$/, "")}/` : base;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The single authority on which files a `runInit` invocation scaffolds that are
|
|
195
|
+
* SAFE to recommend for commit (BAPI-708, Part C).
|
|
196
|
+
*
|
|
197
|
+
* Derived from the run's own resolved targets — the detected IDEs and the
|
|
198
|
+
* resolved `BAPI_PIPELINES_DIR` — rather than a hand-copied literal, so the list
|
|
199
|
+
* cannot drift from what was actually written. See {@link InitScaffoldAssets} for
|
|
200
|
+
* the hard exclusion contract; nothing gitignored or credential-bearing is
|
|
201
|
+
* constructible from this function's inputs.
|
|
202
|
+
*/
|
|
203
|
+
export async function resolveInitScaffoldAssets(options) {
|
|
204
|
+
const { cwd } = options;
|
|
205
|
+
const env = options.env ?? process.env;
|
|
206
|
+
const ide = await detectInitIdes({ ...options, env });
|
|
207
|
+
const pipelinesDir = resolveInitPipelinesDir(cwd, env);
|
|
208
|
+
const instructionsDir = path.join(path.dirname(pipelinesDir), "instructions");
|
|
209
|
+
const commitSafePaths = [
|
|
210
|
+
displayScaffoldPath(cwd, path.join(cwd, ".bridge", "config"), false),
|
|
211
|
+
displayScaffoldPath(cwd, path.join(pipelinesDir, "README.md"), false),
|
|
212
|
+
displayScaffoldPath(cwd, path.join(pipelinesDir, "example-pipeline.json"), false),
|
|
213
|
+
displayScaffoldPath(cwd, instructionsDir, true),
|
|
214
|
+
displayScaffoldPath(cwd, path.join(cwd, ".claude", "commands"), true),
|
|
215
|
+
...(ide.cursor ? [displayScaffoldPath(cwd, path.join(cwd, ".cursor", "commands"), true)] : []),
|
|
216
|
+
displayScaffoldPath(cwd, path.join(cwd, ".claude", "agents"), true),
|
|
217
|
+
...(ide.vscode ? [displayScaffoldPath(cwd, path.join(cwd, ".github", "agents"), true)] : []),
|
|
218
|
+
// Sourced from the bundled DOCS map, so a new scaffolded doc is covered
|
|
219
|
+
// automatically rather than needing a second edit here.
|
|
220
|
+
...Object.keys(DOCS)
|
|
221
|
+
.slice()
|
|
222
|
+
.sort()
|
|
223
|
+
.map((target) => displayScaffoldPath(cwd, path.join(cwd, target), false)),
|
|
224
|
+
];
|
|
225
|
+
return { ide, pipelinesDir, instructionsDir, commitSafePaths };
|
|
226
|
+
}
|
|
89
227
|
/**
|
|
90
228
|
* Merge a group token (e.g. "sfcc") into the BRIDGE_MCP_PROFILE env value of the
|
|
91
229
|
* `bridge-api` entry in each present MCP config file (.mcp.json, .vscode/mcp.json,
|
|
@@ -143,11 +281,11 @@ export async function mergeBridgeApiProfileToken(cwd, token) {
|
|
|
143
281
|
}
|
|
144
282
|
return lastProfile;
|
|
145
283
|
}
|
|
146
|
-
async function ensureGitignored(cwd, filePath) {
|
|
284
|
+
async function ensureGitignored(cwd, filePath, io) {
|
|
147
285
|
await ensureGitignoredShared(cwd, filePath, {
|
|
148
|
-
readFile: (p) => readFile(p, "utf-8"),
|
|
149
|
-
writeFile: (p, data) => writeFile(p, data, "utf-8"),
|
|
150
|
-
mkdir: (p, options) => mkdir(p, options),
|
|
286
|
+
readFile: (p) => (io?.readFile ?? readFile)(p, "utf-8"),
|
|
287
|
+
writeFile: (p, data) => (io?.writeFile ?? writeFile)(p, data, "utf-8"),
|
|
288
|
+
mkdir: (p, options) => (io?.mkdir ?? mkdir)(p, options),
|
|
151
289
|
});
|
|
152
290
|
}
|
|
153
291
|
/**
|
|
@@ -177,41 +315,152 @@ async function scaffoldFile(target, content, mkdirDir) {
|
|
|
177
315
|
return existed ? "overwritten" : "written";
|
|
178
316
|
}
|
|
179
317
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
318
|
+
* A preflight failure: an existing config we must not write over. Thrown BEFORE
|
|
319
|
+
* any mutation so a run either reconciles everything or changes nothing.
|
|
182
320
|
*/
|
|
183
|
-
export
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
321
|
+
export class InitPreflightError extends Error {
|
|
322
|
+
failures;
|
|
323
|
+
constructor(failures) {
|
|
324
|
+
super(`Cannot reconcile MCP launcher configs:\n${failures
|
|
325
|
+
.map((f) => ` ${f.relPath}: ${f.reason}`)
|
|
326
|
+
.join("\n")}`);
|
|
327
|
+
this.name = "InitPreflightError";
|
|
328
|
+
this.failures = failures;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Resolve the non-secret identity a NEW `bridge-api` entry should carry.
|
|
333
|
+
*
|
|
334
|
+
* Precedence (plan Step 3.9): an explicit caller context wins; then an explicit
|
|
335
|
+
* `BAPI_REPO_NAME`; then the committed `.bridge/config` / git-derived identity.
|
|
336
|
+
* A cwd basename is only ever an unconfirmed candidate — it is promoted to the
|
|
337
|
+
* real identity ONLY when a `bapi:<repo>` credential actually resolves for it,
|
|
338
|
+
* so a directory name can never masquerade as a configured repository.
|
|
339
|
+
*
|
|
340
|
+
* The resolved API key is reduced immediately to `credentialAvailable`. It is
|
|
341
|
+
* never returned, logged, persisted, or written into a generated MCP entry.
|
|
342
|
+
*/
|
|
343
|
+
async function resolveNewEntryIdentity(cwd, options, io) {
|
|
344
|
+
const env = options.env ?? process.env;
|
|
345
|
+
const explicitBaseUrl = typeof env.BAPI_BASE_URL === "string" ? env.BAPI_BASE_URL.trim() : "";
|
|
346
|
+
const baseUrl = explicitBaseUrl.length > 0 ? explicitBaseUrl : undefined;
|
|
347
|
+
// A caller that already did verified resolution is authoritative.
|
|
348
|
+
if (options.creationContext?.repoName) {
|
|
349
|
+
return {
|
|
350
|
+
repoName: options.creationContext.repoName,
|
|
351
|
+
baseUrl: options.creationContext.baseUrl ?? baseUrl,
|
|
352
|
+
credentialAvailable: options.creationContext.credentialAvailable ?? true,
|
|
353
|
+
baseUrlDefaulted: !(options.creationContext.baseUrl ?? baseUrl),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const checkCredential = async (repoName) => {
|
|
357
|
+
try {
|
|
358
|
+
const result = options.resolveCredentials
|
|
359
|
+
? await options.resolveCredentials(repoName)
|
|
360
|
+
: await resolveBapiCredentials(repoName, {
|
|
361
|
+
env,
|
|
362
|
+
homedir: options.homedir ?? os.homedir,
|
|
363
|
+
platform: options.platform ?? process.platform,
|
|
364
|
+
readFile: (p) => io.readFile(p, "utf-8"),
|
|
365
|
+
stat: async (p) => {
|
|
366
|
+
const s = (await io.stat(p));
|
|
367
|
+
return { mode: typeof s?.mode === "number" ? s.mode : 0o600 };
|
|
368
|
+
},
|
|
369
|
+
// Silence the resolver's own stderr: init owns its reporting.
|
|
370
|
+
stderr: () => { },
|
|
371
|
+
});
|
|
372
|
+
return result.ok;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
190
377
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
378
|
+
/** Candidate identities in precedence order; the first that authenticates wins. */
|
|
379
|
+
const confirmed = [];
|
|
380
|
+
const explicitRepo = typeof env.BAPI_REPO_NAME === "string" ? env.BAPI_REPO_NAME.trim() : "";
|
|
381
|
+
if (explicitRepo.length > 0 && explicitRepo !== PLACEHOLDER_REPO_NAME) {
|
|
382
|
+
const validated = validateRepoName(explicitRepo);
|
|
383
|
+
if (validated.ok) {
|
|
384
|
+
// An explicit environment identity is a deliberate operator statement, so
|
|
385
|
+
// it is used even when no credential is present locally; readiness still
|
|
386
|
+
// reflects the missing credential.
|
|
387
|
+
return {
|
|
388
|
+
repoName: validated.value,
|
|
389
|
+
baseUrl,
|
|
390
|
+
credentialAvailable: await checkCredential(validated.value),
|
|
391
|
+
baseUrlDefaulted: baseUrl === undefined,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
194
394
|
}
|
|
195
|
-
|
|
395
|
+
const manifestDeps = {
|
|
396
|
+
readFile: (p) => io.readFile(p, "utf-8"),
|
|
397
|
+
runCommand: undefined,
|
|
398
|
+
};
|
|
196
399
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
400
|
+
const resolved = options.resolveRepoName
|
|
401
|
+
? await options.resolveRepoName(cwd)
|
|
402
|
+
: await resolveRepoNameForProjectRoot(cwd, manifestDeps);
|
|
403
|
+
if (resolved.ok && resolved.value !== PLACEHOLDER_REPO_NAME) {
|
|
404
|
+
confirmed.push(resolved.value);
|
|
405
|
+
}
|
|
199
406
|
}
|
|
200
|
-
catch {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
await stat(path.join(cwd, ".windsurf"));
|
|
205
|
-
ideDetection.windsurf = true;
|
|
407
|
+
catch {
|
|
408
|
+
// A malformed or unreadable manifest is not fatal for identity resolution;
|
|
409
|
+
// it just means this source contributes no candidate.
|
|
206
410
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
411
|
+
for (const candidate of confirmed) {
|
|
412
|
+
if (await checkCredential(candidate)) {
|
|
413
|
+
return {
|
|
414
|
+
repoName: candidate,
|
|
415
|
+
baseUrl,
|
|
416
|
+
credentialAvailable: true,
|
|
417
|
+
baseUrlDefaulted: baseUrl === undefined,
|
|
418
|
+
};
|
|
212
419
|
}
|
|
213
|
-
catch { }
|
|
214
420
|
}
|
|
421
|
+
// The cwd basename is the weakest signal: accepted ONLY when a credential
|
|
422
|
+
// exists for it. Otherwise it stays an unconfirmed candidate and the entry is
|
|
423
|
+
// written explicitly unconfigured rather than looking configured.
|
|
424
|
+
const basename = validateRepoName(path.basename(cwd));
|
|
425
|
+
if (basename.ok && (await checkCredential(basename.value))) {
|
|
426
|
+
return {
|
|
427
|
+
repoName: basename.value,
|
|
428
|
+
baseUrl,
|
|
429
|
+
credentialAvailable: true,
|
|
430
|
+
baseUrlDefaulted: baseUrl === undefined,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Nothing resolvable. Fall back to a manifest/git identity WITHOUT claiming it
|
|
434
|
+
// is configured, so the entry at least names the right repo for a human to fix.
|
|
435
|
+
return {
|
|
436
|
+
repoName: confirmed[0],
|
|
437
|
+
baseUrl,
|
|
438
|
+
credentialAvailable: false,
|
|
439
|
+
baseUrlDefaulted: baseUrl === undefined,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Core initialization logic shared by --init and --upgrade.
|
|
444
|
+
* Runs all scaffolding phases without calling process.exit().
|
|
445
|
+
*
|
|
446
|
+
* BAPI-728: performs a complete read-only launcher preflight BEFORE its first
|
|
447
|
+
* write, and returns structured, secret-free results instead of `void`.
|
|
448
|
+
*/
|
|
449
|
+
export async function runInit(cwd, options = {}) {
|
|
450
|
+
const io = {
|
|
451
|
+
readFile: options.io?.readFile ?? readFile,
|
|
452
|
+
writeFile: options.io?.writeFile ?? writeFile,
|
|
453
|
+
mkdir: options.io?.mkdir ?? mkdir,
|
|
454
|
+
stat: options.io?.stat ?? stat,
|
|
455
|
+
};
|
|
456
|
+
const targetVersion = options.targetVersion ?? VERSION;
|
|
457
|
+
const targetSpec = `@bridge_gpt/mcp-server@${targetVersion}`;
|
|
458
|
+
const say = options.stdout ?? ((m) => console.log(m));
|
|
459
|
+
const warn = options.stderr ?? ((m) => console.error(m));
|
|
460
|
+
// ---- Phase 1: IDE Detection ----
|
|
461
|
+
// BAPI-708: detection lives in `detectInitIdes` so the commit-safe asset
|
|
462
|
+
// manifest resolves the SAME conditional targets this scaffolder writes.
|
|
463
|
+
const ideDetection = await detectInitIdes({ cwd });
|
|
215
464
|
const detectedIDEs = Object.entries(ideDetection)
|
|
216
465
|
.filter(([, v]) => v)
|
|
217
466
|
.map(([k]) => k);
|
|
@@ -224,78 +473,215 @@ export async function runInit(cwd) {
|
|
|
224
473
|
windsurfSnippet + "\n");
|
|
225
474
|
}
|
|
226
475
|
// ---- Phase 3: Config file handling ----
|
|
476
|
+
// Order is preserved from the original implementation so existing console
|
|
477
|
+
// narration and its tests are unchanged; the per-path metadata (which root
|
|
478
|
+
// key each config uses) now comes from the shared inspection module.
|
|
227
479
|
const configTargets = [
|
|
228
|
-
{ path: ".mcp.json",
|
|
229
|
-
{ path: ".vscode/mcp.json",
|
|
230
|
-
{ path: ".cursor/mcp.json",
|
|
231
|
-
]
|
|
480
|
+
{ path: ".mcp.json", shouldCreate: true },
|
|
481
|
+
{ path: ".vscode/mcp.json", shouldCreate: ideDetection.vscode },
|
|
482
|
+
{ path: ".cursor/mcp.json", shouldCreate: ideDetection.cursor },
|
|
483
|
+
].map((t) => ({
|
|
484
|
+
...t,
|
|
485
|
+
topLevelKey: launcherTargetFor(t.path).topLevelKey,
|
|
486
|
+
}));
|
|
487
|
+
// ---- Phase 3a: read-only preflight, BEFORE the first write ----
|
|
488
|
+
// A run must either reconcile every config or change nothing. Discovering a
|
|
489
|
+
// malformed second config only after the first was rewritten would leave the
|
|
490
|
+
// project half-migrated with no record of what moved.
|
|
491
|
+
const inspections = await inspectLauncherConfigs({
|
|
492
|
+
cwd,
|
|
493
|
+
targetVersion,
|
|
494
|
+
readFile: (p) => io.readFile(p, "utf-8"),
|
|
495
|
+
isCreatable: (relPath) => configTargets.find((t) => t.path === relPath)?.shouldCreate ?? false,
|
|
496
|
+
targets: configTargets.map((t) => ({
|
|
497
|
+
relPath: t.path,
|
|
498
|
+
topLevelKey: t.topLevelKey,
|
|
499
|
+
})),
|
|
500
|
+
});
|
|
501
|
+
const blocking = inspections.filter(isPrerequisiteFailure);
|
|
502
|
+
if (blocking.length > 0) {
|
|
503
|
+
throw new InitPreflightError(blocking.map((f) => ({
|
|
504
|
+
relPath: f.relPath,
|
|
505
|
+
reason: describeLauncherReason(f.reason),
|
|
506
|
+
})));
|
|
507
|
+
}
|
|
508
|
+
const byPath = new Map(inspections.map((i) => [i.relPath, i]));
|
|
509
|
+
// Resolve the identity a NEW entry would carry — only when one is actually
|
|
510
|
+
// needed, so an ordinary repin run never touches the credential store.
|
|
511
|
+
const needsNewEntry = inspections.some((i) => i.action === "create" || i.action === "add");
|
|
512
|
+
const identity = needsNewEntry
|
|
513
|
+
? await resolveNewEntryIdentity(cwd, options, io)
|
|
514
|
+
: { credentialAvailable: true, baseUrlDefaulted: false };
|
|
515
|
+
const entryReady = Boolean(identity.repoName) && identity.credentialAvailable;
|
|
516
|
+
const newEntryMetadata = {
|
|
517
|
+
repoName: entryReady ? identity.repoName : undefined,
|
|
518
|
+
baseUrl: identity.baseUrl,
|
|
519
|
+
};
|
|
232
520
|
const configActions = [];
|
|
233
|
-
|
|
521
|
+
const structuredActions = [];
|
|
522
|
+
const readinessWarnings = [];
|
|
234
523
|
for (const target of configTargets) {
|
|
524
|
+
const found = byPath.get(target.path);
|
|
235
525
|
const fullPath = path.join(cwd, target.path);
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
await stat(fullPath);
|
|
239
|
-
fileExists = true;
|
|
240
|
-
}
|
|
241
|
-
catch { }
|
|
242
|
-
if (!target.shouldCreate && !fileExists) {
|
|
526
|
+
if (found.action === "skip-inactive") {
|
|
243
527
|
configActions.push({ path: target.path, action: "skipped — IDE not detected" });
|
|
528
|
+
structuredActions.push({ relPath: target.path, kind: "skipped-inactive" });
|
|
244
529
|
continue;
|
|
245
530
|
}
|
|
246
|
-
if (
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
531
|
+
if (found.action === "skip-worktree-shim") {
|
|
532
|
+
// BAPI-714 / worktree provisioning: this registration points at a sibling
|
|
533
|
+
// checkout's build. Gate BEFORE touching `command`, `args`, or `env` —
|
|
534
|
+
// rewriting any of them would break the worktree's server.
|
|
535
|
+
configActions.push({
|
|
536
|
+
path: target.path,
|
|
537
|
+
action: "skipped — worktree mcp-invoke shim preserved",
|
|
538
|
+
});
|
|
539
|
+
structuredActions.push({
|
|
540
|
+
relPath: target.path,
|
|
541
|
+
kind: "skipped-worktree-shim",
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (found.action === "ahead") {
|
|
546
|
+
// Never walk a project backwards. `upgrade` blocks on this before it
|
|
547
|
+
// ever calls init; a direct `--init` from an older CLI reaches here, and
|
|
548
|
+
// must leave the newer pin exactly as it found it.
|
|
549
|
+
configActions.push({
|
|
550
|
+
path: target.path,
|
|
551
|
+
action: `skipped — pinned ahead at ${found.currentVersion}`,
|
|
552
|
+
});
|
|
553
|
+
structuredActions.push({
|
|
554
|
+
relPath: target.path,
|
|
555
|
+
kind: "skipped-ahead",
|
|
556
|
+
fromVersion: found.currentVersion ?? undefined,
|
|
557
|
+
fromSpec: found.spec ?? undefined,
|
|
558
|
+
});
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (found.action === "create") {
|
|
562
|
+
await io.mkdir(path.dirname(fullPath), { recursive: true });
|
|
563
|
+
const content = {
|
|
564
|
+
[target.topLevelKey]: {
|
|
565
|
+
[BRIDGE_ENTRY_KEY]: buildBridgeApiEntry(cwd, newEntryMetadata),
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
await io.writeFile(fullPath, JSON.stringify(content, null, 2) + "\n", "utf-8");
|
|
569
|
+
await ensureGitignored(cwd, target.path, io);
|
|
570
|
+
configActions.push({ path: target.path, action: "created" });
|
|
571
|
+
structuredActions.push({
|
|
572
|
+
relPath: target.path,
|
|
573
|
+
kind: "created",
|
|
574
|
+
toVersion: targetVersion,
|
|
575
|
+
unconfigured: !entryReady,
|
|
576
|
+
});
|
|
577
|
+
if (!entryReady) {
|
|
578
|
+
readinessWarnings.push({
|
|
579
|
+
relPath: target.path,
|
|
580
|
+
kind: "unconfigured-entry-created",
|
|
581
|
+
baseUrlDefaulted: identity.baseUrlDefaulted,
|
|
582
|
+
});
|
|
275
583
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
// Every remaining action reads and rewrites an existing, already-validated
|
|
587
|
+
// file. The preflight above proved it parses and carries a shape we can
|
|
588
|
+
// safely reconcile, so no defensive re-parse branch is needed here.
|
|
589
|
+
const raw = await io.readFile(fullPath, "utf-8");
|
|
590
|
+
const parsed = JSON.parse(raw);
|
|
591
|
+
if (found.action === "add") {
|
|
592
|
+
if (!parsed[target.topLevelKey])
|
|
593
|
+
parsed[target.topLevelKey] = {};
|
|
594
|
+
parsed[target.topLevelKey][BRIDGE_ENTRY_KEY] = buildBridgeApiEntry(cwd, newEntryMetadata);
|
|
595
|
+
await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
596
|
+
configActions.push({ path: target.path, action: "added entry" });
|
|
597
|
+
structuredActions.push({
|
|
598
|
+
relPath: target.path,
|
|
599
|
+
kind: "added",
|
|
600
|
+
toVersion: targetVersion,
|
|
601
|
+
unconfigured: !entryReady,
|
|
602
|
+
});
|
|
603
|
+
if (!entryReady) {
|
|
604
|
+
readinessWarnings.push({
|
|
605
|
+
relPath: target.path,
|
|
606
|
+
kind: "unconfigured-entry-created",
|
|
607
|
+
baseUrlDefaulted: identity.baseUrlDefaulted,
|
|
608
|
+
});
|
|
284
609
|
}
|
|
610
|
+
continue;
|
|
285
611
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
612
|
+
// Existing standard launcher: change ONLY the package-spec token and
|
|
613
|
+
// BAPI_PROJECT_ROOT. `command` is preserved (BAPI-728 — the old code
|
|
614
|
+
// rewrote it to "npx" unconditionally, silently replacing a custom
|
|
615
|
+
// launcher), and `refreshBridgeApiPackageSpec` preserves the bare-versus-
|
|
616
|
+
// `serve` composition plus every surrounding argument (BAPI-714 C-3).
|
|
617
|
+
const entry = parsed[target.topLevelKey][BRIDGE_ENTRY_KEY];
|
|
618
|
+
const refreshed = refreshBridgeApiPackageSpec(entry.args, targetSpec);
|
|
619
|
+
if (refreshed === null) {
|
|
620
|
+
// Unreachable: preflight classifies an unrecognizable args array as
|
|
621
|
+
// `unsupported` and throws above. Fail closed rather than fall back to
|
|
622
|
+
// the template, which is what used to clobber custom launchers.
|
|
623
|
+
throw new InitPreflightError([
|
|
624
|
+
{ relPath: target.path, reason: describeLauncherReason("no-package-token") },
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
entry.args = refreshed;
|
|
628
|
+
if (!entry.env)
|
|
629
|
+
entry.env = {};
|
|
630
|
+
entry.env.BAPI_PROJECT_ROOT = cwd;
|
|
631
|
+
await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
632
|
+
const kind = found.action === "already-current"
|
|
633
|
+
? "already-current"
|
|
634
|
+
: found.action === "normalize-unpinned"
|
|
635
|
+
? "normalized"
|
|
636
|
+
: "repinned";
|
|
637
|
+
configActions.push({ path: target.path, action: "updated entry and pinned version" });
|
|
638
|
+
structuredActions.push({
|
|
639
|
+
relPath: target.path,
|
|
640
|
+
kind,
|
|
641
|
+
fromVersion: found.currentVersion ?? undefined,
|
|
642
|
+
fromSpec: found.spec ?? undefined,
|
|
643
|
+
toVersion: targetVersion,
|
|
644
|
+
});
|
|
645
|
+
// An existing entry that still says YOUR_REPO_NAME loads without error and
|
|
646
|
+
// authenticates as nobody. Its env is left exactly as-is — but the run is
|
|
647
|
+
// no longer allowed to report itself as a working configuration.
|
|
648
|
+
if (entry.env?.BAPI_REPO_NAME === PLACEHOLDER_REPO_NAME) {
|
|
649
|
+
readinessWarnings.push({
|
|
650
|
+
relPath: target.path,
|
|
651
|
+
kind: "unconfigured-entry-existing",
|
|
652
|
+
});
|
|
294
653
|
}
|
|
295
654
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
655
|
+
if (!options.suppressConfigSummary) {
|
|
656
|
+
say("\nMCP config files:");
|
|
657
|
+
for (const entry of configActions) {
|
|
658
|
+
say(` ${entry.path}: ${entry.action}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Loud, secret-free readiness reporting. A config that looks configured but
|
|
662
|
+
// silently 401s is the failure mode BAPI-728 exists to end, so this warning
|
|
663
|
+
// names every affected path and what to do about it.
|
|
664
|
+
if (readinessWarnings.length > 0) {
|
|
665
|
+
const created = readinessWarnings.filter((w) => w.kind === "unconfigured-entry-created");
|
|
666
|
+
const existing = readinessWarnings.filter((w) => w.kind === "unconfigured-entry-existing");
|
|
667
|
+
warn("\n⚠ Bridge API MCP configuration is NOT ready to authenticate.");
|
|
668
|
+
if (created.length > 0) {
|
|
669
|
+
warn(` Wrote an UNCONFIGURED bridge-api entry (BAPI_REPO_NAME=${PLACEHOLDER_REPO_NAME}) to: ${created
|
|
670
|
+
.map((w) => w.relPath)
|
|
671
|
+
.join(", ")}`);
|
|
672
|
+
warn(identity.repoName
|
|
673
|
+
? ` Reason: no API key could be resolved for repository "${identity.repoName}".`
|
|
674
|
+
: " Reason: no repository identity could be resolved from the environment, .bridge/config, or the credential store.");
|
|
675
|
+
if (identity.baseUrlDefaulted) {
|
|
676
|
+
warn(` BAPI_BASE_URL was defaulted to the production endpoint ${DEFAULT_BRIDGE_BASE_URL}.`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (existing.length > 0) {
|
|
680
|
+
warn(` Existing bridge-api entry still uses the ${PLACEHOLDER_REPO_NAME} placeholder in: ${existing
|
|
681
|
+
.map((w) => w.relPath)
|
|
682
|
+
.join(", ")}`);
|
|
683
|
+
}
|
|
684
|
+
warn(" Fix with: npx -y @bridge_gpt/mcp-server install (or set BAPI_REPO_NAME and store a key with the install flow)");
|
|
299
685
|
}
|
|
300
686
|
// ---- Phase 4: Scaffold slash command directories ----
|
|
301
687
|
const commandDirs = [path.join(cwd, ".claude", "commands")];
|
|
@@ -415,7 +801,9 @@ export async function runInit(cwd) {
|
|
|
415
801
|
if (agentSkipped.size > 0)
|
|
416
802
|
console.log(` Skipped (unchanged): ${agentSkipped.size}`);
|
|
417
803
|
// ---- Phase 6: Scaffold custom pipeline directories ----
|
|
418
|
-
|
|
804
|
+
// BAPI-708: shared with `resolveInitScaffoldAssets`, so a `BAPI_PIPELINES_DIR`
|
|
805
|
+
// override moves the scaffold and the commit-safe manifest in lockstep.
|
|
806
|
+
const pipelinesDir = resolveInitPipelinesDir(cwd);
|
|
419
807
|
const instrDir = path.join(path.dirname(pipelinesDir), "instructions");
|
|
420
808
|
await mkdir(pipelinesDir, { recursive: true });
|
|
421
809
|
await mkdir(instrDir, { recursive: true });
|
|
@@ -575,21 +963,25 @@ body explicitly says to serialize structured output.
|
|
|
575
963
|
bridgeConfigExists = true;
|
|
576
964
|
}
|
|
577
965
|
catch { }
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
966
|
+
// BAPI-708 (Part B): the file is still written with UNCHANGED content — only
|
|
967
|
+
// its narration goes. The written/skipped line, the runtime-credential
|
|
968
|
+
// paragraph, and the old `Set BAPI_REPO_NAME…` summary were redundant on all
|
|
969
|
+
// three callers (`--init`, `upgrade`, and `install-bridge`): each scaffolds the
|
|
970
|
+
// same secret-free manifest, and install-bridge separately reports both where
|
|
971
|
+
// the credential landed and which generated assets are safe to commit. Removed
|
|
972
|
+
// outright rather than behind an output-mode flag — parameterizing would change
|
|
973
|
+
// the injected `deps.runInit` seam signature for no user-visible gain.
|
|
974
|
+
if (!bridgeConfigExists) {
|
|
582
975
|
await mkdir(path.dirname(bridgeConfigPath), { recursive: true });
|
|
583
976
|
await writeFile(bridgeConfigPath, buildBridgeConfigManifest(chooseScaffoldRepoName(cwd)), "utf-8");
|
|
584
|
-
console.log(`\n.bridge/config: written`);
|
|
585
|
-
}
|
|
586
|
-
console.log(" Credentials are resolved at runtime from BAPI_API_KEY or " +
|
|
587
|
-
"~/.config/bridge/credentials.json (no secrets are written to .bridge/config).");
|
|
588
|
-
// ---- Phase 7: Final summary ----
|
|
589
|
-
if (anyCreatedOrAdded) {
|
|
590
|
-
console.log("\nSet BAPI_REPO_NAME in your config files. Do NOT put BAPI_API_KEY in the " +
|
|
591
|
-
"generated MCP config — supply it via the BAPI_API_KEY environment variable, " +
|
|
592
|
-
"or store it under \"bapi:<repo_name>\" in ~/.config/bridge/credentials.json. " +
|
|
593
|
-
"Get your values from the Bridge API setup UI at https://bridgegpt-api.com");
|
|
594
977
|
}
|
|
978
|
+
// BAPI-728: authoritative, secret-free outcome. `ok` is false whenever any
|
|
979
|
+
// entry authenticates as nobody, so no caller can report full success over a
|
|
980
|
+
// configuration that will 401 on its first request.
|
|
981
|
+
return {
|
|
982
|
+
ok: readinessWarnings.length === 0,
|
|
983
|
+
configActions: structuredActions,
|
|
984
|
+
readinessWarnings,
|
|
985
|
+
targetVersion,
|
|
986
|
+
};
|
|
595
987
|
}
|