@bridge_gpt/mcp-server 0.2.38 → 0.2.41
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 +189 -14
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +479 -0
- package/build/conduct-epic/cli.js +1765 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +286 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/reads-site-preference.js +6 -0
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
package/build/install-bridge.js
CHANGED
|
@@ -144,11 +144,19 @@ import os from "os";
|
|
|
144
144
|
import path from "path";
|
|
145
145
|
import readline from "readline";
|
|
146
146
|
import { runInit, buildBridgeApiEntry, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
|
|
147
|
+
import { hasProjectRootMarker as sharedHasProjectRootMarker } from "./project-root.js";
|
|
147
148
|
import { VERSION } from "./version.generated.js";
|
|
148
149
|
import { validateRepoName } from "./bridge-config.js";
|
|
149
150
|
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
150
151
|
import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
|
|
151
|
-
import { recordInstalledProjectArtifact, writeMcpInstallState } from "./mcp-install-state.js";
|
|
152
|
+
import { recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
153
|
+
// Executor provisioning (BAPI-779). The generator and the lifecycle module are
|
|
154
|
+
// imported DIRECTLY and driven programmatically — never by spawning
|
|
155
|
+
// `executor install-service` as a subprocess, which would lose the typed plan,
|
|
156
|
+
// the write outcome, and the unit path this flow has to record.
|
|
157
|
+
import { collectExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
158
|
+
import { inspectExecutorServiceState, startExecutorService, } from "./executor/service-lifecycle.js";
|
|
159
|
+
import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunchdPlistPathForId, executorSystemdUnitPathForId, resolvePackagedExecutorInvocation, writeExecutorServicePlan, } from "./executor/service-unit.js";
|
|
152
160
|
import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
153
161
|
import { runConductorInstallDoctor, } from "./conductor/install-doctor.js";
|
|
154
162
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
@@ -846,8 +854,12 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
|
|
|
846
854
|
`GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
|
|
847
855
|
}
|
|
848
856
|
}
|
|
849
|
-
/**
|
|
850
|
-
|
|
857
|
+
/**
|
|
858
|
+
* Echoed single-line prompt on stderr (used for repo confirmation / value).
|
|
859
|
+
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
860
|
+
* as `install`'s missing-project-root confirmation (BAPI-818, R1).
|
|
861
|
+
*/
|
|
862
|
+
export function promptLineViaReadline(promptText) {
|
|
851
863
|
return new Promise((resolve) => {
|
|
852
864
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
853
865
|
// See promptSecretViaReadline: EOF must resolve rather than deadlock the
|
|
@@ -983,6 +995,9 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
983
995
|
fetch: params.fetch,
|
|
984
996
|
reviewPolicySource: params.reviewPolicySource,
|
|
985
997
|
readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
|
|
998
|
+
// Read-only service-state probe (BAPI-779). Typed to return a state,
|
|
999
|
+
// never a runner, so the doctor cannot start/enable/reload anything.
|
|
1000
|
+
inspectExecutorServiceState: params.inspectExecutorServiceState,
|
|
986
1001
|
installDoctorDeps: {
|
|
987
1002
|
env: params.env,
|
|
988
1003
|
cwd: params.cwd,
|
|
@@ -1025,6 +1040,133 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1025
1040
|
conductorRunSetupEpicDryRun: async (argv) => ({
|
|
1026
1041
|
exitCode: await runSetupEpicCli(argv),
|
|
1027
1042
|
}),
|
|
1043
|
+
// ---- Executor provisioning seams (BAPI-779) ----
|
|
1044
|
+
lookupExecutable: (name) => defaultExecutablePresenceProbe(name),
|
|
1045
|
+
runLifecycleCommand: (executable, args) => defaultShellFreeRunner(executable, args),
|
|
1046
|
+
// `process.getuid` is absent on win32; a null uid makes the launchd start
|
|
1047
|
+
// plan refuse to build rather than interpolating `undefined` into a domain
|
|
1048
|
+
// target.
|
|
1049
|
+
getuid: () => (typeof process.getuid === "function" ? process.getuid() : null),
|
|
1050
|
+
// MONOTONIC, not wall clock: a budget measured with `Date.now()` would be
|
|
1051
|
+
// shortened or extended by an NTP step mid-observation.
|
|
1052
|
+
monotonicNow: () => Number(process.hrtime.bigint() / 1000000n),
|
|
1053
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Shell-free command runner used for every service-manager invocation.
|
|
1058
|
+
*
|
|
1059
|
+
* `shell: false` and an argument ARRAY are the whole point: a unit path or unit
|
|
1060
|
+
* name containing shell metacharacters is data here, never syntax. Output is
|
|
1061
|
+
* captured for internal classification and is deliberately NOT surfaced —
|
|
1062
|
+
* `service-lifecycle.ts` reports a stage and an exit code instead.
|
|
1063
|
+
*/
|
|
1064
|
+
function defaultShellFreeRunner(executable, args) {
|
|
1065
|
+
return new Promise((resolve, reject) => {
|
|
1066
|
+
const child = spawn(executable, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
1067
|
+
let stdout = "";
|
|
1068
|
+
let stderr = "";
|
|
1069
|
+
child.stdout?.on("data", (chunk) => {
|
|
1070
|
+
stdout += chunk.toString("utf-8");
|
|
1071
|
+
});
|
|
1072
|
+
child.stderr?.on("data", (chunk) => {
|
|
1073
|
+
stderr += chunk.toString("utf-8");
|
|
1074
|
+
});
|
|
1075
|
+
// A spawn error (ENOENT on `systemctl`, a sandbox denial) rejects, which the
|
|
1076
|
+
// lifecycle module treats as ONE attempted failure — never a retry.
|
|
1077
|
+
child.on("error", reject);
|
|
1078
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
/** Presence probe: `<tool> --version` exiting 0 means the tool resolves on PATH. */
|
|
1082
|
+
async function defaultExecutablePresenceProbe(name) {
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await defaultShellFreeRunner(name, ["--version"]);
|
|
1085
|
+
return result.exitCode === 0;
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Compose the executor-provisioning bundle the nested conductor phase runs on
|
|
1093
|
+
* (BAPI-779), from the FINAL merged deps.
|
|
1094
|
+
*
|
|
1095
|
+
* Every effect is a thin adapter over an existing seam — nothing here decides
|
|
1096
|
+
* anything. The orchestration (consent, ordering, degradation, observation)
|
|
1097
|
+
* stays in `install-bridge-conductor.ts`; moving it into this dispatch layer
|
|
1098
|
+
* would put policy in the place least able to test it.
|
|
1099
|
+
*/
|
|
1100
|
+
function buildConductorExecutorDeps(deps, baseUrl) {
|
|
1101
|
+
const homeDir = deps.homedir();
|
|
1102
|
+
const lookupExecutable = deps.lookupExecutable ?? defaultExecutablePresenceProbe;
|
|
1103
|
+
const runLifecycleCommand = deps.runLifecycleCommand ?? defaultShellFreeRunner;
|
|
1104
|
+
return {
|
|
1105
|
+
platform: deps.platform,
|
|
1106
|
+
homeDir,
|
|
1107
|
+
uid: deps.getuid ? deps.getuid() : null,
|
|
1108
|
+
// Captured ONCE here, never re-read inside a renderer (BAPI-327): the unit
|
|
1109
|
+
// bakes the generating shell's PATH so a service-launched executor can
|
|
1110
|
+
// resolve the bare `claude` worker command.
|
|
1111
|
+
envPath: deps.env.PATH ?? deps.env.Path ?? "",
|
|
1112
|
+
resolveInvocation: () => resolvePackagedExecutorInvocation(),
|
|
1113
|
+
runPreflight: (input) => collectExecutorInstallPreflight(input, {
|
|
1114
|
+
lookupExecutable: (name) => lookupExecutable(name),
|
|
1115
|
+
homedir: deps.homedir,
|
|
1116
|
+
readFile: deps.readFile,
|
|
1117
|
+
}),
|
|
1118
|
+
// The programmatic generator, NOT a spawned `executor install-service`: the
|
|
1119
|
+
// typed plan, the write outcome, and the unit path all have to come back.
|
|
1120
|
+
writeUnit: (plan) => writeExecutorServicePlan(plan, {
|
|
1121
|
+
stat: (filePath) => deps.stat(filePath),
|
|
1122
|
+
mkdir: (dirPath, options) => deps.mkdir(dirPath, options),
|
|
1123
|
+
writeFile: (filePath, content) => deps.writeFile(filePath, content),
|
|
1124
|
+
// Supplying `readFile` is what enables the `unchanged` outcome, so a
|
|
1125
|
+
// re-run against an identical unit reports the truth instead of an
|
|
1126
|
+
// unnecessary rewrite.
|
|
1127
|
+
readFile: deps.readFile,
|
|
1128
|
+
}),
|
|
1129
|
+
recordUnit: async (unitPath) => {
|
|
1130
|
+
const result = await recordInstalledExecutorServiceUnit(deps.cwd, unitPath, homeDir, {
|
|
1131
|
+
readFile: deps.readFile,
|
|
1132
|
+
writeFile: (p, data) => deps.writeFile(p, data),
|
|
1133
|
+
rename: deps.rename,
|
|
1134
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
1135
|
+
unlink: deps.unlink,
|
|
1136
|
+
});
|
|
1137
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
1138
|
+
},
|
|
1139
|
+
startService: (plan) => startExecutorService(plan, runLifecycleCommand),
|
|
1140
|
+
now: deps.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)),
|
|
1141
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Read-only local service-state inspector for the unified doctor (BAPI-779).
|
|
1146
|
+
*
|
|
1147
|
+
* Resolves the unit's service-manager handle from the executor id through the
|
|
1148
|
+
* generator's own naming helpers, so the doctor addresses exactly the unit the
|
|
1149
|
+
* generator would have written. Returns `unknown` on an unsupported platform
|
|
1150
|
+
* rather than probing anything.
|
|
1151
|
+
*/
|
|
1152
|
+
function buildDoctorServiceStateInspector(deps) {
|
|
1153
|
+
const runLifecycleCommand = deps.runLifecycleCommand ?? defaultShellFreeRunner;
|
|
1154
|
+
const homeDir = deps.homedir();
|
|
1155
|
+
const uid = deps.getuid ? deps.getuid() : null;
|
|
1156
|
+
return async ({ executorId }) => {
|
|
1157
|
+
if (deps.platform !== "darwin" && deps.platform !== "linux")
|
|
1158
|
+
return "unknown";
|
|
1159
|
+
const inspection = await inspectExecutorServiceState({
|
|
1160
|
+
platform: deps.platform,
|
|
1161
|
+
unitPath: deps.platform === "darwin"
|
|
1162
|
+
? executorLaunchdPlistPathForId(executorId, homeDir)
|
|
1163
|
+
: executorSystemdUnitPathForId(executorId, homeDir),
|
|
1164
|
+
serviceIdentifier: deps.platform === "darwin"
|
|
1165
|
+
? executorLaunchdLabelForId(executorId)
|
|
1166
|
+
: executorSystemdUnitNameForId(executorId),
|
|
1167
|
+
uid,
|
|
1168
|
+
}, runLifecycleCommand);
|
|
1169
|
+
return inspection.state;
|
|
1028
1170
|
};
|
|
1029
1171
|
}
|
|
1030
1172
|
/**
|
|
@@ -1502,8 +1644,6 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1502
1644
|
if (options.tools !== undefined) {
|
|
1503
1645
|
return options.tools;
|
|
1504
1646
|
}
|
|
1505
|
-
const ctx = await buildDetectionContext(deps);
|
|
1506
|
-
const detected = new Set(detectDefaultPlatforms(ctx));
|
|
1507
1647
|
// 2. Interactive single-shot numbered picker on a TTY. No row is pre-selected
|
|
1508
1648
|
// — the picker has no default state and requires an explicit selection.
|
|
1509
1649
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
@@ -1512,6 +1652,19 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1512
1652
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
1513
1653
|
}
|
|
1514
1654
|
// 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
|
|
1655
|
+
return resolveLegacyAutoDetectedHostPlatforms(deps);
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* The legacy non-TTY automatic host set (Claude + detected Cursor / Copilot
|
|
1659
|
+
* VS Code) — branch 3 of {@link resolveSelectedHostPlatforms}, extracted so a
|
|
1660
|
+
* `--dry-run` preview (BAPI-818) can compute the SAME deterministic answer
|
|
1661
|
+
* without touching the TTY branch above it (which would prompt). Pure/local:
|
|
1662
|
+
* only reads the injected `stat` boundary, never prompts, never touches the
|
|
1663
|
+
* network.
|
|
1664
|
+
*/
|
|
1665
|
+
async function resolveLegacyAutoDetectedHostPlatforms(deps) {
|
|
1666
|
+
const ctx = await buildDetectionContext(deps);
|
|
1667
|
+
const detected = new Set(detectDefaultPlatforms(ctx));
|
|
1515
1668
|
const legacy = ["claude-code"];
|
|
1516
1669
|
if (detected.has("cursor"))
|
|
1517
1670
|
legacy.push("cursor");
|
|
@@ -1519,6 +1672,21 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1519
1672
|
legacy.push("copilot-vscode");
|
|
1520
1673
|
return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
|
|
1521
1674
|
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Host-platform selection as known LOCALLY at dry-run time (BAPI-818, R2):
|
|
1677
|
+
* an explicit `--tools`, or — when the run would otherwise fall back to the
|
|
1678
|
+
* legacy non-TTY automatic set (no TTY, or no `promptMultiSelect` seam) — that
|
|
1679
|
+
* SAME deterministic local detection. Returns `undefined` ONLY when a real run
|
|
1680
|
+
* would need the interactive multi-select prompt, which a dry run may never
|
|
1681
|
+
* invoke.
|
|
1682
|
+
*/
|
|
1683
|
+
async function resolveDryRunHostPlatforms(deps, options) {
|
|
1684
|
+
if (options.tools !== undefined)
|
|
1685
|
+
return options.tools;
|
|
1686
|
+
if (deps.isTTY && deps.promptMultiSelect)
|
|
1687
|
+
return undefined;
|
|
1688
|
+
return resolveLegacyAutoDetectedHostPlatforms(deps);
|
|
1689
|
+
}
|
|
1522
1690
|
/**
|
|
1523
1691
|
* Launch agents install-bridge will NEVER auto-spawn from a host SELECTION, even
|
|
1524
1692
|
* though their CLI exists and the host's MCP config is still written normally.
|
|
@@ -3118,45 +3286,72 @@ export function buildCommitGeneratedAssetsNotice(safePaths, gitignoredPaths) {
|
|
|
3118
3286
|
return "";
|
|
3119
3287
|
return [INSTALL_BRIDGE_COMMIT_ASSETS_HEADING, ...lines].join("\n");
|
|
3120
3288
|
}
|
|
3289
|
+
/** The repo-name preview line, honest about whether the value is known yet. */
|
|
3290
|
+
function describeDryRunRepoLine(repo, deferredHint) {
|
|
3291
|
+
if (repo.kind === "deferred")
|
|
3292
|
+
return `Repo name: (not yet known — ${deferredHint})`;
|
|
3293
|
+
const source = repo.kind === "explicit" ? "from --repo" : "from BAPI_REPO_NAME";
|
|
3294
|
+
return `Repo name: ${repo.value} (${source})`;
|
|
3295
|
+
}
|
|
3296
|
+
/** A human-readable stand-in for an unresolved repo name inside a sentence. */
|
|
3297
|
+
function repoNamePlaceholder(repo) {
|
|
3298
|
+
return repo.kind === "deferred" ? "<repo name — not yet known>" : repo.value;
|
|
3299
|
+
}
|
|
3300
|
+
/** The project-root preview line. Reports the LOCAL read-only result only — never prompts. */
|
|
3301
|
+
function describeDryRunRootLine(projectRootPresent) {
|
|
3302
|
+
return projectRootPresent
|
|
3303
|
+
? "Project root: .git found"
|
|
3304
|
+
: "Project root: no .git found here — a real run would ask to continue (default No)";
|
|
3305
|
+
}
|
|
3121
3306
|
/**
|
|
3122
|
-
* Render the --dry-run preview lines. Every
|
|
3123
|
-
*
|
|
3124
|
-
*
|
|
3125
|
-
*
|
|
3126
|
-
*
|
|
3127
|
-
*
|
|
3307
|
+
* Render the --dry-run preview lines. Every value is either a pure LOCAL
|
|
3308
|
+
* computation or an explicit CLI/env input — nothing here required a prompt, a
|
|
3309
|
+
* credential read/resolution, or a network call to produce (BAPI-818, R2).
|
|
3310
|
+
* Genuinely unresolved information (the repo name absent both `--repo` and
|
|
3311
|
+
* `BAPI_REPO_NAME`; the tool selection absent `--tools`) is reported as
|
|
3312
|
+
* deferred rather than guessed, inferred, or silently omitted.
|
|
3128
3313
|
*/
|
|
3129
|
-
export function buildDryRunPreview(
|
|
3130
|
-
if (
|
|
3131
|
-
return buildBootstrapDryRunPreview(
|
|
3314
|
+
export function buildDryRunPreview(ctx) {
|
|
3315
|
+
if (ctx.bootstrapInvite || ctx.selfServeSignup)
|
|
3316
|
+
return buildBootstrapDryRunPreview(ctx);
|
|
3317
|
+
const pingStep = ctx.repo.kind === "deferred"
|
|
3318
|
+
? ["Step 2 — connectivity ping: deferred (the repo name is not yet known)."]
|
|
3319
|
+
: [
|
|
3320
|
+
`Step 2 — connectivity ping (before any durable key write): GET ${buildPingUrl(ctx.baseUrl, ctx.repo.value)} (X-API-Key: ${REDACTED_API_KEY})`,
|
|
3321
|
+
];
|
|
3322
|
+
const hostConfigStep = ctx.host.kind === "deferred"
|
|
3323
|
+
? [
|
|
3324
|
+
"Step 3 — write per-host MCP config: deferred (tool selection is not yet known —",
|
|
3325
|
+
" a real run asks interactively on a TTY, or auto-detects non-interactively).",
|
|
3326
|
+
]
|
|
3327
|
+
: [
|
|
3328
|
+
"Step 3 — write per-host MCP config (read-merge-write, launcher version-pinned;",
|
|
3329
|
+
" a git-tracked config needs default-No consent for the real key, else a",
|
|
3330
|
+
" secret-free entry; an unparseable config is skipped and left untouched):",
|
|
3331
|
+
...ctx.host.configTargets.map((t) => ` ${t}: BAPI_REPO_NAME=${repoNamePlaceholder(ctx.repo)}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${ctx.baseUrl}, BAPI_DOCS_DIR=${ctx.docsDir}`),
|
|
3332
|
+
...(ctx.host.manualEditors.length > 0
|
|
3333
|
+
? [` ${ctx.host.manualEditors.join(" + ")}: detected (global config) — manual setup instructions would be printed.`]
|
|
3334
|
+
: []),
|
|
3335
|
+
];
|
|
3336
|
+
const credentialStep = ctx.repo.kind === "deferred"
|
|
3337
|
+
? [`Step 4 — persist routing credential: deferred (the repo name is not yet known) at ${ctx.credentialStorePath}`]
|
|
3338
|
+
: [`Step 4 — persist routing credential: target bapi:${ctx.repo.value} at ${ctx.credentialStorePath}`];
|
|
3132
3339
|
return [
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
`Repo name: ${plan.repoName}${plan.serverRepoResolutionStatus === "resolved"
|
|
3140
|
-
? " (resolved server-side from your API key)"
|
|
3141
|
-
: ""}`,
|
|
3142
|
-
`Base URL (ping): ${plan.baseUrl}`,
|
|
3143
|
-
`Docs dir: ${plan.docsDir}`,
|
|
3144
|
-
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
3340
|
+
"install --dry-run (no writes, no network, no spawns, no credential resolved or prompted for)",
|
|
3341
|
+
describeDryRunRepoLine(ctx.repo, "a real run would resolve it from your API key, or prompt if it cannot be"),
|
|
3342
|
+
`Base URL: ${ctx.baseUrl}`,
|
|
3343
|
+
`Docs dir: ${ctx.docsDir}`,
|
|
3344
|
+
describeDryRunRootLine(ctx.projectRootPresent),
|
|
3345
|
+
`Agent: ${ctx.host.kind === "explicit" ? describePlannedLaunchAgent(ctx.host.launch) : "(deferred — tool selection is not yet known)"}`,
|
|
3145
3346
|
"",
|
|
3146
3347
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
" secret-free entry; an unparseable config is skipped and left untouched):",
|
|
3151
|
-
...plan.configTargets.map((t) => ` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),
|
|
3152
|
-
...(plan.manualEditors.length > 0
|
|
3153
|
-
? [` ${plan.manualEditors.join(" + ")}: detected (global config) — manual setup instructions would be printed.`]
|
|
3154
|
-
: []),
|
|
3155
|
-
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
|
|
3348
|
+
...pingStep,
|
|
3349
|
+
...hostConfigStep,
|
|
3350
|
+
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${ctx.prewarmCommand}`,
|
|
3156
3351
|
MCP_TIMEOUT_GUIDANCE,
|
|
3157
|
-
|
|
3158
|
-
...buildCommitAssetsPreviewLines(
|
|
3159
|
-
...buildLaunchStepPreview(
|
|
3352
|
+
...credentialStep,
|
|
3353
|
+
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
3354
|
+
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
3160
3355
|
];
|
|
3161
3356
|
}
|
|
3162
3357
|
/**
|
|
@@ -3164,12 +3359,12 @@ export function buildDryRunPreview(plan) {
|
|
|
3164
3359
|
* returns before the live emission point, and this module's convention is that a
|
|
3165
3360
|
* preview never advertises different behavior — so the same notice, built from the
|
|
3166
3361
|
* same resolved manifest, appears here too. The gitignored exclusion set is the
|
|
3167
|
-
*
|
|
3168
|
-
* computes it.
|
|
3362
|
+
* caller's own config targets plus the install-state file, exactly as the live
|
|
3363
|
+
* path computes it.
|
|
3169
3364
|
*/
|
|
3170
|
-
function buildCommitAssetsPreviewLines(
|
|
3171
|
-
const notice = buildCommitGeneratedAssetsNotice(
|
|
3172
|
-
...
|
|
3365
|
+
function buildCommitAssetsPreviewLines(commitSafeAssets, configTargets) {
|
|
3366
|
+
const notice = buildCommitGeneratedAssetsNotice(commitSafeAssets ?? [], [
|
|
3367
|
+
...configTargets,
|
|
3173
3368
|
INSTALL_BRIDGE_INSTALL_STATE_PATH,
|
|
3174
3369
|
]);
|
|
3175
3370
|
return notice.length > 0 ? ["", notice] : [];
|
|
@@ -3202,7 +3397,7 @@ function describePlannedLaunchAgent(launch) {
|
|
|
3202
3397
|
* A --dry-run never writes the launch script, prompts for consent, or spawns, so
|
|
3203
3398
|
* these lines describe the outcome rather than performing it.
|
|
3204
3399
|
*/
|
|
3205
|
-
function buildLaunchStepPreview(
|
|
3400
|
+
function buildLaunchStepPreview(launch) {
|
|
3206
3401
|
const githubLines = [
|
|
3207
3402
|
// BAPI-631: described, never performed in --dry-run — a preview must not open a
|
|
3208
3403
|
// browser or reach the network. It is also strictly optional, so it carries no step
|
|
@@ -3220,18 +3415,18 @@ function buildLaunchStepPreview(plan) {
|
|
|
3220
3415
|
" It opens github.com in your browser, and shares no GitHub credential with Bridge; it",
|
|
3221
3416
|
` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`,
|
|
3222
3417
|
];
|
|
3223
|
-
if (
|
|
3418
|
+
if (launch.kind === "spawn") {
|
|
3224
3419
|
return [
|
|
3225
3420
|
...githubLines,
|
|
3226
|
-
`Step 5 — agent session (${toolLabelForLaunchAgent(
|
|
3421
|
+
`Step 5 — agent session (${toolLabelForLaunchAgent(launch.agent)}): on a TTY the wizard first asks`,
|
|
3227
3422
|
" 'Open a … session to do that now? (Y/n)'; only on consent is the full command below",
|
|
3228
3423
|
" stored in a restricted launch script (mode 0600, under the system temp dir) and a short",
|
|
3229
3424
|
" sourced runner spawned (the script itself is NOT written in --dry-run):",
|
|
3230
|
-
` ${
|
|
3425
|
+
` ${launch.spawnCommand}`,
|
|
3231
3426
|
];
|
|
3232
3427
|
}
|
|
3233
|
-
if (
|
|
3234
|
-
const labels =
|
|
3428
|
+
if (launch.kind === "choose-one") {
|
|
3429
|
+
const labels = launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
|
|
3235
3430
|
return [
|
|
3236
3431
|
...githubLines,
|
|
3237
3432
|
`Step 5 — agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`,
|
|
@@ -3240,33 +3435,47 @@ function buildLaunchStepPreview(plan) {
|
|
|
3240
3435
|
}
|
|
3241
3436
|
return [
|
|
3242
3437
|
...githubLines,
|
|
3243
|
-
|
|
3438
|
+
launch.reason === "empty-selection"
|
|
3244
3439
|
? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
|
|
3245
3440
|
: "Step 5 — no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
|
|
3246
3441
|
];
|
|
3247
3442
|
}
|
|
3443
|
+
/**
|
|
3444
|
+
* The deferred form of {@link buildLaunchStepPreview}, used when the tool
|
|
3445
|
+
* selection itself was not resolved (no `--tools`) — the launch outcome cannot
|
|
3446
|
+
* be computed without it, so this states that plainly instead of guessing.
|
|
3447
|
+
*/
|
|
3448
|
+
function buildDeferredLaunchStepPreview() {
|
|
3449
|
+
return [
|
|
3450
|
+
"Step 4b — GitHub connect offer and agent session: deferred (tool selection is",
|
|
3451
|
+
" not yet known — a real run resolves it first, then follows from there).",
|
|
3452
|
+
];
|
|
3453
|
+
}
|
|
3248
3454
|
/**
|
|
3249
3455
|
* Bootstrap-invite dry-run preview. A --dry-run must not consume the invite OR
|
|
3250
3456
|
* LEAVE STATE BEHIND: skipping the HTTP call is not enough — a preview that wrote
|
|
3251
3457
|
* a pending `key_secret` would have left credential material on disk for a
|
|
3252
|
-
* redemption that never happened. So
|
|
3253
|
-
*
|
|
3458
|
+
* redemption that never happened. So this is reached BEFORE the CSPRNG runs and
|
|
3459
|
+
* before anything is written — indeed before the invite/email is even resolved,
|
|
3460
|
+
* since the early dry-run short-circuit in `runInstallBridgeCli` returns before
|
|
3461
|
+
* `resolveInviteToken`/`resolveSignupEmail` run at all.
|
|
3254
3462
|
*/
|
|
3255
|
-
function buildBootstrapDryRunPreview(
|
|
3256
|
-
const pendingTarget = `bootstrap-pending:${
|
|
3463
|
+
function buildBootstrapDryRunPreview(ctx) {
|
|
3464
|
+
const pendingTarget = `bootstrap-pending:${repoNamePlaceholder(ctx.repo)}`;
|
|
3257
3465
|
// Self-serve signup (BAPI-618): the ONLY structural difference from a pre-issued
|
|
3258
3466
|
// invite is a mint-from-email step (Step 2·pre) that turns an email into the
|
|
3259
3467
|
// invite token before the unchanged redemption below. The preview states plainly
|
|
3260
3468
|
// that in --dry-run that signup/mint is previewed-and-skipped: no account is
|
|
3261
3469
|
// created, no mint request is sent, no email leaves the machine. The email and
|
|
3262
3470
|
// any synthetic token are deliberately absent from this output.
|
|
3263
|
-
const header =
|
|
3471
|
+
const header = ctx.selfServeSignup
|
|
3264
3472
|
? "install --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)"
|
|
3265
3473
|
: "install --invite --dry-run (no writes, no network, no spawns, no secret generated)";
|
|
3266
|
-
const
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3474
|
+
const createdBy = ctx.selfServeSignup ? "the self-serve exchange" : "the exchange";
|
|
3475
|
+
const repoLine = ctx.repo.kind === "deferred"
|
|
3476
|
+
? `Repo name: (not yet known — a real run would prompt for a new, globally-unique project name)`
|
|
3477
|
+
: `Repo name: ${ctx.repo.value} (would be created by ${createdBy}; globally unique)`;
|
|
3478
|
+
const selfServeStep = ctx.selfServeSignup
|
|
3270
3479
|
? [
|
|
3271
3480
|
"Step 2·pre — self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace",
|
|
3272
3481
|
" signup is requested, no mint call is made, no email is sent or transmitted, and",
|
|
@@ -3277,33 +3486,51 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
3277
3486
|
" redemption protocol below.",
|
|
3278
3487
|
]
|
|
3279
3488
|
: [];
|
|
3489
|
+
const exchangeUrl = buildBootstrapExchangeUrl(ctx.baseUrl);
|
|
3490
|
+
const pingStep = ctx.repo.kind === "deferred"
|
|
3491
|
+
? ["Step 2c — connectivity ping with the newly-minted key: deferred (the repo name is not yet known)."]
|
|
3492
|
+
: [
|
|
3493
|
+
`Step 2c — connectivity ping with the newly-minted key (before any durable key write): GET ${buildPingUrl(ctx.baseUrl, ctx.repo.value)} (X-API-Key: ${REDACTED_API_KEY})`,
|
|
3494
|
+
];
|
|
3495
|
+
const hostConfigStep = ctx.host.kind === "deferred"
|
|
3496
|
+
? [
|
|
3497
|
+
"Step 3 — write per-host MCP config: deferred (tool selection is not yet known —",
|
|
3498
|
+
" a real run asks interactively on a TTY, or auto-detects non-interactively).",
|
|
3499
|
+
]
|
|
3500
|
+
: [
|
|
3501
|
+
"Step 3 — write per-host MCP config (read-merge-write, launcher version-pinned;",
|
|
3502
|
+
" a git-tracked config needs default-No consent for the real key, else a",
|
|
3503
|
+
" secret-free entry; an unparseable config is skipped and left untouched):",
|
|
3504
|
+
...ctx.host.configTargets.map((t) => ` ${t}: BAPI_REPO_NAME=${repoNamePlaceholder(ctx.repo)}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${ctx.baseUrl}, BAPI_DOCS_DIR=${ctx.docsDir}`),
|
|
3505
|
+
...(ctx.host.manualEditors.length > 0
|
|
3506
|
+
? [` ${ctx.host.manualEditors.join(" + ")}: detected (global config) — manual setup instructions would be printed.`]
|
|
3507
|
+
: []),
|
|
3508
|
+
];
|
|
3509
|
+
const promoteStep = ctx.repo.kind === "deferred"
|
|
3510
|
+
? [`Step 4 — promote ${pendingTarget} to its final credential (only after the exchange succeeds) at ${ctx.credentialStorePath}`]
|
|
3511
|
+
: [`Step 4 — promote ${pendingTarget} → bapi:${ctx.repo.value} at ${ctx.credentialStorePath} (only after the exchange succeeds)`];
|
|
3280
3512
|
return [
|
|
3281
3513
|
header,
|
|
3282
3514
|
repoLine,
|
|
3283
|
-
`Base URL: ${
|
|
3284
|
-
`Docs dir: ${
|
|
3285
|
-
|
|
3515
|
+
`Base URL: ${ctx.baseUrl}`,
|
|
3516
|
+
`Docs dir: ${ctx.docsDir}`,
|
|
3517
|
+
describeDryRunRootLine(ctx.projectRootPresent),
|
|
3518
|
+
`Agent: ${ctx.host.kind === "explicit" ? describePlannedLaunchAgent(ctx.host.launch) : "(deferred — tool selection is not yet known)"}`,
|
|
3286
3519
|
"",
|
|
3287
3520
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
3288
3521
|
...selfServeStep,
|
|
3289
|
-
`Step 2a — generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${
|
|
3522
|
+
`Step 2a — generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${ctx.credentialStorePath}`,
|
|
3290
3523
|
" BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.",
|
|
3291
3524
|
`Step 2b — redeem the bootstrap invite (replaces the pre-flight ping — there is no key yet):`,
|
|
3292
|
-
` POST ${
|
|
3293
|
-
` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
" secret-free entry; an unparseable config is skipped and left untouched):",
|
|
3298
|
-
...plan.configTargets.map((t) => ` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),
|
|
3299
|
-
...(plan.manualEditors.length > 0
|
|
3300
|
-
? [` ${plan.manualEditors.join(" + ")}: detected (global config) — manual setup instructions would be printed.`]
|
|
3301
|
-
: []),
|
|
3302
|
-
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
|
|
3525
|
+
` POST ${exchangeUrl}`,
|
|
3526
|
+
` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${repoNamePlaceholder(ctx.repo)}", "key_secret": "${REDACTED_API_KEY}"}`,
|
|
3527
|
+
...pingStep,
|
|
3528
|
+
...hostConfigStep,
|
|
3529
|
+
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${ctx.prewarmCommand}`,
|
|
3303
3530
|
MCP_TIMEOUT_GUIDANCE,
|
|
3304
|
-
|
|
3305
|
-
...buildCommitAssetsPreviewLines(
|
|
3306
|
-
...buildLaunchStepPreview(
|
|
3531
|
+
...promoteStep,
|
|
3532
|
+
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
3533
|
+
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
3307
3534
|
];
|
|
3308
3535
|
}
|
|
3309
3536
|
/**
|
|
@@ -3635,20 +3862,15 @@ export const INSTALL_BRIDGE_NO_GIT_ABORT = "Aborted — run install from your pr
|
|
|
3635
3862
|
/** The non-TTY form: same finding, stated as a warning, then the run continues. */
|
|
3636
3863
|
export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} — continuing (non-interactive).`;
|
|
3637
3864
|
/**
|
|
3638
|
-
* Existence probe for `<cwd>/.git`,
|
|
3865
|
+
* Existence probe for `<cwd>/.git`, delegated to the shared project-root probe
|
|
3866
|
+
* (BAPI-818) so `--init` and `install` use exactly one marker definition. A git
|
|
3639
3867
|
* worktree's `.git` is a FILE rather than a directory, so only existence is
|
|
3640
3868
|
* checked — never the entry type. Any probe failure is treated as "absent"; this
|
|
3641
3869
|
* is a hint for a warning, never an authorization decision. Repository-name
|
|
3642
3870
|
* inference is untouched by it.
|
|
3643
3871
|
*/
|
|
3644
3872
|
async function hasProjectRootMarker(deps) {
|
|
3645
|
-
|
|
3646
|
-
await deps.stat(path.join(deps.cwd, ".git"));
|
|
3647
|
-
return true;
|
|
3648
|
-
}
|
|
3649
|
-
catch {
|
|
3650
|
-
return false;
|
|
3651
|
-
}
|
|
3873
|
+
return sharedHasProjectRootMarker(deps.cwd, deps.stat);
|
|
3652
3874
|
}
|
|
3653
3875
|
// ---------------------------------------------------------------------------
|
|
3654
3876
|
// CLI entry
|
|
@@ -3744,6 +3966,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3744
3966
|
cwd: deps.cwd,
|
|
3745
3967
|
env: deps.env,
|
|
3746
3968
|
readFile: deps.readFile,
|
|
3969
|
+
// Read-only service-state probe (BAPI-779), built from the FINAL
|
|
3970
|
+
// merged deps so a test's platform/runner overrides reach the doctor.
|
|
3971
|
+
inspectExecutorServiceState: buildDoctorServiceStateInspector(deps),
|
|
3747
3972
|
}),
|
|
3748
3973
|
writeWorkflow: (cwd, content, options) => (deps.conductorWriteWorkflow ??
|
|
3749
3974
|
(async () => ({ ok: false, error: "workflow seam unavailable" })))(cwd, content, options, deps),
|
|
@@ -3751,6 +3976,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3751
3976
|
(async () => ({ ok: false, error: "install-state seam unavailable" })))(cwd, relPath, deps),
|
|
3752
3977
|
runSetupEpicDryRun: (setupArgv) => (deps.conductorRunSetupEpicDryRun ??
|
|
3753
3978
|
(async () => ({ exitCode: 1 })))(setupArgv),
|
|
3979
|
+
// Built from the FINAL merged deps (BAPI-779), for the same reason the
|
|
3980
|
+
// seams above are: a test that overrides only `fetch` or only `homedir`
|
|
3981
|
+
// must see its override here, not a stale default captured earlier.
|
|
3982
|
+
executorProvisioning: buildConductorExecutorDeps(deps, conductorBaseUrl),
|
|
3754
3983
|
};
|
|
3755
3984
|
return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
|
|
3756
3985
|
}
|
|
@@ -3782,6 +4011,101 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3782
4011
|
// every downstream `deps.env` consumer observes it. The caller-owned object (and
|
|
3783
4012
|
// `process.env`) is never mutated.
|
|
3784
4013
|
deps.env = { ...deps.env, BAPI_BASE_URL: baseUrl };
|
|
4014
|
+
// ---- BAPI-818 (R2): --dry-run short-circuits HERE — genuinely inert ----
|
|
4015
|
+
// Fires immediately after argument parsing and the pure base-URL validation
|
|
4016
|
+
// above, and strictly BEFORE the project-root confirmation prompt, onboarding
|
|
4017
|
+
// branch selection, credential resolution, the server-side repo-resolution
|
|
4018
|
+
// round-trip, the host multi-select, and every bootstrap/invite, write,
|
|
4019
|
+
// prewarm, or spawn action below. That ordering IS the fix: a dry run used to
|
|
4020
|
+
// reach every one of those first (README:258/119's "no writes, no contacting
|
|
4021
|
+
// Bridge, no opening anything" was false). Everything the preview below reports
|
|
4022
|
+
// is either an explicit CLI/env input or a pure local computation — never a
|
|
4023
|
+
// credential read, an invite exchange, or a network call. The bootstrap-invite
|
|
4024
|
+
// property this ordering must preserve (a preview must neither consume the
|
|
4025
|
+
// invite NOR leave a pending secret on disk) holds trivially here: this branch
|
|
4026
|
+
// returns before `resolveInviteToken`/`resolveSignupEmail` even run.
|
|
4027
|
+
if (options.dryRun) {
|
|
4028
|
+
log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`);
|
|
4029
|
+
const repo = typeof options.repo === "string" && options.repo.trim().length > 0
|
|
4030
|
+
? { kind: "explicit", value: options.repo.trim() }
|
|
4031
|
+
: typeof deps.env.BAPI_REPO_NAME === "string" && deps.env.BAPI_REPO_NAME.trim().length > 0
|
|
4032
|
+
? { kind: "configured", value: deps.env.BAPI_REPO_NAME.trim() }
|
|
4033
|
+
: { kind: "deferred" };
|
|
4034
|
+
// Read-only local .git probe — the SAME check the live path uses, but
|
|
4035
|
+
// reported here rather than turned into a confirmation prompt.
|
|
4036
|
+
const projectRootPresent = await hasProjectRootMarker(deps);
|
|
4037
|
+
// Pure, deterministic, no I/O (see its own doc comment) — safe to call here
|
|
4038
|
+
// even though the interactive wrapper `resolveInstallBridgeOnboardingBranchForRun`
|
|
4039
|
+
// (which CAN prompt on a bare TTY run) is not.
|
|
4040
|
+
const onboardingBranch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
4041
|
+
const bootstrapInvite = onboardingBranch.kind === "need-key";
|
|
4042
|
+
const selfServeSignup = onboardingBranch.kind === "need-key" && onboardingBranch.method === "self-serve";
|
|
4043
|
+
// Host/tool selection: known locally whenever it would be deterministic in a
|
|
4044
|
+
// real run too — an explicit `--tools`, or the legacy non-TTY auto-detected
|
|
4045
|
+
// set (`resolveDryRunHostPlatforms` mirrors `resolveSelectedHostPlatforms`
|
|
4046
|
+
// exactly, minus the branch that prompts). Only genuinely deferred when a
|
|
4047
|
+
// real run would need the interactive multi-select, which a dry run may not
|
|
4048
|
+
// invoke.
|
|
4049
|
+
const dryRunPlatforms = await resolveDryRunHostPlatforms(deps, options);
|
|
4050
|
+
let host;
|
|
4051
|
+
if (dryRunPlatforms !== undefined) {
|
|
4052
|
+
const dryRunTargets = hostConfigTargetsForPlatforms(dryRunPlatforms);
|
|
4053
|
+
const launchDecision = resolveInstallBridgeLaunchDecision(dryRunPlatforms, options.agentName);
|
|
4054
|
+
let launch;
|
|
4055
|
+
if (launchDecision.kind === "spawn") {
|
|
4056
|
+
const spec = resolveAgentSpec(launchDecision.agent);
|
|
4057
|
+
if (!spec) {
|
|
4058
|
+
errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
|
|
4059
|
+
return 1;
|
|
4060
|
+
}
|
|
4061
|
+
launch = {
|
|
4062
|
+
kind: "spawn",
|
|
4063
|
+
agent: launchDecision.agent,
|
|
4064
|
+
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
else if (launchDecision.kind === "choose-one") {
|
|
4068
|
+
launch = { kind: "choose-one", agents: launchDecision.agents };
|
|
4069
|
+
}
|
|
4070
|
+
else {
|
|
4071
|
+
launch = { kind: "manual", reason: launchDecision.reason };
|
|
4072
|
+
}
|
|
4073
|
+
host = {
|
|
4074
|
+
kind: "explicit",
|
|
4075
|
+
configTargets: dryRunTargets.map((t) => t.relPath),
|
|
4076
|
+
manualEditors: manualEditorNames(await detectManualEditors(deps)),
|
|
4077
|
+
launch,
|
|
4078
|
+
};
|
|
4079
|
+
}
|
|
4080
|
+
else {
|
|
4081
|
+
host = { kind: "deferred" };
|
|
4082
|
+
}
|
|
4083
|
+
// Local, read-only filesystem probes — never a credential, never the network.
|
|
4084
|
+
const dryRunScaffoldAssets = await resolveInitScaffoldAssets({
|
|
4085
|
+
cwd: deps.cwd,
|
|
4086
|
+
env: deps.env,
|
|
4087
|
+
stat: deps.stat,
|
|
4088
|
+
});
|
|
4089
|
+
const dryRunCredentialStorePath = getPrimaryCredentialStorePath({
|
|
4090
|
+
env: deps.env,
|
|
4091
|
+
homedir: deps.homedir,
|
|
4092
|
+
});
|
|
4093
|
+
for (const line of buildDryRunPreview({
|
|
4094
|
+
baseUrl,
|
|
4095
|
+
docsDir: deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR,
|
|
4096
|
+
credentialStorePath: dryRunCredentialStorePath,
|
|
4097
|
+
prewarmCommand: buildPrewarmCommandPreview(),
|
|
4098
|
+
commitSafeAssets: dryRunScaffoldAssets.commitSafePaths,
|
|
4099
|
+
projectRootPresent,
|
|
4100
|
+
repo,
|
|
4101
|
+
host,
|
|
4102
|
+
bootstrapInvite,
|
|
4103
|
+
selfServeSignup,
|
|
4104
|
+
})) {
|
|
4105
|
+
log(line);
|
|
4106
|
+
}
|
|
4107
|
+
return 0;
|
|
4108
|
+
}
|
|
3785
4109
|
// ---- BAPI-669 (U2): working-directory banner + project-root gate ----
|
|
3786
4110
|
// See the section comment on `hasProjectRootMarker`: read-only, and positioned
|
|
3787
4111
|
// ahead of ALL credential and configuration work so a decline costs nothing.
|
|
@@ -4098,35 +4422,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4098
4422
|
env: deps.env,
|
|
4099
4423
|
stat: deps.stat,
|
|
4100
4424
|
});
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
configTargets: targets.map((t) => t.relPath),
|
|
4107
|
-
commitSafeAssets: scaffoldAssets.commitSafePaths,
|
|
4108
|
-
manualEditors: manualEditorNames(manualEditors),
|
|
4109
|
-
credentialTarget: `bapi:${repoName}`,
|
|
4110
|
-
credentialStorePath,
|
|
4111
|
-
pingUrl: buildPingUrl(baseUrl, repoName),
|
|
4112
|
-
prewarmCommand: buildPrewarmCommandPreview(),
|
|
4113
|
-
...(bootstrapInviteMode
|
|
4114
|
-
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
4115
|
-
: {}),
|
|
4116
|
-
...(selfServeSignupMode ? { selfServeSignup: true } : {}),
|
|
4117
|
-
...(serverRepoResolutionStatus !== undefined
|
|
4118
|
-
? { serverRepoResolutionStatus }
|
|
4119
|
-
: {}),
|
|
4120
|
-
};
|
|
4121
|
-
// ---- --dry-run: preview every step, strictly no side effects ----
|
|
4122
|
-
// Positioned BEFORE the CSPRNG, the pending write, the exchange, the scaffold,
|
|
4123
|
-
// the config writes, the pre-warm, and the spawn: in bootstrap-invite mode a
|
|
4124
|
-
// preview must neither consume the invite NOR leave a pending secret on disk.
|
|
4125
|
-
if (options.dryRun) {
|
|
4126
|
-
for (const line of buildDryRunPreview(plan))
|
|
4127
|
-
log(line);
|
|
4128
|
-
return 0;
|
|
4129
|
-
}
|
|
4425
|
+
// NOTE (BAPI-818, R2): `--dry-run` no longer branches here. It short-circuits
|
|
4426
|
+
// far earlier — immediately after argument parsing and base-URL validation,
|
|
4427
|
+
// before any of the credential/repo/host resolution above — so by this point
|
|
4428
|
+
// the run is unconditionally the LIVE path. See the early return above for
|
|
4429
|
+
// the inert preview.
|
|
4130
4430
|
// ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
|
|
4131
4431
|
// AC-3: with zero configured tools there is nothing to write and no session to open.
|
|
4132
4432
|
// Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
|