@bridge_gpt/mcp-server 0.2.24 → 0.2.25
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 +39 -19
- package/build/commands.generated.js +3 -3
- package/build/conductor/epic-reconcile.js +7 -1
- package/build/conductor/epic-runtime.js +5 -0
- package/build/connect-github-api.js +365 -0
- package/build/connect-github.js +415 -0
- package/build/decision-page-schema.js +34 -5
- package/build/decision-page-template.js +117 -35
- package/build/index.js +1124 -368
- package/build/install-bridge.js +278 -34
- package/build/install-doctor.js +64 -0
- package/build/pipelines.generated.js +122 -128
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +48 -13
- package/build/version.generated.js +1 -1
- package/docs/install/github-app.md +80 -17
- package/package.json +2 -2
- package/pipelines/learn-repository.json +111 -119
- package/public/css/main.min.css +258 -65
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +187 -73
- package/public/js/main.min.js.map +1 -1
package/build/install-bridge.js
CHANGED
|
@@ -36,8 +36,26 @@
|
|
|
36
36
|
* reload the just-written `.mcp.json`, and field derivation needs an agent
|
|
37
37
|
* runtime the shell does not have.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* That spawn command embeds the entire agent prompt and runs to multiple KB, which
|
|
40
|
+
* no macOS terminal will accept as one typed line. So it is never typed: the full
|
|
41
|
+
* command is written to a restricted (0600) launch script and only a short
|
|
42
|
+
* `. '<path>'` runner is spawned (BAPI-626, via `materializeWorkerLaunchCommand`).
|
|
43
|
+
* Materialization happens BEFORE any install side effect, because a command that
|
|
44
|
+
* cannot be launched is a whole-run failure, not a Step 5 warning.
|
|
45
|
+
*
|
|
46
|
+
* ONBOARDING BRANCHES — `have-key` (consume an existing key) vs. `need-key` (this
|
|
47
|
+
* command creates the project AND its first admin key). There are two need-key
|
|
48
|
+
* methods, differing only in how the first token is obtained: `bootstrap-invite`
|
|
49
|
+
* (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
|
|
50
|
+
* email. Both then feed the SAME redemption protocol. Selection is pure and
|
|
51
|
+
* deterministic from flags/env, except that a BARE interactive run is asked which
|
|
52
|
+
* branch it wants (BAPI-626) — without that question a first-time user cannot
|
|
53
|
+
* discover self-serve at all. Either need-key method NAMES a new project, so the
|
|
54
|
+
* repository prompt asks for a new project name rather than an existing
|
|
55
|
+
* registration (see `RepoNamePromptMode`).
|
|
56
|
+
*
|
|
57
|
+
* BOOTSTRAP-INVITE MODE (BAPI-606) — one of the two exceptions to "this command
|
|
58
|
+
* consumes a key, it does not create one". With `--invite` (or `BAPI_INVITE`) there is no
|
|
41
59
|
* API key yet, so the pre-flight ping of Step 2 CANNOT be made: the exchange is
|
|
42
60
|
* what mints the key, and it REPLACES that ping. The order becomes:
|
|
43
61
|
*
|
|
@@ -71,9 +89,13 @@ import { runInit, buildBridgeApiEntry } from "./init.js";
|
|
|
71
89
|
import { VERSION } from "./version.generated.js";
|
|
72
90
|
import { validateRepoName } from "./bridge-config.js";
|
|
73
91
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
74
|
-
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, } from "./credential-store.js";
|
|
92
|
+
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, resolveBapiCredentials, } from "./credential-store.js";
|
|
93
|
+
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
94
|
+
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
95
|
+
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
96
|
+
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
75
97
|
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
76
|
-
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "./start-tickets.js";
|
|
98
|
+
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
|
|
77
99
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
78
100
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
79
101
|
/**
|
|
@@ -126,8 +148,10 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
|
|
|
126
148
|
"claim the job was queued — report the sanitized result and leave indexing pending. " +
|
|
127
149
|
"On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
|
|
128
150
|
"continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
|
|
129
|
-
"Never request, echo, or transport any credential — only ever direct the human to
|
|
130
|
-
"
|
|
151
|
+
"Never request, echo, or transport any credential — only ever direct the human to that " +
|
|
152
|
+
"integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
|
|
153
|
+
"always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
|
|
154
|
+
"Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
|
|
131
155
|
"End with an explicit summary line stating how many config fields the apply_install_manifest call " +
|
|
132
156
|
"applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending — " +
|
|
133
157
|
"if 0 fields were applied, say so loudly and explain what is still pending.";
|
|
@@ -146,13 +170,20 @@ export function getInstallBridgeUsage() {
|
|
|
146
170
|
"routing credential, then opens a fresh agent session to derive the remaining",
|
|
147
171
|
"config, present a capability report, and offer optional repository indexing.",
|
|
148
172
|
"",
|
|
173
|
+
"Run it bare — `install-bridge` with no flags — in a terminal and it asks",
|
|
174
|
+
`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
|
|
175
|
+
"existing-key flow below; answer no and it asks for an email and creates a new",
|
|
176
|
+
"Bridge workspace for you (the self-serve flow). That question is asked ONLY for a",
|
|
177
|
+
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without",
|
|
178
|
+
"an interactive terminal keeps the existing deterministic behavior and no prompt.",
|
|
179
|
+
"",
|
|
149
180
|
"Inputs (the only two irreducible ones):",
|
|
150
181
|
" --api-key <key> Bridge API key. Falls back to the BAPI_API_KEY env var,",
|
|
151
182
|
" then an interactive (no-echo) prompt. Generate one in the",
|
|
152
183
|
" Bridge API web UI Security page — this command consumes a",
|
|
153
|
-
" key, it does not create one (--invite
|
|
154
|
-
"
|
|
155
|
-
" printed or logged.",
|
|
184
|
+
" key, it does not create one (--email and --invite are the",
|
|
185
|
+
" exceptions: they CREATE the project and its first admin key).",
|
|
186
|
+
" NEVER printed or logged.",
|
|
156
187
|
" --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
|
|
157
188
|
" priority and short-circuit before any network call. When",
|
|
158
189
|
" neither is set, a compatible server resolves the unique",
|
|
@@ -160,10 +191,13 @@ export function getInstallBridgeUsage() {
|
|
|
160
191
|
" server is older, the key is unresolvable, or resolution",
|
|
161
192
|
" fails, it falls back to an inferred default you confirm",
|
|
162
193
|
" interactively (and to a required --repo when stdin is",
|
|
163
|
-
" non-interactive). MUST match the
|
|
164
|
-
" registration (it keys the credential store
|
|
165
|
-
"
|
|
166
|
-
"
|
|
194
|
+
" non-interactive). In the existing-key flow it MUST match the",
|
|
195
|
+
" server-side repo registration (it keys the credential store",
|
|
196
|
+
" as bapi:<repo>). In either new-project flow (--email,",
|
|
197
|
+
" --invite, or a negative answer to the key question above) it",
|
|
198
|
+
" instead NAMES the project this run creates, so you are asked",
|
|
199
|
+
" to name a new project rather than match an existing one; the",
|
|
200
|
+
" name must be globally unique.",
|
|
167
201
|
"",
|
|
168
202
|
"Self-serve onboarding (no account, no API key, no pre-issued invite):",
|
|
169
203
|
" --email <addr> Create a brand-new Bridge workspace from just an email —",
|
|
@@ -171,8 +205,10 @@ export function getInstallBridgeUsage() {
|
|
|
171
205
|
" It requests a fresh workspace for that email, then creates",
|
|
172
206
|
" the project and mints your own admin API key in one command.",
|
|
173
207
|
" Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
|
|
174
|
-
" interactive prompt
|
|
175
|
-
"
|
|
208
|
+
" interactive prompt — which is also what a negative answer to",
|
|
209
|
+
" the bare-run key question above reaches. The email is NOT a",
|
|
210
|
+
" secret (it is shown as you type), but it is never printed to",
|
|
211
|
+
" a log. Mutually",
|
|
176
212
|
" exclusive with --api-key and --invite. No email verification",
|
|
177
213
|
" is performed and no message is sent to the address — it only",
|
|
178
214
|
" labels the new workspace.",
|
|
@@ -420,6 +456,66 @@ export function promptSecretViaReadline(promptText, input = process.stdin, outpu
|
|
|
420
456
|
muted = true;
|
|
421
457
|
});
|
|
422
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Offer to connect GitHub, if it is not already connected (BAPI-631).
|
|
461
|
+
*
|
|
462
|
+
* Entirely best-effort and non-destructive: by the time this runs the install itself is
|
|
463
|
+
* already complete and durable, so nothing here may fail the run. Every branch that is
|
|
464
|
+
* not "the user said yes and it worked" simply proceeds to the agent session.
|
|
465
|
+
*
|
|
466
|
+
* Reuses the shared connect-github flow rather than duplicating the API, polling,
|
|
467
|
+
* browser, or picker logic — there is exactly one implementation of that handshake.
|
|
468
|
+
*/
|
|
469
|
+
async function offerGithubConnection(repoName, deps, log) {
|
|
470
|
+
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
471
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
472
|
+
return;
|
|
473
|
+
try {
|
|
474
|
+
const credDeps = {
|
|
475
|
+
env: deps.env,
|
|
476
|
+
homedir: deps.homedir,
|
|
477
|
+
platform: deps.platform,
|
|
478
|
+
readFile: deps.readFile,
|
|
479
|
+
stat: deps.stat,
|
|
480
|
+
stderr: () => { },
|
|
481
|
+
};
|
|
482
|
+
// Resolve through the shared resolver rather than reusing an in-memory key from
|
|
483
|
+
// this run: the offered flow is the same shell-spawned surface the standalone
|
|
484
|
+
// command uses, and it must resolve credentials the same way (project MCP config
|
|
485
|
+
// env is NOT visible to a spawned shell).
|
|
486
|
+
const cred = await resolveBapiCredentials(repoName, credDeps);
|
|
487
|
+
if (!cred.ok)
|
|
488
|
+
return;
|
|
489
|
+
const api = {
|
|
490
|
+
fetch: deps.fetch,
|
|
491
|
+
baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
|
|
492
|
+
apiKey: cred.credentials.apiKey,
|
|
493
|
+
};
|
|
494
|
+
const state = await fetchGithubConfigurationState(api, repoName);
|
|
495
|
+
if (state === "configured")
|
|
496
|
+
return;
|
|
497
|
+
if (state === "unavailable") {
|
|
498
|
+
// Do NOT fabricate "unconfigured" from a probe that simply failed — offering to
|
|
499
|
+
// connect an already-connected repo is worse than staying quiet.
|
|
500
|
+
log(" note: could not read GitHub configuration status; skipping the GitHub offer.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const answer = (await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();
|
|
504
|
+
if (answer === "n" || answer === "no")
|
|
505
|
+
return; // declining is a normal outcome
|
|
506
|
+
const connectDeps = createDefaultConnectGithubDeps();
|
|
507
|
+
const code = await runGithubConnectionFlow(connectDeps, api, repoName);
|
|
508
|
+
if (code !== 0) {
|
|
509
|
+
log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
|
|
510
|
+
`'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// The install is already durable; a failure here is never fatal to it.
|
|
515
|
+
log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
|
|
516
|
+
`GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
423
519
|
/** Echoed single-line prompt on stderr (used for repo confirmation / value). */
|
|
424
520
|
function promptLineViaReadline(promptText) {
|
|
425
521
|
return new Promise((resolve) => {
|
|
@@ -666,6 +762,68 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
666
762
|
return { kind: "need-key", method: "self-serve" };
|
|
667
763
|
return { kind: "have-key" };
|
|
668
764
|
}
|
|
765
|
+
/** The exact visible text of the bare-TTY onboarding selector (BAPI-626). */
|
|
766
|
+
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Do you have a Bridge API key? [Y/n] ";
|
|
767
|
+
/**
|
|
768
|
+
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
769
|
+
*
|
|
770
|
+
* The pure resolver defaults every un-signalled invocation to `have-key`, which is
|
|
771
|
+
* correct for a script but wrong for a human: a first-time user with nothing yet
|
|
772
|
+
* runs a bare `install-bridge`, gets the hidden API-key prompt, and has no way to
|
|
773
|
+
* discover that the self-serve email path exists. So a BARE INTERACTIVE run — and
|
|
774
|
+
* only that — is asked which branch it wants.
|
|
775
|
+
*
|
|
776
|
+
* "Bare" is deliberately strict. Any of these keeps the existing deterministic
|
|
777
|
+
* behaviour with NO prompt:
|
|
778
|
+
*
|
|
779
|
+
* - stdin is not a TTY, or no `promptLine` seam is available (scripts, CI);
|
|
780
|
+
* - any CLI argument was supplied (the user already stated an intent);
|
|
781
|
+
* - a non-blank `BAPI_API_KEY` is present (that IS the existing-key intent);
|
|
782
|
+
* - the pure resolver already chose a need-key branch explicitly.
|
|
783
|
+
*
|
|
784
|
+
* A prompt failure becomes a typed, secret-free failure rather than an exception:
|
|
785
|
+
* the caller turns it into an exit code, and terminal/internal error text never
|
|
786
|
+
* reaches the user.
|
|
787
|
+
*/
|
|
788
|
+
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
789
|
+
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
790
|
+
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
791
|
+
if (branch.kind === "need-key")
|
|
792
|
+
return { ok: true, branch };
|
|
793
|
+
const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
|
|
794
|
+
const isBareInvocation = argv.length === 0;
|
|
795
|
+
if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
|
|
796
|
+
return { ok: true, branch };
|
|
797
|
+
}
|
|
798
|
+
const promptLine = deps.promptLine;
|
|
799
|
+
try {
|
|
800
|
+
// Bounded so a prompt seam that returns the same invalid value forever (a
|
|
801
|
+
// misbehaving pipe that passes the TTY check) cannot spin indefinitely.
|
|
802
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
803
|
+
const answer = (await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();
|
|
804
|
+
// Blank = accept the bracketed default (Y), matching the prompt's own contract.
|
|
805
|
+
if (answer.length === 0 || answer === "y" || answer === "yes") {
|
|
806
|
+
return { ok: true, branch: { kind: "have-key" } };
|
|
807
|
+
}
|
|
808
|
+
if (answer === "n" || answer === "no") {
|
|
809
|
+
return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
|
|
810
|
+
}
|
|
811
|
+
deps.log("Please answer y or n (press Enter for yes).");
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
error: "No valid answer to the Bridge API key question. Re-run and answer y or n.",
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// Secret-free by construction: the caught value is never surfaced.
|
|
820
|
+
return {
|
|
821
|
+
ok: false,
|
|
822
|
+
error: "Could not read your answer from the terminal. Re-run with --api-key <key> if you have a " +
|
|
823
|
+
"Bridge API key, or --email <addr> to create a new Bridge workspace.",
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
669
827
|
/**
|
|
670
828
|
* Return the explicitly configured repository name (`--repo`, then
|
|
671
829
|
* `BAPI_REPO_NAME`), trimmed, or `undefined` when neither is supplied. Pure: no
|
|
@@ -682,7 +840,18 @@ export function resolveConfiguredRepoName(options, env) {
|
|
|
682
840
|
}
|
|
683
841
|
return undefined;
|
|
684
842
|
}
|
|
685
|
-
|
|
843
|
+
/**
|
|
844
|
+
* Resolve the repo name: `--repo` → `BAPI_REPO_NAME` env → inferred default
|
|
845
|
+
* (from .bridge/config, else the cwd basename) confirmed interactively. Fails
|
|
846
|
+
* fast (no inference) when neither is supplied and stdin is non-interactive —
|
|
847
|
+
* the repo identity keys the credential store, so it is never silently inferred
|
|
848
|
+
* non-interactively.
|
|
849
|
+
*
|
|
850
|
+
* `mode` selects only the WORDING (prompt and non-interactive error); resolution
|
|
851
|
+
* order, inference, and validation are identical in both. It defaults to
|
|
852
|
+
* `existing-registration` to preserve the behaviour of pre-BAPI-626 callers.
|
|
853
|
+
*/
|
|
854
|
+
export async function resolveRepoName(options, deps, mode = "existing-registration") {
|
|
686
855
|
const configured = resolveConfiguredRepoName(options, deps.env);
|
|
687
856
|
if (configured !== undefined) {
|
|
688
857
|
return { ok: true, value: configured };
|
|
@@ -692,9 +861,13 @@ export async function resolveRepoName(options, deps) {
|
|
|
692
861
|
if (!deps.isTTY || !deps.promptLine) {
|
|
693
862
|
return {
|
|
694
863
|
ok: false,
|
|
695
|
-
error:
|
|
696
|
-
"
|
|
697
|
-
|
|
864
|
+
error: mode === "new-project"
|
|
865
|
+
? "A project name is required. Pass --repo or set the BAPI_REPO_NAME environment " +
|
|
866
|
+
"variable (no interactive terminal is available to confirm an inferred name). It " +
|
|
867
|
+
"names the new Bridge project this run creates and must be globally unique."
|
|
868
|
+
: "A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable " +
|
|
869
|
+
"(no interactive terminal is available to confirm an inferred name). It must match the " +
|
|
870
|
+
"server-side repository registration.",
|
|
698
871
|
};
|
|
699
872
|
}
|
|
700
873
|
// Infer a sensible default: existing .bridge/config, else the cwd basename.
|
|
@@ -709,13 +882,19 @@ export async function resolveRepoName(options, deps) {
|
|
|
709
882
|
inferred = validated.value;
|
|
710
883
|
}
|
|
711
884
|
if (inferred) {
|
|
712
|
-
const
|
|
885
|
+
const promptText = mode === "new-project"
|
|
886
|
+
? `Name your new Bridge project [${inferred}]: `
|
|
887
|
+
: `Repo name [${inferred}] (must match server-side registration): `;
|
|
888
|
+
const answer = (await deps.promptLine(promptText)).trim();
|
|
713
889
|
const chosen = answer.length > 0 ? answer : inferred;
|
|
714
890
|
if (chosen.length > 0)
|
|
715
891
|
return { ok: true, value: chosen };
|
|
716
892
|
}
|
|
717
893
|
else {
|
|
718
|
-
const
|
|
894
|
+
const promptText = mode === "new-project"
|
|
895
|
+
? "Name your new Bridge project: "
|
|
896
|
+
: "Repo name (must match server-side registration): ";
|
|
897
|
+
const answer = (await deps.promptLine(promptText)).trim();
|
|
719
898
|
if (answer.length > 0)
|
|
720
899
|
return { ok: true, value: answer };
|
|
721
900
|
}
|
|
@@ -1159,7 +1338,29 @@ export function buildDryRunPreview(plan) {
|
|
|
1159
1338
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
|
|
1160
1339
|
MCP_TIMEOUT_GUIDANCE,
|
|
1161
1340
|
`Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
|
|
1162
|
-
|
|
1341
|
+
...buildLaunchStepPreview(plan),
|
|
1342
|
+
];
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* The Step 5 preview lines, shared by both previews so the launch description
|
|
1346
|
+
* cannot drift between the have-key and need-key flows.
|
|
1347
|
+
*
|
|
1348
|
+
* A --dry-run never writes the launch script, so the preview describes the
|
|
1349
|
+
* materialization rather than performing it — but it still shows the full command,
|
|
1350
|
+
* because the command is what the user is previewing and it is secret-free.
|
|
1351
|
+
*/
|
|
1352
|
+
function buildLaunchStepPreview(plan) {
|
|
1353
|
+
return [
|
|
1354
|
+
// BAPI-631: described, never performed in --dry-run — a preview must not open a
|
|
1355
|
+
// browser or reach the network. It is also strictly optional, so it carries no step
|
|
1356
|
+
// number of its own and never changes the 5-step count.
|
|
1357
|
+
"Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
|
|
1358
|
+
" via the install manifest and, only when it is unconfigured and the terminal is",
|
|
1359
|
+
" interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
|
|
1360
|
+
"Step 5 — spawn agent session: the full command below is stored in a restricted launch script",
|
|
1361
|
+
" (mode 0600, under the system temp dir) and only a short sourced runner is spawned",
|
|
1362
|
+
" (the script itself is NOT written in --dry-run):",
|
|
1363
|
+
` ${plan.spawnCommand}`,
|
|
1163
1364
|
];
|
|
1164
1365
|
}
|
|
1165
1366
|
/**
|
|
@@ -1214,7 +1415,7 @@ function buildBootstrapDryRunPreview(plan) {
|
|
|
1214
1415
|
`Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
|
|
1215
1416
|
MCP_TIMEOUT_GUIDANCE,
|
|
1216
1417
|
`Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
|
|
1217
|
-
|
|
1418
|
+
...buildLaunchStepPreview(plan),
|
|
1218
1419
|
];
|
|
1219
1420
|
}
|
|
1220
1421
|
/**
|
|
@@ -1301,12 +1502,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1301
1502
|
return 1;
|
|
1302
1503
|
}
|
|
1303
1504
|
const options = parsed.options;
|
|
1304
|
-
// Onboarding branch
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1505
|
+
// Onboarding branch: have-key vs. a need-key method (`bootstrap-invite` = redeem
|
|
1506
|
+
// a pre-issued invite, `self-serve` = mint one from an email, BAPI-618). BOTH
|
|
1507
|
+
// need-key methods share the downstream redemption protocol, so
|
|
1508
|
+
// `bootstrapInviteMode` is true for both; `selfServeSignupMode` discriminates the
|
|
1509
|
+
// one extra step (mint-from-email) the self-serve path adds.
|
|
1510
|
+
//
|
|
1511
|
+
// The selection is pure and deterministic for every explicit, env-driven, and
|
|
1512
|
+
// non-TTY invocation; only a BARE interactive run is asked which branch it wants
|
|
1513
|
+
// (BAPI-626 — otherwise a first-time user can never reach self-serve).
|
|
1514
|
+
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
|
|
1515
|
+
if (!branchResult.ok) {
|
|
1516
|
+
errorLog(`Error: ${branchResult.error}`);
|
|
1517
|
+
return 1;
|
|
1518
|
+
}
|
|
1519
|
+
const branch = branchResult.branch;
|
|
1310
1520
|
const bootstrapInviteMode = branch.kind === "need-key";
|
|
1311
1521
|
const selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
|
|
1312
1522
|
// ---- Resolve inputs (may prompt when interactive) ----
|
|
@@ -1348,15 +1558,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1348
1558
|
const baseUrl = deps.env.BAPI_BASE_URL ?? DEFAULT_BAPI_BASE_URL;
|
|
1349
1559
|
const docsDir = deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR;
|
|
1350
1560
|
// ---- Resolve the repository name ----
|
|
1351
|
-
//
|
|
1561
|
+
// Need-key (invite or self-serve): choose-a-name for the project this run is
|
|
1562
|
+
// about to create, so the prompt says exactly that. Have-key:
|
|
1352
1563
|
// `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve
|
|
1353
1564
|
// it server-side from the API key (BAPI-616), and on ANY non-resolution outcome
|
|
1354
1565
|
// (unresolved / not-deployed / error) fall back to the existing local
|
|
1355
|
-
// prompt/inference — never a hard failure.
|
|
1566
|
+
// prompt/inference — never a hard failure. That fallback keeps the
|
|
1567
|
+
// existing-registration wording: there the name must match a real project.
|
|
1356
1568
|
let repoName;
|
|
1357
1569
|
let attemptedServerResolution = false;
|
|
1358
1570
|
if (bootstrapInviteMode) {
|
|
1359
|
-
const repoResult = await resolveRepoName(options, deps);
|
|
1571
|
+
const repoResult = await resolveRepoName(options, deps, "new-project");
|
|
1360
1572
|
if (!repoResult.ok) {
|
|
1361
1573
|
errorLog(`Error: ${repoResult.error}`);
|
|
1362
1574
|
return 1;
|
|
@@ -1387,7 +1599,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1387
1599
|
// Feature-detect + degrade: 404 (old server), 409 (unresolved/ambiguous/
|
|
1388
1600
|
// client-scoped), and network/other errors all fall back to the existing
|
|
1389
1601
|
// local resolution WITHOUT a cause-specific message or leaked detail.
|
|
1390
|
-
const repoResult = await resolveRepoName(options, deps);
|
|
1602
|
+
const repoResult = await resolveRepoName(options, deps, "existing-registration");
|
|
1391
1603
|
if (!repoResult.ok) {
|
|
1392
1604
|
errorLog(`Error: ${repoResult.error}`);
|
|
1393
1605
|
return 1;
|
|
@@ -1432,6 +1644,31 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1432
1644
|
log(line);
|
|
1433
1645
|
return 0;
|
|
1434
1646
|
}
|
|
1647
|
+
// ---- Materialize the Step 5 launch command BEFORE any install side effect ----
|
|
1648
|
+
// `spawnCommand` embeds the whole INSTALL_BRIDGE_AGENT_PROMPT and is multiple KB
|
|
1649
|
+
// — far past what osascript can type into a Terminal/iTerm tab, which is why the
|
|
1650
|
+
// spawn silently delivered a truncated line. It must travel via a launch script.
|
|
1651
|
+
//
|
|
1652
|
+
// This runs HERE, before the scaffold/mint/credential/config writes, because a
|
|
1653
|
+
// command that cannot be launched safely is a whole-run failure, not a Step 5
|
|
1654
|
+
// warning: the deterministic setup would otherwise complete and leave the user
|
|
1655
|
+
// in the "configured but never configured by the agent" state the warning below
|
|
1656
|
+
// explicitly calls out. Failing first means nothing is half-done.
|
|
1657
|
+
const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
|
|
1658
|
+
if (!materialized.ok) {
|
|
1659
|
+
errorLog(`Error: ${materialized.error}`);
|
|
1660
|
+
return 1;
|
|
1661
|
+
}
|
|
1662
|
+
const launchCommand = materialized.command;
|
|
1663
|
+
// Independent final guard on the line ACTUALLY handed to the terminal — not on
|
|
1664
|
+
// the original command. It catches the two ways a "successful" materialization
|
|
1665
|
+
// can still be unlaunchable: no writer seam (inline command preserved verbatim),
|
|
1666
|
+
// and a runner whose script path is unexpectedly long.
|
|
1667
|
+
if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
|
|
1668
|
+
errorLog("Error: the agent session command is too long to send to the terminal safely. " +
|
|
1669
|
+
"Check that the system temporary directory is writable so the launch script can be used.");
|
|
1670
|
+
return 1;
|
|
1671
|
+
}
|
|
1435
1672
|
const credentialWriteDeps = {
|
|
1436
1673
|
env: deps.env,
|
|
1437
1674
|
homedir: deps.homedir,
|
|
@@ -1721,10 +1958,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
1721
1958
|
"'npx -y @bridge_gpt/mcp-server doctor'.");
|
|
1722
1959
|
}
|
|
1723
1960
|
}
|
|
1961
|
+
// ---- optional GitHub connect offer (BAPI-631) ----
|
|
1962
|
+
// Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
|
|
1963
|
+
// the agent spawn, for two reasons: the spawned session's capability report should
|
|
1964
|
+
// observe GitHub as configured if the user connects it here, and running it after the
|
|
1965
|
+
// spawn would put two prompts on the same terminal at once.
|
|
1966
|
+
await offerGithubConnection(repoName, deps, log);
|
|
1724
1967
|
// ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
|
|
1725
1968
|
log(`Step 5/5 — opening a ${agent.name} session for /install-bridge configuration + capability report…`);
|
|
1726
1969
|
const terminal = detectTerminal(undefined, deps.env);
|
|
1727
|
-
|
|
1970
|
+
// Only the validated short runner reaches the terminal — never the inline prompt.
|
|
1971
|
+
const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
|
|
1728
1972
|
key: "install",
|
|
1729
1973
|
worktreePath: deps.cwd,
|
|
1730
1974
|
});
|
package/build/install-doctor.js
CHANGED
|
@@ -141,6 +141,27 @@ function summarizeIntegrations(body) {
|
|
|
141
141
|
}
|
|
142
142
|
return { total, unconfigured };
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Read `github_app.is_configured` from a manifest response (BAPI-631).
|
|
146
|
+
*
|
|
147
|
+
* Returns null when the entry is absent or is not a boolean — the caller must render
|
|
148
|
+
* that as SKIP rather than inventing a configured/unconfigured verdict. GitHub is
|
|
149
|
+
* legitimately absent from the checklist on a Bitbucket project.
|
|
150
|
+
*/
|
|
151
|
+
function readGithubConfiguredFlag(body) {
|
|
152
|
+
if (!body || typeof body !== "object")
|
|
153
|
+
return null;
|
|
154
|
+
const integrations = body.integrations;
|
|
155
|
+
if (!Array.isArray(integrations))
|
|
156
|
+
return null;
|
|
157
|
+
for (const item of integrations) {
|
|
158
|
+
if (item?.id !== "github_app")
|
|
159
|
+
continue;
|
|
160
|
+
const configured = item.is_configured;
|
|
161
|
+
return typeof configured === "boolean" ? configured : null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
144
165
|
/**
|
|
145
166
|
* Collect the install-status checklist. Never throws; every failure path becomes
|
|
146
167
|
* a WARN or SKIP check line. Performs at most three read-only GETs, and only
|
|
@@ -333,6 +354,49 @@ export async function collectInstallStatusChecks(deps) {
|
|
|
333
354
|
detail: "manifest unavailable",
|
|
334
355
|
});
|
|
335
356
|
}
|
|
357
|
+
// --- GitHub connection (BAPI-631) ---
|
|
358
|
+
// Read-only, and derived from the SAME manifest response above — never from the
|
|
359
|
+
// nonce-scoped CLI status endpoint, which describes one transient connection attempt
|
|
360
|
+
// rather than the durable project binding doctor reports on. Doctor diagnoses; it
|
|
361
|
+
// never mints, syncs, confirms, opens a browser, writes a credential, or repairs.
|
|
362
|
+
if (manifest.ok && manifest.status === 200) {
|
|
363
|
+
const github = readGithubConfiguredFlag(manifest.body);
|
|
364
|
+
if (github === null) {
|
|
365
|
+
checks.push({
|
|
366
|
+
id: "github",
|
|
367
|
+
label: "GitHub connection",
|
|
368
|
+
status: "SKIP",
|
|
369
|
+
detail: "the manifest response carried no GitHub integration entry",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else if (github) {
|
|
373
|
+
checks.push({
|
|
374
|
+
id: "github",
|
|
375
|
+
label: "GitHub connection",
|
|
376
|
+
status: "PASS",
|
|
377
|
+
detail: "a GitHub repository is connected to this project",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
checks.push({
|
|
382
|
+
id: "github",
|
|
383
|
+
label: "GitHub connection",
|
|
384
|
+
status: "WARN",
|
|
385
|
+
detail: "no GitHub repository is connected to this project",
|
|
386
|
+
remediation: `run 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${target.repoName}'.`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// Unavailable is NOT unconfigured: reporting either concrete state from a failed
|
|
392
|
+
// probe would be a fabricated finding.
|
|
393
|
+
checks.push({
|
|
394
|
+
id: "github",
|
|
395
|
+
label: "GitHub connection",
|
|
396
|
+
status: "SKIP",
|
|
397
|
+
detail: "manifest unavailable",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
336
400
|
// --- indexing ---
|
|
337
401
|
const parse = await probeGet(deps, `${target.baseUrl}/jira/parse-status?${repoQuery}`, apiKey);
|
|
338
402
|
if (!parse.ok || parse.status !== 200) {
|