@bridge_gpt/mcp-server 0.2.67 → 0.2.69
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 +272 -51
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/http-client.js +43 -4
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1109 -695
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
package/build/install-bridge.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { validateBootstrapDisplayName, validateBootstrapServerIdentity } from "./bootstrap-project-identity.js";
|
|
2
|
+
import { lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
|
|
3
|
+
import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-build-advisory.js";
|
|
1
4
|
/**
|
|
2
5
|
* install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
|
|
3
6
|
*
|
|
@@ -64,9 +67,9 @@
|
|
|
64
67
|
* (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
|
|
65
68
|
* email. Both then feed the SAME redemption protocol. Selection is pure and
|
|
66
69
|
* deterministic from flags/env, except that a BARE interactive run is asked ONE
|
|
67
|
-
* question (BAPI-626/708): `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
+
* question (BAPI-626/708/1448): `Email (or press Enter if you already have a
|
|
71
|
+
* token): `. A valid email is the self-serve route, which a first-time user could
|
|
72
|
+
* not otherwise discover. A blank answer opens a single hidden,
|
|
70
73
|
* CREDENTIAL-AGNOSTIC token prompt and the route is then inferred from the pasted
|
|
71
74
|
* value's SHAPE — the user is never asked to pre-classify their own credential,
|
|
72
75
|
* because the shape checks below do that with no probe request. Either need-key
|
|
@@ -137,7 +140,7 @@
|
|
|
137
140
|
* 0600, fsync'd pending record as the `key_secret`, purely so the signup can be
|
|
138
141
|
* replayed, and it is discarded when that record is promoted.
|
|
139
142
|
*/
|
|
140
|
-
import { readFile, writeFile, mkdir, stat, rename, chmod, unlink, open } from "fs/promises";
|
|
143
|
+
import { readFile, writeFile, mkdir, stat, rename, chmod, unlink, open, realpath } from "fs/promises";
|
|
141
144
|
import { spawn } from "child_process";
|
|
142
145
|
import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
|
|
143
146
|
import os from "os";
|
|
@@ -152,7 +155,7 @@ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform
|
|
|
152
155
|
import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
|
|
153
156
|
import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
|
|
154
157
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
|
|
155
|
-
import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
|
|
158
|
+
import { DUPLICATE_REGISTRATION_GUIDANCE, discoverClaudeCodeEffectivePin } from "./launcher-config-inspection.js";
|
|
156
159
|
import { codexHome, configureBridgeCodexProfile, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
|
|
157
160
|
import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
158
161
|
// BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
|
|
@@ -168,9 +171,13 @@ import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunch
|
|
|
168
171
|
import { collectConductorNativeLedgerSafe, } from "./conductor/doctor.js";
|
|
169
172
|
import { installConductorGitHooks, } from "./conductor/git-hooks.js";
|
|
170
173
|
import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
174
|
+
import { createQualificationExecutorRunner, } from "./install-bridge-conductor-gate.js";
|
|
175
|
+
import { createNodeGateDiscoveryFs } from "./conductor/gate-discovery.js";
|
|
176
|
+
import { runExecutorCli } from "./executor/cli.js";
|
|
177
|
+
import { runExecutor } from "./executor/runner.js";
|
|
171
178
|
// BAPI-1136: the merge-workflow generate-and-PR installer. Domain logic lives in
|
|
172
179
|
// its own module; this file supplies the parser and the process seams.
|
|
173
|
-
import { runMergeWorkflowInstall, } from "./merge-workflow-install.js";
|
|
180
|
+
import { parseGithubRemote, runMergeWorkflowInstall, } from "./merge-workflow-install.js";
|
|
174
181
|
import { MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
|
|
175
182
|
import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
|
|
176
183
|
import { runConductorInstallDoctor, EPIC_IMPLEMENTER_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
@@ -180,15 +187,17 @@ import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-r
|
|
|
180
187
|
// doctor's optional drift seam below.
|
|
181
188
|
import { probeClaudeReviewWorkflowDrift, resolveCurrentBranch, resolveRepositoryDefaultBranch, } from "./claude-review-workflow-drift-probe.js";
|
|
182
189
|
import { runSetupEpicCli } from "./setup-epic.js";
|
|
190
|
+
import { runBridgePermissionsInstallStep, runBridgePermissionsRemoval, } from "./bridge-permissions.js";
|
|
183
191
|
import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
|
|
184
192
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
185
193
|
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
186
194
|
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
187
195
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
188
|
-
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
196
|
+
import { fetchGithubConfigurationDetails, fetchGithubConfigurationState, } from "./connect-github-api.js";
|
|
189
197
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
190
198
|
import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
|
|
191
199
|
import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
|
|
200
|
+
import { defaultGitRemoteRunner, resolveBitbucketWorkspace, } from "./bitbucket-workspace-remote.js";
|
|
192
201
|
import { resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, } from "./agent-registry.js";
|
|
193
202
|
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, validateResolvedModelAliasForAgent, } from "./start-tickets.js";
|
|
194
203
|
import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
|
|
@@ -265,7 +274,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
265
274
|
"'Needs setup' — and each item in them carries display_name, description, how_to_use, and (in the " +
|
|
266
275
|
"'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
|
|
267
276
|
"and its how_to_use verbatim when that value is a non-empty string; render availability_text " +
|
|
268
|
-
"verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, " +
|
|
277
|
+
"verbatim as the setup guidance for a 'Needs setup' item — except when that section carries a non-empty string heading: print it verbatim as the section's sub-heading instead of its name and print NO item's availability_text; with no usable heading, render each availability_text as usual — and show NO availability caveat, " +
|
|
269
278
|
"asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
|
|
270
279
|
"contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
|
|
271
280
|
"and never branch on it. Keep an empty section visible with a short neutral line rather than " +
|
|
@@ -294,7 +303,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
294
303
|
"short, plain-language sentence covering what is happening and why it matters to the person running " +
|
|
295
304
|
"this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
|
|
296
305
|
"internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
|
|
297
|
-
"get_install_manifest
|
|
306
|
+
"get_install_manifest or apply_install_manifest), or a manifest field key (like " +
|
|
298
307
|
"command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
|
|
299
308
|
"This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
|
|
300
309
|
"name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
|
|
@@ -308,11 +317,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
308
317
|
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
309
318
|
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
310
319
|
"invocation Stage 6 reports its success exactly as it does today. " +
|
|
311
|
-
"The install-spawn skip set is exactly Stage 8 and Stage 9.
|
|
312
|
-
"(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
|
|
313
|
-
"caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
|
|
314
|
-
"otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key " +
|
|
315
|
-
"exactly once and never repeat it in any summary, retry, or diagnostic. " +
|
|
320
|
+
"The install-spawn skip set is exactly Stage 8 and Stage 9. " +
|
|
316
321
|
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
317
322
|
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
318
323
|
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
@@ -385,9 +390,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
385
390
|
"the next step the server returned — when it returns one.",
|
|
386
391
|
"If the project is already configured it instead",
|
|
387
392
|
"joins you to it without proposing or applying any changes and just shows the",
|
|
388
|
-
"concise capability report plus that same conditional next step
|
|
389
|
-
"
|
|
390
|
-
"step). So not every run applies config fields. Indexing is never asked about —",
|
|
393
|
+
"concise capability report plus that same conditional next step.",
|
|
394
|
+
"So not every run applies config fields. Indexing is never asked about —",
|
|
391
395
|
"it starts automatically once the repository reaches full parse readiness.",
|
|
392
396
|
"",
|
|
393
397
|
"Your API key is written to a project MCP config only when that file is safe: a",
|
|
@@ -397,17 +401,15 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
397
401
|
"resolves your key from the credential store at runtime. A config file that",
|
|
398
402
|
"cannot be parsed is left untouched, with manual-merge instructions printed.",
|
|
399
403
|
"",
|
|
400
|
-
"Run it bare — `install` with no flags — in a terminal and it asks",
|
|
401
|
-
`\`${
|
|
402
|
-
|
|
403
|
-
"
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"Bridge API key connects to the project that already exists. You are",
|
|
410
|
-
"never asked which kind of token you hold. That question is asked ONLY for a",
|
|
404
|
+
"Run it bare — `install` with no flags — in a terminal and it asks one question:",
|
|
405
|
+
`\`${INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT.trim()}\``,
|
|
406
|
+
"Type your email to create a brand-new Bridge workspace for you (the self-serve",
|
|
407
|
+
"flow). Press Enter if you already have a token: a single hidden token prompt",
|
|
408
|
+
"opens, and whatever you paste has its SHAPE pick the route — a bootstrap invite",
|
|
409
|
+
"(bapi_inv_…) is redeemed to CREATE a new project and mint your first admin key,",
|
|
410
|
+
"while a Bridge API key connects to the project that already exists. One",
|
|
411
|
+
"malformed answer re-prompts once, then exits with guidance. You are",
|
|
412
|
+
"never asked which kind of token you hold. The question is asked ONLY for a",
|
|
411
413
|
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
412
414
|
"terminal keeps the existing deterministic behavior and no prompt.",
|
|
413
415
|
"",
|
|
@@ -443,7 +445,10 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
443
445
|
" DECLARES the key route, so that switch is confirmed on a",
|
|
444
446
|
" terminal; the bare run's hidden token prompt infers instead",
|
|
445
447
|
" and only announces. NEVER printed or logged.",
|
|
446
|
-
" --
|
|
448
|
+
" --display-name <label> Friendly creation label; required for non-TTY modern invites.",
|
|
449
|
+
" Do not combine with --repo or BAPI_REPO_NAME. Existing keys use canonical selectors.",
|
|
450
|
+
" Self-serve verification still requires an interactive terminal.",
|
|
451
|
+
" --repo <name> Canonical selector for reconnect or explicit legacy creation. --repo and BAPI_REPO_NAME still take",
|
|
447
452
|
" priority and short-circuit before any network call. When",
|
|
448
453
|
" neither is set, a compatible server resolves the unique",
|
|
449
454
|
" repository from your existing API key automatically. If the",
|
|
@@ -520,9 +525,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
520
525
|
" --tools claude-code,codex are accepted. On an",
|
|
521
526
|
" interactive terminal WITHOUT this flag you get a",
|
|
522
527
|
" numbered picker: enter one or more tool numbers,",
|
|
523
|
-
" comma-separated (e.g. 1,3), and press Enter once
|
|
524
|
-
"
|
|
525
|
-
"
|
|
528
|
+
" comma-separated (e.g. 1,3), and press Enter once.",
|
|
529
|
+
" Tools Bridge detects on this machine are pre-ticked:",
|
|
530
|
+
" a bare Enter accepts them, and typed numbers",
|
|
531
|
+
" replace them. With none detected you must select",
|
|
532
|
+
" at least one. A non-interactive",
|
|
526
533
|
" (non-TTY) run without",
|
|
527
534
|
" --tools writes the legacy automatic set (Claude",
|
|
528
535
|
" Code plus any detected Cursor / Copilot VS Code)",
|
|
@@ -551,6 +558,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
551
558
|
" session — it always wins, and there is NO Claude",
|
|
552
559
|
" default. Without it the launch tool is derived",
|
|
553
560
|
" from your selection: Claude Code opens `claude`.",
|
|
561
|
+
" With more than one launchable tool selected, the",
|
|
562
|
+
" first one is used and no question is asked.",
|
|
554
563
|
" Cursor is NOT auto-opened (its cursor-agent CLI",
|
|
555
564
|
" first-run workspace-trust prompt collapses a spawned",
|
|
556
565
|
" session); selecting Cursor writes .cursor/mcp.json",
|
|
@@ -561,6 +570,13 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
561
570
|
" --agent cursor-agent still force-spawns it. Codex",
|
|
562
571
|
" is accepted only for manual continuation: neither",
|
|
563
572
|
" this bootstrap nor start-tickets launches it yet.",
|
|
573
|
+
" --remove-bridge-permissions Remove ONLY the permission rules and autoMode",
|
|
574
|
+
" entries Bridge wrote for /drive-epic-run",
|
|
575
|
+
" (recorded in .bridge/bridge-permissions.json)",
|
|
576
|
+
" from .claude/settings.local.json and",
|
|
577
|
+
" ~/.claude/settings.json. Must be used alone.",
|
|
578
|
+
" Entries you added yourself stay. Runs nothing",
|
|
579
|
+
" else: no onboarding, no credential, no network.",
|
|
564
580
|
" -h, --help Show this help.",
|
|
565
581
|
"",
|
|
566
582
|
"Subcommands:",
|
|
@@ -642,6 +658,7 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
642
658
|
}
|
|
643
659
|
let apiKey;
|
|
644
660
|
let repo;
|
|
661
|
+
let displayName;
|
|
645
662
|
let force = false;
|
|
646
663
|
let dryRun = false;
|
|
647
664
|
// Undefined until `--agent` is explicitly supplied — install-bridge never falls
|
|
@@ -667,8 +684,32 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
667
684
|
}
|
|
668
685
|
return { value: argv[i + 1], nextIndex: i + 1 };
|
|
669
686
|
};
|
|
687
|
+
// BAPI-1398: the removal flag selects a different, narrow action, so it is
|
|
688
|
+
// accepted only on its own — combining it with an install flag is ambiguous.
|
|
689
|
+
if (argv.includes("--remove-bridge-permissions")) {
|
|
690
|
+
if (argv.length !== 1) {
|
|
691
|
+
return {
|
|
692
|
+
status: "error",
|
|
693
|
+
message: "--remove-bridge-permissions must be used alone; it removes Bridge's permission rules and does nothing else.",
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return { status: "ok", options: { force: false, dryRun: false, removeBridgePermissions: true } };
|
|
697
|
+
}
|
|
670
698
|
for (let i = 0; i < argv.length; i++) {
|
|
671
699
|
const arg = argv[i];
|
|
700
|
+
if (arg === "--display-name" || arg.startsWith("--display-name=")) {
|
|
701
|
+
const result = readValue(arg, "--display-name", i);
|
|
702
|
+
if ("error" in result)
|
|
703
|
+
return { status: "error", message: result.error };
|
|
704
|
+
try {
|
|
705
|
+
displayName = validateBootstrapDisplayName(result.value);
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return { status: "error", message: "--display-name requires a safe label of 1–255 Unicode characters." };
|
|
709
|
+
}
|
|
710
|
+
i = result.nextIndex;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
672
713
|
if (arg === "--force") {
|
|
673
714
|
force = true;
|
|
674
715
|
continue;
|
|
@@ -797,7 +838,7 @@ export function parseInstallBridgeArgs(argv) {
|
|
|
797
838
|
}
|
|
798
839
|
return {
|
|
799
840
|
status: "ok",
|
|
800
|
-
options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools, codexProfile },
|
|
841
|
+
options: { apiKey, repo, displayName, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools, codexProfile },
|
|
801
842
|
};
|
|
802
843
|
}
|
|
803
844
|
/**
|
|
@@ -918,6 +959,8 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
|
|
|
918
959
|
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
919
960
|
if (!deps.isTTY || !deps.promptLine)
|
|
920
961
|
return;
|
|
962
|
+
let attempted = false;
|
|
963
|
+
const fallbackContext = " This attempt did not establish a successful Bridge GitHub link. Bitbucket may be offered next as an optional alternative; you can decline it and retry GitHub later.";
|
|
921
964
|
try {
|
|
922
965
|
const credDeps = {
|
|
923
966
|
env: deps.env,
|
|
@@ -961,6 +1004,7 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
|
|
|
961
1004
|
if (answer !== "y" && answer !== "yes")
|
|
962
1005
|
return; // declining is a normal outcome
|
|
963
1006
|
const connectDeps = createDefaultConnectGithubDeps();
|
|
1007
|
+
attempted = true;
|
|
964
1008
|
const code = await runGithubConnectionFlow(connectDeps, api, repoName);
|
|
965
1009
|
if (code === 0) {
|
|
966
1010
|
// BAPI-1171: only a CONFIRMED success suppresses the Bitbucket offer below —
|
|
@@ -968,12 +1012,15 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
|
|
|
968
1012
|
session.vcsConnected = true;
|
|
969
1013
|
}
|
|
970
1014
|
else {
|
|
1015
|
+
log(fallbackContext);
|
|
971
1016
|
log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
|
|
972
1017
|
`'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
|
|
973
1018
|
}
|
|
974
1019
|
}
|
|
975
1020
|
catch {
|
|
976
1021
|
// The install is already durable; a failure here is never fatal to it.
|
|
1022
|
+
if (attempted)
|
|
1023
|
+
log(fallbackContext);
|
|
977
1024
|
log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
|
|
978
1025
|
`GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
|
|
979
1026
|
}
|
|
@@ -995,7 +1042,10 @@ export const INSTALL_BRIDGE_BITBUCKET_OFFER_CONTEXT = [
|
|
|
995
1042
|
// BAPI-1247: name the destination precisely. "Admin panel" sent people looking for a
|
|
996
1043
|
// page that does not exist under that name — the app lives under the workspace's own
|
|
997
1044
|
// settings, and saying so is the difference between a 20-second step and a hunt.
|
|
998
|
-
|
|
1045
|
+
// BAPI-1401: say up front that there are two steps. Bitbucket's install screen gives no
|
|
1046
|
+
// sign that a second one exists.
|
|
1047
|
+
" There are two steps: (1) install the app, then (2) approve the request on the app's page",
|
|
1048
|
+
' under Workspace settings → Forge Apps → "Connect Bitbucket to Bridge API".',
|
|
999
1049
|
` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_BITBUCKET_COMMAND}`,
|
|
1000
1050
|
];
|
|
1001
1051
|
/** Default-No Bitbucket offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
|
|
@@ -1060,11 +1110,13 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1060
1110
|
if (answer !== "y" && answer !== "yes")
|
|
1061
1111
|
return; // declining is a normal outcome
|
|
1062
1112
|
const connectDeps = createDefaultConnectBitbucketDeps();
|
|
1113
|
+
// BAPI-1401: the workspace comes from the project's origin remote, the same way the
|
|
1114
|
+
// standalone command infers it. With one, the flow mints an approval-mode request
|
|
1115
|
+
// (and prints the direct link when the server has a template); without one it falls
|
|
1116
|
+
// back to the code flow and its textual navigation path.
|
|
1117
|
+
const workspace = await resolveBitbucketWorkspace(undefined, deps.cwd, deps.runGit ?? defaultGitRemoteRunner, log);
|
|
1063
1118
|
const result = await runBitbucketConnectionFlow(connectDeps, api, repoName, {
|
|
1064
|
-
|
|
1065
|
-
// AFTER redemption stages candidates, which is later than this. The connection flow
|
|
1066
|
-
// falls back to its textual navigation path rather than guessing a link.
|
|
1067
|
-
workspace: undefined,
|
|
1119
|
+
workspace,
|
|
1068
1120
|
// The Step 4b prompt just above IS the approval for the first mint. Asking again
|
|
1069
1121
|
// inside the flow would be a second confirmation for one decision.
|
|
1070
1122
|
initialMintApproved: true,
|
|
@@ -1092,12 +1144,6 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1092
1144
|
`Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
|
|
1093
1145
|
}
|
|
1094
1146
|
}
|
|
1095
|
-
/**
|
|
1096
|
-
* The exact agent-notes consent-prompt prefix, shared with tests so the copy
|
|
1097
|
-
* cannot drift. Mirrors {@link INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}: the
|
|
1098
|
-
* prefix is pinned, the platform tail varies with the selection.
|
|
1099
|
-
*/
|
|
1100
|
-
export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX = "Write guidance so ";
|
|
1101
1147
|
/**
|
|
1102
1148
|
* Describe the selected notes platforms for the offer copy: a naturally joined
|
|
1103
1149
|
* subject and whether it is plural (so a verb can agree). `platformLabels` are
|
|
@@ -1120,64 +1166,30 @@ function joinLabelsWithAnd(labels) {
|
|
|
1120
1166
|
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
1121
1167
|
}
|
|
1122
1168
|
/**
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
/**
|
|
1133
|
-
* The offer-context lines printed immediately before the agent-notes prompt
|
|
1134
|
-
* (BAPI-1207). Leads with the payoff — teaching the named platform(s) WHEN to
|
|
1135
|
-
* reach for Bridge's MCP tools — then states the marker-scoped, non-destructive
|
|
1136
|
-
* contract as a secondary line, mirroring the GitHub/Bitbucket offers' "explain
|
|
1137
|
-
* before asking" convention.
|
|
1138
|
-
*/
|
|
1139
|
-
export function buildAgentNotesOfferContext(targets, platformLabels) {
|
|
1140
|
-
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
1141
|
-
return [
|
|
1142
|
-
"",
|
|
1143
|
-
`Step 4c — optional: Bridge MCP guidance for ${subject}.`,
|
|
1144
|
-
` Write guidance so ${subject} know${plural ? "" : "s"} when to reach for Bridge's MCP tools`,
|
|
1145
|
-
" during a task — for project standards, a second opinion, a council, or a ticket review.",
|
|
1146
|
-
` It writes a Bridge-owned region to ${targets.join(" and ")}; only that marked region is`,
|
|
1147
|
-
" created or refreshed — any existing content, inside or outside it, is otherwise preserved.",
|
|
1148
|
-
];
|
|
1149
|
-
}
|
|
1150
|
-
/**
|
|
1151
|
-
* Offer to create or refresh the cross-platform agent notes, if the selected
|
|
1152
|
-
* platforms resolve to at least one writable target (BAPI-1207).
|
|
1169
|
+
* Decide whether to create or refresh the cross-platform agent notes, if the
|
|
1170
|
+
* selected platforms resolve to at least one writable target (BAPI-1207).
|
|
1171
|
+
*
|
|
1172
|
+
* BAPI-1448: an interactive run consents by default — the question is gone, with
|
|
1173
|
+
* the product sign-off recorded in the install notes. The write is still bounded to
|
|
1174
|
+
* Bridge's marked region (see {@link writeAgentNotes}) and reported afterwards with
|
|
1175
|
+
* a removal hint. A non-interactive run keeps `headless-skip`: it never writes
|
|
1176
|
+
* guidance on its own.
|
|
1153
1177
|
*
|
|
1154
|
-
* Entirely best-effort and non-destructive
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
* reaches this call) and the live path share exactly one write path.
|
|
1178
|
+
* Entirely best-effort and non-destructive: by the time this runs the install
|
|
1179
|
+
* itself is already complete and durable, so nothing here may fail the run. This
|
|
1180
|
+
* function only resolves the CONSENT decision — the caller performs the actual
|
|
1181
|
+
* write via {@link writeAgentNotes}, so a `--dry-run` (which never reaches this
|
|
1182
|
+
* call) and the live path share exactly one write path.
|
|
1160
1183
|
*/
|
|
1161
|
-
export
|
|
1184
|
+
export function offerAgentGuidanceNotes(deps, targets) {
|
|
1162
1185
|
if (targets.length === 0)
|
|
1163
1186
|
return { kind: "not-applicable" };
|
|
1164
|
-
|
|
1165
|
-
if (!deps.isTTY || !deps.promptLine)
|
|
1187
|
+
if (!deps.isTTY)
|
|
1166
1188
|
return { kind: "headless-skip" };
|
|
1167
|
-
|
|
1168
|
-
for (const line of buildAgentNotesOfferContext(targets, platformLabels))
|
|
1169
|
-
log(line);
|
|
1170
|
-
const answer = (await deps.promptLine(buildAgentNotesPrompt(platformLabels))).trim().toLowerCase();
|
|
1171
|
-
// Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
|
|
1172
|
-
// all decline without writing.
|
|
1173
|
-
return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
|
|
1174
|
-
}
|
|
1175
|
-
catch {
|
|
1176
|
-
// The install is already durable; a failure here is never fatal to it.
|
|
1177
|
-
log(" note: the agent-notes offer could not run. No notes files were written.");
|
|
1178
|
-
return { kind: "declined" };
|
|
1179
|
-
}
|
|
1189
|
+
return { kind: "consented" };
|
|
1180
1190
|
}
|
|
1191
|
+
/** The one-line removal hint printed after guidance was actually written (BAPI-1448). */
|
|
1192
|
+
export const INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT = "To remove it, delete the lines from <!-- BEGIN BRIDGE MCP GUIDANCE --> to <!-- END BRIDGE MCP GUIDANCE -->.";
|
|
1181
1193
|
/**
|
|
1182
1194
|
* One secret-free summary line for the final install output (BAPI-1207). Never
|
|
1183
1195
|
* reveals guidance content, file bytes, or a credential — only which files
|
|
@@ -1192,11 +1204,13 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1192
1204
|
return unsupportedPlatforms.length > 0 ? `Agent notes: not written${unsupportedNote}.` : undefined;
|
|
1193
1205
|
}
|
|
1194
1206
|
if (offer.kind === "headless-skip") {
|
|
1195
|
-
return `Agent notes: skipped (non-interactive session)${unsupportedNote}.`;
|
|
1207
|
+
return `Agent notes: skipped (non-interactive session or submission provenance unavailable)${unsupportedNote}.`;
|
|
1196
1208
|
}
|
|
1197
1209
|
if (offer.kind === "declined") {
|
|
1198
|
-
return `Agent notes: skipped (declined)${unsupportedNote}.`;
|
|
1210
|
+
return `Agent notes: skipped (declined or no submitted consent)${unsupportedNote}.`;
|
|
1199
1211
|
}
|
|
1212
|
+
if (offer.kind === "prompt-error")
|
|
1213
|
+
return undefined;
|
|
1200
1214
|
if (writeFailed || !result) {
|
|
1201
1215
|
return `Agent notes: could not be written (unexpected error)${unsupportedNote}.`;
|
|
1202
1216
|
}
|
|
@@ -1212,6 +1226,47 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1212
1226
|
parts.push(`failed for ${failed.join(", ")}`);
|
|
1213
1227
|
return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
|
|
1214
1228
|
}
|
|
1229
|
+
const OPTIMIZATION_POSTURE_SUMMARY_LABELS = {
|
|
1230
|
+
quality: "Quality",
|
|
1231
|
+
speed: "Speed",
|
|
1232
|
+
maximize_subscription: "Token Efficiency",
|
|
1233
|
+
};
|
|
1234
|
+
/**
|
|
1235
|
+
* The aligned "defaults Bridge chose" block printed once, right before the launch
|
|
1236
|
+
* consent question. Pure: it formats what the run already knows and performs no I/O.
|
|
1237
|
+
*
|
|
1238
|
+
* Layout: a header, then two rows of two cells. Both rows are indented two spaces and
|
|
1239
|
+
* the left cells are padded to the longer left cell plus three spaces, so the right
|
|
1240
|
+
* cells line up. The Ticket keys cell is omitted entirely when the server supplied no
|
|
1241
|
+
* valid prefix; the prefix is printed as given and never derived locally.
|
|
1242
|
+
*/
|
|
1243
|
+
export function buildInstallBridgeDefaultsSummary(input) {
|
|
1244
|
+
const optimization = input.persistedPosture
|
|
1245
|
+
? OPTIMIZATION_POSTURE_SUMMARY_LABELS[input.persistedPosture]
|
|
1246
|
+
: input.isNewProject
|
|
1247
|
+
? "Quality"
|
|
1248
|
+
: "unchanged";
|
|
1249
|
+
const guidanceLabel = input.claudeCodeSelected ? "Claude Code guidance" : "Agent guidance";
|
|
1250
|
+
const guidanceValue = input.guidance.written.length > 0
|
|
1251
|
+
? `added to ${input.guidance.written.join(", ")}`
|
|
1252
|
+
: input.guidance.unchanged.length > 0
|
|
1253
|
+
? "already up to date"
|
|
1254
|
+
: "not written";
|
|
1255
|
+
const leftTop = `Project name: ${input.displayName}`;
|
|
1256
|
+
const leftBottom = `Optimization: ${optimization}`;
|
|
1257
|
+
const rightTop = input.ticketPrefix
|
|
1258
|
+
? `Ticket keys: ${input.ticketPrefix}-1, ${input.ticketPrefix}-2, …`
|
|
1259
|
+
: undefined;
|
|
1260
|
+
const rightBottom = `${guidanceLabel}: ${guidanceValue}`;
|
|
1261
|
+
const width = Math.max(leftTop.length, leftBottom.length) + 3;
|
|
1262
|
+
const rowTop = rightTop === undefined ? ` ${leftTop}` : ` ${leftTop.padEnd(width)}${rightTop}`;
|
|
1263
|
+
const rowBottom = ` ${leftBottom.padEnd(width)}${rightBottom}`;
|
|
1264
|
+
return [
|
|
1265
|
+
"Bridge chose these defaults — change them later on the Setup page, or ask your agent:",
|
|
1266
|
+
rowTop,
|
|
1267
|
+
rowBottom,
|
|
1268
|
+
].join("\n");
|
|
1269
|
+
}
|
|
1215
1270
|
/**
|
|
1216
1271
|
* Echoed single-line prompt on stderr (used for repo confirmation / value).
|
|
1217
1272
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
@@ -1328,6 +1383,7 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1328
1383
|
return {
|
|
1329
1384
|
env: process.env,
|
|
1330
1385
|
cwd: process.cwd(),
|
|
1386
|
+
realpath: async (folder) => realpath(folder),
|
|
1331
1387
|
platform: process.platform,
|
|
1332
1388
|
homedir: os.homedir,
|
|
1333
1389
|
isTTY,
|
|
@@ -1349,7 +1405,9 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1349
1405
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
1350
1406
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
1351
1407
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
1352
|
-
promptMultiSelect: isTTY
|
|
1408
|
+
promptMultiSelect: isTTY
|
|
1409
|
+
? (promptText, options, preselected) => promptMultiSelectViaReadline(promptText, options, process.stdin, process.stderr, preselected)
|
|
1410
|
+
: undefined,
|
|
1353
1411
|
vendor: createDefaultVendorProcessDeps(spawn),
|
|
1354
1412
|
fetch: productionFetch,
|
|
1355
1413
|
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
@@ -1643,6 +1701,77 @@ function defaultShellFreeRunnerWithEnv(executable, args, extraEnv) {
|
|
|
1643
1701
|
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1644
1702
|
});
|
|
1645
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* BAPI-1405 — the owner's own `gh`, shell-free, for the epic-branch CI step.
|
|
1706
|
+
*
|
|
1707
|
+
* Same inherited environment as {@link defaultShellFreeRunnerWithEnv} (the
|
|
1708
|
+
* customer's own login is the point), plus an optional stdin body so a ruleset
|
|
1709
|
+
* update can use `gh api --input -` and keep JSON out of argv. A spawn failure
|
|
1710
|
+
* (no `gh` on PATH) resolves as exit 127 rather than throwing.
|
|
1711
|
+
*/
|
|
1712
|
+
export function defaultUserGhRunner(args, opts) {
|
|
1713
|
+
return new Promise((resolve) => {
|
|
1714
|
+
const child = spawn("gh", args, {
|
|
1715
|
+
shell: false,
|
|
1716
|
+
stdio: [opts?.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
1717
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" },
|
|
1718
|
+
});
|
|
1719
|
+
let stdout = "";
|
|
1720
|
+
let stderr = "";
|
|
1721
|
+
child.stdout?.on("data", (chunk) => {
|
|
1722
|
+
stdout += chunk.toString("utf-8");
|
|
1723
|
+
});
|
|
1724
|
+
child.stderr?.on("data", (chunk) => {
|
|
1725
|
+
stderr += chunk.toString("utf-8");
|
|
1726
|
+
});
|
|
1727
|
+
child.on("error", () => resolve({ exitCode: 127, stdout: "", stderr: "gh could not be started" }));
|
|
1728
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
1729
|
+
if (opts?.input !== undefined) {
|
|
1730
|
+
child.stdin?.on("error", () => undefined);
|
|
1731
|
+
child.stdin?.end(opts.input);
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* BAPI-1405 — the production epic-branch CI seams for one repository root.
|
|
1737
|
+
*
|
|
1738
|
+
* Provider detection reads `origin` with the owner's own git; only a canonical
|
|
1739
|
+
* github.com remote counts as GitHub (the same parser the merge-workflow
|
|
1740
|
+
* installer uses). The example epic branch is a local remote-tracking
|
|
1741
|
+
* `origin/epic/*` ref when one exists — a cheap, offline read — and otherwise
|
|
1742
|
+
* the gate module's never-created probe name.
|
|
1743
|
+
*/
|
|
1744
|
+
export function buildEpicBranchCiDeps(cwd) {
|
|
1745
|
+
const git = async (args) => {
|
|
1746
|
+
try {
|
|
1747
|
+
const r = await defaultShellFreeRunner("git", ["-C", cwd, ...args]);
|
|
1748
|
+
return r.exitCode === 0 ? r.stdout.trim() : null;
|
|
1749
|
+
}
|
|
1750
|
+
catch {
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
return {
|
|
1755
|
+
fs: createNodeGateDiscoveryFs(cwd),
|
|
1756
|
+
runGh: defaultUserGhRunner,
|
|
1757
|
+
detectProvider: async () => {
|
|
1758
|
+
const remoteUrl = await git(["remote", "get-url", "origin"]);
|
|
1759
|
+
if (!remoteUrl)
|
|
1760
|
+
return { kind: "unknown" };
|
|
1761
|
+
const ownerRepo = parseGithubRemote(remoteUrl);
|
|
1762
|
+
return ownerRepo ? { kind: "github", ownerRepo } : { kind: "not_github" };
|
|
1763
|
+
},
|
|
1764
|
+
findExampleEpicBranch: async () => {
|
|
1765
|
+
const ref = await git([
|
|
1766
|
+
"for-each-ref",
|
|
1767
|
+
"--count=1",
|
|
1768
|
+
"--format=%(refname:strip=3)",
|
|
1769
|
+
"refs/remotes/origin/epic/",
|
|
1770
|
+
]);
|
|
1771
|
+
return ref ? ref.split("\n")[0].trim() || null : null;
|
|
1772
|
+
},
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1646
1775
|
/** Presence probe: `<tool> --version` exiting 0 means the tool resolves on PATH. */
|
|
1647
1776
|
async function defaultExecutablePresenceProbe(name) {
|
|
1648
1777
|
try {
|
|
@@ -1954,9 +2083,17 @@ const INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+$/;
|
|
|
1954
2083
|
/** The one hint line printed after a blank/malformed email, before the single re-prompt. */
|
|
1955
2084
|
export const INSTALL_BRIDGE_SIGNUP_EMAIL_HINT = "Enter a valid email address (e.g. you@example.com).";
|
|
1956
2085
|
export function buildInstallBridgeSignupPrivacyNotice(baseUrl) {
|
|
1957
|
-
return `Privacy Policy: ${baseUrl.replace(/\/+$/, "")}/privacy`;
|
|
2086
|
+
return `Your email is used to verify signup and set up your Bridge workspace. Privacy Policy — how data is handled: ${baseUrl.replace(/\/+$/, "")}/privacy`;
|
|
2087
|
+
}
|
|
2088
|
+
export function buildInstallBridgeMaintenanceHint() {
|
|
2089
|
+
return [
|
|
2090
|
+
`Bridge MCP installer package: ${MCP_PACKAGE_NAME}@${VERSION}`,
|
|
2091
|
+
"From the project root, use /upgrade-bridge or run:",
|
|
2092
|
+
`npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`,
|
|
2093
|
+
"Persistent host registrations remain pinned until an explicit upgrade; the editor may need to reconnect to use the new server.",
|
|
2094
|
+
];
|
|
1958
2095
|
}
|
|
1959
|
-
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2096
|
+
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL, onboarding) {
|
|
1960
2097
|
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
1961
2098
|
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
1962
2099
|
// so an unattended run fails before any request is made, no verification email
|
|
@@ -1967,6 +2104,13 @@ export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_B
|
|
|
1967
2104
|
if (!deps.isTTY || !deps.promptLine) {
|
|
1968
2105
|
return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
|
|
1969
2106
|
}
|
|
2107
|
+
// BAPI-1448: an email already answered at the merged entry prompt is consumed the
|
|
2108
|
+
// way `--email` is, with no second question and no second privacy notice.
|
|
2109
|
+
if (onboarding) {
|
|
2110
|
+
if (!onboarding.privacyNoticeShown)
|
|
2111
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2112
|
+
return { ok: true, value: onboarding.email };
|
|
2113
|
+
}
|
|
1970
2114
|
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
1971
2115
|
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
1972
2116
|
return { ok: true, value: options.email.trim() };
|
|
@@ -2030,49 +2174,24 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
2030
2174
|
return { kind: "have-key" };
|
|
2031
2175
|
}
|
|
2032
2176
|
/**
|
|
2033
|
-
* The bare-TTY
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
* make for itself: whether the string in their clipboard is an invite or an API
|
|
2038
|
-
* key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
|
|
2039
|
-
* SHAPE alone, with no probe request — so the only thing a first-time user
|
|
2040
|
-
* genuinely knows, and the only thing worth asking, is whether they have a token
|
|
2041
|
-
* at all. Hence a yes/no question, with the route inferred afterwards.
|
|
2042
|
-
*
|
|
2043
|
-
* Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
|
|
2044
|
-
* render the heading and the numbered options exactly once, accept ONE strict
|
|
2045
|
-
* line, no default (a bare Enter selects nothing), bounded to a single re-prompt
|
|
2046
|
-
* after one hint, and EOF resolves as an empty answer so it can never hang.
|
|
2047
|
-
*/
|
|
2048
|
-
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
|
|
2049
|
-
/**
|
|
2050
|
-
* The question's numbered options, in render order. Index + 1 is the accepted
|
|
2051
|
-
* token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
|
|
2052
|
-
* option 1 — first-time signup is the common case and now leads.
|
|
2053
|
-
*
|
|
2054
|
-
* "Token" is deliberately credential-agnostic: an invite and an API key are both
|
|
2055
|
-
* tokens as far as this question is concerned, and answering "2" leads to ONE
|
|
2056
|
-
* hidden prompt whose value selects the route by shape.
|
|
2177
|
+
* The bare-TTY entry prompt (BAPI-1448). One question replaces the BAPI-708 "do you
|
|
2178
|
+
* have a token yet? 1/2" chooser: a typed email selects self-serve signup, and a
|
|
2179
|
+
* blank answer selects the have-key branch, whose value is read ONLY through the
|
|
2180
|
+
* hidden `Paste your Bridge token (input hidden): ` prompt downstream.
|
|
2057
2181
|
*/
|
|
2058
|
-
export const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
];
|
|
2062
|
-
/** The single-line answer prompt drawn after the options (no bracketed default). */
|
|
2063
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
|
|
2064
|
-
/** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
|
|
2065
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
|
|
2182
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT = "Email (or press Enter if you already have a token): ";
|
|
2183
|
+
/** The one hint line printed after a non-blank malformed answer, before the single re-prompt. */
|
|
2184
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT = "Enter an email address, or press Enter if you already have a token.";
|
|
2066
2185
|
/**
|
|
2067
2186
|
* The one place the "no usable credential" copy lives (BAPI-667, U1-3).
|
|
2068
2187
|
*
|
|
2069
|
-
* Centralized because the
|
|
2188
|
+
* Centralized because the entry-prompt failure and {@link resolveApiKey}'s two failure
|
|
2070
2189
|
* branches previously drifted: only one of them mentioned self-serve, and one
|
|
2071
2190
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
2072
2191
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
2073
2192
|
*/
|
|
2074
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run
|
|
2075
|
-
"already have
|
|
2193
|
+
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run, type your email to sign up, or press Enter if you " +
|
|
2194
|
+
"already have a token (an invite or an API key — either one is accepted at the hidden prompt). " +
|
|
2076
2195
|
"Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
2077
2196
|
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
2078
2197
|
/**
|
|
@@ -2120,7 +2239,7 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare int
|
|
|
2120
2239
|
* key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
|
|
2121
2240
|
* two.
|
|
2122
2241
|
*/
|
|
2123
|
-
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
2242
|
+
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2124
2243
|
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
2125
2244
|
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
2126
2245
|
if (branch.kind === "need-key")
|
|
@@ -2132,38 +2251,35 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
2132
2251
|
}
|
|
2133
2252
|
const promptLine = deps.promptLine;
|
|
2134
2253
|
try {
|
|
2135
|
-
//
|
|
2136
|
-
|
|
2137
|
-
deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);
|
|
2138
|
-
for (const choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)
|
|
2139
|
-
deps.log(choice);
|
|
2254
|
+
// The answer may be an email, so the privacy notice comes first and prints once.
|
|
2255
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2140
2256
|
// Bounded to two reads total: the first answer, then ONE re-prompt after a
|
|
2141
2257
|
// single hint line. EOF resolves as an empty answer (promptLineViaReadline
|
|
2142
|
-
// resolves "" on close),
|
|
2143
|
-
// it can never hang or silently select a branch.
|
|
2258
|
+
// resolves "" on close), which selects the have-key branch rather than hanging.
|
|
2144
2259
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2145
|
-
const answer = (await promptLine(
|
|
2146
|
-
//
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
|
|
2260
|
+
const answer = String((await promptLine(INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT)) ?? "").trim();
|
|
2261
|
+
// Blank (or EOF): the user has a token; it is read only by the hidden prompt.
|
|
2262
|
+
if (answer.length === 0) {
|
|
2263
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2264
|
+
}
|
|
2265
|
+
// A typed value is accepted ONLY as a signup email. It is never classified as a
|
|
2266
|
+
// credential, logged, or persisted here; a token-shaped value fails the shape
|
|
2267
|
+
// check and gets the hint.
|
|
2268
|
+
if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(answer)) {
|
|
2153
2269
|
return {
|
|
2154
2270
|
ok: true,
|
|
2155
2271
|
branch: { kind: "need-key", method: "self-serve" },
|
|
2156
2272
|
credentialIntent: "inferred",
|
|
2273
|
+
onboardingEmail: answer,
|
|
2274
|
+
privacyNoticeShown: true,
|
|
2157
2275
|
};
|
|
2158
2276
|
}
|
|
2159
|
-
if (answer === "2")
|
|
2160
|
-
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2161
2277
|
if (attempt === 0)
|
|
2162
|
-
deps.log(
|
|
2278
|
+
deps.log(INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT);
|
|
2163
2279
|
}
|
|
2164
2280
|
return {
|
|
2165
2281
|
ok: false,
|
|
2166
|
-
error: `No
|
|
2282
|
+
error: `No email or token choice was made. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
2167
2283
|
};
|
|
2168
2284
|
}
|
|
2169
2285
|
catch {
|
|
@@ -2272,22 +2388,31 @@ const SELECTION_TOKEN_PATTERN = /^[1-9][0-9]*$/;
|
|
|
2272
2388
|
* Interactive single-shot numbered picker on stderr (TTY only). Renders the
|
|
2273
2389
|
* heading and each option exactly once, then accepts ONE strict
|
|
2274
2390
|
* comma-separated line of one-based numbers and resolves immediately — there
|
|
2275
|
-
* is no
|
|
2391
|
+
* is no toggle state and no separate confirmation step (BAPI-663).
|
|
2276
2392
|
*
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2393
|
+
* BAPI-1448: when `preselected` names at least one offered option, those rows
|
|
2394
|
+
* render as `[x]` (the rest as `[ ]`) and a blank line accepts the ticked set.
|
|
2395
|
+
* Typed numbers REPLACE the ticked set; they never toggle it. With nothing
|
|
2396
|
+
* pre-ticked the original behaviour is unchanged: a blank line is rejected with
|
|
2397
|
+
* "Select at least one tool."
|
|
2398
|
+
*
|
|
2399
|
+
* A line containing any invalid token (non-numeric, zero, signed,
|
|
2400
|
+
* leading-zero, decimal, out-of-range) is rejected atomically — the whole line
|
|
2401
|
+
* is discarded, one error line is printed, and the picker re-prompts without
|
|
2402
|
+
* re-rendering the option list, so the ticks stay as first shown. A valid line
|
|
2403
|
+
* is deduplicated and returned in `options` order regardless of entry order.
|
|
2282
2404
|
*
|
|
2283
2405
|
* Resolves to `[]` on EOF / synchronous close before any line has been
|
|
2284
2406
|
* committed, so a readline regression can never hang. Returns `[]`
|
|
2285
2407
|
* immediately, without creating a readline interface, when `options` is
|
|
2286
2408
|
* empty.
|
|
2287
2409
|
*/
|
|
2288
|
-
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr) {
|
|
2410
|
+
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr, preselected = []) {
|
|
2289
2411
|
if (options.length === 0)
|
|
2290
2412
|
return Promise.resolve([]);
|
|
2413
|
+
// Canonical option order, restricted to options actually offered.
|
|
2414
|
+
const ticked = options.filter((o) => preselected.includes(o.id)).map((o) => o.id);
|
|
2415
|
+
const hasTicks = ticked.length > 0;
|
|
2291
2416
|
return new Promise((resolve) => {
|
|
2292
2417
|
const rl = readline.createInterface({ input, output });
|
|
2293
2418
|
let settled = false;
|
|
@@ -2304,13 +2429,20 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
2304
2429
|
rl.on("close", () => finish([]));
|
|
2305
2430
|
output.write(`\n${promptText}\n`);
|
|
2306
2431
|
options.forEach((opt, idx) => {
|
|
2307
|
-
|
|
2432
|
+
const box = hasTicks ? `${ticked.includes(opt.id) ? "[x]" : "[ ]"} ` : "";
|
|
2433
|
+
output.write(` ${idx + 1}. ${box}${opt.label}\n`);
|
|
2308
2434
|
});
|
|
2309
|
-
output.write(
|
|
2435
|
+
output.write(hasTicks
|
|
2436
|
+
? "Press Enter to use the ticked tools, or type the numbers you use (e.g. 1,3).\n"
|
|
2437
|
+
: "Enter the number(s) of the tools you use (e.g. 1,3), then Enter.\n");
|
|
2310
2438
|
const prompt = () => {
|
|
2311
2439
|
rl.question("> ", (answer) => {
|
|
2312
2440
|
const trimmed = answer.trim();
|
|
2313
2441
|
if (trimmed.length === 0) {
|
|
2442
|
+
if (hasTicks) {
|
|
2443
|
+
finish(ticked);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2314
2446
|
output.write("Select at least one tool.\n");
|
|
2315
2447
|
prompt();
|
|
2316
2448
|
return;
|
|
@@ -2361,10 +2493,6 @@ export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
|
|
|
2361
2493
|
const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
|
|
2362
2494
|
return mapped ? `${mapped} subscription` : "coding subscription";
|
|
2363
2495
|
}
|
|
2364
|
-
/** Option three's exact wording, platform-aware (BAPI-1221 Step 2). */
|
|
2365
|
-
function buildOptimizationPostureOptionThree(primaryPlatform) {
|
|
2366
|
-
return `Get the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}?`;
|
|
2367
|
-
}
|
|
2368
2496
|
/** The neutral single-line correction reprompted for any malformed answer. */
|
|
2369
2497
|
const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
2370
2498
|
/**
|
|
@@ -2380,15 +2508,21 @@ const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
|
2380
2508
|
const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
|
|
2381
2509
|
/**
|
|
2382
2510
|
* Render the one-time posture menu and its input instruction. Exactly three
|
|
2383
|
-
* vertically ordered numbered options; option three
|
|
2384
|
-
* wrap-safe text
|
|
2511
|
+
* vertically ordered numbered options; option three names the user's own
|
|
2512
|
+
* subscription. Plain, wrap-safe text (every line within 80 columns) — no
|
|
2513
|
+
* color-only state, icons, or decorative framing.
|
|
2385
2514
|
*/
|
|
2386
2515
|
function buildOptimizationPostureMenu(primaryPlatform) {
|
|
2387
|
-
return ("How should Bridge
|
|
2388
|
-
" 1. Quality\n" +
|
|
2389
|
-
"
|
|
2390
|
-
|
|
2391
|
-
|
|
2516
|
+
return ("How should Bridge balance quality and cost on this project?\n" +
|
|
2517
|
+
" 1. Quality — Bridge plans and reviews each ticket in full depth; slowest and\n" +
|
|
2518
|
+
" uses the most credits, but catches the most problems.\n" +
|
|
2519
|
+
" 2. Speed — Bridge plans in one pass and reviews once; faster and cheaper, but\n" +
|
|
2520
|
+
" may miss things on some tickets.\n" +
|
|
2521
|
+
" 3. Token Efficiency — Bridge will use Speed settings and hand simpler tickets\n" +
|
|
2522
|
+
" to smaller models more often, which stretches your\n" +
|
|
2523
|
+
` ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} the furthest.\n` +
|
|
2524
|
+
"Press Enter to keep Quality (or your saved choice).\n" +
|
|
2525
|
+
"Enter 1, 2, or 3: ");
|
|
2392
2526
|
}
|
|
2393
2527
|
/**
|
|
2394
2528
|
* The purpose-built, strict single-select optimization-posture picker
|
|
@@ -2478,7 +2612,7 @@ function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
|
|
|
2478
2612
|
}
|
|
2479
2613
|
/** Printed immediately after a successful posture write, on its own line. */
|
|
2480
2614
|
const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
|
|
2481
|
-
const OPTIMIZATION_POSTURE_REMEDIATION = "
|
|
2615
|
+
const OPTIMIZATION_POSTURE_REMEDIATION = "Change it later by running /teach-bridge in your agent.";
|
|
2482
2616
|
/**
|
|
2483
2617
|
* The generic, secret-free fail-open message shared by every non-success,
|
|
2484
2618
|
* non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
|
|
@@ -2496,7 +2630,7 @@ const OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE = ` Optimization posture was not c
|
|
|
2496
2630
|
*/
|
|
2497
2631
|
function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
2498
2632
|
if (result.kind === "success") {
|
|
2499
|
-
log(` Preference saved
|
|
2633
|
+
log(` Preference saved — we'll optimize for ${optimizationPostureConfirmationPhrase(value, primaryPlatform)}.`);
|
|
2500
2634
|
log(` ${OPTIMIZATION_POSTURE_BLOG_URL}`);
|
|
2501
2635
|
return;
|
|
2502
2636
|
}
|
|
@@ -2512,8 +2646,9 @@ function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
|
2512
2646
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
2513
2647
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
2514
2648
|
* back to detection),
|
|
2515
|
-
* 2. interactive multi-select on a TTY
|
|
2516
|
-
*
|
|
2649
|
+
* 2. interactive multi-select on a TTY, pre-ticked with the detected tools
|
|
2650
|
+
* (BAPI-1448 reverses the BAPI-635 AC-2 empty picker; with nothing detected
|
|
2651
|
+
* the picker still requires an explicit ≥1 selection),
|
|
2517
2652
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
2518
2653
|
* detected Copilot VS Code and Cursor automatic targets (never Codex just
|
|
2519
2654
|
* because a global directory exists).
|
|
@@ -2523,11 +2658,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
2523
2658
|
if (options.tools !== undefined) {
|
|
2524
2659
|
return options.tools;
|
|
2525
2660
|
}
|
|
2526
|
-
// 2. Interactive single-shot numbered picker on a TTY
|
|
2527
|
-
//
|
|
2661
|
+
// 2. Interactive single-shot numbered picker on a TTY, seeded with the locally
|
|
2662
|
+
// detected tools (BAPI-1448): Enter accepts them, typed numbers replace them.
|
|
2528
2663
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
2529
2664
|
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
2530
|
-
const
|
|
2665
|
+
const detected = detectDefaultPlatforms(await buildDetectionContext(deps));
|
|
2666
|
+
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, detected);
|
|
2531
2667
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
2532
2668
|
}
|
|
2533
2669
|
// 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
|
|
@@ -2645,42 +2781,6 @@ export function toolLabelForLaunchAgent(agent) {
|
|
|
2645
2781
|
const target = allHostTargets().find((t) => t.launchAgent === agent);
|
|
2646
2782
|
return target?.label ?? UNKNOWN_LAUNCH_TOOL_LABEL;
|
|
2647
2783
|
}
|
|
2648
|
-
/**
|
|
2649
|
-
* Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
|
|
2650
|
-
* (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
|
|
2651
|
-
* label, asks for an exact numeric choice with NO default, validates the range, and
|
|
2652
|
-
* returns the chosen agent. Returns `null` — never a silent first-candidate default —
|
|
2653
|
-
* for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
|
|
2654
|
-
* input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
|
|
2655
|
-
* it must run only AFTER the multi-select promise has resolved and closed its readline
|
|
2656
|
-
* interface so two readers never contend for stdin.
|
|
2657
|
-
*/
|
|
2658
|
-
export async function chooseInstallBridgeLaunchAgent(agents, deps) {
|
|
2659
|
-
if (!deps.isTTY || !deps.promptLine)
|
|
2660
|
-
return null;
|
|
2661
|
-
const promptLine = deps.promptLine;
|
|
2662
|
-
try {
|
|
2663
|
-
deps.log("");
|
|
2664
|
-
deps.log("More than one selected tool can host the configuration session:");
|
|
2665
|
-
agents.forEach((agent, i) => {
|
|
2666
|
-
deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
|
|
2667
|
-
});
|
|
2668
|
-
// No default: a stray Enter must not silently pick a tool.
|
|
2669
|
-
const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
|
|
2670
|
-
const index = Number(answer);
|
|
2671
|
-
if (!/^\d+$/.test(answer) ||
|
|
2672
|
-
!Number.isInteger(index) ||
|
|
2673
|
-
index < 1 ||
|
|
2674
|
-
index > agents.length) {
|
|
2675
|
-
return null;
|
|
2676
|
-
}
|
|
2677
|
-
return agents[index - 1];
|
|
2678
|
-
}
|
|
2679
|
-
catch {
|
|
2680
|
-
// Never surface caught exception text; an unavailable chooser is a no-spawn.
|
|
2681
|
-
return null;
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
2784
|
/**
|
|
2685
2785
|
* Shared, copy-pasteable "we didn't open a session for you" guidance so every
|
|
2686
2786
|
* no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
|
|
@@ -3386,6 +3486,7 @@ async function removeUnprovenBridgeConfig(deps, createdText) {
|
|
|
3386
3486
|
* ONCE for the whole batch, so selecting both does not print it twice.
|
|
3387
3487
|
*/
|
|
3388
3488
|
async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
3489
|
+
let complete = true;
|
|
3389
3490
|
const logLines = [];
|
|
3390
3491
|
const duplicateRegistrations = [];
|
|
3391
3492
|
let anyManualRequired = false;
|
|
@@ -3418,14 +3519,17 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
3418
3519
|
logLines.push(` configured ${target.label} (${outcome.displayPath})`);
|
|
3419
3520
|
break;
|
|
3420
3521
|
case "manual-required":
|
|
3522
|
+
complete = false;
|
|
3421
3523
|
anyManualRequired = true;
|
|
3422
3524
|
logLines.push(` ${target.label}: add the ${MCP_SERVER_NAME} MCP server manually to ${outcome.displayPath} ` +
|
|
3423
3525
|
"(the API key is redacted in printed instructions).");
|
|
3424
3526
|
break;
|
|
3425
3527
|
case "skipped-invalid":
|
|
3528
|
+
complete = false;
|
|
3426
3529
|
logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
|
|
3427
3530
|
break;
|
|
3428
3531
|
case "skipped-duplicate-registration":
|
|
3532
|
+
complete = false;
|
|
3429
3533
|
// BAPI-807: NOT an ordinary vendor-fallback condition. The target was
|
|
3430
3534
|
// left byte-identical and the caller must fail loudly rather than report
|
|
3431
3535
|
// a configured host.
|
|
@@ -3433,6 +3537,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
3433
3537
|
logLines.push(` ${target.label}: skipped ${outcome.displayPath} — ${outcome.detail}`);
|
|
3434
3538
|
break;
|
|
3435
3539
|
case "failed":
|
|
3540
|
+
complete = false;
|
|
3436
3541
|
logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
|
|
3437
3542
|
break;
|
|
3438
3543
|
}
|
|
@@ -3440,7 +3545,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
3440
3545
|
if (needKey && anyManualRequired) {
|
|
3441
3546
|
logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
|
|
3442
3547
|
}
|
|
3443
|
-
return { logLines, duplicateRegistrations };
|
|
3548
|
+
return { logLines, duplicateRegistrations, complete };
|
|
3444
3549
|
}
|
|
3445
3550
|
/** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
|
|
3446
3551
|
export function buildPingUrl(baseUrl, repoName) {
|
|
@@ -3642,8 +3747,20 @@ export function generateBootstrapKeySecret(randomBytes) {
|
|
|
3642
3747
|
* the identity guard in both shapes, and prepare / repoint / promote / discard all
|
|
3643
3748
|
* still match on it.
|
|
3644
3749
|
*/
|
|
3645
|
-
export function fingerprintBootstrapInvite(token) {
|
|
3646
|
-
|
|
3750
|
+
export function fingerprintBootstrapInvite(token, baseUrl) {
|
|
3751
|
+
if (baseUrl === undefined)
|
|
3752
|
+
return createHash("sha256").update(token, "utf-8").digest("hex");
|
|
3753
|
+
return fingerprintBootstrapFields("bridge-bootstrap-invite-v1", [baseUrl, token]);
|
|
3754
|
+
}
|
|
3755
|
+
function fingerprintBootstrapFields(domain, fields) {
|
|
3756
|
+
const hash = createHash("sha256").update(domain + "\0", "utf-8");
|
|
3757
|
+
for (const field of fields) {
|
|
3758
|
+
const bytes = Buffer.from(field, "utf-8");
|
|
3759
|
+
const length = Buffer.alloc(4);
|
|
3760
|
+
length.writeUInt32BE(bytes.length);
|
|
3761
|
+
hash.update(length).update(bytes);
|
|
3762
|
+
}
|
|
3763
|
+
return hash.digest("hex");
|
|
3647
3764
|
}
|
|
3648
3765
|
/** The exchange endpoint: `<base>/setup/bootstrap`. No query string — ever. */
|
|
3649
3766
|
export function buildBootstrapExchangeUrl(baseUrl) {
|
|
@@ -3659,15 +3776,15 @@ export function buildBootstrapExchangeUrl(baseUrl) {
|
|
|
3659
3776
|
* the legacy three-field one, byte for byte.
|
|
3660
3777
|
*/
|
|
3661
3778
|
export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, keySecret, email) {
|
|
3779
|
+
const modern = typeof repoName !== "string";
|
|
3780
|
+
const identity = modern ? { repo_display_name: validateBootstrapDisplayName(repoName.repoDisplayName) } : { repo_name: repoName };
|
|
3662
3781
|
const url = buildBootstrapExchangeUrl(baseUrl);
|
|
3663
3782
|
let resp;
|
|
3664
3783
|
try {
|
|
3665
3784
|
resp = await deps.fetch(url, {
|
|
3666
3785
|
method: "POST",
|
|
3667
3786
|
headers: { "Content-Type": "application/json" },
|
|
3668
|
-
body: JSON.stringify(email
|
|
3669
|
-
? { token, repo_name: repoName, key_secret: keySecret, email }
|
|
3670
|
-
: { token, repo_name: repoName, key_secret: keySecret }),
|
|
3787
|
+
body: JSON.stringify({ token, ...identity, key_secret: keySecret, ...(email ? { email } : {}) }),
|
|
3671
3788
|
signal: AbortSignal.timeout(10_000),
|
|
3672
3789
|
});
|
|
3673
3790
|
}
|
|
@@ -3683,13 +3800,17 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
3683
3800
|
ok: false,
|
|
3684
3801
|
kind: "failed",
|
|
3685
3802
|
message: `Could not reach the Bridge API at ${baseUrl} to redeem the bootstrap invite. Check ` +
|
|
3686
|
-
"BAPI_BASE_URL and your network.
|
|
3803
|
+
"BAPI_BASE_URL and your network. Creation outcome is unknown; preserve proof and rerun the same attempt.",
|
|
3687
3804
|
};
|
|
3688
3805
|
}
|
|
3689
3806
|
if (resp.ok) {
|
|
3690
3807
|
let repo;
|
|
3691
3808
|
try {
|
|
3692
3809
|
const body = (await resp.json());
|
|
3810
|
+
if (modern) {
|
|
3811
|
+
const identity = validateBootstrapServerIdentity(body);
|
|
3812
|
+
return { ok: true, repoName: identity.repo_name, displayName: identity.repo_display_name };
|
|
3813
|
+
}
|
|
3693
3814
|
repo = body?.repo_name;
|
|
3694
3815
|
}
|
|
3695
3816
|
catch {
|
|
@@ -3711,7 +3832,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
3711
3832
|
}
|
|
3712
3833
|
return { ok: true, repoName: validated.value };
|
|
3713
3834
|
}
|
|
3714
|
-
if (resp.status === 409) {
|
|
3835
|
+
if (resp.status === 409 && !modern) {
|
|
3715
3836
|
return {
|
|
3716
3837
|
ok: false,
|
|
3717
3838
|
kind: "repo-name-taken",
|
|
@@ -3744,7 +3865,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
3744
3865
|
return {
|
|
3745
3866
|
ok: false,
|
|
3746
3867
|
kind: "failed",
|
|
3747
|
-
message: `The bootstrap exchange failed (HTTP ${resp.status}).
|
|
3868
|
+
message: `The bootstrap exchange failed (HTTP ${resp.status}). Preserve proof and rerun the same attempt; verify server compatibility and contact installation support if it persists.`,
|
|
3748
3869
|
};
|
|
3749
3870
|
}
|
|
3750
3871
|
/** The invite-email prompt (BAPI-1263), shown once after an `email_required` refusal. */
|
|
@@ -3910,6 +4031,27 @@ export function buildSelfServeMintUrl(baseUrl) {
|
|
|
3910
4031
|
export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
|
|
3911
4032
|
/** The one prompt label. The E2E harness and the static contract both pin it. */
|
|
3912
4033
|
export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
|
|
4034
|
+
/**
|
|
4035
|
+
* The address verification codes are sent from. Mirrors the server's
|
|
4036
|
+
* `DEFAULT_FROM_ADDRESS` in `api/library/email/provider.py`; keep the two in step.
|
|
4037
|
+
*/
|
|
4038
|
+
export const INSTALL_BRIDGE_VERIFICATION_SENDER = "noreply@bridgegpt-api.com";
|
|
4039
|
+
/**
|
|
4040
|
+
* The shared lead-in printed immediately before every self-serve
|
|
4041
|
+
* {@link promptForVerificationCode} call (modern and legacy paths, fresh or resumed):
|
|
4042
|
+
* three unindented lines, then a blank line. It prints neither the prompt nor any
|
|
4043
|
+
* expiry or time-left text.
|
|
4044
|
+
*
|
|
4045
|
+
* BAPI-1449 (verification recovery) builds on this lead-in instead of adding its
|
|
4046
|
+
* own, and may later return the sender from the request endpoint rather than using
|
|
4047
|
+
* {@link INSTALL_BRIDGE_VERIFICATION_SENDER}.
|
|
4048
|
+
*/
|
|
4049
|
+
export function logVerificationCodeLeadIn(log, email) {
|
|
4050
|
+
log(`We emailed a ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to ${maskSignupEmail(email)}.`);
|
|
4051
|
+
log(`It usually arrives within two minutes. It comes from ${INSTALL_BRIDGE_VERIFICATION_SENDER} — check your`);
|
|
4052
|
+
log("spam or junk folder if you don't see it.");
|
|
4053
|
+
log("");
|
|
4054
|
+
}
|
|
3913
4055
|
/** Shown when a stored, unexpired challenge is picked up on a later run. */
|
|
3914
4056
|
export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
|
|
3915
4057
|
/**
|
|
@@ -4680,8 +4822,8 @@ function buildDeferredLaunchStepPreview() {
|
|
|
4680
4822
|
/**
|
|
4681
4823
|
* The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
|
|
4682
4824
|
* prompts or writes; when the selection resolves to at least one writable
|
|
4683
|
-
* notes target this states plainly that a real interactive run
|
|
4684
|
-
*
|
|
4825
|
+
* notes target this states plainly that a real interactive run writes it
|
|
4826
|
+
* without asking. Empty when the selection resolves to
|
|
4685
4827
|
* no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
|
|
4686
4828
|
* lines are unconditional but this offer's applicability is not.
|
|
4687
4829
|
*/
|
|
@@ -4696,10 +4838,10 @@ function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
|
|
|
4696
4838
|
"Step 4c — reached only after the Step 4b VCS connect offer has completed or been",
|
|
4697
4839
|
" explicitly deferred (a declined or incomplete connect is announced first,",
|
|
4698
4840
|
" with the command to finish it later, and never blocks this step).",
|
|
4699
|
-
`Step 4c —
|
|
4700
|
-
` terminal is interactive,
|
|
4701
|
-
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}
|
|
4702
|
-
" to
|
|
4841
|
+
`Step 4c — Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
|
|
4842
|
+
` terminal is interactive, a real run writes guidance so ${subject} know${plural ? "" : "s"} when to reach`,
|
|
4843
|
+
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}, without asking,`,
|
|
4844
|
+
" then prints how to remove it. Existing content outside that region is preserved. No notes files are",
|
|
4703
4845
|
" written in --dry-run.",
|
|
4704
4846
|
];
|
|
4705
4847
|
}
|
|
@@ -5315,6 +5457,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5315
5457
|
// Built from the FINAL merged deps for the same reason (BAPI-775): a test
|
|
5316
5458
|
// that overrides only `readFile` or only `cwd` must see its override here.
|
|
5317
5459
|
localProvisioning: buildConductorLocalDeps(deps),
|
|
5460
|
+
// BAPI-1391: the read-only discovery filesystem over THIS working tree, and
|
|
5461
|
+
// the short-lived qualifying executor (the existing executor CLI with a
|
|
5462
|
+
// `runExecutor` override limiting it to `gate_qualification` jobs).
|
|
5463
|
+
gate: deps.conductorGate ?? {
|
|
5464
|
+
discoveryFs: createNodeGateDiscoveryFs(deps.cwd),
|
|
5465
|
+
// BAPI-1405: epic-branch CI inspection over this working tree, with the
|
|
5466
|
+
// owner's own shell-free `gh`.
|
|
5467
|
+
epicBranchCi: buildEpicBranchCiDeps(deps.cwd),
|
|
5468
|
+
runQualificationExecutor: createQualificationExecutorRunner({
|
|
5469
|
+
runExecutorCli,
|
|
5470
|
+
runExecutor,
|
|
5471
|
+
errorLog: deps.errorLog,
|
|
5472
|
+
}),
|
|
5473
|
+
},
|
|
5318
5474
|
};
|
|
5319
5475
|
return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
|
|
5320
5476
|
}
|
|
@@ -5330,6 +5486,29 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5330
5486
|
return 1;
|
|
5331
5487
|
}
|
|
5332
5488
|
const options = parsed.options;
|
|
5489
|
+
if (options.displayName !== undefined && (options.repo !== undefined || deps.env.BAPI_REPO_NAME?.trim())) {
|
|
5490
|
+
errorLog("Error: --display-name conflicts with --repo or BAPI_REPO_NAME. Use a friendly label for creation or a canonical selector for legacy creation/reconnect.");
|
|
5491
|
+
return 1;
|
|
5492
|
+
}
|
|
5493
|
+
if (options.displayName !== undefined && !options.inviteMode && !options.email && !deps.env.BAPI_INVITE?.trim() && !deps.env.BAPI_SIGNUP_EMAIL?.trim() && (options.apiKey !== undefined || deps.env.BAPI_API_KEY?.trim())) {
|
|
5494
|
+
errorLog("Error: --display-name applies only to new creation. Existing keys use canonical --repo selection or single-project resolution.");
|
|
5495
|
+
return 1;
|
|
5496
|
+
}
|
|
5497
|
+
// ---- BAPI-1398: --remove-bridge-permissions — removal only, nothing else ----
|
|
5498
|
+
// Ahead of the base-URL check, onboarding, every prompt, every credential read,
|
|
5499
|
+
// and every network call: it touches only the two settings files and Bridge's
|
|
5500
|
+
// ownership manifest.
|
|
5501
|
+
if (options.removeBridgePermissions) {
|
|
5502
|
+
return runBridgePermissionsRemoval({
|
|
5503
|
+
cwd: deps.cwd,
|
|
5504
|
+
homeDir: deps.homedir(),
|
|
5505
|
+
readFile: deps.readFile,
|
|
5506
|
+
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
5507
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
5508
|
+
log,
|
|
5509
|
+
errorLog,
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5333
5512
|
// Deliberately still ahead of the onboarding branch, any prompt, any network
|
|
5334
5513
|
// call, and the --dry-run preview: a scheme-less value (the classic error) used
|
|
5335
5514
|
// to throw ERR_INVALID_URL deep inside `buildPingUrl` — even in --dry-run — often
|
|
@@ -5490,7 +5669,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5490
5669
|
// The selection is pure and deterministic for every explicit, env-driven, and
|
|
5491
5670
|
// non-TTY invocation; only a BARE interactive run is asked which branch it wants
|
|
5492
5671
|
// (BAPI-626 — otherwise a first-time user can never reach self-serve).
|
|
5493
|
-
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
|
|
5672
|
+
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl);
|
|
5494
5673
|
if (!branchResult.ok) {
|
|
5495
5674
|
errorLog(`Error: ${branchResult.error}`);
|
|
5496
5675
|
return 1;
|
|
@@ -5532,7 +5711,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5532
5711
|
// key-authentication ping, so there is no source to attribute.
|
|
5533
5712
|
let apiKeySource;
|
|
5534
5713
|
if (selfServeSignupMode) {
|
|
5535
|
-
const emailResult = await resolveSignupEmail(options, deps, baseUrl
|
|
5714
|
+
const emailResult = await resolveSignupEmail(options, deps, baseUrl, branchResult.onboardingEmail
|
|
5715
|
+
? {
|
|
5716
|
+
email: branchResult.onboardingEmail,
|
|
5717
|
+
privacyNoticeShown: branchResult.privacyNoticeShown === true,
|
|
5718
|
+
}
|
|
5719
|
+
: undefined);
|
|
5536
5720
|
if (!emailResult.ok) {
|
|
5537
5721
|
errorLog(`Error: ${emailResult.error}`);
|
|
5538
5722
|
return 1;
|
|
@@ -5667,6 +5851,56 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5667
5851
|
// 404 is the BAPI-616 feature-detect for an older or self-hosted server, and
|
|
5668
5852
|
// `error` covers a transient network failure or a 401. Collapsing these into
|
|
5669
5853
|
// the hard stop would make a blip unrecoverable.
|
|
5854
|
+
if (!bootstrapInviteMode && options.displayName !== undefined) {
|
|
5855
|
+
errorLog("Error: --display-name applies only to creation; existing-key registration uses canonical selection.");
|
|
5856
|
+
return 1;
|
|
5857
|
+
}
|
|
5858
|
+
let modernAttempt;
|
|
5859
|
+
if (options.tools?.length === 0 && !options.agentName) {
|
|
5860
|
+
log(buildManualInstallBridgeContinuation("empty-selection", []));
|
|
5861
|
+
return 0;
|
|
5862
|
+
}
|
|
5863
|
+
if (bootstrapInviteMode && resolveConfiguredRepoName(options, deps.env) === undefined) {
|
|
5864
|
+
if (!selfServeSignupMode) {
|
|
5865
|
+
const legacy = await lookupLegacyBootstrapInvite(fingerprintBootstrapInvite(inviteToken), modernWriteDeps(deps));
|
|
5866
|
+
if (!legacy.ok) {
|
|
5867
|
+
errorLog(legacy.error);
|
|
5868
|
+
return 1;
|
|
5869
|
+
}
|
|
5870
|
+
if (legacy.repoName)
|
|
5871
|
+
options.repo = legacy.repoName;
|
|
5872
|
+
}
|
|
5873
|
+
else {
|
|
5874
|
+
// Historical default creation used the folder basename as its canonical name.
|
|
5875
|
+
const oldName = validateRepoName(path.basename(deps.cwd));
|
|
5876
|
+
if (oldName.ok) {
|
|
5877
|
+
const legacy = await deps.lookupSelfServeBootstrapPending({ repoName: oldName.value, now: deps.now() }, modernWriteDeps(deps));
|
|
5878
|
+
if (!legacy.ok) {
|
|
5879
|
+
errorLog(legacy.error);
|
|
5880
|
+
return 1;
|
|
5881
|
+
}
|
|
5882
|
+
if (legacy.state === "resumable" || legacy.state === "challenge")
|
|
5883
|
+
options.repo = oldName.value;
|
|
5884
|
+
if (legacy.state === "challenge-expired") {
|
|
5885
|
+
errorLog("Saved signup expired. Preserve proof and contact installation support.");
|
|
5886
|
+
return 1;
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
if (options.repo === undefined) {
|
|
5891
|
+
try {
|
|
5892
|
+
modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode);
|
|
5893
|
+
apiKey = modernAttempt.record.keySecret;
|
|
5894
|
+
options.repo = modernAttempt.repoName;
|
|
5895
|
+
bootstrapInviteMode = false;
|
|
5896
|
+
deps.log(`Project '${modernAttempt.displayName}' recovered; completing installation.`);
|
|
5897
|
+
}
|
|
5898
|
+
catch (error) {
|
|
5899
|
+
errorLog(error instanceof ModernInstallRecoveryError ? error.message : "Installation could not complete. Preserve recovery proof and resume from the same folder and invite/signup; contact installation support if needed.");
|
|
5900
|
+
return 1;
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5670
5904
|
let repoName;
|
|
5671
5905
|
/** The resolver's OUTCOME (C-4), or undefined when it was never called. */
|
|
5672
5906
|
let serverRepoResolutionStatus;
|
|
@@ -5798,6 +6032,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5798
6032
|
// before any of the credential/repo/host resolution above — so by this point
|
|
5799
6033
|
// the run is unconditionally the LIVE path. See the early return above for
|
|
5800
6034
|
// the inert preview.
|
|
6035
|
+
// One advisory per live interactive handoff. Never construct probe dependencies
|
|
6036
|
+
// on dry-run/noninteractive paths, and never change install/consent outcomes.
|
|
6037
|
+
let advisoryShown = false;
|
|
6038
|
+
const showFirstBuildAdvisory = async (agent) => {
|
|
6039
|
+
if (advisoryShown || !deps.isTTY || options.dryRun)
|
|
6040
|
+
return;
|
|
6041
|
+
advisoryShown = true;
|
|
6042
|
+
try {
|
|
6043
|
+
const advisory = await collectFirstBuildAdvisory({
|
|
6044
|
+
decision: agent ? { kind: "spawn", agent } : { kind: "manual", reason: "manual-continuation" },
|
|
6045
|
+
configuredHosts: labelsForPlatforms(selectedPlatforms),
|
|
6046
|
+
}, { ...deps.startTicketsDeps, platform: deps.platform, env: deps.env, cwd: deps.cwd });
|
|
6047
|
+
log(formatFirstBuildAdvisory(advisory));
|
|
6048
|
+
}
|
|
6049
|
+
catch {
|
|
6050
|
+
log(`First-build prerequisites: diagnostic availability unknown. Run ${INSTALL_BRIDGE_DOCTOR_COMMAND} for read-only follow-up.`);
|
|
6051
|
+
}
|
|
6052
|
+
};
|
|
5801
6053
|
// ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
|
|
5802
6054
|
// AC-3: with zero configured tools there is nothing to write and no session to open.
|
|
5803
6055
|
// Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
|
|
@@ -5805,14 +6057,15 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5805
6057
|
if (planLaunch.kind === "manual" && planLaunch.reason === "empty-selection") {
|
|
5806
6058
|
// Explicit empty selection: no labels to name, so the generic fallback is
|
|
5807
6059
|
// intentional here — pass [] rather than `labelsForPlatforms(selectedPlatforms)`.
|
|
6060
|
+
await showFirstBuildAdvisory(null);
|
|
5808
6061
|
log(buildManualInstallBridgeContinuation("empty-selection", []));
|
|
5809
6062
|
return 0;
|
|
5810
6063
|
}
|
|
5811
6064
|
// ---- Finalize the launch agent for spawn-capable decisions ----
|
|
5812
|
-
// choose-one:
|
|
5813
|
-
//
|
|
5814
|
-
//
|
|
5815
|
-
//
|
|
6065
|
+
// choose-one: on a TTY, take the first launchable agent the user selected and say so
|
|
6066
|
+
// (BAPI-1448 retired the launch-agent chooser; `--agent` still wins
|
|
6067
|
+
// upstream in the pure launch decision). A non-TTY run degrades to the manual
|
|
6068
|
+
// continuation (finalAgentName stays null).
|
|
5816
6069
|
let finalAgentName = null;
|
|
5817
6070
|
let finalSpawnCommand = null;
|
|
5818
6071
|
if (planLaunch.kind === "spawn") {
|
|
@@ -5820,8 +6073,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5820
6073
|
finalSpawnCommand = planLaunch.spawnCommand;
|
|
5821
6074
|
}
|
|
5822
6075
|
else if (planLaunch.kind === "choose-one") {
|
|
5823
|
-
const chosen =
|
|
6076
|
+
const chosen = deps.isTTY ? (planLaunch.agents[0] ?? null) : null;
|
|
5824
6077
|
if (chosen) {
|
|
6078
|
+
log(`Using ${toolLabelForLaunchAgent(chosen)} for the setup session (the first tool you selected). Pass --agent to choose another.`);
|
|
5825
6079
|
const spec = resolveAgentSpec(chosen);
|
|
5826
6080
|
if (!spec) {
|
|
5827
6081
|
errorLog(`Error: no launch agent is registered for '${chosen}'.`);
|
|
@@ -5831,7 +6085,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5831
6085
|
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
5832
6086
|
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
|
|
5833
6087
|
}
|
|
5834
|
-
// chosen === null → non-TTY
|
|
6088
|
+
// chosen === null → non-TTY → manual continuation below.
|
|
5835
6089
|
}
|
|
5836
6090
|
// planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
|
|
5837
6091
|
// ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
|
|
@@ -6020,7 +6274,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6020
6274
|
challengeExpiresAt = liveChallenge.expiresAt;
|
|
6021
6275
|
challengeEmail = liveChallenge.email;
|
|
6022
6276
|
log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
|
|
6023
|
-
log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6024
6277
|
}
|
|
6025
6278
|
else {
|
|
6026
6279
|
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
@@ -6080,12 +6333,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6080
6333
|
"No workspace has been set up — fix the problem and re-run.");
|
|
6081
6334
|
return false;
|
|
6082
6335
|
}
|
|
6083
|
-
log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6084
6336
|
}
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
if (remaining)
|
|
6088
|
-
log(` ${remaining}`);
|
|
6337
|
+
// BAPI-1448: the shared lead-in, fresh or resumed, directly before code entry.
|
|
6338
|
+
logVerificationCodeLeadIn(log, challengeEmail);
|
|
6089
6339
|
// Phase 2, second half, and phase 3. One resend is offered on the recovery
|
|
6090
6340
|
// path only — never proactively — so the budget is spent on the user's
|
|
6091
6341
|
// explicit request rather than by the CLI's own retry logic.
|
|
@@ -6467,7 +6717,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6467
6717
|
log(currentStep);
|
|
6468
6718
|
}
|
|
6469
6719
|
// BAPI-668: a bootstrap-minted key is verified WITHOUT source attribution.
|
|
6470
|
-
const ping = await verifyConnectivity(deps, baseUrl, repoName, apiKey, bootstrapInviteMode ? undefined : apiKeySource);
|
|
6720
|
+
const ping = modernAttempt ? { ok: true } : await verifyConnectivity(deps, baseUrl, repoName, apiKey, bootstrapInviteMode ? undefined : apiKeySource);
|
|
6471
6721
|
if (!ping.ok) {
|
|
6472
6722
|
// BAPI-667: in either need-key mode the exchange already succeeded and the
|
|
6473
6723
|
// pending record survives, so this failure IS resumable — but only with the
|
|
@@ -6482,14 +6732,22 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6482
6732
|
// file. Advisory: it warns on any problem and never fails the install.
|
|
6483
6733
|
unprovenBridgeConfigPending = false;
|
|
6484
6734
|
await reconcileBridgeConfigAfterPing(deps, repoName, scaffoldedBridgeConfigText);
|
|
6735
|
+
if (modernAttempt) {
|
|
6736
|
+
const reconciled = parseBridgeConfigToml(await deps.readFile(bridgeConfigPath(deps.cwd)));
|
|
6737
|
+
if (!reconciled.ok || reconciled.manifest.repoName !== repoName)
|
|
6738
|
+
return fatal("Installation incomplete: canonical configuration read-back failed. Preserve proof and resume from this folder.");
|
|
6739
|
+
}
|
|
6485
6740
|
// ---- BAPI-1221 (S2): persist the captured posture, now that connectivity
|
|
6486
6741
|
// is validated and `repoName`/`apiKey` are both final. Overwrites any
|
|
6487
6742
|
// existing value unconditionally — the old value is never read first. A
|
|
6488
6743
|
// no-answer (or a prompt/stream exception the picker already absorbed)
|
|
6489
6744
|
// reports the same fail-open remediation without ever attempting a write.
|
|
6745
|
+
let persistedPosture;
|
|
6490
6746
|
if (optimizationPostureSelection) {
|
|
6491
6747
|
const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
|
|
6492
6748
|
reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
|
|
6749
|
+
if (postureResult.kind === "success")
|
|
6750
|
+
persistedPosture = optimizationPostureSelection;
|
|
6493
6751
|
}
|
|
6494
6752
|
else if (optimizationPostureEligible) {
|
|
6495
6753
|
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
@@ -6535,6 +6793,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6535
6793
|
trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
|
|
6536
6794
|
}
|
|
6537
6795
|
const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
|
|
6796
|
+
if (modernAttempt) {
|
|
6797
|
+
if (writeResult.skipped.length || writeResult.written.length !== targets.length) {
|
|
6798
|
+
return fatal("Installation incomplete: selected host configuration was declined or failed. Recovery proof is preserved.");
|
|
6799
|
+
}
|
|
6800
|
+
for (const item of writeResult.written) {
|
|
6801
|
+
const config = JSON.parse(await deps.readFile(path.join(deps.cwd, item.relPath)));
|
|
6802
|
+
const target = targets.find(target => target.relPath === item.relPath);
|
|
6803
|
+
const binding = target ? config[target.topLevelKey]?.[item.registrationKey] : undefined;
|
|
6804
|
+
const keyMatches = item.mode === "secret-free"
|
|
6805
|
+
? binding?.env && !Object.prototype.hasOwnProperty.call(binding.env, "BAPI_API_KEY")
|
|
6806
|
+
: binding?.env?.BAPI_API_KEY === apiKey;
|
|
6807
|
+
if (binding?.env?.BAPI_REPO_NAME !== repoName || !keyMatches)
|
|
6808
|
+
return fatal("Installation incomplete: host binding read-back failed. Recovery proof is preserved.");
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6538
6811
|
// BAPI-807: a both-key project config is a FAIL-LOUD condition, not an
|
|
6539
6812
|
// ordinary skip. Nothing was written (the pre-write scan returned before the
|
|
6540
6813
|
// first target), and continuing would report a successful install over a
|
|
@@ -6572,6 +6845,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6572
6845
|
const globalProvisioning = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry, needKeyManual);
|
|
6573
6846
|
for (const line of globalProvisioning.logLines)
|
|
6574
6847
|
log(line);
|
|
6848
|
+
if (modernAttempt && !globalProvisioning.complete)
|
|
6849
|
+
return fatal("Installation incomplete: a selected host requires completion. Recovery proof is preserved.");
|
|
6575
6850
|
// BAPI-807: a global target that refused because it already carries both
|
|
6576
6851
|
// registrations is fail-loud, exactly like the project-config case above —
|
|
6577
6852
|
// never an ordinary vendor fallback the run can shrug off.
|
|
@@ -6653,7 +6928,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6653
6928
|
// in bootstrap mode the exchange loop above is the ONLY way to reach here, so the
|
|
6654
6929
|
// two are equivalent — but keying promotion to the same state that justifies the
|
|
6655
6930
|
// catch block's resume guidance makes that invariant explicit and un-driftable.
|
|
6656
|
-
if (
|
|
6931
|
+
if (modernAttempt) {
|
|
6932
|
+
currentStep = INSTALL_BRIDGE_STEP_LABELS.persistCredential;
|
|
6933
|
+
log(currentStep);
|
|
6934
|
+
let promoted = await promoteModernBootstrapCredential(modernAttempt.scope, modernAttempt.record, credentialWriteDeps, options.force);
|
|
6935
|
+
if (!promoted.ok && promoted.needsConsent && deps.isTTY && deps.promptLine) {
|
|
6936
|
+
const consent = (await deps.promptLine("Replace the existing canonical project credential? [y/N]: ")).trim().toLowerCase();
|
|
6937
|
+
if (consent === "y" || consent === "yes")
|
|
6938
|
+
promoted = await promoteModernBootstrapCredential(modernAttempt.scope, modernAttempt.record, credentialWriteDeps, true, promoted.observedKey);
|
|
6939
|
+
}
|
|
6940
|
+
if (!promoted.ok)
|
|
6941
|
+
return fatal("Installation incomplete: credential promotion failed. Preserve proof and resume from the same folder.");
|
|
6942
|
+
log(formatProjectCredentialStoreLine(repoName, credentialStorePath));
|
|
6943
|
+
log(`Project '${modernAttempt.displayName}' installed successfully.`);
|
|
6944
|
+
}
|
|
6945
|
+
else if (bootstrapExchangeSucceeded) {
|
|
6657
6946
|
// FAIL-CLOSED (unlike normal mode below). The pending secret IS the minted admin
|
|
6658
6947
|
// key — if it never lands under bapi:<repo>, the user has a live key they cannot
|
|
6659
6948
|
// resolve. Promotion removes the pending record and writes the credential in one
|
|
@@ -6742,8 +7031,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6742
7031
|
// selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
|
|
6743
7032
|
// other persisted install-state contract — consent is re-evaluated every run.
|
|
6744
7033
|
const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
|
|
6745
|
-
const
|
|
6746
|
-
const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets, notesPlatformLabels);
|
|
7034
|
+
const notesOffer = offerAgentGuidanceNotes(deps, notesPlan.targets);
|
|
6747
7035
|
let notesResult;
|
|
6748
7036
|
let notesWriteFailed = false;
|
|
6749
7037
|
if (notesOffer.kind === "consented") {
|
|
@@ -6759,6 +7047,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6759
7047
|
for (const file of notesResult.files) {
|
|
6760
7048
|
if (file.status === "failed") {
|
|
6761
7049
|
errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`);
|
|
7050
|
+
errorLog(` Resolve the reported issue in ${file.target}, then rerun an interactive install to retry guidance.`);
|
|
6762
7051
|
}
|
|
6763
7052
|
}
|
|
6764
7053
|
}
|
|
@@ -6772,6 +7061,42 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6772
7061
|
const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
|
|
6773
7062
|
if (notesSummaryLine)
|
|
6774
7063
|
log(notesSummaryLine);
|
|
7064
|
+
// The removal hint only when something was actually written (BAPI-1448).
|
|
7065
|
+
const notesWrittenTargets = (notesResult?.files ?? [])
|
|
7066
|
+
.filter((f) => f.status === "written")
|
|
7067
|
+
.map((f) => f.target);
|
|
7068
|
+
if (notesWrittenTargets.length > 0)
|
|
7069
|
+
log(INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT);
|
|
7070
|
+
const notesUnchangedTargets = (notesResult?.files ?? [])
|
|
7071
|
+
.filter((f) => f.status === "skipped")
|
|
7072
|
+
.map((f) => f.target);
|
|
7073
|
+
// ---- BAPI-1398: Bridge permission rules for /drive-epic-run (Claude Code) ----
|
|
7074
|
+
// After the durable install work and the other offers, before the session
|
|
7075
|
+
// opens. Project rules are written as part of the normal install. The user
|
|
7076
|
+
// settings change is NOT part of install (BAPI-1448): it lives in the
|
|
7077
|
+
// `bridge-permissions` subcommand, offered in-session by /drive-epic-run. Every
|
|
7078
|
+
// failure here is advisory: a malformed settings file is reported and skipped,
|
|
7079
|
+
// and the install still completes.
|
|
7080
|
+
if (selectedPlatforms.includes("claude-code")) {
|
|
7081
|
+
try {
|
|
7082
|
+
await (deps.installBridgePermissions ?? runBridgePermissionsInstallStep)({
|
|
7083
|
+
cwd: deps.cwd,
|
|
7084
|
+
homeDir: deps.homedir(),
|
|
7085
|
+
env: deps.env,
|
|
7086
|
+
readFile: deps.readFile,
|
|
7087
|
+
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
7088
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
7089
|
+
log,
|
|
7090
|
+
errorLog,
|
|
7091
|
+
ensureGitignored: (relPath) => ensureGitignoredShared(deps.cwd, relPath, gitignoreDeps),
|
|
7092
|
+
// BAPI-1446: pin the SessionStart upgrade-notice hook to the effective Claude Code pin.
|
|
7093
|
+
sessionStartHook: { pinnedVersion: await discoverClaudeCodeEffectivePin({ cwd: deps.cwd, readFile: deps.readFile, targetVersion: VERSION }) },
|
|
7094
|
+
});
|
|
7095
|
+
}
|
|
7096
|
+
catch {
|
|
7097
|
+
errorLog(" warning: could not write Bridge's permission rules (unexpected error; non-fatal).");
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
6775
7100
|
// ---- Step 3b (await) — settle the background pre-warm before the session opens ----
|
|
6776
7101
|
// The warm was kicked off (only on a spawn-capable path) right after the
|
|
6777
7102
|
// launch-command guards; await it HERE so the bucket is settled before the spawned
|
|
@@ -6794,6 +7119,30 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6794
7119
|
`The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
|
|
6795
7120
|
}
|
|
6796
7121
|
}
|
|
7122
|
+
for (const line of buildInstallBridgeMaintenanceHint())
|
|
7123
|
+
log(line);
|
|
7124
|
+
await showFirstBuildAdvisory(finalAgentName);
|
|
7125
|
+
// ---- BAPI-1448: the defaults summary, once, right before the launch consent ----
|
|
7126
|
+
// Every input is already in memory except the optional ticket prefix, which is one
|
|
7127
|
+
// bounded read of the manifest the GitHub offer already uses. Any failure there just
|
|
7128
|
+
// omits the cell: the summary never blocks or changes the install's outcome.
|
|
7129
|
+
let ticketPrefix;
|
|
7130
|
+
try {
|
|
7131
|
+
const details = await fetchGithubConfigurationDetails({ fetch: deps.fetch, baseUrl, apiKey }, repoName);
|
|
7132
|
+
ticketPrefix = details.ticketPrefix;
|
|
7133
|
+
}
|
|
7134
|
+
catch {
|
|
7135
|
+
ticketPrefix = undefined;
|
|
7136
|
+
}
|
|
7137
|
+
log("");
|
|
7138
|
+
log(buildInstallBridgeDefaultsSummary({
|
|
7139
|
+
displayName: modernAttempt?.displayName ?? repoName,
|
|
7140
|
+
...(ticketPrefix ? { ticketPrefix } : {}),
|
|
7141
|
+
isNewProject: Boolean(modernAttempt) || bootstrapExchangeSucceeded,
|
|
7142
|
+
...(persistedPosture ? { persistedPosture } : {}),
|
|
7143
|
+
claudeCodeSelected: selectedPlatforms.includes("claude-code"),
|
|
7144
|
+
guidance: { written: notesWrittenTargets, unchanged: notesUnchangedTargets },
|
|
7145
|
+
}));
|
|
6797
7146
|
// ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
|
|
6798
7147
|
// AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
|
|
6799
7148
|
// resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
|
|
@@ -6908,3 +7257,124 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6908
7257
|
}
|
|
6909
7258
|
}
|
|
6910
7259
|
}
|
|
7260
|
+
class ModernInstallRecoveryError extends Error {
|
|
7261
|
+
}
|
|
7262
|
+
function modernWriteDeps(deps) {
|
|
7263
|
+
return { env: deps.env, homedir: deps.homedir, platform: deps.platform, readFile: deps.readFile,
|
|
7264
|
+
mkdir: deps.mkdir, writeFile: deps.writeFile, rename: deps.rename, chmod: deps.chmod, unlink: deps.unlink, open: deps.open };
|
|
7265
|
+
}
|
|
7266
|
+
async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe) {
|
|
7267
|
+
if (!deps.realpath)
|
|
7268
|
+
throw new ModernInstallRecoveryError("Canonical folder resolution is unavailable. Contact installation support.");
|
|
7269
|
+
const folder = await deps.realpath(deps.cwd);
|
|
7270
|
+
const folderDigest = fingerprintBootstrapFields("bridge-bootstrap-folder-v1", [baseUrl, folder]);
|
|
7271
|
+
const signupLocator = `bootstrap-pending:signup:${folderDigest}`;
|
|
7272
|
+
const makeScope = (locator) => ({ baseUrl, folder, locator, folderReference: `bootstrap-folder:${folderDigest}` });
|
|
7273
|
+
let fingerprint = token ? fingerprintBootstrapInvite(token, baseUrl) : "";
|
|
7274
|
+
let scope = makeScope(selfServe ? signupLocator : `bootstrap-pending:invite:${fingerprint}`);
|
|
7275
|
+
const writeDeps = modernWriteDeps(deps);
|
|
7276
|
+
const loaded = await lookupModernBootstrapRecord(scope, writeDeps);
|
|
7277
|
+
if (!loaded.ok)
|
|
7278
|
+
throw new Error(loaded.error);
|
|
7279
|
+
let record = loaded.record;
|
|
7280
|
+
const resumed = record !== null;
|
|
7281
|
+
let label = record?.label ?? options.displayName;
|
|
7282
|
+
if (label === undefined) {
|
|
7283
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
7284
|
+
throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
|
|
7285
|
+
// BAPI-1448: a valid folder name is the label, with no question. The prompt
|
|
7286
|
+
// survives only for a folder name the validator rejects.
|
|
7287
|
+
let suggestion = "";
|
|
7288
|
+
try {
|
|
7289
|
+
suggestion = validateBootstrapDisplayName(path.basename(folder));
|
|
7290
|
+
}
|
|
7291
|
+
catch { /* explicit label required */ }
|
|
7292
|
+
if (suggestion) {
|
|
7293
|
+
label = suggestion;
|
|
7294
|
+
}
|
|
7295
|
+
else {
|
|
7296
|
+
label = validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
if (selfServe) {
|
|
7300
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
7301
|
+
throw new ModernInstallRecoveryError("Self-serve email verification requires an interactive terminal.");
|
|
7302
|
+
if (!record) {
|
|
7303
|
+
const intent = await prepareModernBootstrapSignup(scope, label, signupEmail, () => generateBootstrapKeySecret(deps.randomBytes), writeDeps);
|
|
7304
|
+
if (!intent.ok || !intent.record)
|
|
7305
|
+
throw new Error(intent.ok ? "Signup proof missing." : intent.error);
|
|
7306
|
+
record = intent.record;
|
|
7307
|
+
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
7308
|
+
if (!requested.ok)
|
|
7309
|
+
throw new ModernInstallRecoveryError("Signup handle could not be established. Preserve recovery proof; contact installation support before restarting signup.");
|
|
7310
|
+
const saved = await mutateModernBootstrapRecord(scope, writeDeps, current => {
|
|
7311
|
+
if (!current)
|
|
7312
|
+
throw new ModernInstallRecoveryError("Signup proof missing.");
|
|
7313
|
+
return { ...current, challengeHandle: requested.handle, challengeExpiresAt: requested.expiresAt };
|
|
7314
|
+
});
|
|
7315
|
+
if (!saved.ok || !saved.record)
|
|
7316
|
+
throw new Error(saved.ok ? "Signup proof missing." : saved.error);
|
|
7317
|
+
record = saved.record;
|
|
7318
|
+
}
|
|
7319
|
+
if (record.replayToken && record.fingerprint) {
|
|
7320
|
+
token = record.replayToken;
|
|
7321
|
+
fingerprint = record.fingerprint;
|
|
7322
|
+
scope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
|
|
7323
|
+
}
|
|
7324
|
+
else {
|
|
7325
|
+
if (!record.challengeHandle || !record.challengeExpiresAt)
|
|
7326
|
+
throw new ModernInstallRecoveryError("The previous signup response is unresolved. Preserve proof and contact installation support before restarting.");
|
|
7327
|
+
if (Date.parse(record.challengeExpiresAt) <= deps.now())
|
|
7328
|
+
throw new ModernInstallRecoveryError("The saved verification challenge expired. Preserve proof and contact installation support.");
|
|
7329
|
+
logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
|
|
7330
|
+
const code = await promptForVerificationCode(deps, deps.log);
|
|
7331
|
+
if (!code)
|
|
7332
|
+
throw new ModernInstallRecoveryError("Verification incomplete. Resume the same signup from this folder.");
|
|
7333
|
+
const verified = await verifySelfServeChallenge(deps, baseUrl, record.challengeHandle, code);
|
|
7334
|
+
if (!verified.ok)
|
|
7335
|
+
throw new ModernInstallRecoveryError("Verification did not complete. Preserve proof and resume the same signup.");
|
|
7336
|
+
token = verified.token;
|
|
7337
|
+
fingerprint = fingerprintBootstrapInvite(token, baseUrl);
|
|
7338
|
+
const inviteScope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
|
|
7339
|
+
const transitioned = await transitionModernBootstrapSignup(makeScope(signupLocator), inviteScope, fingerprint, token, record.challengeHandle, writeDeps);
|
|
7340
|
+
if (!transitioned.ok || !transitioned.record)
|
|
7341
|
+
throw new Error(transitioned.ok ? "Signup proof missing." : transitioned.error);
|
|
7342
|
+
record = transitioned.record;
|
|
7343
|
+
scope = inviteScope;
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
else {
|
|
7347
|
+
const prepared = await prepareModernBootstrapInvite(scope, fingerprint, label, () => generateBootstrapKeySecret(deps.randomBytes), writeDeps);
|
|
7348
|
+
if (!prepared.ok || !prepared.record)
|
|
7349
|
+
throw new Error(prepared.ok ? "Invite proof missing." : prepared.error);
|
|
7350
|
+
record = prepared.record;
|
|
7351
|
+
}
|
|
7352
|
+
if (!record)
|
|
7353
|
+
throw new ModernInstallRecoveryError("Recovery proof missing.");
|
|
7354
|
+
if (resumed)
|
|
7355
|
+
deps.log("Resuming the original installation; changing the entered label does not rename its project.");
|
|
7356
|
+
if (record.canonical && record.serverLabel) {
|
|
7357
|
+
const ping = await verifyConnectivity(deps, baseUrl, record.canonical, record.keySecret);
|
|
7358
|
+
if (!ping.ok)
|
|
7359
|
+
throw new ModernInstallRecoveryError("Canonical connectivity failed. Preserve proof and resume the same attempt.");
|
|
7360
|
+
return { scope, record, repoName: record.canonical, displayName: record.serverLabel };
|
|
7361
|
+
}
|
|
7362
|
+
let exchange = await exchangeBootstrapInvite(deps, baseUrl, token, { repoDisplayName: record.label }, record.keySecret);
|
|
7363
|
+
if (!exchange.ok && exchange.kind === "email-required") {
|
|
7364
|
+
const email = deps.isTTY ? await promptForBoundedEmail(deps, INVITE_EMAIL_PROMPT) : undefined;
|
|
7365
|
+
if (!email)
|
|
7366
|
+
throw new ModernInstallRecoveryError("An email is required; resume the same invite in an interactive terminal.");
|
|
7367
|
+
exchange = await exchangeBootstrapInvite(deps, baseUrl, token, { repoDisplayName: record.label }, record.keySecret, email);
|
|
7368
|
+
}
|
|
7369
|
+
if (!exchange.ok && exchange.message.includes("Creation outcome is unknown"))
|
|
7370
|
+
throw new ModernInstallRecoveryError("Creation outcome is unknown. Preserve recovery proof and rerun the same attempt from the original folder and invite/signup.");
|
|
7371
|
+
if (!exchange.ok || !exchange.displayName)
|
|
7372
|
+
throw new ModernInstallRecoveryError("Creation or replay did not complete. Preserve proof and rerun from this folder with the same invite/signup; verify server compatibility or contact installation support.");
|
|
7373
|
+
const checkpoint = await checkpointModernBootstrapIdentity(scope, record, exchange.repoName, exchange.displayName, writeDeps);
|
|
7374
|
+
if (!checkpoint.ok || !checkpoint.record)
|
|
7375
|
+
throw new Error(checkpoint.ok ? "Checkpoint missing." : checkpoint.error);
|
|
7376
|
+
const ping = await verifyConnectivity(deps, baseUrl, exchange.repoName, record.keySecret);
|
|
7377
|
+
if (!ping.ok)
|
|
7378
|
+
throw new ModernInstallRecoveryError("Canonical connectivity failed. Preserve proof and resume the same attempt.");
|
|
7379
|
+
return { scope, record: checkpoint.record, repoName: exchange.repoName, displayName: exchange.displayName };
|
|
7380
|
+
}
|