@bridge_gpt/mcp-server 0.2.27 → 0.2.29
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 +43 -28
- package/build/agent-registry.js +5 -0
- package/build/commands.generated.js +2 -2
- package/build/conductor/bridge-api-client.js +98 -1
- package/build/conductor/epic-reconcile.js +28 -0
- package/build/conductor/epic-runtime.js +28 -1
- package/build/conductor-bin.js +2 -2
- package/build/connect-github-api.js +9 -0
- package/build/connect-github.js +10 -0
- package/build/doctor.js +2 -2
- package/build/env-flags.js +23 -0
- package/build/index.js +79 -32
- package/build/install-bridge.js +602 -187
- package/build/mcp-host-targets.js +12 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +198 -4
- package/build/start-tickets.js +44 -13
- package/build/tool-surface-gating.js +13 -2
- package/build/version.generated.js +1 -1
- package/package.json +3 -3
package/build/install-bridge.js
CHANGED
|
@@ -30,11 +30,14 @@
|
|
|
30
30
|
*
|
|
31
31
|
* then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
|
|
32
32
|
* remainder: `/install-bridge` config-field derivation → the read-after-write
|
|
33
|
-
* capability report → a
|
|
34
|
-
* chain `/learn-repository`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* concise capability report → a `/learn-repository` recommendation (BAPI-658).
|
|
34
|
+
* It does NOT chain into running `/learn-repository` itself — that stays the
|
|
35
|
+
* human's next explicit invocation — and it never asks about, decides, or
|
|
36
|
+
* claims anything about repository indexing: indexing is entirely automatic,
|
|
37
|
+
* gated server-side by the existing readiness funnel, with no prompt on any
|
|
38
|
+
* path. The fresh session is required because a CLI cannot force the running
|
|
39
|
+
* editor to reload the just-written `.mcp.json`, and field derivation needs an
|
|
40
|
+
* agent runtime the shell does not have.
|
|
38
41
|
*
|
|
39
42
|
* That spawn command embeds the entire agent prompt and runs to multiple KB, which
|
|
40
43
|
* no macOS terminal will accept as one typed line. So it is never typed: the full
|
|
@@ -54,10 +57,23 @@
|
|
|
54
57
|
* repository prompt asks for a new project name rather than an existing
|
|
55
58
|
* registration (see `RepoNamePromptMode`).
|
|
56
59
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
60
|
+
* ORDINARY-ENTRY INVITE RECLASSIFICATION (BAPI-661) — the have-key branch's own
|
|
61
|
+
* credential input (`--api-key`, `BAPI_API_KEY`, or its hidden prompt) is
|
|
62
|
+
* CREDENTIAL-AGNOSTIC: it accepts a full API key OR a bootstrap-invite token,
|
|
63
|
+
* because a user handed only an invite has no way to know it belongs in a
|
|
64
|
+
* different flag. `classifyEnteredCredential` inspects the resolved value
|
|
65
|
+
* immediately after input resolution (before repository-name resolution); a
|
|
66
|
+
* `bapi_inv_…` value flips the branch's `bootstrapInviteMode`/`inviteToken` and
|
|
67
|
+
* the run falls into the SAME bootstrap-invite protocol described below, verbatim.
|
|
68
|
+
* Explicit `--invite`/`BAPI_INVITE` and `--email`/`BAPI_SIGNUP_EMAIL` are never
|
|
69
|
+
* reclassified — they are already unambiguous and remain authoritative.
|
|
70
|
+
*
|
|
71
|
+
* BOOTSTRAP-INVITE MODE (BAPI-606) — one of the exceptions to "this command
|
|
72
|
+
* consumes a key, it does not create one" (the ordinary-entry reclassification
|
|
73
|
+
* above is the other). With `--invite` (or `BAPI_INVITE`, or a reclassified
|
|
74
|
+
* ordinary-entry value) there is no API key yet, so the pre-flight ping of Step 2
|
|
75
|
+
* CANNOT be made: the exchange is what mints the key, and it REPLACES that ping.
|
|
76
|
+
* The order becomes:
|
|
61
77
|
*
|
|
62
78
|
* resolve bootstrap-invite token (no-echo prompt by DEFAULT — the delivered
|
|
63
79
|
* one-liner is secret-free) → resolve repo name → scaffold → generate
|
|
@@ -88,7 +104,7 @@ import readline from "readline";
|
|
|
88
104
|
import { runInit, buildBridgeApiEntry } from "./init.js";
|
|
89
105
|
import { VERSION } from "./version.generated.js";
|
|
90
106
|
import { validateRepoName } from "./bridge-config.js";
|
|
91
|
-
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
107
|
+
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
92
108
|
import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
|
|
93
109
|
import { writeMcpInstallState } from "./mcp-install-state.js";
|
|
94
110
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
@@ -98,7 +114,7 @@ import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPe
|
|
|
98
114
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
99
115
|
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
100
116
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
101
|
-
import {
|
|
117
|
+
import { resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
102
118
|
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
|
|
103
119
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
104
120
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
@@ -125,44 +141,37 @@ export function buildPrewarmCommandPreview() {
|
|
|
125
141
|
}
|
|
126
142
|
/**
|
|
127
143
|
* The natural-language prompt handed to the spawned agent session. It is
|
|
128
|
-
* CONFIGURE-ONLY: it derives and applies configuration, presents the
|
|
129
|
-
* report, and
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* that must appear AFTER the spawned session's capability report. Asking it via
|
|
136
|
-
* the parent's `deps.promptLine` would display the question before the report
|
|
137
|
-
* even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
|
|
138
|
-
* prompts (repository / overwrite / invite confirmation) and is intentionally
|
|
139
|
-
* NOT used for this post-report question.
|
|
144
|
+
* CONFIGURE-ONLY: it derives and applies configuration, presents the concise
|
|
145
|
+
* capability report, and closes with a `/learn-repository` recommendation
|
|
146
|
+
* (BAPI-658). It never chains straight into running /learn-repository itself —
|
|
147
|
+
* that remains the human's next explicit invocation — and it never asks about,
|
|
148
|
+
* decides, or claims anything about repository indexing: indexing is entirely
|
|
149
|
+
* automatic, gated by the existing readiness funnel
|
|
150
|
+
* (`maybe_auto_parse_if_ready`), with no prompt on any path.
|
|
140
151
|
*/
|
|
141
152
|
export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
|
|
142
|
-
"in the install-spawn context so it SKIPS its Stage 8 and Stage
|
|
143
|
-
"closing interaction is the
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"indexing
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
|
|
158
|
-
"continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
|
|
153
|
+
"in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers — this " +
|
|
154
|
+
"session's only closing interaction is the concise capability report and learn recommendation " +
|
|
155
|
+
"below). " +
|
|
156
|
+
"Complete the command's read-after-write concise capability report first: the single 'What Bridge " +
|
|
157
|
+
"can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it " +
|
|
158
|
+
"(Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's " +
|
|
159
|
+
"server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do " +
|
|
160
|
+
"not locally filter, count, or fall back to the complete tool_capabilities catalog. " +
|
|
161
|
+
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
162
|
+
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
163
|
+
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
164
|
+
"After the capability report, recommend /learn-repository as the next step: explain briefly that it " +
|
|
165
|
+
"learns repository-specific architecture, review, testing, correctness, and validation-manual " +
|
|
166
|
+
"configuration by researching the actual codebase. Do NOT run /learn-repository yourself — only " +
|
|
167
|
+
"recommend it; running it is the human's next explicit invocation. " +
|
|
159
168
|
"Never request, echo, or transport any credential — only ever direct the human to that " +
|
|
160
169
|
"integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
|
|
161
170
|
"always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
|
|
162
171
|
"Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
|
|
163
172
|
"End with an explicit summary line stating how many config fields the apply_install_manifest call " +
|
|
164
|
-
"applied (e.g. 'Applied 8 of 9 derived fields')
|
|
165
|
-
"
|
|
173
|
+
"applied (e.g. 'Applied 8 of 9 derived fields') — if 0 fields were applied, say so loudly and " +
|
|
174
|
+
"explain what is still pending.";
|
|
166
175
|
/** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
|
|
167
176
|
export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
|
|
168
177
|
/** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
|
|
@@ -175,8 +184,11 @@ export function getInstallBridgeUsage() {
|
|
|
175
184
|
"",
|
|
176
185
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
177
186
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
178
|
-
"routing credential, then
|
|
179
|
-
"
|
|
187
|
+
"routing credential, then — on a TTY, only after a Y/N consent prompt — opens a",
|
|
188
|
+
"fresh session in your selected tool to derive the remaining config, present a",
|
|
189
|
+
"concise capability report, and recommend /learn-repository. Indexing is never",
|
|
190
|
+
"asked about — it starts automatically once the repository reaches full parse",
|
|
191
|
+
"readiness.",
|
|
180
192
|
"",
|
|
181
193
|
"Run it bare — `install-bridge` with no flags — in a terminal and it asks",
|
|
182
194
|
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
|
|
@@ -186,11 +198,15 @@ export function getInstallBridgeUsage() {
|
|
|
186
198
|
"an interactive terminal keeps the existing deterministic behavior and no prompt.",
|
|
187
199
|
"",
|
|
188
200
|
"Inputs (the only two irreducible ones):",
|
|
189
|
-
" --api-key <key> Bridge API key. Falls back to the
|
|
190
|
-
" then an interactive (no-echo) prompt.
|
|
191
|
-
" Bridge API web UI Security page — this
|
|
192
|
-
" key, it does not create one
|
|
193
|
-
"
|
|
201
|
+
" --api-key <key> Bridge API key OR bootstrap invite. Falls back to the",
|
|
202
|
+
" BAPI_API_KEY env var, then an interactive (no-echo) prompt.",
|
|
203
|
+
" Generate a key in the Bridge API web UI Security page — this",
|
|
204
|
+
" command consumes a key, it does not create one. A value",
|
|
205
|
+
" detected as a bootstrap invite (bapi_inv_…), from any of the",
|
|
206
|
+
" three sources above, is instead redeemed to CREATE a new",
|
|
207
|
+
" project and its first admin key — the same as --invite —",
|
|
208
|
+
" so it skips repository lookup entirely (--email and --invite",
|
|
209
|
+
" remain the explicit, preferred entry points for a new project).",
|
|
194
210
|
" NEVER printed or logged.",
|
|
195
211
|
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
196
212
|
" priority and short-circuit before any network call. When",
|
|
@@ -245,24 +261,35 @@ export function getInstallBridgeUsage() {
|
|
|
245
261
|
` ${HOST_PLATFORM_ORDER.join(", ")}.`,
|
|
246
262
|
" Both --tools=claude-code,codex and",
|
|
247
263
|
" --tools claude-code,codex are accepted. On an",
|
|
248
|
-
" interactive terminal WITHOUT this flag
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
" automatic set (Claude
|
|
253
|
-
" Cursor / Copilot VS Code)
|
|
254
|
-
"
|
|
264
|
+
" interactive terminal WITHOUT this flag the",
|
|
265
|
+
" checklist starts EMPTY — no tool is pre-selected",
|
|
266
|
+
" (not even Claude Code) and you must select at",
|
|
267
|
+
" least one. A non-interactive (non-TTY) run without",
|
|
268
|
+
" --tools writes the legacy automatic set (Claude",
|
|
269
|
+
" Code plus any detected Cursor / Copilot VS Code)",
|
|
270
|
+
" and never opens an agent terminal (it prints",
|
|
271
|
+
" manual continuation instead). --tools= (empty) is",
|
|
272
|
+
" an explicit empty selection: it writes nothing and",
|
|
273
|
+
" launches nothing.",
|
|
255
274
|
" --force Overwrite an existing real BAPI_API_KEY in a",
|
|
256
275
|
" host config (or in the credential store) without",
|
|
257
276
|
" prompting.",
|
|
258
277
|
" --dry-run Preview every step (scaffold targets, config",
|
|
259
278
|
" files + keys with the key REDACTED, ping",
|
|
260
|
-
" target, credential target,
|
|
261
|
-
" without writing, pinging,
|
|
262
|
-
" With --invite it
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
279
|
+
" target, credential target, and the consent-gated",
|
|
280
|
+
" launch outcome) without writing, pinging,",
|
|
281
|
+
" prompting, or spawning anything. With --invite it",
|
|
282
|
+
" also never calls the exchange endpoint and never",
|
|
283
|
+
" generates or stores a secret.",
|
|
284
|
+
" --agent claude|cursor-agent Explicit launch override for the post-install",
|
|
285
|
+
" session — it always wins, and there is NO Claude",
|
|
286
|
+
" default. Without it the launch tool is derived",
|
|
287
|
+
" from your selection: Claude Code opens `claude`,",
|
|
288
|
+
" Cursor opens `cursor-agent`; if several launchable",
|
|
289
|
+
" tools are selected the wizard asks which single one",
|
|
290
|
+
" to open; and a selection whose tools have no",
|
|
291
|
+
" agentic CLI (e.g. Copilot) opens nothing and prints",
|
|
292
|
+
" how to finish configuring later.",
|
|
266
293
|
" -h, --help Show this help.",
|
|
267
294
|
"",
|
|
268
295
|
"Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
|
|
@@ -288,7 +315,10 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
288
315
|
let repo;
|
|
289
316
|
let force = false;
|
|
290
317
|
let dryRun = false;
|
|
291
|
-
|
|
318
|
+
// Undefined until `--agent` is explicitly supplied — install-bridge never falls
|
|
319
|
+
// back to DEFAULT_AGENT_NAME (BAPI-657). An omitted agent is derived from the tool
|
|
320
|
+
// selection downstream; only an explicit `--agent` value lands here.
|
|
321
|
+
let agentName;
|
|
292
322
|
let invite;
|
|
293
323
|
let email;
|
|
294
324
|
let tools;
|
|
@@ -697,8 +727,16 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
697
727
|
};
|
|
698
728
|
}
|
|
699
729
|
/**
|
|
700
|
-
* Resolve the
|
|
701
|
-
* prompt. Fails (secret-free) when none is available and
|
|
730
|
+
* Resolve the ordinary credential entry: `--api-key` → `BAPI_API_KEY` env →
|
|
731
|
+
* interactive no-echo prompt. Fails (secret-free) when none is available and
|
|
732
|
+
* stdin is non-interactive.
|
|
733
|
+
*
|
|
734
|
+
* Despite the name, this is CREDENTIAL-AGNOSTIC (BAPI-661): the returned value may
|
|
735
|
+
* be a full Bridge API key or a bootstrap-invite token (`bapi_inv_…`) — the caller
|
|
736
|
+
* classifies it with {@link classifyEnteredCredential} immediately after this
|
|
737
|
+
* resolves and routes an invite into the existing redemption workflow. This
|
|
738
|
+
* function itself performs no classification; it only resolves which raw string
|
|
739
|
+
* was entered.
|
|
702
740
|
*/
|
|
703
741
|
export async function resolveApiKey(options, deps) {
|
|
704
742
|
if (typeof options.apiKey === "string" && options.apiKey.trim().length > 0) {
|
|
@@ -709,16 +747,20 @@ export async function resolveApiKey(options, deps) {
|
|
|
709
747
|
return { ok: true, value: fromEnv.trim() };
|
|
710
748
|
}
|
|
711
749
|
if (deps.isTTY && deps.promptSecret) {
|
|
712
|
-
const entered = (await deps.promptSecret("Bridge API key (input hidden): ")).trim();
|
|
750
|
+
const entered = (await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();
|
|
713
751
|
if (entered.length > 0) {
|
|
714
752
|
return { ok: true, value: entered };
|
|
715
753
|
}
|
|
716
|
-
return {
|
|
754
|
+
return {
|
|
755
|
+
ok: false,
|
|
756
|
+
error: "No Bridge API key or invite entered. Pass --api-key, set the BAPI_API_KEY environment " +
|
|
757
|
+
"variable, or try the hidden prompt again.",
|
|
758
|
+
};
|
|
717
759
|
}
|
|
718
760
|
return {
|
|
719
761
|
ok: false,
|
|
720
|
-
error: "
|
|
721
|
-
"(no interactive terminal is available to prompt for it).",
|
|
762
|
+
error: "A Bridge API key or invite is required. Pass --api-key or set the BAPI_API_KEY " +
|
|
763
|
+
"environment variable (no interactive terminal is available to prompt for it).",
|
|
722
764
|
};
|
|
723
765
|
}
|
|
724
766
|
/**
|
|
@@ -823,8 +865,8 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
823
865
|
return { kind: "need-key", method: "self-serve" };
|
|
824
866
|
return { kind: "have-key" };
|
|
825
867
|
}
|
|
826
|
-
/** The exact visible text of the bare-TTY onboarding selector (BAPI-626). */
|
|
827
|
-
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Do you have a Bridge API key? [Y/n] ";
|
|
868
|
+
/** The exact visible text of the bare-TTY onboarding selector (BAPI-626, BAPI-661). */
|
|
869
|
+
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Do you have a Bridge API key or invite? [Y/n] ";
|
|
828
870
|
/**
|
|
829
871
|
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
830
872
|
*
|
|
@@ -968,22 +1010,27 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
|
|
|
968
1010
|
/** Stable wording of the interactive tool-selection prompt (BAPI-635). */
|
|
969
1011
|
export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
|
|
970
1012
|
/**
|
|
971
|
-
* Interactive numbered multi-select prompt on stderr (TTY only). Displays
|
|
972
|
-
* option with a checked/unchecked marker seeded
|
|
973
|
-
* comma-separated list of numbers
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1013
|
+
* Interactive numbered multi-select prompt on stderr (TTY only). Displays a
|
|
1014
|
+
* legend and usage line plus each option with a checked/unchecked marker seeded
|
|
1015
|
+
* from `defaults`, accepts a comma-separated list of numbers that TOGGLE the rows
|
|
1016
|
+
* and RE-PROMPT (acceptance is a separate bare Enter), and accepts only a NON-EMPTY
|
|
1017
|
+
* selection on a bare Enter — an empty one prints "Select at least one tool." and
|
|
1018
|
+
* re-prompts (AC-1/AC-3). Reprints on an invalid token rather than corrupting
|
|
1019
|
+
* state. Resolves safely (to the current set, including an empty one) on EOF /
|
|
1020
|
+
* synchronous close so a readline regression can never hang or discard the answer.
|
|
977
1021
|
*/
|
|
978
1022
|
export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
|
|
979
1023
|
return new Promise((resolve) => {
|
|
980
1024
|
const selected = new Set(defaults);
|
|
981
1025
|
const render = () => {
|
|
982
1026
|
output.write(`\n${promptText}\n`);
|
|
1027
|
+
output.write("[x] = selected · [ ] = not selected\n");
|
|
983
1028
|
options.forEach((opt, idx) => {
|
|
984
1029
|
const mark = selected.has(opt.id) ? "[x]" : "[ ]";
|
|
985
1030
|
output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
|
|
986
1031
|
});
|
|
1032
|
+
output.write("Type numbers to toggle, e.g. 1,3 — then Enter.\n");
|
|
1033
|
+
output.write("Press Enter on an empty line to confirm — re-typing a number un-selects it.\n");
|
|
987
1034
|
output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
|
|
988
1035
|
};
|
|
989
1036
|
const rl = readline.createInterface({ input, output });
|
|
@@ -1004,6 +1051,14 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1004
1051
|
rl.question("", (answer) => {
|
|
1005
1052
|
const trimmed = answer.trim();
|
|
1006
1053
|
if (trimmed.length === 0) {
|
|
1054
|
+
// AC-3: a bare Enter accepts ONLY a non-empty selection; an empty one
|
|
1055
|
+
// re-prompts rather than resolving to nothing. EOF/close (handled above)
|
|
1056
|
+
// still resolves the current — possibly empty — set so input can never hang.
|
|
1057
|
+
if (selected.size === 0) {
|
|
1058
|
+
output.write("Select at least one tool.\n");
|
|
1059
|
+
ask();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1007
1062
|
finish();
|
|
1008
1063
|
return;
|
|
1009
1064
|
}
|
|
@@ -1030,7 +1085,9 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1030
1085
|
else
|
|
1031
1086
|
selected.add(opt.id);
|
|
1032
1087
|
}
|
|
1033
|
-
|
|
1088
|
+
// Toggling re-renders the updated checklist and re-prompts; acceptance is a
|
|
1089
|
+
// separate, deliberate bare Enter (AC-1/AC-3) rather than an implicit accept.
|
|
1090
|
+
ask();
|
|
1034
1091
|
});
|
|
1035
1092
|
};
|
|
1036
1093
|
ask();
|
|
@@ -1040,8 +1097,8 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
|
|
|
1040
1097
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
1041
1098
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
1042
1099
|
* back to detection),
|
|
1043
|
-
* 2. interactive multi-select on a TTY (
|
|
1044
|
-
*
|
|
1100
|
+
* 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
|
|
1101
|
+
* starts empty and requires an explicit ≥1 selection),
|
|
1045
1102
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
1046
1103
|
* detected Copilot VS Code and Cursor automatic targets (never Codex or
|
|
1047
1104
|
* Copilot CLI just because a global directory exists).
|
|
@@ -1053,11 +1110,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1053
1110
|
}
|
|
1054
1111
|
const ctx = await buildDetectionContext(deps);
|
|
1055
1112
|
const detected = new Set(detectDefaultPlatforms(ctx));
|
|
1056
|
-
// 2. Interactive multi-select on a TTY.
|
|
1113
|
+
// 2. Interactive multi-select on a TTY. AC-2: no row is pre-selected — the
|
|
1114
|
+
// picker seeds an EMPTY default set (dropping the former forced Claude Code
|
|
1115
|
+
// inclusion and detected-platform seeding) and requires an explicit selection.
|
|
1057
1116
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
1058
1117
|
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
1059
|
-
|
|
1060
|
-
const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
|
|
1118
|
+
const defaults = [];
|
|
1061
1119
|
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
|
|
1062
1120
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
1063
1121
|
}
|
|
@@ -1069,6 +1127,167 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
1069
1127
|
legacy.push("copilot-vscode");
|
|
1070
1128
|
return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
|
|
1071
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Resolve the post-install launch decision from the selected host platforms and an
|
|
1132
|
+
* OPTIONAL explicit `--agent` override (BAPI-657). Pure and total:
|
|
1133
|
+
*
|
|
1134
|
+
* - An explicit `--agent` is an ABSOLUTE override (ratified AC-4): a valid explicit
|
|
1135
|
+
* agent always returns `spawn(agent)`, regardless of which tools were selected —
|
|
1136
|
+
* the flag is honored even for a host outside the selection.
|
|
1137
|
+
* - Otherwise the selection drives it: map each selected host through
|
|
1138
|
+
* {@link agentForPlatform}, drop non-launchable hosts, dedup in registry order,
|
|
1139
|
+
* then return `manual("empty-selection")` for an empty selection,
|
|
1140
|
+
* `manual("no-launchable-agent")` for zero launchable agents, `spawn` for one, or
|
|
1141
|
+
* `choose-one` for more than one.
|
|
1142
|
+
*
|
|
1143
|
+
* Never prompts, logs, resolves an {@link AgentSpec}, or touches the filesystem.
|
|
1144
|
+
*/
|
|
1145
|
+
export function resolveInstallBridgeLaunchDecision(selectedPlatforms, explicitAgent) {
|
|
1146
|
+
if (explicitAgent) {
|
|
1147
|
+
return { kind: "spawn", agent: explicitAgent };
|
|
1148
|
+
}
|
|
1149
|
+
if (selectedPlatforms.length === 0) {
|
|
1150
|
+
return { kind: "manual", reason: "empty-selection" };
|
|
1151
|
+
}
|
|
1152
|
+
const agents = [];
|
|
1153
|
+
for (const id of HOST_PLATFORM_ORDER) {
|
|
1154
|
+
if (!selectedPlatforms.includes(id))
|
|
1155
|
+
continue;
|
|
1156
|
+
const agent = agentForPlatform(id);
|
|
1157
|
+
if (agent && !agents.includes(agent))
|
|
1158
|
+
agents.push(agent);
|
|
1159
|
+
}
|
|
1160
|
+
if (agents.length === 0)
|
|
1161
|
+
return { kind: "manual", reason: "no-launchable-agent" };
|
|
1162
|
+
if (agents.length === 1)
|
|
1163
|
+
return { kind: "spawn", agent: agents[0] };
|
|
1164
|
+
return { kind: "choose-one", agents };
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* The human-facing tool label for a launch agent, derived by scanning the host
|
|
1168
|
+
* registry's {@link HostTargetDefinition.launchAgent} metadata rather than keeping a
|
|
1169
|
+
* duplicate agent→label map (BAPI-657). Returns e.g. `"Claude Code"` for `"claude"`
|
|
1170
|
+
* and `"Cursor"` for `"cursor-agent"`, falling back to the raw agent name if no host
|
|
1171
|
+
* advertises it (never expected for a registered launch agent).
|
|
1172
|
+
*/
|
|
1173
|
+
export function toolLabelForLaunchAgent(agent) {
|
|
1174
|
+
const target = allHostTargets().find((t) => t.launchAgent === agent);
|
|
1175
|
+
return target?.label ?? agent;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
|
|
1179
|
+
* (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
|
|
1180
|
+
* label, asks for an exact numeric choice with NO default, validates the range, and
|
|
1181
|
+
* returns the chosen agent. Returns `null` — never a silent first-candidate default —
|
|
1182
|
+
* for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
|
|
1183
|
+
* input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
|
|
1184
|
+
* it must run only AFTER the multi-select promise has resolved and closed its readline
|
|
1185
|
+
* interface so two readers never contend for stdin.
|
|
1186
|
+
*/
|
|
1187
|
+
export async function chooseInstallBridgeLaunchAgent(agents, deps) {
|
|
1188
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
1189
|
+
return null;
|
|
1190
|
+
const promptLine = deps.promptLine;
|
|
1191
|
+
try {
|
|
1192
|
+
deps.log("");
|
|
1193
|
+
deps.log("More than one selected tool can host the configuration session:");
|
|
1194
|
+
agents.forEach((agent, i) => {
|
|
1195
|
+
deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
|
|
1196
|
+
});
|
|
1197
|
+
// No default: a stray Enter must not silently pick a tool.
|
|
1198
|
+
const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
|
|
1199
|
+
const index = Number(answer);
|
|
1200
|
+
if (!/^\d+$/.test(answer) ||
|
|
1201
|
+
!Number.isInteger(index) ||
|
|
1202
|
+
index < 1 ||
|
|
1203
|
+
index > agents.length) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
return agents[index - 1];
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
// Never surface caught exception text; an unavailable chooser is a no-spawn.
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Shared, copy-pasteable "we didn't open a session for you" guidance so every
|
|
1215
|
+
* no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
|
|
1216
|
+
* consent decline, terminal-spawn failure) emits ONE consistent remediation
|
|
1217
|
+
* (AC-5 / AC-7). Two variants:
|
|
1218
|
+
*
|
|
1219
|
+
* - `"empty-selection"` — no MCP host config was written at all, so it does NOT tell
|
|
1220
|
+
* the user to run `/install-bridge` (there is no configured host to run it in); it
|
|
1221
|
+
* tells them to re-run `install-bridge` and select at least one tool.
|
|
1222
|
+
* - `"configured"` — at least one host WAS configured, so it tells the user to open
|
|
1223
|
+
* the project in an applicable AI coding tool and run `/install-bridge`, plus the
|
|
1224
|
+
* required notice that their Bridge MCP tools stay limited until configuration
|
|
1225
|
+
* completes.
|
|
1226
|
+
*
|
|
1227
|
+
* Condition-specific warning prefixes are added by the caller, never inside here.
|
|
1228
|
+
*
|
|
1229
|
+
* `toolLabels` (BAPI-661) names the actually-selected platform(s) in the
|
|
1230
|
+
* `"configured"` message so "open it in an AI coding tool" becomes "open it in
|
|
1231
|
+
* GitHub Copilot (VS Code)" when that is what was selected — an empty array (the
|
|
1232
|
+
* explicit empty-selection call site, or any caller with nothing to name) falls
|
|
1233
|
+
* back to the prior generic "an AI coding tool" wording unchanged.
|
|
1234
|
+
*/
|
|
1235
|
+
export function buildManualInstallBridgeContinuation(kind, toolLabels) {
|
|
1236
|
+
if (kind === "empty-selection") {
|
|
1237
|
+
return [
|
|
1238
|
+
"No AI coding tools were configured, so nothing was set up for this project.",
|
|
1239
|
+
"Re-run install-bridge and select at least one tool to configure it.",
|
|
1240
|
+
].join("\n");
|
|
1241
|
+
}
|
|
1242
|
+
const toolPhrase = formatToolLabelPhrase(toolLabels);
|
|
1243
|
+
return [
|
|
1244
|
+
`To finish configuring this project, open it in ${toolPhrase} that has the`,
|
|
1245
|
+
"Bridge MCP server configured and run /install-bridge.",
|
|
1246
|
+
"Until the project is configured, your Bridge MCP tools stay limited.",
|
|
1247
|
+
].join("\n");
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Format 0, 1, or many tool labels into a readable phrase for the continuation
|
|
1251
|
+
* message. Empty falls back to the generic "an AI coding tool"; one label is used
|
|
1252
|
+
* bare; two or more join with a serial (Oxford) comma and "and".
|
|
1253
|
+
*/
|
|
1254
|
+
function formatToolLabelPhrase(labels) {
|
|
1255
|
+
if (labels.length === 0)
|
|
1256
|
+
return "an AI coding tool";
|
|
1257
|
+
if (labels.length === 1)
|
|
1258
|
+
return labels[0];
|
|
1259
|
+
if (labels.length === 2)
|
|
1260
|
+
return `${labels[0]} and ${labels[1]}`;
|
|
1261
|
+
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
1262
|
+
}
|
|
1263
|
+
/** The exact consent-prompt prefix, shared with tests so the copy cannot drift. */
|
|
1264
|
+
export const INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX = "Bridge can configure and set up this project for you automatically. Open a ";
|
|
1265
|
+
/**
|
|
1266
|
+
* Ask for explicit TTY consent immediately before opening the selected tool (AC-7).
|
|
1267
|
+
* Returns `"spawn"` only when a live terminal user affirmatively agrees; returns
|
|
1268
|
+
* `"no-spawn"` WITHOUT prompting on a non-TTY run or a missing `promptLine` seam, and
|
|
1269
|
+
* on an `n`/`no` answer. Mirrors {@link offerGithubConnection}'s Y/N semantics: a
|
|
1270
|
+
* blank line, `y`, or `yes` accept; `n` or `no` decline. A prompt that throws safely
|
|
1271
|
+
* declines without surfacing the caught exception text. Uses only the per-call
|
|
1272
|
+
* `promptLine` seam, so it must run AFTER the tool-selection, chooser, and GitHub
|
|
1273
|
+
* flows have closed their readline interfaces — never while another prompt is active.
|
|
1274
|
+
*/
|
|
1275
|
+
export async function requestInstallBridgeLaunchConsent(toolLabel, deps) {
|
|
1276
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
1277
|
+
return "no-spawn";
|
|
1278
|
+
try {
|
|
1279
|
+
const answer = (await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `))
|
|
1280
|
+
.trim()
|
|
1281
|
+
.toLowerCase();
|
|
1282
|
+
if (answer === "n" || answer === "no")
|
|
1283
|
+
return "no-spawn";
|
|
1284
|
+
return "spawn";
|
|
1285
|
+
}
|
|
1286
|
+
catch {
|
|
1287
|
+
// Never surface caught exception text; treat a prompt failure as a decline.
|
|
1288
|
+
return "no-spawn";
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1072
1291
|
/**
|
|
1073
1292
|
* Build a registry detection context from install deps. install-bridge has only
|
|
1074
1293
|
* async `stat`, but the registry's `detect` callbacks are synchronous, so we
|
|
@@ -1119,6 +1338,17 @@ function hostConfigTargetsForPlatforms(platforms) {
|
|
|
1119
1338
|
.filter((t) => t.scope === "project" && t.format === "json")
|
|
1120
1339
|
.map((t) => ({ relPath: t.relPath, topLevelKey: t.topLevelKey }));
|
|
1121
1340
|
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Derive the human-facing labels for a set of selected platforms, in registry
|
|
1343
|
+
* (`HOST_PLATFORM_ORDER`) order rather than the caller's input order, so a
|
|
1344
|
+
* continuation message names selected tools deterministically (BAPI-661).
|
|
1345
|
+
* Deduplicates by platform ID and reads every label from `MCP_HOST_TARGETS` —
|
|
1346
|
+
* never a second, hand-maintained label map.
|
|
1347
|
+
*/
|
|
1348
|
+
export function labelsForPlatforms(platforms) {
|
|
1349
|
+
const set = new Set(platforms);
|
|
1350
|
+
return HOST_PLATFORM_ORDER.filter((id) => set.has(id)).map((id) => MCP_HOST_TARGETS[id].label);
|
|
1351
|
+
}
|
|
1122
1352
|
/** Resolve which project-local host configs to write, mirroring runInit detection. */
|
|
1123
1353
|
async function resolveHostConfigTargets(deps) {
|
|
1124
1354
|
const targets = [
|
|
@@ -1279,9 +1509,19 @@ export function buildPingUrl(baseUrl, repoName) {
|
|
|
1279
1509
|
return url.toString();
|
|
1280
1510
|
}
|
|
1281
1511
|
/**
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1512
|
+
* Fixed, secret-free fallback for an HTTP 403 whose body carries no usable
|
|
1513
|
+
* `detail` (absent, malformed, unreadable, or a non-string `detail`). Deliberately
|
|
1514
|
+
* does NOT claim the key is invalid or expired — a 403 means the credential was
|
|
1515
|
+
* understood but denied access, which is a different (and often more actionable)
|
|
1516
|
+
* failure than an invalid/expired key.
|
|
1517
|
+
*/
|
|
1518
|
+
const CONNECTIVITY_ACCESS_DENIED_FALLBACK = "The Bridge API denied access to this repository (HTTP 403). Verify the repo_name and that this " +
|
|
1519
|
+
"credential is authorized for it.";
|
|
1520
|
+
/**
|
|
1521
|
+
* Verify connectivity. Distinguishes a rejected/expired credential (401) from a
|
|
1522
|
+
* repository-specific access denial (403, BAPI-661) from an unknown repo /
|
|
1523
|
+
* not-found (404) where the response allows. The key is sent in the `X-API-Key`
|
|
1524
|
+
* header and NEVER appears in any returned message.
|
|
1285
1525
|
*/
|
|
1286
1526
|
export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
1287
1527
|
const url = buildPingUrl(baseUrl, repoName);
|
|
@@ -1306,7 +1546,7 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
1306
1546
|
}
|
|
1307
1547
|
if (resp.ok)
|
|
1308
1548
|
return { ok: true };
|
|
1309
|
-
if (resp.status === 401
|
|
1549
|
+
if (resp.status === 401) {
|
|
1310
1550
|
return {
|
|
1311
1551
|
ok: false,
|
|
1312
1552
|
message: `The Bridge API rejected the credential (HTTP ${resp.status}). The API key may be invalid or expired ` +
|
|
@@ -1314,6 +1554,27 @@ export async function verifyConnectivity(deps, baseUrl, repoName, apiKey) {
|
|
|
1314
1554
|
"permission error.)",
|
|
1315
1555
|
};
|
|
1316
1556
|
}
|
|
1557
|
+
if (resp.status === 403) {
|
|
1558
|
+
// BAPI-661: a 403 means the credential was understood but access to THIS repo
|
|
1559
|
+
// was denied — the server's `detail` (e.g. "repository '<name>' is not
|
|
1560
|
+
// registered; verify the repo_name") is a safe, specific, non-secret message
|
|
1561
|
+
// that is far more actionable than the blanket invalid-key diagnosis 401 uses.
|
|
1562
|
+
// Only a nonblank string `detail` is ever surfaced; any other shape (missing
|
|
1563
|
+
// body, invalid JSON, a non-string/blank detail, or a body-read exception)
|
|
1564
|
+
// collapses to the fixed fallback so nothing unvalidated ever reaches the user.
|
|
1565
|
+
let detail;
|
|
1566
|
+
try {
|
|
1567
|
+
const body = (await resp.json());
|
|
1568
|
+
detail = body?.detail;
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
return { ok: false, message: CONNECTIVITY_ACCESS_DENIED_FALLBACK };
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof detail === "string" && detail.trim().length > 0) {
|
|
1574
|
+
return { ok: false, message: detail.trim() };
|
|
1575
|
+
}
|
|
1576
|
+
return { ok: false, message: CONNECTIVITY_ACCESS_DENIED_FALLBACK };
|
|
1577
|
+
}
|
|
1317
1578
|
if (resp.status === 404) {
|
|
1318
1579
|
return {
|
|
1319
1580
|
ok: false,
|
|
@@ -1483,6 +1744,21 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
1483
1744
|
* treated as a `failed` mint rather than fed into the redemption path.
|
|
1484
1745
|
*/
|
|
1485
1746
|
export const BOOTSTRAP_INVITE_TOKEN_PREFIX = "bapi_inv_";
|
|
1747
|
+
/**
|
|
1748
|
+
* Classify a value entered through the ORDINARY credential input (`--api-key`,
|
|
1749
|
+
* `BAPI_API_KEY`, or the have-key hidden prompt) as a bootstrap-invite token or a
|
|
1750
|
+
* full API key (BAPI-661). This lets a user who was handed an invite but reaches
|
|
1751
|
+
* for the ordinary key input still get redeemed correctly, instead of the invite
|
|
1752
|
+
* being sent as an (invalid) API key to repository resolution / connectivity.
|
|
1753
|
+
*
|
|
1754
|
+
* Pure and total: trims first, then does a case-sensitive prefix check against
|
|
1755
|
+
* {@link BOOTSTRAP_INVITE_TOKEN_PREFIX}. Blank and every non-matching value is
|
|
1756
|
+
* `"api-key"` — the existing resolvers already reject a blank value, so this
|
|
1757
|
+
* function does not need to special-case it. Never logs or returns the value.
|
|
1758
|
+
*/
|
|
1759
|
+
export function classifyEnteredCredential(value) {
|
|
1760
|
+
return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX) ? "invite" : "api-key";
|
|
1761
|
+
}
|
|
1486
1762
|
/** The self-serve mint endpoint: `<base>/setup/bootstrap/self-serve`. No query string — ever. */
|
|
1487
1763
|
export function buildSelfServeMintUrl(baseUrl) {
|
|
1488
1764
|
return `${baseUrl.replace(/\/+$/, "")}/setup/bootstrap/self-serve`;
|
|
@@ -1597,7 +1873,7 @@ export function buildDryRunPreview(plan) {
|
|
|
1597
1873
|
`Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
|
|
1598
1874
|
`Base URL (ping): ${plan.baseUrl}`,
|
|
1599
1875
|
`Docs dir: ${plan.docsDir}`,
|
|
1600
|
-
`Agent: ${plan.
|
|
1876
|
+
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
1601
1877
|
"",
|
|
1602
1878
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1603
1879
|
`Step 2 — connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,
|
|
@@ -1612,26 +1888,66 @@ export function buildDryRunPreview(plan) {
|
|
|
1612
1888
|
...buildLaunchStepPreview(plan),
|
|
1613
1889
|
];
|
|
1614
1890
|
}
|
|
1891
|
+
/**
|
|
1892
|
+
* One-line description of the planned launch agent for the dry-run header (BAPI-657).
|
|
1893
|
+
* Never claims an unconditional Claude spawn: it reflects the discriminated launch
|
|
1894
|
+
* outcome — a single consent-gated tool, a choose-one among launchable tools, or a
|
|
1895
|
+
* no-automatic-launch manual outcome.
|
|
1896
|
+
*/
|
|
1897
|
+
function describePlannedLaunchAgent(launch) {
|
|
1898
|
+
if (launch.kind === "spawn") {
|
|
1899
|
+
return `${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) — opens only after Y/N consent`;
|
|
1900
|
+
}
|
|
1901
|
+
if (launch.kind === "choose-one") {
|
|
1902
|
+
const labels = launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
|
|
1903
|
+
return `choose one of ${labels} — the chosen tool opens only after Y/N consent`;
|
|
1904
|
+
}
|
|
1905
|
+
return launch.reason === "empty-selection"
|
|
1906
|
+
? "none — no tools selected"
|
|
1907
|
+
: "none — no launchable tool selected; manual continuation is printed";
|
|
1908
|
+
}
|
|
1615
1909
|
/**
|
|
1616
1910
|
* The Step 5 preview lines, shared by both previews so the launch description
|
|
1617
|
-
* cannot drift between the have-key and need-key flows.
|
|
1911
|
+
* cannot drift between the have-key and need-key flows (BAPI-657). The Step 5 body
|
|
1912
|
+
* is dispatched on the discriminated launch outcome: a consent-gated single-tool
|
|
1913
|
+
* spawn (with the full secret-free command shown), a consent-gated choose-one, or a
|
|
1914
|
+
* no-automatic-launch manual continuation — never an unconditional Claude spawn.
|
|
1618
1915
|
*
|
|
1619
|
-
* A --dry-run never writes the launch script,
|
|
1620
|
-
*
|
|
1621
|
-
* because the command is what the user is previewing and it is secret-free.
|
|
1916
|
+
* A --dry-run never writes the launch script, prompts for consent, or spawns, so
|
|
1917
|
+
* these lines describe the outcome rather than performing it.
|
|
1622
1918
|
*/
|
|
1623
1919
|
function buildLaunchStepPreview(plan) {
|
|
1624
|
-
|
|
1920
|
+
const githubLines = [
|
|
1625
1921
|
// BAPI-631: described, never performed in --dry-run — a preview must not open a
|
|
1626
1922
|
// browser or reach the network. It is also strictly optional, so it carries no step
|
|
1627
1923
|
// number of its own and never changes the 5-step count.
|
|
1628
1924
|
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
1629
1925
|
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
1630
1926
|
" interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1927
|
+
];
|
|
1928
|
+
if (plan.launch.kind === "spawn") {
|
|
1929
|
+
return [
|
|
1930
|
+
...githubLines,
|
|
1931
|
+
`Step 5 — agent session (${toolLabelForLaunchAgent(plan.launch.agent)}): on a TTY the wizard first asks`,
|
|
1932
|
+
" 'Open a … session to do that now? (Y/n)'; only on consent is the full command below",
|
|
1933
|
+
" stored in a restricted launch script (mode 0600, under the system temp dir) and a short",
|
|
1934
|
+
" sourced runner spawned (the script itself is NOT written in --dry-run):",
|
|
1935
|
+
` ${plan.launch.spawnCommand}`,
|
|
1936
|
+
];
|
|
1937
|
+
}
|
|
1938
|
+
if (plan.launch.kind === "choose-one") {
|
|
1939
|
+
const labels = plan.launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
|
|
1940
|
+
return [
|
|
1941
|
+
...githubLines,
|
|
1942
|
+
`Step 5 — agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`,
|
|
1943
|
+
" asks which single tool to open, then asks Y/N consent before spawning that one session.",
|
|
1944
|
+
];
|
|
1945
|
+
}
|
|
1946
|
+
return [
|
|
1947
|
+
...githubLines,
|
|
1948
|
+
plan.launch.reason === "empty-selection"
|
|
1949
|
+
? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
|
|
1950
|
+
: "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).",
|
|
1635
1951
|
];
|
|
1636
1952
|
}
|
|
1637
1953
|
/**
|
|
@@ -1668,7 +1984,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
1668
1984
|
repoLine,
|
|
1669
1985
|
`Base URL: ${plan.baseUrl}`,
|
|
1670
1986
|
`Docs dir: ${plan.docsDir}`,
|
|
1671
|
-
`Agent: ${plan.
|
|
1987
|
+
`Agent: ${describePlannedLaunchAgent(plan.launch)}`,
|
|
1672
1988
|
"",
|
|
1673
1989
|
"Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
|
|
1674
1990
|
...selfServeStep,
|
|
@@ -1788,8 +2104,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1788
2104
|
return 1;
|
|
1789
2105
|
}
|
|
1790
2106
|
const branch = branchResult.branch;
|
|
1791
|
-
|
|
1792
|
-
|
|
2107
|
+
// Mutable (BAPI-661): an ordinary have-key credential that classifies as a
|
|
2108
|
+
// bootstrap invite (see the have-key arm below) flips `bootstrapInviteMode` to
|
|
2109
|
+
// true AFTER branch selection, before repository-name resolution — everything
|
|
2110
|
+
// downstream that branches on these two flags then follows the existing
|
|
2111
|
+
// bootstrap-invite protocol automatically. Explicit `--invite`/`BAPI_INVITE` and
|
|
2112
|
+
// `--email`/`BAPI_SIGNUP_EMAIL` never pass through this reclassification.
|
|
2113
|
+
let bootstrapInviteMode = branch.kind === "need-key";
|
|
2114
|
+
let selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
|
|
1793
2115
|
// ---- Resolve inputs (may prompt when interactive) ----
|
|
1794
2116
|
// Resolve the credential/entry input first so a fully-empty non-interactive
|
|
1795
2117
|
// invocation fails with the (more relevant) missing-input message before the repo
|
|
@@ -1822,7 +2144,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1822
2144
|
errorLog(`Error: ${keyResult.error}`);
|
|
1823
2145
|
return 1;
|
|
1824
2146
|
}
|
|
1825
|
-
|
|
2147
|
+
// BAPI-661: an invite pasted into the ordinary credential input (flag, env, or
|
|
2148
|
+
// hidden prompt) is redeemed like an explicit --invite, not sent as an API key
|
|
2149
|
+
// to repository resolution / connectivity. Reclassifying BEFORE that
|
|
2150
|
+
// resolution is what lets the rest of the function reuse the existing
|
|
2151
|
+
// bootstrap-invite protocol verbatim.
|
|
2152
|
+
if (classifyEnteredCredential(keyResult.value) === "invite") {
|
|
2153
|
+
inviteToken = keyResult.value.trim();
|
|
2154
|
+
apiKey = "";
|
|
2155
|
+
bootstrapInviteMode = true;
|
|
2156
|
+
selfServeSignupMode = false;
|
|
2157
|
+
}
|
|
2158
|
+
else {
|
|
2159
|
+
apiKey = keyResult.value;
|
|
2160
|
+
}
|
|
1826
2161
|
}
|
|
1827
2162
|
// baseUrl is resolved BEFORE repository input because the have-key branch needs
|
|
1828
2163
|
// it for the server-resolution request.
|
|
@@ -1869,7 +2204,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1869
2204
|
else {
|
|
1870
2205
|
// Feature-detect + degrade: 404 (old server), 409 (unresolved/ambiguous/
|
|
1871
2206
|
// client-scoped), and network/other errors all fall back to the existing
|
|
1872
|
-
// local resolution WITHOUT a cause-specific message or leaked detail
|
|
2207
|
+
// local resolution — WITHOUT a cause-specific message or leaked detail, but
|
|
2208
|
+
// (BAPI-661) WITH a visible signal that the fallback is happening, so a
|
|
2209
|
+
// guessed name is never confirmed silently as if it had been verified.
|
|
2210
|
+
log("Couldn't auto-resolve your repo from the key; falling back to a guessed name — confirm " +
|
|
2211
|
+
"it matches the setup UI.");
|
|
1873
2212
|
const repoResult = await resolveRepoName(options, deps, "existing-registration");
|
|
1874
2213
|
if (!repoResult.ok) {
|
|
1875
2214
|
errorLog(`Error: ${repoResult.error}`);
|
|
@@ -1879,33 +2218,58 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1879
2218
|
}
|
|
1880
2219
|
}
|
|
1881
2220
|
}
|
|
1882
|
-
const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
|
|
1883
|
-
const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
1884
2221
|
const credentialStorePath = getPrimaryCredentialStorePath({
|
|
1885
2222
|
env: deps.env,
|
|
1886
2223
|
homedir: deps.homedir,
|
|
1887
2224
|
});
|
|
1888
|
-
// BAPI-635: resolve the AI-coding-tool selection
|
|
1889
|
-
//
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
2225
|
+
// BAPI-635: resolve the AI-coding-tool selection FIRST — BAPI-657 makes this
|
|
2226
|
+
// selection (not a hidden --agent default) drive which post-install session opens.
|
|
2227
|
+
// Explicit --tools, then the TTY multi-select, then the legacy non-TTY automatic
|
|
2228
|
+
// set. The selected project JSON targets drive the read-merge-write path; selected
|
|
2229
|
+
// global (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
|
|
1892
2230
|
// registry-driven emitter after the connectivity check.
|
|
1893
2231
|
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
1894
2232
|
const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
|
|
1895
2233
|
// Read-only detection (safe in dry-run) of global-config editors we can't write.
|
|
1896
2234
|
const manualEditors = await detectManualEditors(deps);
|
|
2235
|
+
// BAPI-657: the pure, selection-driven launch decision. An explicit --agent is an
|
|
2236
|
+
// absolute override (ratified AC-4); otherwise the selected tools map to an agent
|
|
2237
|
+
// (Claude Code → claude, Cursor → cursor-agent), yielding spawn / choose-one /
|
|
2238
|
+
// manual. Performs no prompting, resolveAgentSpec, or I/O.
|
|
2239
|
+
const launchDecision = resolveInstallBridgeLaunchDecision(selectedPlatforms, options.agentName);
|
|
2240
|
+
// Derive the plan's launch data. For a single-agent spawn we resolve the spec and
|
|
2241
|
+
// build the (secret-free) command now so the dry-run preview can show it; a registry
|
|
2242
|
+
// miss here is a hard error — install-bridge NEVER falls back to DEFAULT_AGENT_NAME.
|
|
2243
|
+
let planLaunch;
|
|
2244
|
+
if (launchDecision.kind === "spawn") {
|
|
2245
|
+
const spec = resolveAgentSpec(launchDecision.agent);
|
|
2246
|
+
if (!spec) {
|
|
2247
|
+
errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
|
|
2248
|
+
return 1;
|
|
2249
|
+
}
|
|
2250
|
+
planLaunch = {
|
|
2251
|
+
kind: "spawn",
|
|
2252
|
+
agent: launchDecision.agent,
|
|
2253
|
+
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
else if (launchDecision.kind === "choose-one") {
|
|
2257
|
+
planLaunch = { kind: "choose-one", agents: launchDecision.agents };
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
planLaunch = { kind: "manual", reason: launchDecision.reason };
|
|
2261
|
+
}
|
|
1897
2262
|
const plan = {
|
|
1898
2263
|
repoName,
|
|
1899
2264
|
baseUrl,
|
|
1900
2265
|
docsDir,
|
|
1901
|
-
|
|
2266
|
+
launch: planLaunch,
|
|
1902
2267
|
configTargets: targets.map((t) => t.relPath),
|
|
1903
2268
|
manualEditors: manualEditorNames(manualEditors),
|
|
1904
2269
|
credentialTarget: `bapi:${repoName}`,
|
|
1905
2270
|
credentialStorePath,
|
|
1906
2271
|
pingUrl: buildPingUrl(baseUrl, repoName),
|
|
1907
2272
|
prewarmCommand: buildPrewarmCommandPreview(),
|
|
1908
|
-
spawnCommand,
|
|
1909
2273
|
...(bootstrapInviteMode
|
|
1910
2274
|
? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
|
|
1911
2275
|
: {}),
|
|
@@ -1921,46 +2285,74 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1921
2285
|
log(line);
|
|
1922
2286
|
return 0;
|
|
1923
2287
|
}
|
|
1924
|
-
// ----
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
// explicitly calls out. Failing first means nothing is half-done.
|
|
1934
|
-
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
|
|
1935
|
-
if (!materialized.ok) {
|
|
1936
|
-
errorLog(`Error: ${materialized.error}`);
|
|
1937
|
-
return 1;
|
|
2288
|
+
// ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
|
|
2289
|
+
// AC-3: with zero configured tools there is nothing to write and no session to open.
|
|
2290
|
+
// Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
|
|
2291
|
+
// bare `--tools=` (or an EOF-resolved picker) cleanly does nothing but guide a re-run.
|
|
2292
|
+
if (planLaunch.kind === "manual" && planLaunch.reason === "empty-selection") {
|
|
2293
|
+
// Explicit empty selection: no labels to name, so the generic fallback is
|
|
2294
|
+
// intentional here — pass [] rather than `labelsForPlatforms(selectedPlatforms)`.
|
|
2295
|
+
log(buildManualInstallBridgeContinuation("empty-selection", []));
|
|
2296
|
+
return 0;
|
|
1938
2297
|
}
|
|
1939
|
-
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2298
|
+
// ---- Finalize the launch agent for spawn-capable decisions ----
|
|
2299
|
+
// choose-one: pick exactly one via the numbered chooser (TTY only); a non-TTY run or
|
|
2300
|
+
// a failed/aborted chooser degrades to the manual continuation (finalAgentName stays
|
|
2301
|
+
// null). This runs AFTER the multi-select readline closed and BEFORE the GitHub /
|
|
2302
|
+
// consent flows, so no two stdin readers ever overlap.
|
|
2303
|
+
let finalAgentName = null;
|
|
2304
|
+
let finalSpawnCommand = null;
|
|
2305
|
+
if (planLaunch.kind === "spawn") {
|
|
2306
|
+
finalAgentName = planLaunch.agent;
|
|
2307
|
+
finalSpawnCommand = planLaunch.spawnCommand;
|
|
2308
|
+
}
|
|
2309
|
+
else if (planLaunch.kind === "choose-one") {
|
|
2310
|
+
const chosen = await chooseInstallBridgeLaunchAgent(planLaunch.agents, deps);
|
|
2311
|
+
if (chosen) {
|
|
2312
|
+
const spec = resolveAgentSpec(chosen);
|
|
2313
|
+
if (!spec) {
|
|
2314
|
+
errorLog(`Error: no launch agent is registered for '${chosen}'.`);
|
|
2315
|
+
return 1;
|
|
2316
|
+
}
|
|
2317
|
+
finalAgentName = chosen;
|
|
2318
|
+
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
|
|
2319
|
+
}
|
|
2320
|
+
// chosen === null → non-TTY / unavailable / invalid → manual continuation below.
|
|
2321
|
+
}
|
|
2322
|
+
// planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
|
|
2323
|
+
// ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
|
|
2324
|
+
// spawn-capable path. A manual/no-launchable/chooser-declined path opens no session,
|
|
2325
|
+
// so it neither materializes a launch script nor warms a bucket it will never use.
|
|
2326
|
+
//
|
|
2327
|
+
// Materialization runs HERE, before the scaffold/mint/credential/config writes,
|
|
2328
|
+
// because a command that cannot be launched safely is a whole-run failure, not a
|
|
2329
|
+
// Step 5 warning: the deterministic setup would otherwise complete and leave the user
|
|
2330
|
+
// in the "configured but never configured by the agent" state. Failing first means
|
|
2331
|
+
// nothing is half-done. The @VERSION-pinned launcher lives in a DIFFERENT _npx bucket
|
|
2332
|
+
// than the @latest bucket this invocation warmed, so the pre-warm (fire-and-forget,
|
|
2333
|
+
// strictly fail-open) overlaps the ping/exchange/scaffold/writes and is awaited just
|
|
2334
|
+
// before the consent-gated spawn.
|
|
2335
|
+
let launchCommand = null;
|
|
2336
|
+
let prewarmPromise = null;
|
|
2337
|
+
if (finalAgentName && finalSpawnCommand) {
|
|
2338
|
+
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", finalSpawnCommand);
|
|
2339
|
+
if (!materialized.ok) {
|
|
2340
|
+
errorLog(`Error: ${materialized.error}`);
|
|
2341
|
+
return 1;
|
|
2342
|
+
}
|
|
2343
|
+
launchCommand = materialized.command;
|
|
2344
|
+
// Independent final guard on the line ACTUALLY handed to the terminal — not on the
|
|
2345
|
+
// original command. It catches the two ways a "successful" materialization can still
|
|
2346
|
+
// be unlaunchable: no writer seam (inline command preserved verbatim), and a runner
|
|
2347
|
+
// whose script path is unexpectedly long.
|
|
2348
|
+
if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
|
|
2349
|
+
errorLog("Error: the agent session command is too long to send to the terminal safely. " +
|
|
2350
|
+
"Check that the system temporary directory is writable so the launch script can be used.");
|
|
2351
|
+
return 1;
|
|
2352
|
+
}
|
|
2353
|
+
log(" pre-warming the version-pinned launcher bucket (in the background)…");
|
|
2354
|
+
prewarmPromise = deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
|
|
1948
2355
|
}
|
|
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);
|
|
1964
2356
|
const credentialWriteDeps = {
|
|
1965
2357
|
env: deps.env,
|
|
1966
2358
|
homedir: deps.homedir,
|
|
@@ -2307,47 +2699,70 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
2307
2699
|
// spawn would put two prompts on the same terminal at once.
|
|
2308
2700
|
await offerGithubConnection(repoName, deps, log);
|
|
2309
2701
|
// ---- Step 3b (await) — settle the background pre-warm before the session opens ----
|
|
2310
|
-
// The warm was kicked off
|
|
2311
|
-
// HERE so the bucket is settled before the spawned
|
|
2312
|
-
// AFTER all the work it overlapped with
|
|
2313
|
-
//
|
|
2314
|
-
|
|
2315
|
-
if (
|
|
2316
|
-
|
|
2317
|
-
|
|
2702
|
+
// The warm was kicked off (only on a spawn-capable path) right after the
|
|
2703
|
+
// launch-command guards; await it HERE so the bucket is settled before the spawned
|
|
2704
|
+
// session's first MCP launch, but AFTER all the work it overlapped with — and BEFORE
|
|
2705
|
+
// the consent prompt, so no spawn ever races an in-flight warm. Strictly fail-open —
|
|
2706
|
+
// the exit code is never affected. Skipped entirely when no session will be opened.
|
|
2707
|
+
if (prewarmPromise) {
|
|
2708
|
+
const prewarm = await prewarmPromise;
|
|
2709
|
+
if (prewarm.ok) {
|
|
2710
|
+
log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
|
|
2711
|
+
log(` ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2712
|
+
}
|
|
2713
|
+
else {
|
|
2714
|
+
// Escalate to a stronger, still secret-free warning when pre-warm didn't
|
|
2715
|
+
// complete cleanly — but the install itself still succeeds (exit unaffected).
|
|
2716
|
+
errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
|
|
2717
|
+
`The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
|
|
2718
|
+
}
|
|
2318
2719
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2720
|
+
// ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
|
|
2721
|
+
// AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
|
|
2722
|
+
// resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
|
|
2723
|
+
// selection, a non-TTY run, or a declined/aborted chooser all skip the spawn and fall
|
|
2724
|
+
// through to the shared /install-bridge continuation + limited-tools notice below — so
|
|
2725
|
+
// every no-session outcome gets identical, accurate remediation.
|
|
2726
|
+
if (finalAgentName && launchCommand) {
|
|
2727
|
+
const consent = await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName), deps);
|
|
2728
|
+
if (consent === "spawn") {
|
|
2729
|
+
log(`Step 5/5 — opening a ${finalAgentName} session for /install-bridge configuration + concise capability report…`);
|
|
2730
|
+
const terminal = detectTerminal(undefined, deps.env);
|
|
2731
|
+
// Only the validated short runner reaches the terminal — never the inline prompt.
|
|
2732
|
+
// The branded "Bridge Install" title labels the tab/badge instead of "install
|
|
2733
|
+
// Implementation" (AC-6).
|
|
2734
|
+
const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
|
|
2735
|
+
key: "install",
|
|
2736
|
+
worktreePath: deps.cwd,
|
|
2737
|
+
title: "Bridge Install",
|
|
2738
|
+
});
|
|
2739
|
+
if (!spawnResult.ok) {
|
|
2740
|
+
// Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
|
|
2741
|
+
// succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
|
|
2742
|
+
// deterministic setup is complete, so this is a non-fatal warning (exit 0), with
|
|
2743
|
+
// the same shared continuation so the remediation is consistent.
|
|
2744
|
+
errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`);
|
|
2745
|
+
log(buildManualInstallBridgeContinuation("configured", labelsForPlatforms(selectedPlatforms)));
|
|
2746
|
+
return 0;
|
|
2747
|
+
}
|
|
2748
|
+
log("");
|
|
2749
|
+
log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying ` +
|
|
2750
|
+
"configuration, presenting the concise capability report, and recommending /learn-repository.");
|
|
2751
|
+
log("NOTE: the install is not finished until that session's apply reports applied fields — " +
|
|
2752
|
+
"it will pause to ask you to approve the project description. Indexing starts automatically " +
|
|
2753
|
+
"once the repository reaches full parse readiness — there is no indexing question to answer. " +
|
|
2754
|
+
"Verify afterwards on the project's Get Started page (install status panel) or via the " +
|
|
2755
|
+
"session's 'Applied N of M' summary.");
|
|
2756
|
+
return 0;
|
|
2757
|
+
}
|
|
2758
|
+
// Declined (or non-TTY suppression): the deterministic setup is complete and durable.
|
|
2759
|
+
log(buildManualInstallBridgeContinuation("configured", labelsForPlatforms(selectedPlatforms)));
|
|
2343
2760
|
return 0;
|
|
2344
2761
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
"'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
|
|
2351
|
-
"(install status panel) or via the session's 'Applied N of M' summary.");
|
|
2762
|
+
// No launchable session (a no-launchable selection, or a non-TTY / declined chooser):
|
|
2763
|
+
// finalAgentName is null, so nothing was materialized. The configured hosts are
|
|
2764
|
+
// durable — print the shared continuation + limited-tools note (never the
|
|
2765
|
+
// empty-selection variant, which already returned above).
|
|
2766
|
+
log(buildManualInstallBridgeContinuation("configured", labelsForPlatforms(selectedPlatforms)));
|
|
2352
2767
|
return 0;
|
|
2353
2768
|
}
|