@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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 +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- 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 +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- 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 +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
package/build/install-bridge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateBootstrapDisplayName, validateBootstrapServerIdentity } from "./bootstrap-project-identity.js";
|
|
2
|
-
import { lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
|
|
2
|
+
import { BRIDGE_SUPPORT_EMAIL, archiveAndReplaceModernBootstrapSignup, archiveExpiredLegacySignupAndInstallModern, lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
|
|
3
3
|
import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-build-advisory.js";
|
|
4
4
|
/**
|
|
5
5
|
* install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
|
|
@@ -67,9 +67,9 @@ import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-bui
|
|
|
67
67
|
* (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
|
|
68
68
|
* email. Both then feed the SAME redemption protocol. Selection is pure and
|
|
69
69
|
* deterministic from flags/env, except that a BARE interactive run is asked ONE
|
|
70
|
-
* question (BAPI-626/708): `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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,
|
|
73
73
|
* CREDENTIAL-AGNOSTIC token prompt and the route is then inferred from the pasted
|
|
74
74
|
* value's SHAPE — the user is never asked to pre-classify their own credential,
|
|
75
75
|
* because the shape checks below do that with no probe request. Either need-key
|
|
@@ -145,6 +145,14 @@ import { spawn } from "child_process";
|
|
|
145
145
|
import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
|
|
146
146
|
import os from "os";
|
|
147
147
|
import path from "path";
|
|
148
|
+
import { detectClaudeLogin } from "./claude-login.js";
|
|
149
|
+
import { checkGithubSignIn } from "./github-sign-in.js";
|
|
150
|
+
import { readLocalGithubRemote } from "./conductor/stored-merge-credential.js";
|
|
151
|
+
import { probeStoredMergeToken } from "./conductor/stored-merge-token-probe.js";
|
|
152
|
+
import { readGithubMergeCredential, removeGithubMergeCredential, upsertGithubMergeCredential, } from "./credential-store.js";
|
|
153
|
+
import { createBridgeApiUrls } from "./bridge-api-urls.js";
|
|
154
|
+
import { installEpicCiWorkflow } from "./epic-ci-workflow-install.js";
|
|
155
|
+
import { fetchParseStatus, triggerRepositoryParse, } from "./conductor/bridge-api-client.js";
|
|
148
156
|
import readline from "readline";
|
|
149
157
|
import { runInit, InitPreflightError, buildBridgeApiEntry, buildBridgeConfigManifest, mergeBridgeApiProfileToken, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, classifyTrackedProbeExitCode, decideExistingProjectRoot, formatUndeterminedRootReplacedWarning, } from "./init.js";
|
|
150
158
|
import { hasProjectRootMarker as sharedHasProjectRootMarker } from "./project-root.js";
|
|
@@ -155,7 +163,7 @@ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform
|
|
|
155
163
|
import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
|
|
156
164
|
import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
|
|
157
165
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
|
|
158
|
-
import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
|
|
166
|
+
import { DUPLICATE_REGISTRATION_GUIDANCE, discoverClaudeCodeEffectivePin } from "./launcher-config-inspection.js";
|
|
159
167
|
import { codexHome, configureBridgeCodexProfile, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
|
|
160
168
|
import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
161
169
|
// BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
|
|
@@ -193,7 +201,7 @@ import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
|
193
201
|
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
194
202
|
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
195
203
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
196
|
-
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
204
|
+
import { fetchGithubConfigurationDetails, fetchGithubConfigurationState, } from "./connect-github-api.js";
|
|
197
205
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
198
206
|
import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
|
|
199
207
|
import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
|
|
@@ -274,7 +282,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
274
282
|
"'Needs setup' — and each item in them carries display_name, description, how_to_use, and (in the " +
|
|
275
283
|
"'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
|
|
276
284
|
"and its how_to_use verbatim when that value is a non-empty string; render availability_text " +
|
|
277
|
-
"verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, " +
|
|
285
|
+
"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, " +
|
|
278
286
|
"asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
|
|
279
287
|
"contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
|
|
280
288
|
"and never branch on it. Keep an empty section visible with a short neutral line rather than " +
|
|
@@ -303,7 +311,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
303
311
|
"short, plain-language sentence covering what is happening and why it matters to the person running " +
|
|
304
312
|
"this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
|
|
305
313
|
"internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
|
|
306
|
-
"get_install_manifest
|
|
314
|
+
"get_install_manifest or apply_install_manifest), or a manifest field key (like " +
|
|
307
315
|
"command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
|
|
308
316
|
"This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
|
|
309
317
|
"name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
|
|
@@ -317,11 +325,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
317
325
|
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
318
326
|
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
319
327
|
"invocation Stage 6 reports its success exactly as it does today. " +
|
|
320
|
-
"The install-spawn skip set is exactly Stage 8 and Stage 9.
|
|
321
|
-
"(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
|
|
322
|
-
"caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
|
|
323
|
-
"otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key " +
|
|
324
|
-
"exactly once and never repeat it in any summary, retry, or diagnostic. " +
|
|
328
|
+
"The install-spawn skip set is exactly Stage 8 and Stage 9. " +
|
|
325
329
|
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
326
330
|
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
327
331
|
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
@@ -394,9 +398,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
394
398
|
"the next step the server returned — when it returns one.",
|
|
395
399
|
"If the project is already configured it instead",
|
|
396
400
|
"joins you to it without proposing or applying any changes and just shows the",
|
|
397
|
-
"concise capability report plus that same conditional next step
|
|
398
|
-
"
|
|
399
|
-
"step). So not every run applies config fields. Indexing is never asked about —",
|
|
401
|
+
"concise capability report plus that same conditional next step.",
|
|
402
|
+
"So not every run applies config fields. Indexing is never asked about —",
|
|
400
403
|
"it starts automatically once the repository reaches full parse readiness.",
|
|
401
404
|
"",
|
|
402
405
|
"Your API key is written to a project MCP config only when that file is safe: a",
|
|
@@ -406,17 +409,15 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
406
409
|
"resolves your key from the credential store at runtime. A config file that",
|
|
407
410
|
"cannot be parsed is left untouched, with manual-merge instructions printed.",
|
|
408
411
|
"",
|
|
409
|
-
"Run it bare — `install` with no flags — in a terminal and it asks",
|
|
410
|
-
`\`${
|
|
411
|
-
|
|
412
|
-
"
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"Bridge API key connects to the project that already exists. You are",
|
|
419
|
-
"never asked which kind of token you hold. That question is asked ONLY for a",
|
|
412
|
+
"Run it bare — `install` with no flags — in a terminal and it asks one question:",
|
|
413
|
+
`\`${INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT.trim()}\``,
|
|
414
|
+
"Type your email to create a brand-new Bridge workspace for you (the self-serve",
|
|
415
|
+
"flow). Press Enter if you already have a token: a single hidden token prompt",
|
|
416
|
+
"opens, and whatever you paste has its SHAPE pick the route — a bootstrap invite",
|
|
417
|
+
"(bapi_inv_…) is redeemed to CREATE a new project and mint your first admin key,",
|
|
418
|
+
"while a Bridge API key connects to the project that already exists. One",
|
|
419
|
+
"malformed answer re-prompts once, then exits with guidance. You are",
|
|
420
|
+
"never asked which kind of token you hold. The question is asked ONLY for a",
|
|
420
421
|
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
421
422
|
"terminal keeps the existing deterministic behavior and no prompt.",
|
|
422
423
|
"",
|
|
@@ -532,9 +533,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
532
533
|
" --tools claude-code,codex are accepted. On an",
|
|
533
534
|
" interactive terminal WITHOUT this flag you get a",
|
|
534
535
|
" numbered picker: enter one or more tool numbers,",
|
|
535
|
-
" comma-separated (e.g. 1,3), and press Enter once
|
|
536
|
-
"
|
|
537
|
-
"
|
|
536
|
+
" comma-separated (e.g. 1,3), and press Enter once.",
|
|
537
|
+
" Tools Bridge detects on this machine are pre-ticked:",
|
|
538
|
+
" a bare Enter accepts them, and typed numbers",
|
|
539
|
+
" replace them. With none detected you must select",
|
|
540
|
+
" at least one. A non-interactive",
|
|
538
541
|
" (non-TTY) run without",
|
|
539
542
|
" --tools writes the legacy automatic set (Claude",
|
|
540
543
|
" Code plus any detected Cursor / Copilot VS Code)",
|
|
@@ -563,6 +566,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
563
566
|
" session — it always wins, and there is NO Claude",
|
|
564
567
|
" default. Without it the launch tool is derived",
|
|
565
568
|
" from your selection: Claude Code opens `claude`.",
|
|
569
|
+
" With more than one launchable tool selected, the",
|
|
570
|
+
" first one is used and no question is asked.",
|
|
566
571
|
" Cursor is NOT auto-opened (its cursor-agent CLI",
|
|
567
572
|
" first-run workspace-trust prompt collapses a spawned",
|
|
568
573
|
" session); selecting Cursor writes .cursor/mcp.json",
|
|
@@ -1147,12 +1152,6 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1147
1152
|
`Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
|
|
1148
1153
|
}
|
|
1149
1154
|
}
|
|
1150
|
-
/**
|
|
1151
|
-
* The exact agent-notes consent-prompt prefix, shared with tests so the copy
|
|
1152
|
-
* cannot drift. Mirrors {@link INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}: the
|
|
1153
|
-
* prefix is pinned, the platform tail varies with the selection.
|
|
1154
|
-
*/
|
|
1155
|
-
export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX = "Write guidance so ";
|
|
1156
1155
|
/**
|
|
1157
1156
|
* Describe the selected notes platforms for the offer copy: a naturally joined
|
|
1158
1157
|
* subject and whether it is plural (so a verb can agree). `platformLabels` are
|
|
@@ -1175,68 +1174,30 @@ function joinLabelsWithAnd(labels) {
|
|
|
1175
1174
|
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
1176
1175
|
}
|
|
1177
1176
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* The offer-context lines printed immediately before the agent-notes prompt
|
|
1189
|
-
* (BAPI-1207). Leads with the payoff — teaching the named platform(s) WHEN to
|
|
1190
|
-
* reach for Bridge's MCP tools — then states the marker-scoped, non-destructive
|
|
1191
|
-
* contract as a secondary line, mirroring the GitHub/Bitbucket offers' "explain
|
|
1192
|
-
* before asking" convention.
|
|
1193
|
-
*/
|
|
1194
|
-
export function buildAgentNotesOfferContext(targets, platformLabels) {
|
|
1195
|
-
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
1196
|
-
return [
|
|
1197
|
-
"",
|
|
1198
|
-
`Step 4c — optional: Bridge MCP guidance for ${subject}.`,
|
|
1199
|
-
` Write guidance so ${subject} know${plural ? "" : "s"} when to reach for Bridge's MCP tools`,
|
|
1200
|
-
" during a task — for project standards, a second opinion, a council, or a ticket review.",
|
|
1201
|
-
` It writes a Bridge-owned region to ${targets.join(" and ")}; only that marked region is`,
|
|
1202
|
-
" created or refreshed — any existing content, inside or outside it, is otherwise preserved.",
|
|
1203
|
-
];
|
|
1204
|
-
}
|
|
1205
|
-
function agentGuidancePromptConsents(result) {
|
|
1206
|
-
if (result?.kind !== "answer" || typeof result.value !== "string")
|
|
1207
|
-
return false;
|
|
1208
|
-
const value = result.value.trim().toLowerCase();
|
|
1209
|
-
return value === "" || value === "y" || value === "yes";
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Offer to create or refresh the cross-platform agent notes, if the selected
|
|
1213
|
-
* platforms resolve to at least one writable target (BAPI-1207).
|
|
1177
|
+
* Decide whether to create or refresh the cross-platform agent notes, if the
|
|
1178
|
+
* selected platforms resolve to at least one writable target (BAPI-1207).
|
|
1179
|
+
*
|
|
1180
|
+
* BAPI-1448: an interactive run consents by default — the question is gone, with
|
|
1181
|
+
* the product sign-off recorded in the install notes. The write is still bounded to
|
|
1182
|
+
* Bridge's marked region (see {@link writeAgentNotes}) and reported afterwards with
|
|
1183
|
+
* a removal hint. A non-interactive run keeps `headless-skip`: it never writes
|
|
1184
|
+
* guidance on its own.
|
|
1214
1185
|
*
|
|
1215
|
-
* Entirely best-effort and non-destructive
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
* reaches this call) and the live path share exactly one write path.
|
|
1186
|
+
* Entirely best-effort and non-destructive: by the time this runs the install
|
|
1187
|
+
* itself is already complete and durable, so nothing here may fail the run. This
|
|
1188
|
+
* function only resolves the CONSENT decision — the caller performs the actual
|
|
1189
|
+
* write via {@link writeAgentNotes}, so a `--dry-run` (which never reaches this
|
|
1190
|
+
* call) and the live path share exactly one write path.
|
|
1221
1191
|
*/
|
|
1222
|
-
export
|
|
1192
|
+
export function offerAgentGuidanceNotes(deps, targets) {
|
|
1223
1193
|
if (targets.length === 0)
|
|
1224
1194
|
return { kind: "not-applicable" };
|
|
1225
|
-
|
|
1226
|
-
if (!deps.isTTY || !deps.promptGuidanceLine)
|
|
1195
|
+
if (!deps.isTTY)
|
|
1227
1196
|
return { kind: "headless-skip" };
|
|
1228
|
-
|
|
1229
|
-
for (const line of buildAgentNotesOfferContext(targets, platformLabels))
|
|
1230
|
-
log(line);
|
|
1231
|
-
const answer = await deps.promptGuidanceLine(buildAgentNotesPrompt(platformLabels));
|
|
1232
|
-
return agentGuidancePromptConsents(answer) ? { kind: "consented" } : { kind: "declined" };
|
|
1233
|
-
}
|
|
1234
|
-
catch {
|
|
1235
|
-
// The install is already durable; a failure here is never fatal to it.
|
|
1236
|
-
deps.errorLog("Agent guidance skipped: the prompt could not be read; no guidance files were changed.");
|
|
1237
|
-
return { kind: "prompt-error" };
|
|
1238
|
-
}
|
|
1197
|
+
return { kind: "consented" };
|
|
1239
1198
|
}
|
|
1199
|
+
/** The one-line removal hint printed after guidance was actually written (BAPI-1448). */
|
|
1200
|
+
export const INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT = "To remove it, delete the lines from <!-- BEGIN BRIDGE MCP GUIDANCE --> to <!-- END BRIDGE MCP GUIDANCE -->.";
|
|
1240
1201
|
/**
|
|
1241
1202
|
* One secret-free summary line for the final install output (BAPI-1207). Never
|
|
1242
1203
|
* reveals guidance content, file bytes, or a credential — only which files
|
|
@@ -1273,6 +1234,47 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1273
1234
|
parts.push(`failed for ${failed.join(", ")}`);
|
|
1274
1235
|
return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
|
|
1275
1236
|
}
|
|
1237
|
+
const OPTIMIZATION_POSTURE_SUMMARY_LABELS = {
|
|
1238
|
+
quality: "Quality",
|
|
1239
|
+
speed: "Speed",
|
|
1240
|
+
maximize_subscription: "Token Efficiency",
|
|
1241
|
+
};
|
|
1242
|
+
/**
|
|
1243
|
+
* The aligned "defaults Bridge chose" block printed once, right before the launch
|
|
1244
|
+
* consent question. Pure: it formats what the run already knows and performs no I/O.
|
|
1245
|
+
*
|
|
1246
|
+
* Layout: a header, then two rows of two cells. Both rows are indented two spaces and
|
|
1247
|
+
* the left cells are padded to the longer left cell plus three spaces, so the right
|
|
1248
|
+
* cells line up. The Ticket keys cell is omitted entirely when the server supplied no
|
|
1249
|
+
* valid prefix; the prefix is printed as given and never derived locally.
|
|
1250
|
+
*/
|
|
1251
|
+
export function buildInstallBridgeDefaultsSummary(input) {
|
|
1252
|
+
const optimization = input.persistedPosture
|
|
1253
|
+
? OPTIMIZATION_POSTURE_SUMMARY_LABELS[input.persistedPosture]
|
|
1254
|
+
: input.isNewProject
|
|
1255
|
+
? "Quality"
|
|
1256
|
+
: "unchanged";
|
|
1257
|
+
const guidanceLabel = input.claudeCodeSelected ? "Claude Code guidance" : "Agent guidance";
|
|
1258
|
+
const guidanceValue = input.guidance.written.length > 0
|
|
1259
|
+
? `added to ${input.guidance.written.join(", ")}`
|
|
1260
|
+
: input.guidance.unchanged.length > 0
|
|
1261
|
+
? "already up to date"
|
|
1262
|
+
: "not written";
|
|
1263
|
+
const leftTop = `Project name: ${input.displayName}`;
|
|
1264
|
+
const leftBottom = `Optimization: ${optimization}`;
|
|
1265
|
+
const rightTop = input.ticketPrefix
|
|
1266
|
+
? `Ticket keys: ${input.ticketPrefix}-1, ${input.ticketPrefix}-2, …`
|
|
1267
|
+
: undefined;
|
|
1268
|
+
const rightBottom = `${guidanceLabel}: ${guidanceValue}`;
|
|
1269
|
+
const width = Math.max(leftTop.length, leftBottom.length) + 3;
|
|
1270
|
+
const rowTop = rightTop === undefined ? ` ${leftTop}` : ` ${leftTop.padEnd(width)}${rightTop}`;
|
|
1271
|
+
const rowBottom = ` ${leftBottom.padEnd(width)}${rightBottom}`;
|
|
1272
|
+
return [
|
|
1273
|
+
"Bridge chose these defaults — change them later on the Setup page, or ask your agent:",
|
|
1274
|
+
rowTop,
|
|
1275
|
+
rowBottom,
|
|
1276
|
+
].join("\n");
|
|
1277
|
+
}
|
|
1276
1278
|
/**
|
|
1277
1279
|
* Echoed single-line prompt on stderr (used for repo confirmation / value).
|
|
1278
1280
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
@@ -1296,56 +1298,6 @@ export function promptLineViaReadline(promptText, input = process.stdin, output
|
|
|
1296
1298
|
});
|
|
1297
1299
|
});
|
|
1298
1300
|
}
|
|
1299
|
-
/** Observe CR/LF before readline, counting split CRLF as one submission. */
|
|
1300
|
-
export function promptGuidanceLineViaReadline(promptText, input = process.stdin, output = process.stderr) {
|
|
1301
|
-
return new Promise((resolve) => {
|
|
1302
|
-
const readable = input;
|
|
1303
|
-
if (readable.readableEnded || readable.destroyed) {
|
|
1304
|
-
resolve({ kind: "eof" });
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
let terminators = 0;
|
|
1308
|
-
let previousCR = false;
|
|
1309
|
-
let settled = false;
|
|
1310
|
-
let rl;
|
|
1311
|
-
const observe = (chunk) => {
|
|
1312
|
-
for (const char of chunk.toString()) {
|
|
1313
|
-
if (char === "\r")
|
|
1314
|
-
terminators++;
|
|
1315
|
-
else if (char === "\n" && !previousCR)
|
|
1316
|
-
terminators++;
|
|
1317
|
-
previousCR = char === "\r";
|
|
1318
|
-
}
|
|
1319
|
-
};
|
|
1320
|
-
const finish = (result) => {
|
|
1321
|
-
if (settled)
|
|
1322
|
-
return;
|
|
1323
|
-
settled = true;
|
|
1324
|
-
input.removeListener("data", observe);
|
|
1325
|
-
input.removeListener("end", eof);
|
|
1326
|
-
input.removeListener("close", eof);
|
|
1327
|
-
input.removeListener("error", eof);
|
|
1328
|
-
rl?.removeListener("close", eof);
|
|
1329
|
-
rl?.close();
|
|
1330
|
-
resolve(result);
|
|
1331
|
-
};
|
|
1332
|
-
const eof = () => finish({ kind: "eof" });
|
|
1333
|
-
input.prependListener("data", observe);
|
|
1334
|
-
input.once("end", eof);
|
|
1335
|
-
input.once("close", eof);
|
|
1336
|
-
input.once("error", eof);
|
|
1337
|
-
rl = readline.createInterface({ input, output });
|
|
1338
|
-
rl.once("close", eof);
|
|
1339
|
-
if (settled || readable.readableEnded || readable.destroyed) {
|
|
1340
|
-
rl.close();
|
|
1341
|
-
eof();
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
rl.question(promptText, (value) => {
|
|
1345
|
-
finish(terminators > 0 ? { kind: "answer", value } : { kind: "eof" });
|
|
1346
|
-
});
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
1301
|
/**
|
|
1350
1302
|
* Default pre-warm spawner (BAPI-451 W3). Spawns array-based (`shell: false`) so
|
|
1351
1303
|
* no argument is ever shell-interpreted, ignores all child stdio (node-gyp /
|
|
@@ -1461,8 +1413,9 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1461
1413
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
1462
1414
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
1463
1415
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
1464
|
-
|
|
1465
|
-
|
|
1416
|
+
promptMultiSelect: isTTY
|
|
1417
|
+
? (promptText, options, preselected) => promptMultiSelectViaReadline(promptText, options, process.stdin, process.stderr, preselected)
|
|
1418
|
+
: undefined,
|
|
1466
1419
|
vendor: createDefaultVendorProcessDeps(spawn),
|
|
1467
1420
|
fetch: productionFetch,
|
|
1468
1421
|
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
@@ -1522,6 +1475,19 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1522
1475
|
// Read-only service-state probe (BAPI-779). Typed to return a state,
|
|
1523
1476
|
// never a runner, so the doctor cannot start/enable/reload anything.
|
|
1524
1477
|
inspectExecutorServiceState: params.inspectExecutorServiceState,
|
|
1478
|
+
// BAPI-1461: only the converging setup passes these; without them the
|
|
1479
|
+
// conductor doctor is built exactly as before.
|
|
1480
|
+
...(params.skipDenyProbe !== undefined || params.storedMergeCredential !== undefined
|
|
1481
|
+
? {
|
|
1482
|
+
conductorDoctorDeps: {
|
|
1483
|
+
env: params.env,
|
|
1484
|
+
...(params.skipDenyProbe === true ? { skipDenyProbe: true } : {}),
|
|
1485
|
+
...(params.storedMergeCredential !== undefined
|
|
1486
|
+
? { storedMergeCredential: params.storedMergeCredential }
|
|
1487
|
+
: {}),
|
|
1488
|
+
},
|
|
1489
|
+
}
|
|
1490
|
+
: {}),
|
|
1525
1491
|
installDoctorDeps: {
|
|
1526
1492
|
env: params.env,
|
|
1527
1493
|
cwd: params.cwd,
|
|
@@ -1687,6 +1653,54 @@ export function getMergeWorkflowInstallUsage() {
|
|
|
1687
1653
|
* `shell: false` — and `gh` additionally receives the non-interactive pins so a
|
|
1688
1654
|
* prompt can never hang an install. The resulting environment is never printed.
|
|
1689
1655
|
*/
|
|
1656
|
+
/**
|
|
1657
|
+
* BAPI-1461 — the production seams for the converging `install conductor`.
|
|
1658
|
+
*
|
|
1659
|
+
* Each one is a thin adapter over an implementation that already exists: the
|
|
1660
|
+
* Claude login reader, one bounded `gh auth status`, the credential store's
|
|
1661
|
+
* typed merge-token functions (locked, durable writes), the shared capability
|
|
1662
|
+
* probe, the stored-test-command client, the CI workflow artifact-set installer
|
|
1663
|
+
* (sharing the merge-workflow installer's git/gh runners), and the parse routes.
|
|
1664
|
+
*/
|
|
1665
|
+
export function buildConductorSetupDeps(deps, log, errorLog) {
|
|
1666
|
+
const installerDeps = buildMergeWorkflowInstallDeps(deps, log, errorLog);
|
|
1667
|
+
const storeDeps = modernWriteDeps(deps);
|
|
1668
|
+
return {
|
|
1669
|
+
detectClaudeLogin: () => detectClaudeLogin({ homedir: deps.homedir, readFile: deps.readFile }),
|
|
1670
|
+
// An injected installer runner (tests) is used for these reads too, so a
|
|
1671
|
+
// harness never reaches the real `gh` or `git`.
|
|
1672
|
+
checkGithubSignIn: () => deps.mergeWorkflowRunGh
|
|
1673
|
+
? checkGithubSignIn(async (args) => ({ exitCode: (await deps.mergeWorkflowRunGh(args)).status }))
|
|
1674
|
+
: checkGithubSignIn(),
|
|
1675
|
+
readLocalGithubRemote: () => readLocalGithubRemote(deps.cwd, deps.mergeWorkflowRunGit
|
|
1676
|
+
? async () => {
|
|
1677
|
+
const r = await deps.mergeWorkflowRunGit(["remote", "get-url", "origin"]);
|
|
1678
|
+
return r.status === 0 ? r.stdout : null;
|
|
1679
|
+
}
|
|
1680
|
+
: undefined),
|
|
1681
|
+
...(deps.isTTY && deps.promptSecret ? { promptSecret: deps.promptSecret } : {}),
|
|
1682
|
+
readMergeToken: (identity) => readGithubMergeCredential(identity, storeDeps),
|
|
1683
|
+
storeMergeToken: (identity, token) => upsertGithubMergeCredential(identity, token, storeDeps),
|
|
1684
|
+
removeMergeToken: (identity) => removeGithubMergeCredential(identity, storeDeps),
|
|
1685
|
+
probeMergeToken: (args) => probeStoredMergeToken({ ...args, env: deps.env }),
|
|
1686
|
+
testingConfig: (access) => ({
|
|
1687
|
+
repoName: access.repoName,
|
|
1688
|
+
urls: createBridgeApiUrls(access.baseUrl),
|
|
1689
|
+
fetch: deps.fetch,
|
|
1690
|
+
getGetHeaders: async () => ({ "X-API-Key": access.apiKey }),
|
|
1691
|
+
getPostHeaders: async () => ({ "X-API-Key": access.apiKey, "Content-Type": "application/json" }),
|
|
1692
|
+
}),
|
|
1693
|
+
readRepositoryFile: (relPath) => deps.readFile(path.join(deps.cwd, relPath)),
|
|
1694
|
+
installCiWorkflow: (testCommand, options, confirmWrite) => installEpicCiWorkflow(testCommand, { dryRun: options.dryRun }, {
|
|
1695
|
+
...installerDeps,
|
|
1696
|
+
...(options.quiet === true ? { log: () => { }, errorLog: () => { } } : {}),
|
|
1697
|
+
confirmWrite,
|
|
1698
|
+
}),
|
|
1699
|
+
runGh: installerDeps.runGh,
|
|
1700
|
+
fetchParseStatus: (access) => fetchParseStatus(access, deps.fetch),
|
|
1701
|
+
triggerParse: (access) => triggerRepositoryParse(access, deps.fetch),
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1690
1704
|
export function buildMergeWorkflowInstallDeps(deps, log, errorLog) {
|
|
1691
1705
|
const toResult = (r) => ({
|
|
1692
1706
|
status: r.exitCode,
|
|
@@ -2148,7 +2162,7 @@ export function buildInstallBridgeMaintenanceHint() {
|
|
|
2148
2162
|
"Persistent host registrations remain pinned until an explicit upgrade; the editor may need to reconnect to use the new server.",
|
|
2149
2163
|
];
|
|
2150
2164
|
}
|
|
2151
|
-
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2165
|
+
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL, onboarding) {
|
|
2152
2166
|
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
2153
2167
|
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
2154
2168
|
// so an unattended run fails before any request is made, no verification email
|
|
@@ -2159,6 +2173,13 @@ export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_B
|
|
|
2159
2173
|
if (!deps.isTTY || !deps.promptLine) {
|
|
2160
2174
|
return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
|
|
2161
2175
|
}
|
|
2176
|
+
// BAPI-1448: an email already answered at the merged entry prompt is consumed the
|
|
2177
|
+
// way `--email` is, with no second question and no second privacy notice.
|
|
2178
|
+
if (onboarding) {
|
|
2179
|
+
if (!onboarding.privacyNoticeShown)
|
|
2180
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2181
|
+
return { ok: true, value: onboarding.email };
|
|
2182
|
+
}
|
|
2162
2183
|
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2163
2184
|
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
2164
2185
|
return { ok: true, value: options.email.trim() };
|
|
@@ -2222,49 +2243,24 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
2222
2243
|
return { kind: "have-key" };
|
|
2223
2244
|
}
|
|
2224
2245
|
/**
|
|
2225
|
-
* The bare-TTY
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
* make for itself: whether the string in their clipboard is an invite or an API
|
|
2230
|
-
* key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
|
|
2231
|
-
* SHAPE alone, with no probe request — so the only thing a first-time user
|
|
2232
|
-
* genuinely knows, and the only thing worth asking, is whether they have a token
|
|
2233
|
-
* at all. Hence a yes/no question, with the route inferred afterwards.
|
|
2234
|
-
*
|
|
2235
|
-
* Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
|
|
2236
|
-
* render the heading and the numbered options exactly once, accept ONE strict
|
|
2237
|
-
* line, no default (a bare Enter selects nothing), bounded to a single re-prompt
|
|
2238
|
-
* after one hint, and EOF resolves as an empty answer so it can never hang.
|
|
2246
|
+
* The bare-TTY entry prompt (BAPI-1448). One question replaces the BAPI-708 "do you
|
|
2247
|
+
* have a token yet? 1/2" chooser: a typed email selects self-serve signup, and a
|
|
2248
|
+
* blank answer selects the have-key branch, whose value is read ONLY through the
|
|
2249
|
+
* hidden `Paste your Bridge token (input hidden): ` prompt downstream.
|
|
2239
2250
|
*/
|
|
2240
|
-
export const
|
|
2241
|
-
/**
|
|
2242
|
-
|
|
2243
|
-
* token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
|
|
2244
|
-
* option 1 — first-time signup is the common case and now leads.
|
|
2245
|
-
*
|
|
2246
|
-
* "Token" is deliberately credential-agnostic: an invite and an API key are both
|
|
2247
|
-
* tokens as far as this question is concerned, and answering "2" leads to ONE
|
|
2248
|
-
* hidden prompt whose value selects the route by shape.
|
|
2249
|
-
*/
|
|
2250
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
|
|
2251
|
-
"1. I need a token",
|
|
2252
|
-
"2. I already have a token",
|
|
2253
|
-
];
|
|
2254
|
-
/** The single-line answer prompt drawn after the options (no bracketed default). */
|
|
2255
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
|
|
2256
|
-
/** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
|
|
2257
|
-
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
|
|
2251
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT = "Email (or press Enter if you already have a token): ";
|
|
2252
|
+
/** The one hint line printed after a non-blank malformed answer, before the single re-prompt. */
|
|
2253
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT = "Enter an email address, or press Enter if you already have a token.";
|
|
2258
2254
|
/**
|
|
2259
2255
|
* The one place the "no usable credential" copy lives (BAPI-667, U1-3).
|
|
2260
2256
|
*
|
|
2261
|
-
* Centralized because the
|
|
2257
|
+
* Centralized because the entry-prompt failure and {@link resolveApiKey}'s two failure
|
|
2262
2258
|
* branches previously drifted: only one of them mentioned self-serve, and one
|
|
2263
2259
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
2264
2260
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
2265
2261
|
*/
|
|
2266
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run
|
|
2267
|
-
"already have
|
|
2262
|
+
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 " +
|
|
2263
|
+
"already have a token (an invite or an API key — either one is accepted at the hidden prompt). " +
|
|
2268
2264
|
"Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
2269
2265
|
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
2270
2266
|
/**
|
|
@@ -2312,7 +2308,7 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare int
|
|
|
2312
2308
|
* key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
|
|
2313
2309
|
* two.
|
|
2314
2310
|
*/
|
|
2315
|
-
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
2311
|
+
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2316
2312
|
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
2317
2313
|
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
2318
2314
|
if (branch.kind === "need-key")
|
|
@@ -2324,38 +2320,35 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
2324
2320
|
}
|
|
2325
2321
|
const promptLine = deps.promptLine;
|
|
2326
2322
|
try {
|
|
2327
|
-
//
|
|
2328
|
-
|
|
2329
|
-
deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);
|
|
2330
|
-
for (const choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)
|
|
2331
|
-
deps.log(choice);
|
|
2323
|
+
// The answer may be an email, so the privacy notice comes first and prints once.
|
|
2324
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2332
2325
|
// Bounded to two reads total: the first answer, then ONE re-prompt after a
|
|
2333
2326
|
// single hint line. EOF resolves as an empty answer (promptLineViaReadline
|
|
2334
|
-
// resolves "" on close),
|
|
2335
|
-
// it can never hang or silently select a branch.
|
|
2327
|
+
// resolves "" on close), which selects the have-key branch rather than hanging.
|
|
2336
2328
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2337
|
-
const answer = (await promptLine(
|
|
2338
|
-
//
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
|
|
2329
|
+
const answer = String((await promptLine(INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT)) ?? "").trim();
|
|
2330
|
+
// Blank (or EOF): the user has a token; it is read only by the hidden prompt.
|
|
2331
|
+
if (answer.length === 0) {
|
|
2332
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2333
|
+
}
|
|
2334
|
+
// A typed value is accepted ONLY as a signup email. It is never classified as a
|
|
2335
|
+
// credential, logged, or persisted here; a token-shaped value fails the shape
|
|
2336
|
+
// check and gets the hint.
|
|
2337
|
+
if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(answer)) {
|
|
2345
2338
|
return {
|
|
2346
2339
|
ok: true,
|
|
2347
2340
|
branch: { kind: "need-key", method: "self-serve" },
|
|
2348
2341
|
credentialIntent: "inferred",
|
|
2342
|
+
onboardingEmail: answer,
|
|
2343
|
+
privacyNoticeShown: true,
|
|
2349
2344
|
};
|
|
2350
2345
|
}
|
|
2351
|
-
if (answer === "2")
|
|
2352
|
-
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2353
2346
|
if (attempt === 0)
|
|
2354
|
-
deps.log(
|
|
2347
|
+
deps.log(INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT);
|
|
2355
2348
|
}
|
|
2356
2349
|
return {
|
|
2357
2350
|
ok: false,
|
|
2358
|
-
error: `No
|
|
2351
|
+
error: `No email or token choice was made. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
2359
2352
|
};
|
|
2360
2353
|
}
|
|
2361
2354
|
catch {
|
|
@@ -2464,22 +2457,31 @@ const SELECTION_TOKEN_PATTERN = /^[1-9][0-9]*$/;
|
|
|
2464
2457
|
* Interactive single-shot numbered picker on stderr (TTY only). Renders the
|
|
2465
2458
|
* heading and each option exactly once, then accepts ONE strict
|
|
2466
2459
|
* comma-separated line of one-based numbers and resolves immediately — there
|
|
2467
|
-
* is no
|
|
2460
|
+
* is no toggle state and no separate confirmation step (BAPI-663).
|
|
2461
|
+
*
|
|
2462
|
+
* BAPI-1448: when `preselected` names at least one offered option, those rows
|
|
2463
|
+
* render as `[x]` (the rest as `[ ]`) and a blank line accepts the ticked set.
|
|
2464
|
+
* Typed numbers REPLACE the ticked set; they never toggle it. With nothing
|
|
2465
|
+
* pre-ticked the original behaviour is unchanged: a blank line is rejected with
|
|
2466
|
+
* "Select at least one tool."
|
|
2468
2467
|
*
|
|
2469
|
-
* A
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
* re-
|
|
2473
|
-
* deduplicated and returned in `options` order regardless of entry order.
|
|
2468
|
+
* A line containing any invalid token (non-numeric, zero, signed,
|
|
2469
|
+
* leading-zero, decimal, out-of-range) is rejected atomically — the whole line
|
|
2470
|
+
* is discarded, one error line is printed, and the picker re-prompts without
|
|
2471
|
+
* re-rendering the option list, so the ticks stay as first shown. A valid line
|
|
2472
|
+
* is deduplicated and returned in `options` order regardless of entry order.
|
|
2474
2473
|
*
|
|
2475
2474
|
* Resolves to `[]` on EOF / synchronous close before any line has been
|
|
2476
2475
|
* committed, so a readline regression can never hang. Returns `[]`
|
|
2477
2476
|
* immediately, without creating a readline interface, when `options` is
|
|
2478
2477
|
* empty.
|
|
2479
2478
|
*/
|
|
2480
|
-
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr) {
|
|
2479
|
+
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr, preselected = []) {
|
|
2481
2480
|
if (options.length === 0)
|
|
2482
2481
|
return Promise.resolve([]);
|
|
2482
|
+
// Canonical option order, restricted to options actually offered.
|
|
2483
|
+
const ticked = options.filter((o) => preselected.includes(o.id)).map((o) => o.id);
|
|
2484
|
+
const hasTicks = ticked.length > 0;
|
|
2483
2485
|
return new Promise((resolve) => {
|
|
2484
2486
|
const rl = readline.createInterface({ input, output });
|
|
2485
2487
|
let settled = false;
|
|
@@ -2496,13 +2498,20 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
2496
2498
|
rl.on("close", () => finish([]));
|
|
2497
2499
|
output.write(`\n${promptText}\n`);
|
|
2498
2500
|
options.forEach((opt, idx) => {
|
|
2499
|
-
|
|
2501
|
+
const box = hasTicks ? `${ticked.includes(opt.id) ? "[x]" : "[ ]"} ` : "";
|
|
2502
|
+
output.write(` ${idx + 1}. ${box}${opt.label}\n`);
|
|
2500
2503
|
});
|
|
2501
|
-
output.write(
|
|
2504
|
+
output.write(hasTicks
|
|
2505
|
+
? "Press Enter to use the ticked tools, or type the numbers you use (e.g. 1,3).\n"
|
|
2506
|
+
: "Enter the number(s) of the tools you use (e.g. 1,3), then Enter.\n");
|
|
2502
2507
|
const prompt = () => {
|
|
2503
2508
|
rl.question("> ", (answer) => {
|
|
2504
2509
|
const trimmed = answer.trim();
|
|
2505
2510
|
if (trimmed.length === 0) {
|
|
2511
|
+
if (hasTicks) {
|
|
2512
|
+
finish(ticked);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2506
2515
|
output.write("Select at least one tool.\n");
|
|
2507
2516
|
prompt();
|
|
2508
2517
|
return;
|
|
@@ -2553,10 +2562,6 @@ export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
|
|
|
2553
2562
|
const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
|
|
2554
2563
|
return mapped ? `${mapped} subscription` : "coding subscription";
|
|
2555
2564
|
}
|
|
2556
|
-
/** Option three's exact wording, platform-aware (BAPI-1221 Step 2). */
|
|
2557
|
-
function buildOptimizationPostureOptionThree(primaryPlatform) {
|
|
2558
|
-
return `Get the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}?`;
|
|
2559
|
-
}
|
|
2560
2565
|
/** The neutral single-line correction reprompted for any malformed answer. */
|
|
2561
2566
|
const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
2562
2567
|
/**
|
|
@@ -2572,20 +2577,21 @@ const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
|
2572
2577
|
const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
|
|
2573
2578
|
/**
|
|
2574
2579
|
* Render the one-time posture menu and its input instruction. Exactly three
|
|
2575
|
-
* vertically ordered numbered options; option three
|
|
2576
|
-
* wrap-safe text
|
|
2580
|
+
* vertically ordered numbered options; option three names the user's own
|
|
2581
|
+
* subscription. Plain, wrap-safe text (every line within 80 columns) — no
|
|
2582
|
+
* color-only state, icons, or decorative framing.
|
|
2577
2583
|
*/
|
|
2578
2584
|
function buildOptimizationPostureMenu(primaryPlatform) {
|
|
2579
|
-
return ("How should Bridge
|
|
2580
|
-
" 1. Quality —
|
|
2581
|
-
"
|
|
2582
|
-
|
|
2583
|
-
"
|
|
2584
|
-
"
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
"
|
|
2588
|
-
|
|
2585
|
+
return ("How should Bridge balance quality and cost on this project?\n" +
|
|
2586
|
+
" 1. Quality — Bridge plans and reviews each ticket in full depth; slowest and\n" +
|
|
2587
|
+
" uses the most credits, but catches the most problems.\n" +
|
|
2588
|
+
" 2. Speed — Bridge plans in one pass and reviews once; faster and cheaper, but\n" +
|
|
2589
|
+
" may miss things on some tickets.\n" +
|
|
2590
|
+
" 3. Token Efficiency — Bridge will use Speed settings and hand simpler tickets\n" +
|
|
2591
|
+
" to smaller models more often, which stretches your\n" +
|
|
2592
|
+
` ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} the furthest.\n` +
|
|
2593
|
+
"Press Enter to keep Quality (or your saved choice).\n" +
|
|
2594
|
+
"Enter 1, 2, or 3: ");
|
|
2589
2595
|
}
|
|
2590
2596
|
/**
|
|
2591
2597
|
* The purpose-built, strict single-select optimization-posture picker
|
|
@@ -2675,7 +2681,7 @@ function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
|
|
|
2675
2681
|
}
|
|
2676
2682
|
/** Printed immediately after a successful posture write, on its own line. */
|
|
2677
2683
|
const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
|
|
2678
|
-
const OPTIMIZATION_POSTURE_REMEDIATION = "
|
|
2684
|
+
const OPTIMIZATION_POSTURE_REMEDIATION = "Change it later by running /teach-bridge in your agent.";
|
|
2679
2685
|
/**
|
|
2680
2686
|
* The generic, secret-free fail-open message shared by every non-success,
|
|
2681
2687
|
* non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
|
|
@@ -2709,8 +2715,9 @@ function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
|
2709
2715
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
2710
2716
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
2711
2717
|
* back to detection),
|
|
2712
|
-
* 2. interactive multi-select on a TTY
|
|
2713
|
-
*
|
|
2718
|
+
* 2. interactive multi-select on a TTY, pre-ticked with the detected tools
|
|
2719
|
+
* (BAPI-1448 reverses the BAPI-635 AC-2 empty picker; with nothing detected
|
|
2720
|
+
* the picker still requires an explicit ≥1 selection),
|
|
2714
2721
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
2715
2722
|
* detected Copilot VS Code and Cursor automatic targets (never Codex just
|
|
2716
2723
|
* because a global directory exists).
|
|
@@ -2720,11 +2727,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
2720
2727
|
if (options.tools !== undefined) {
|
|
2721
2728
|
return options.tools;
|
|
2722
2729
|
}
|
|
2723
|
-
// 2. Interactive single-shot numbered picker on a TTY
|
|
2724
|
-
//
|
|
2730
|
+
// 2. Interactive single-shot numbered picker on a TTY, seeded with the locally
|
|
2731
|
+
// detected tools (BAPI-1448): Enter accepts them, typed numbers replace them.
|
|
2725
2732
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
2726
2733
|
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
2727
|
-
const
|
|
2734
|
+
const detected = detectDefaultPlatforms(await buildDetectionContext(deps));
|
|
2735
|
+
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, detected);
|
|
2728
2736
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
2729
2737
|
}
|
|
2730
2738
|
// 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
|
|
@@ -2842,42 +2850,6 @@ export function toolLabelForLaunchAgent(agent) {
|
|
|
2842
2850
|
const target = allHostTargets().find((t) => t.launchAgent === agent);
|
|
2843
2851
|
return target?.label ?? UNKNOWN_LAUNCH_TOOL_LABEL;
|
|
2844
2852
|
}
|
|
2845
|
-
/**
|
|
2846
|
-
* Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
|
|
2847
|
-
* (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
|
|
2848
|
-
* label, asks for an exact numeric choice with NO default, validates the range, and
|
|
2849
|
-
* returns the chosen agent. Returns `null` — never a silent first-candidate default —
|
|
2850
|
-
* for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
|
|
2851
|
-
* input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
|
|
2852
|
-
* it must run only AFTER the multi-select promise has resolved and closed its readline
|
|
2853
|
-
* interface so two readers never contend for stdin.
|
|
2854
|
-
*/
|
|
2855
|
-
export async function chooseInstallBridgeLaunchAgent(agents, deps) {
|
|
2856
|
-
if (!deps.isTTY || !deps.promptLine)
|
|
2857
|
-
return null;
|
|
2858
|
-
const promptLine = deps.promptLine;
|
|
2859
|
-
try {
|
|
2860
|
-
deps.log("");
|
|
2861
|
-
deps.log("More than one selected tool can host the configuration session:");
|
|
2862
|
-
agents.forEach((agent, i) => {
|
|
2863
|
-
deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
|
|
2864
|
-
});
|
|
2865
|
-
// No default: a stray Enter must not silently pick a tool.
|
|
2866
|
-
const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
|
|
2867
|
-
const index = Number(answer);
|
|
2868
|
-
if (!/^\d+$/.test(answer) ||
|
|
2869
|
-
!Number.isInteger(index) ||
|
|
2870
|
-
index < 1 ||
|
|
2871
|
-
index > agents.length) {
|
|
2872
|
-
return null;
|
|
2873
|
-
}
|
|
2874
|
-
return agents[index - 1];
|
|
2875
|
-
}
|
|
2876
|
-
catch {
|
|
2877
|
-
// Never surface caught exception text; an unavailable chooser is a no-spawn.
|
|
2878
|
-
return null;
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
2853
|
/**
|
|
2882
2854
|
* Shared, copy-pasteable "we didn't open a session for you" guidance so every
|
|
2883
2855
|
* no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
|
|
@@ -3962,7 +3934,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
|
|
|
3962
3934
|
return {
|
|
3963
3935
|
ok: false,
|
|
3964
3936
|
kind: "failed",
|
|
3965
|
-
message: `The bootstrap exchange failed (HTTP ${resp.status}). Preserve proof and rerun the same attempt; verify server compatibility and contact
|
|
3937
|
+
message: `The bootstrap exchange failed (HTTP ${resp.status}). Preserve proof and rerun the same attempt; verify server compatibility and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} if it persists.`,
|
|
3966
3938
|
};
|
|
3967
3939
|
}
|
|
3968
3940
|
/** The invite-email prompt (BAPI-1263), shown once after an `email_required` refusal. */
|
|
@@ -4128,6 +4100,27 @@ export function buildSelfServeMintUrl(baseUrl) {
|
|
|
4128
4100
|
export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
|
|
4129
4101
|
/** The one prompt label. The E2E harness and the static contract both pin it. */
|
|
4130
4102
|
export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
|
|
4103
|
+
/**
|
|
4104
|
+
* The address verification codes are sent from. Mirrors the server's
|
|
4105
|
+
* `DEFAULT_FROM_ADDRESS` in `api/library/email/provider.py`; keep the two in step.
|
|
4106
|
+
*/
|
|
4107
|
+
export const INSTALL_BRIDGE_VERIFICATION_SENDER = "noreply@bridgegpt-api.com";
|
|
4108
|
+
/**
|
|
4109
|
+
* The shared lead-in printed immediately before every self-serve
|
|
4110
|
+
* {@link promptForVerificationCode} call (modern and legacy paths, fresh or resumed):
|
|
4111
|
+
* three unindented lines, then a blank line. It prints neither the prompt nor any
|
|
4112
|
+
* expiry or time-left text.
|
|
4113
|
+
*
|
|
4114
|
+
* BAPI-1449 (verification recovery) builds on this lead-in instead of adding its
|
|
4115
|
+
* own, and may later return the sender from the request endpoint rather than using
|
|
4116
|
+
* {@link INSTALL_BRIDGE_VERIFICATION_SENDER}.
|
|
4117
|
+
*/
|
|
4118
|
+
export function logVerificationCodeLeadIn(log, email) {
|
|
4119
|
+
log(`We emailed a ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to ${maskSignupEmail(email)}.`);
|
|
4120
|
+
log(`It usually arrives within two minutes. It comes from ${INSTALL_BRIDGE_VERIFICATION_SENDER} — check your`);
|
|
4121
|
+
log("spam or junk folder if you don't see it.");
|
|
4122
|
+
log("");
|
|
4123
|
+
}
|
|
4131
4124
|
/** Shown when a stored, unexpired challenge is picked up on a later run. */
|
|
4132
4125
|
export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
|
|
4133
4126
|
/**
|
|
@@ -4255,6 +4248,95 @@ export const INSTALL_BRIDGE_SIGNUP_BLOCKED = "Error: Bridge signup is not availa
|
|
|
4255
4248
|
export function formatSelfServeUnexpectedStatus(status) {
|
|
4256
4249
|
return `Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`;
|
|
4257
4250
|
}
|
|
4251
|
+
// ===================================================================
|
|
4252
|
+
// BAPI-1449 — modern-path verification recovery copy (S1–S7).
|
|
4253
|
+
//
|
|
4254
|
+
// The wording is settled; only the sender comes from the shared
|
|
4255
|
+
// INSTALL_BRIDGE_VERIFICATION_SENDER constant. None of these says WHY a server
|
|
4256
|
+
// rejection happened: the word "expired" appears only in S4a, which is chosen from
|
|
4257
|
+
// the locally stored expiry and never from a server reply.
|
|
4258
|
+
// ===================================================================
|
|
4259
|
+
/** The existing end-of-run line for a prompt that gave up or timed out empty. */
|
|
4260
|
+
export const INSTALL_BRIDGE_VERIFICATION_INCOMPLETE = "Verification incomplete. Resume the same signup from this folder.";
|
|
4261
|
+
/** S1 — the first empty Enter at the modern code prompt. */
|
|
4262
|
+
export const INSTALL_BRIDGE_EMPTY_CODE_GUIDANCE = [
|
|
4263
|
+
"No code yet? Type r to send a new one, or check spam for mail from",
|
|
4264
|
+
`${INSTALL_BRIDGE_VERIFICATION_SENDER}.`,
|
|
4265
|
+
"Wrong address? Press Ctrl-C and re-run install with a different email.",
|
|
4266
|
+
"Have a teammate on Bridge? Ask them for an invite and choose",
|
|
4267
|
+
'"I already have a token".',
|
|
4268
|
+
].join("\n");
|
|
4269
|
+
/** S2 — after a rejected code while resends may remain. */
|
|
4270
|
+
export const INSTALL_BRIDGE_REJECTED_RESEND_HINT = "Type r to send a new code, or enter the code again.";
|
|
4271
|
+
/** S3 — `r` after the server reported no resends left. */
|
|
4272
|
+
export const INSTALL_BRIDGE_NO_MORE_CODES = [
|
|
4273
|
+
"Bridge cannot send more codes for this signup.",
|
|
4274
|
+
"Enter a code from an email you already received.",
|
|
4275
|
+
].join("\n");
|
|
4276
|
+
/** S4a — the saved challenge is past its locally stored expiry. */
|
|
4277
|
+
export const INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED = "The code for this signup has expired.";
|
|
4278
|
+
/** S4b — the saved signup has no challenge handle (or expiry). */
|
|
4279
|
+
export const INSTALL_BRIDGE_RECOVERY_REASON_NO_CODE = "The last signup in this folder did not finish, so no code is waiting.";
|
|
4280
|
+
/** S4c — this run's email differs from the saved signup's email. */
|
|
4281
|
+
export const INSTALL_BRIDGE_RECOVERY_REASON_DIFFERENT_EMAIL = "This folder has a signup waiting for a different email address.";
|
|
4282
|
+
/** S5 — the rejection cap was reached (state-neutral). */
|
|
4283
|
+
export const INSTALL_BRIDGE_RECOVERY_REASON_CAP = "That signup could not be verified. Nothing has been created.";
|
|
4284
|
+
/** S6 — the user declined a fresh signup. */
|
|
4285
|
+
export const INSTALL_BRIDGE_FRESH_SIGNUP_DECLINED = [
|
|
4286
|
+
"Nothing has been created. Your saved signup is kept.",
|
|
4287
|
+
"Re-run install from this folder when you are ready.",
|
|
4288
|
+
].join("\n");
|
|
4289
|
+
/** S7 — the user accepted; printed after the old attempt is archived. */
|
|
4290
|
+
export const INSTALL_BRIDGE_FRESH_SIGNUP_STARTING = "Starting a new signup. The old attempt was set aside, not deleted.";
|
|
4291
|
+
/**
|
|
4292
|
+
* The default-No question after S4a/S4b/S4c/S5. Names only the MASKED address of
|
|
4293
|
+
* THIS run's email; the full address is never printed.
|
|
4294
|
+
*/
|
|
4295
|
+
export function formatFreshSignupQuestion(thisRunEmail) {
|
|
4296
|
+
return `Start a new signup? We will email a new code to ${maskSignupEmail(thisRunEmail)}. [y/N]: `;
|
|
4297
|
+
}
|
|
4298
|
+
/** The legacy "could not be requested" line for a rejected email (HTTP 400/422). */
|
|
4299
|
+
export const INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID = "Error: Self-serve setup could not be requested. Check the email value and try again.";
|
|
4300
|
+
/**
|
|
4301
|
+
* Map a failed challenge REQUEST to its user-facing line, with the legacy loop's
|
|
4302
|
+
* category mapping. Carries no response body, email, or exception text.
|
|
4303
|
+
*/
|
|
4304
|
+
export function formatSelfServeChallengeRequestFailure(failure) {
|
|
4305
|
+
switch (failure.category) {
|
|
4306
|
+
case "rate-limited":
|
|
4307
|
+
return `Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`;
|
|
4308
|
+
case "unavailable":
|
|
4309
|
+
case "network-error":
|
|
4310
|
+
return INSTALL_BRIDGE_SIGNUP_UNAVAILABLE;
|
|
4311
|
+
case "invalid":
|
|
4312
|
+
return INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID;
|
|
4313
|
+
case "blocked":
|
|
4314
|
+
return INSTALL_BRIDGE_SIGNUP_BLOCKED;
|
|
4315
|
+
case "unexpected":
|
|
4316
|
+
return formatSelfServeUnexpectedStatus(failure.status ?? 0);
|
|
4317
|
+
default:
|
|
4318
|
+
return INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Map a failed VERIFY (other than the uniform `rejected`) to its user-facing line,
|
|
4323
|
+
* with the legacy loop's mapping. Carries no response body, code, or handle.
|
|
4324
|
+
*/
|
|
4325
|
+
export function formatSelfServeVerifyFailure(failure) {
|
|
4326
|
+
switch (failure.category) {
|
|
4327
|
+
case "rate-limited":
|
|
4328
|
+
return `Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`;
|
|
4329
|
+
case "server-error":
|
|
4330
|
+
case "network-error":
|
|
4331
|
+
return INSTALL_BRIDGE_VERIFY_SERVER_ERROR;
|
|
4332
|
+
case "blocked":
|
|
4333
|
+
return INSTALL_BRIDGE_SIGNUP_BLOCKED;
|
|
4334
|
+
case "unexpected":
|
|
4335
|
+
return formatSelfServeUnexpectedStatus(failure.status ?? 0);
|
|
4336
|
+
default:
|
|
4337
|
+
return INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4258
4340
|
/**
|
|
4259
4341
|
* Mask an address for display: first character, then the domain.
|
|
4260
4342
|
*
|
|
@@ -4296,23 +4378,10 @@ export function formatChallengeRemaining(expiresAtIso, nowMs) {
|
|
|
4296
4378
|
const hours = Math.floor(minutes / 60);
|
|
4297
4379
|
return `This code is valid for about another ${hours} hours.`;
|
|
4298
4380
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
* network call.
|
|
4302
|
-
*
|
|
4303
|
-
* Local validation is a courtesy, not a security control — the server re-validates
|
|
4304
|
-
* everything — but it keeps a mistyped code from consuming one of the five attempts
|
|
4305
|
-
* that permanently kill a challenge. The user gets the prompt back instead.
|
|
4306
|
-
*
|
|
4307
|
-
* Pasted input is trimmed: copying a code out of a mail client routinely brings
|
|
4308
|
-
* whitespace with it, and refusing that would be an avoidable dead end.
|
|
4309
|
-
*
|
|
4310
|
-
* The entered value is returned and never echoed, never logged, and never written
|
|
4311
|
-
* to disk. `null` means the user gave up (an empty line, or an unreadable prompt).
|
|
4312
|
-
*/
|
|
4313
|
-
export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
|
|
4381
|
+
export async function promptForVerificationCode(deps, log, maxAttempts = 3, options = {}) {
|
|
4382
|
+
const allowResend = options.allowResend ?? false;
|
|
4314
4383
|
if (!deps.promptLine)
|
|
4315
|
-
return
|
|
4384
|
+
return { kind: "give-up" };
|
|
4316
4385
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
4317
4386
|
let entered;
|
|
4318
4387
|
try {
|
|
@@ -4321,17 +4390,19 @@ export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
|
|
|
4321
4390
|
catch {
|
|
4322
4391
|
// Secret-free by construction: the caught value is never surfaced or
|
|
4323
4392
|
// inspected, because a readline failure can carry the partial input.
|
|
4324
|
-
return
|
|
4393
|
+
return { kind: "give-up" };
|
|
4325
4394
|
}
|
|
4326
4395
|
if (entered.length === 0)
|
|
4327
|
-
return
|
|
4396
|
+
return { kind: "empty" };
|
|
4397
|
+
if (allowResend && entered.toLowerCase() === "r")
|
|
4398
|
+
return { kind: "resend" };
|
|
4328
4399
|
if (new RegExp(`^[0-9]{${SIGNUP_VERIFICATION_CODE_LENGTH}}$`).test(entered)) {
|
|
4329
|
-
return entered;
|
|
4400
|
+
return { kind: "code", code: entered };
|
|
4330
4401
|
}
|
|
4331
4402
|
// Describes the SHAPE, never the value the user typed.
|
|
4332
4403
|
log(` That does not look like a code — it is ${SIGNUP_VERIFICATION_CODE_LENGTH} digits.`);
|
|
4333
4404
|
}
|
|
4334
|
-
return
|
|
4405
|
+
return { kind: "give-up" };
|
|
4335
4406
|
}
|
|
4336
4407
|
/*
|
|
4337
4408
|
* `mintSelfServeInvite` and `SelfServeMintResult` were REMOVED in BAPI-757.
|
|
@@ -4898,8 +4969,8 @@ function buildDeferredLaunchStepPreview() {
|
|
|
4898
4969
|
/**
|
|
4899
4970
|
* The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
|
|
4900
4971
|
* prompts or writes; when the selection resolves to at least one writable
|
|
4901
|
-
* notes target this states plainly that a real interactive run
|
|
4902
|
-
*
|
|
4972
|
+
* notes target this states plainly that a real interactive run writes it
|
|
4973
|
+
* without asking. Empty when the selection resolves to
|
|
4903
4974
|
* no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
|
|
4904
4975
|
* lines are unconditional but this offer's applicability is not.
|
|
4905
4976
|
*/
|
|
@@ -4914,10 +4985,10 @@ function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
|
|
|
4914
4985
|
"Step 4c — reached only after the Step 4b VCS connect offer has completed or been",
|
|
4915
4986
|
" explicitly deferred (a declined or incomplete connect is announced first,",
|
|
4916
4987
|
" with the command to finish it later, and never blocks this step).",
|
|
4917
|
-
`Step 4c —
|
|
4918
|
-
` terminal is interactive,
|
|
4919
|
-
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}
|
|
4920
|
-
"
|
|
4988
|
+
`Step 4c — Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
|
|
4989
|
+
` terminal is interactive, a real run writes guidance so ${subject} know${plural ? "" : "s"} when to reach`,
|
|
4990
|
+
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}, without asking,`,
|
|
4991
|
+
" then prints how to remove it. Existing content outside that region is preserved. No notes files are",
|
|
4921
4992
|
" written in --dry-run.",
|
|
4922
4993
|
];
|
|
4923
4994
|
}
|
|
@@ -5395,10 +5466,6 @@ export function isMinttyNonInteractiveInstall(platform, env, isTTY) {
|
|
|
5395
5466
|
}
|
|
5396
5467
|
export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
5397
5468
|
const deps = { ...createDefaultInstallBridgeDeps(), ...overrides };
|
|
5398
|
-
if (Object.prototype.hasOwnProperty.call(overrides, "promptLine") &&
|
|
5399
|
-
!Object.prototype.hasOwnProperty.call(overrides, "promptGuidanceLine")) {
|
|
5400
|
-
deps.promptGuidanceLine = undefined;
|
|
5401
|
-
}
|
|
5402
5469
|
// When the caller overrode only `fetch` (the common test seam), bind the
|
|
5403
5470
|
// default resolver to the FINAL merged fetch so server resolution still routes
|
|
5404
5471
|
// through the override. An explicit `resolveRepoViaServer` override is honored.
|
|
@@ -5537,6 +5604,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5537
5604
|
// Built from the FINAL merged deps for the same reason (BAPI-775): a test
|
|
5538
5605
|
// that overrides only `readFile` or only `cwd` must see its override here.
|
|
5539
5606
|
localProvisioning: buildConductorLocalDeps(deps),
|
|
5607
|
+
// BAPI-1461: the converging setup's seams (sign-ins, the stored merge token,
|
|
5608
|
+
// the stored test command, the CI workflow installer, and indexing).
|
|
5609
|
+
setup: deps.conductorSetup ?? buildConductorSetupDeps(deps, log, errorLog),
|
|
5540
5610
|
// BAPI-1391: the read-only discovery filesystem over THIS working tree, and
|
|
5541
5611
|
// the short-lived qualifying executor (the existing executor CLI with a
|
|
5542
5612
|
// `runExecutor` override limiting it to `gate_qualification` jobs).
|
|
@@ -5749,7 +5819,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5749
5819
|
// The selection is pure and deterministic for every explicit, env-driven, and
|
|
5750
5820
|
// non-TTY invocation; only a BARE interactive run is asked which branch it wants
|
|
5751
5821
|
// (BAPI-626 — otherwise a first-time user can never reach self-serve).
|
|
5752
|
-
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
|
|
5822
|
+
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl);
|
|
5753
5823
|
if (!branchResult.ok) {
|
|
5754
5824
|
errorLog(`Error: ${branchResult.error}`);
|
|
5755
5825
|
return 1;
|
|
@@ -5791,7 +5861,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5791
5861
|
// key-authentication ping, so there is no source to attribute.
|
|
5792
5862
|
let apiKeySource;
|
|
5793
5863
|
if (selfServeSignupMode) {
|
|
5794
|
-
const emailResult = await resolveSignupEmail(options, deps, baseUrl
|
|
5864
|
+
const emailResult = await resolveSignupEmail(options, deps, baseUrl, branchResult.onboardingEmail
|
|
5865
|
+
? {
|
|
5866
|
+
email: branchResult.onboardingEmail,
|
|
5867
|
+
privacyNoticeShown: branchResult.privacyNoticeShown === true,
|
|
5868
|
+
}
|
|
5869
|
+
: undefined);
|
|
5795
5870
|
if (!emailResult.ok) {
|
|
5796
5871
|
errorLog(`Error: ${emailResult.error}`);
|
|
5797
5872
|
return 1;
|
|
@@ -5931,6 +6006,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5931
6006
|
return 1;
|
|
5932
6007
|
}
|
|
5933
6008
|
let modernAttempt;
|
|
6009
|
+
// BAPI-1449: an expired basename-keyed signup is offered a fresh modern signup by
|
|
6010
|
+
// the modern path (consent first), instead of ending the run here.
|
|
6011
|
+
let legacyExpired;
|
|
5934
6012
|
if (options.tools?.length === 0 && !options.agentName) {
|
|
5935
6013
|
log(buildManualInstallBridgeContinuation("empty-selection", []));
|
|
5936
6014
|
return 0;
|
|
@@ -5956,22 +6034,22 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5956
6034
|
}
|
|
5957
6035
|
if (legacy.state === "resumable" || legacy.state === "challenge")
|
|
5958
6036
|
options.repo = oldName.value;
|
|
5959
|
-
if (legacy.state === "challenge-expired")
|
|
5960
|
-
|
|
5961
|
-
return 1;
|
|
5962
|
-
}
|
|
6037
|
+
if (legacy.state === "challenge-expired")
|
|
6038
|
+
legacyExpired = { repoName: oldName.value, snapshot: legacy.snapshot };
|
|
5963
6039
|
}
|
|
5964
6040
|
}
|
|
5965
6041
|
if (options.repo === undefined) {
|
|
5966
6042
|
try {
|
|
5967
|
-
modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode);
|
|
6043
|
+
modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode, legacyExpired);
|
|
5968
6044
|
apiKey = modernAttempt.record.keySecret;
|
|
5969
6045
|
options.repo = modernAttempt.repoName;
|
|
5970
6046
|
bootstrapInviteMode = false;
|
|
5971
6047
|
deps.log(`Project '${modernAttempt.displayName}' recovered; completing installation.`);
|
|
5972
6048
|
}
|
|
5973
6049
|
catch (error) {
|
|
5974
|
-
|
|
6050
|
+
if (error instanceof ModernInstallStopped)
|
|
6051
|
+
return 1;
|
|
6052
|
+
errorLog(error instanceof ModernInstallRecoveryError ? error.message : `Installation could not complete. Preserve recovery proof and resume from the same folder and invite/signup; contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} if needed.`);
|
|
5975
6053
|
return 1;
|
|
5976
6054
|
}
|
|
5977
6055
|
}
|
|
@@ -6137,10 +6215,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6137
6215
|
return 0;
|
|
6138
6216
|
}
|
|
6139
6217
|
// ---- Finalize the launch agent for spawn-capable decisions ----
|
|
6140
|
-
// choose-one:
|
|
6141
|
-
//
|
|
6142
|
-
//
|
|
6143
|
-
//
|
|
6218
|
+
// choose-one: on a TTY, take the first launchable agent the user selected and say so
|
|
6219
|
+
// (BAPI-1448 retired the launch-agent chooser; `--agent` still wins
|
|
6220
|
+
// upstream in the pure launch decision). A non-TTY run degrades to the manual
|
|
6221
|
+
// continuation (finalAgentName stays null).
|
|
6144
6222
|
let finalAgentName = null;
|
|
6145
6223
|
let finalSpawnCommand = null;
|
|
6146
6224
|
if (planLaunch.kind === "spawn") {
|
|
@@ -6148,8 +6226,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6148
6226
|
finalSpawnCommand = planLaunch.spawnCommand;
|
|
6149
6227
|
}
|
|
6150
6228
|
else if (planLaunch.kind === "choose-one") {
|
|
6151
|
-
const chosen =
|
|
6229
|
+
const chosen = deps.isTTY ? (planLaunch.agents[0] ?? null) : null;
|
|
6152
6230
|
if (chosen) {
|
|
6231
|
+
log(`Using ${toolLabelForLaunchAgent(chosen)} for the setup session (the first tool you selected). Pass --agent to choose another.`);
|
|
6153
6232
|
const spec = resolveAgentSpec(chosen);
|
|
6154
6233
|
if (!spec) {
|
|
6155
6234
|
errorLog(`Error: no launch agent is registered for '${chosen}'.`);
|
|
@@ -6159,7 +6238,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6159
6238
|
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
6160
6239
|
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
|
|
6161
6240
|
}
|
|
6162
|
-
// chosen === null → non-TTY
|
|
6241
|
+
// chosen === null → non-TTY → manual continuation below.
|
|
6163
6242
|
}
|
|
6164
6243
|
// planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
|
|
6165
6244
|
// ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
|
|
@@ -6348,7 +6427,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6348
6427
|
challengeExpiresAt = liveChallenge.expiresAt;
|
|
6349
6428
|
challengeEmail = liveChallenge.email;
|
|
6350
6429
|
log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
|
|
6351
|
-
log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6352
6430
|
}
|
|
6353
6431
|
else {
|
|
6354
6432
|
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
@@ -6365,7 +6443,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6365
6443
|
reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
|
|
6366
6444
|
}
|
|
6367
6445
|
else if (requested.category === "invalid") {
|
|
6368
|
-
reportFatal(
|
|
6446
|
+
reportFatal(INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID);
|
|
6369
6447
|
}
|
|
6370
6448
|
else if (requested.category === "blocked") {
|
|
6371
6449
|
// BAPI-1171: the request was refused before signup logic ran — an
|
|
@@ -6408,12 +6486,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6408
6486
|
"No workspace has been set up — fix the problem and re-run.");
|
|
6409
6487
|
return false;
|
|
6410
6488
|
}
|
|
6411
|
-
log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6412
6489
|
}
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
if (remaining)
|
|
6416
|
-
log(` ${remaining}`);
|
|
6490
|
+
// BAPI-1448: the shared lead-in, fresh or resumed, directly before code entry.
|
|
6491
|
+
logVerificationCodeLeadIn(log, challengeEmail);
|
|
6417
6492
|
// Phase 2, second half, and phase 3. One resend is offered on the recovery
|
|
6418
6493
|
// path only — never proactively — so the budget is spent on the user's
|
|
6419
6494
|
// explicit request rather than by the CLI's own retry logic.
|
|
@@ -6424,12 +6499,15 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6424
6499
|
// never succeed, with Ctrl-C as the only way out.
|
|
6425
6500
|
let rejections = 0;
|
|
6426
6501
|
for (;;) {
|
|
6427
|
-
|
|
6428
|
-
|
|
6502
|
+
// Resend recognition stays off here: on the legacy path `r` is still malformed
|
|
6503
|
+
// input, and an empty line or give-up ends the run as it always has.
|
|
6504
|
+
const prompted = await promptForVerificationCode(deps, log);
|
|
6505
|
+
if (prompted.kind !== "code") {
|
|
6429
6506
|
reportFatal("Error: No verification code entered — nothing has been set up. Re-run this command " +
|
|
6430
6507
|
"to continue; the code already emailed to you stays valid until it expires.");
|
|
6431
6508
|
return false;
|
|
6432
6509
|
}
|
|
6510
|
+
const code = prompted.code;
|
|
6433
6511
|
const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
|
|
6434
6512
|
if (verified.ok) {
|
|
6435
6513
|
inviteToken = verified.token;
|
|
@@ -6820,9 +6898,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6820
6898
|
// existing value unconditionally — the old value is never read first. A
|
|
6821
6899
|
// no-answer (or a prompt/stream exception the picker already absorbed)
|
|
6822
6900
|
// reports the same fail-open remediation without ever attempting a write.
|
|
6901
|
+
let persistedPosture;
|
|
6823
6902
|
if (optimizationPostureSelection) {
|
|
6824
6903
|
const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
|
|
6825
6904
|
reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
|
|
6905
|
+
if (postureResult.kind === "success")
|
|
6906
|
+
persistedPosture = optimizationPostureSelection;
|
|
6826
6907
|
}
|
|
6827
6908
|
else if (optimizationPostureEligible) {
|
|
6828
6909
|
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
@@ -7106,8 +7187,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7106
7187
|
// selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
|
|
7107
7188
|
// other persisted install-state contract — consent is re-evaluated every run.
|
|
7108
7189
|
const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
|
|
7109
|
-
const
|
|
7110
|
-
const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets, notesPlatformLabels);
|
|
7190
|
+
const notesOffer = offerAgentGuidanceNotes(deps, notesPlan.targets);
|
|
7111
7191
|
let notesResult;
|
|
7112
7192
|
let notesWriteFailed = false;
|
|
7113
7193
|
if (notesOffer.kind === "consented") {
|
|
@@ -7137,10 +7217,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7137
7217
|
const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
|
|
7138
7218
|
if (notesSummaryLine)
|
|
7139
7219
|
log(notesSummaryLine);
|
|
7220
|
+
// The removal hint only when something was actually written (BAPI-1448).
|
|
7221
|
+
const notesWrittenTargets = (notesResult?.files ?? [])
|
|
7222
|
+
.filter((f) => f.status === "written")
|
|
7223
|
+
.map((f) => f.target);
|
|
7224
|
+
if (notesWrittenTargets.length > 0)
|
|
7225
|
+
log(INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT);
|
|
7226
|
+
const notesUnchangedTargets = (notesResult?.files ?? [])
|
|
7227
|
+
.filter((f) => f.status === "skipped")
|
|
7228
|
+
.map((f) => f.target);
|
|
7140
7229
|
// ---- BAPI-1398: Bridge permission rules for /drive-epic-run (Claude Code) ----
|
|
7141
7230
|
// After the durable install work and the other offers, before the session
|
|
7142
|
-
// opens. Project rules are written as part of the normal install
|
|
7143
|
-
// settings change
|
|
7231
|
+
// opens. Project rules are written as part of the normal install. The user
|
|
7232
|
+
// settings change is NOT part of install (BAPI-1448): it lives in the
|
|
7233
|
+
// `bridge-permissions` subcommand, offered in-session by /drive-epic-run. Every
|
|
7144
7234
|
// failure here is advisory: a malformed settings file is reported and skipped,
|
|
7145
7235
|
// and the install still completes.
|
|
7146
7236
|
if (selectedPlatforms.includes("claude-code")) {
|
|
@@ -7149,13 +7239,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7149
7239
|
cwd: deps.cwd,
|
|
7150
7240
|
homeDir: deps.homedir(),
|
|
7151
7241
|
env: deps.env,
|
|
7152
|
-
promptLine: deps.isTTY ? deps.promptLine : undefined,
|
|
7153
7242
|
readFile: deps.readFile,
|
|
7154
7243
|
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
7155
7244
|
mkdir: (p, o) => deps.mkdir(p, o),
|
|
7156
7245
|
log,
|
|
7157
7246
|
errorLog,
|
|
7158
7247
|
ensureGitignored: (relPath) => ensureGitignoredShared(deps.cwd, relPath, gitignoreDeps),
|
|
7248
|
+
// BAPI-1446: pin the SessionStart upgrade-notice hook to the effective Claude Code pin.
|
|
7249
|
+
sessionStartHook: { pinnedVersion: await discoverClaudeCodeEffectivePin({ cwd: deps.cwd, readFile: deps.readFile, targetVersion: VERSION }) },
|
|
7159
7250
|
});
|
|
7160
7251
|
}
|
|
7161
7252
|
catch {
|
|
@@ -7187,6 +7278,27 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7187
7278
|
for (const line of buildInstallBridgeMaintenanceHint())
|
|
7188
7279
|
log(line);
|
|
7189
7280
|
await showFirstBuildAdvisory(finalAgentName);
|
|
7281
|
+
// ---- BAPI-1448: the defaults summary, once, right before the launch consent ----
|
|
7282
|
+
// Every input is already in memory except the optional ticket prefix, which is one
|
|
7283
|
+
// bounded read of the manifest the GitHub offer already uses. Any failure there just
|
|
7284
|
+
// omits the cell: the summary never blocks or changes the install's outcome.
|
|
7285
|
+
let ticketPrefix;
|
|
7286
|
+
try {
|
|
7287
|
+
const details = await fetchGithubConfigurationDetails({ fetch: deps.fetch, baseUrl, apiKey }, repoName);
|
|
7288
|
+
ticketPrefix = details.ticketPrefix;
|
|
7289
|
+
}
|
|
7290
|
+
catch {
|
|
7291
|
+
ticketPrefix = undefined;
|
|
7292
|
+
}
|
|
7293
|
+
log("");
|
|
7294
|
+
log(buildInstallBridgeDefaultsSummary({
|
|
7295
|
+
displayName: modernAttempt?.displayName ?? repoName,
|
|
7296
|
+
...(ticketPrefix ? { ticketPrefix } : {}),
|
|
7297
|
+
isNewProject: Boolean(modernAttempt) || bootstrapExchangeSucceeded,
|
|
7298
|
+
...(persistedPosture ? { persistedPosture } : {}),
|
|
7299
|
+
claudeCodeSelected: selectedPlatforms.includes("claude-code"),
|
|
7300
|
+
guidance: { written: notesWrittenTargets, unchanged: notesUnchangedTargets },
|
|
7301
|
+
}));
|
|
7190
7302
|
// ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
|
|
7191
7303
|
// AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
|
|
7192
7304
|
// resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
|
|
@@ -7303,13 +7415,23 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7303
7415
|
}
|
|
7304
7416
|
class ModernInstallRecoveryError extends Error {
|
|
7305
7417
|
}
|
|
7418
|
+
/** The run already printed its own output (for example S6) and stops with exit 1. */
|
|
7419
|
+
class ModernInstallStopped extends Error {
|
|
7420
|
+
}
|
|
7421
|
+
/**
|
|
7422
|
+
* A record that may hold redeemed proof. Presence, not truthiness: an empty-string
|
|
7423
|
+
* field still counts. Such a record is never offered a fresh signup.
|
|
7424
|
+
*/
|
|
7425
|
+
function isProtectedModernRecord(record) {
|
|
7426
|
+
return ["fingerprint", "replayToken", "canonical"].some(field => Object.prototype.hasOwnProperty.call(record, field));
|
|
7427
|
+
}
|
|
7306
7428
|
function modernWriteDeps(deps) {
|
|
7307
7429
|
return { env: deps.env, homedir: deps.homedir, platform: deps.platform, readFile: deps.readFile,
|
|
7308
7430
|
mkdir: deps.mkdir, writeFile: deps.writeFile, rename: deps.rename, chmod: deps.chmod, unlink: deps.unlink, open: deps.open };
|
|
7309
7431
|
}
|
|
7310
|
-
async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe) {
|
|
7432
|
+
async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe, legacyExpired) {
|
|
7311
7433
|
if (!deps.realpath)
|
|
7312
|
-
throw new ModernInstallRecoveryError(
|
|
7434
|
+
throw new ModernInstallRecoveryError(`Canonical folder resolution is unavailable. Contact Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
|
|
7313
7435
|
const folder = await deps.realpath(deps.cwd);
|
|
7314
7436
|
const folderDigest = fingerprintBootstrapFields("bridge-bootstrap-folder-v1", [baseUrl, folder]);
|
|
7315
7437
|
const signupLocator = `bootstrap-pending:signup:${folderDigest}`;
|
|
@@ -7321,30 +7443,50 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
|
|
|
7321
7443
|
if (!loaded.ok)
|
|
7322
7444
|
throw new Error(loaded.error);
|
|
7323
7445
|
let record = loaded.record;
|
|
7324
|
-
|
|
7446
|
+
let resumed = record !== null;
|
|
7325
7447
|
let label = record?.label ?? options.displayName;
|
|
7326
|
-
|
|
7448
|
+
// BAPI-1449: an expired legacy basename signup asks for consent BEFORE any label
|
|
7449
|
+
// question, so its label is derived only after the user accepts.
|
|
7450
|
+
const deferLabelForLegacy = selfServe && record === null && legacyExpired !== undefined;
|
|
7451
|
+
const deriveLabel = async () => {
|
|
7327
7452
|
if (!deps.isTTY || !deps.promptLine)
|
|
7328
7453
|
throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
|
|
7454
|
+
// BAPI-1448: a valid folder name is the label, with no question. The prompt
|
|
7455
|
+
// survives only for a folder name the validator rejects.
|
|
7329
7456
|
let suggestion = "";
|
|
7330
7457
|
try {
|
|
7331
7458
|
suggestion = validateBootstrapDisplayName(path.basename(folder));
|
|
7332
7459
|
}
|
|
7333
7460
|
catch { /* explicit label required */ }
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7461
|
+
if (suggestion)
|
|
7462
|
+
return suggestion;
|
|
7463
|
+
return validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
|
|
7464
|
+
};
|
|
7465
|
+
if (label === undefined && !deferLabelForLegacy)
|
|
7466
|
+
label = await deriveLabel();
|
|
7337
7467
|
if (selfServe) {
|
|
7338
7468
|
if (!deps.isTTY || !deps.promptLine)
|
|
7339
7469
|
throw new ModernInstallRecoveryError("Self-serve email verification requires an interactive terminal.");
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7470
|
+
// At most one fresh signup per invocation, whichever route accepted it.
|
|
7471
|
+
let freshSignupAccepted = false;
|
|
7472
|
+
const generateKeySecret = () => generateBootstrapKeySecret(deps.randomBytes);
|
|
7473
|
+
const generateArchiveSuffix = () => deps.randomBytes(16).toString("hex");
|
|
7474
|
+
// Print the reason, then ask the default-No question. A decline (or EOF, an empty
|
|
7475
|
+
// line, a thrown prompt) prints S6 and stops with the credential store untouched.
|
|
7476
|
+
const askFreshSignup = async (reason) => {
|
|
7477
|
+
deps.log(reason);
|
|
7478
|
+
if (!(await requestCredentialRouteSwitchConfirmation(deps, formatFreshSignupQuestion(signupEmail)))) {
|
|
7479
|
+
deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_DECLINED);
|
|
7480
|
+
throw new ModernInstallStopped();
|
|
7481
|
+
}
|
|
7482
|
+
};
|
|
7483
|
+
// Request a challenge for THIS run's email, then persist its handle and expiry
|
|
7484
|
+
// BEFORE the prompt. The request happens outside the credential-store lock. On
|
|
7485
|
+
// any failure the saved record stays and is never replaced again in this run.
|
|
7486
|
+
const requestAndSaveChallenge = async () => {
|
|
7345
7487
|
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
7346
7488
|
if (!requested.ok)
|
|
7347
|
-
throw new ModernInstallRecoveryError(
|
|
7489
|
+
throw new ModernInstallRecoveryError(formatSelfServeChallengeRequestFailure(requested));
|
|
7348
7490
|
const saved = await mutateModernBootstrapRecord(scope, writeDeps, current => {
|
|
7349
7491
|
if (!current)
|
|
7350
7492
|
throw new ModernInstallRecoveryError("Signup proof missing.");
|
|
@@ -7353,24 +7495,161 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
|
|
|
7353
7495
|
if (!saved.ok || !saved.record)
|
|
7354
7496
|
throw new Error(saved.ok ? "Signup proof missing." : saved.error);
|
|
7355
7497
|
record = saved.record;
|
|
7498
|
+
};
|
|
7499
|
+
// Archive the observed record, install a fresh signup for this run's email, then
|
|
7500
|
+
// (only after that is durable) print S7 and request the new challenge.
|
|
7501
|
+
const replaceWithFreshSignup = async (observed) => {
|
|
7502
|
+
const replaced = await archiveAndReplaceModernBootstrapSignup({ scope, observed, email: signupEmail, generateKeySecret, generateArchiveSuffix }, writeDeps);
|
|
7503
|
+
if (!replaced.ok)
|
|
7504
|
+
throw new ModernInstallRecoveryError(replaced.error);
|
|
7505
|
+
freshSignupAccepted = true;
|
|
7506
|
+
resumed = false;
|
|
7507
|
+
record = replaced.record;
|
|
7508
|
+
deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_STARTING);
|
|
7509
|
+
await requestAndSaveChallenge();
|
|
7510
|
+
};
|
|
7511
|
+
if (!record) {
|
|
7512
|
+
if (legacyExpired) {
|
|
7513
|
+
// The basename-keyed legacy signup is the "no modern record" case: ask, derive
|
|
7514
|
+
// the label from the folder basename, then archive it and install the modern
|
|
7515
|
+
// signup in one write.
|
|
7516
|
+
await askFreshSignup(INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED);
|
|
7517
|
+
label = options.displayName ?? await deriveLabel();
|
|
7518
|
+
const replaced = await archiveExpiredLegacySignupAndInstallModern({ repoName: legacyExpired.repoName, observed: legacyExpired.snapshot, now: deps.now(), scope, label, email: signupEmail, generateKeySecret, generateArchiveSuffix }, writeDeps);
|
|
7519
|
+
if (!replaced.ok)
|
|
7520
|
+
throw new ModernInstallRecoveryError(replaced.error);
|
|
7521
|
+
freshSignupAccepted = true;
|
|
7522
|
+
record = replaced.record;
|
|
7523
|
+
deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_STARTING);
|
|
7524
|
+
await requestAndSaveChallenge();
|
|
7525
|
+
}
|
|
7526
|
+
else {
|
|
7527
|
+
const intent = await prepareModernBootstrapSignup(scope, label, signupEmail, generateKeySecret, writeDeps);
|
|
7528
|
+
if (!intent.ok || !intent.record)
|
|
7529
|
+
throw new Error(intent.ok ? "Signup proof missing." : intent.error);
|
|
7530
|
+
record = intent.record;
|
|
7531
|
+
await requestAndSaveChallenge();
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
else if (!isProtectedModernRecord(record)) {
|
|
7535
|
+
// A saved, unresolved signup. Reasons are checked in this order, and the
|
|
7536
|
+
// expiry is read from the stored value only, never from a server reply.
|
|
7537
|
+
// A malformed stored expiry is treated as unexpired.
|
|
7538
|
+
let reason;
|
|
7539
|
+
if (typeof record.signupEmail === "string" && record.signupEmail.trim().toLowerCase() !== signupEmail.trim().toLowerCase()) {
|
|
7540
|
+
reason = INSTALL_BRIDGE_RECOVERY_REASON_DIFFERENT_EMAIL;
|
|
7541
|
+
}
|
|
7542
|
+
else if (!record.challengeHandle || !record.challengeExpiresAt) {
|
|
7543
|
+
reason = INSTALL_BRIDGE_RECOVERY_REASON_NO_CODE;
|
|
7544
|
+
}
|
|
7545
|
+
else if (Date.parse(record.challengeExpiresAt) <= deps.now()) {
|
|
7546
|
+
reason = INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED;
|
|
7547
|
+
}
|
|
7548
|
+
if (reason !== undefined) {
|
|
7549
|
+
await askFreshSignup(reason);
|
|
7550
|
+
await replaceWithFreshSignup(record);
|
|
7551
|
+
}
|
|
7356
7552
|
}
|
|
7553
|
+
if (!record)
|
|
7554
|
+
throw new ModernInstallRecoveryError("Recovery proof missing.");
|
|
7357
7555
|
if (record.replayToken && record.fingerprint) {
|
|
7358
7556
|
token = record.replayToken;
|
|
7359
7557
|
fingerprint = record.fingerprint;
|
|
7360
7558
|
scope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
|
|
7361
7559
|
}
|
|
7362
7560
|
else {
|
|
7561
|
+
// Only a protected record can reach these two guards unresolved: an unprotected
|
|
7562
|
+
// one was either offered a fresh signup above or already holds a live challenge.
|
|
7363
7563
|
if (!record.challengeHandle || !record.challengeExpiresAt)
|
|
7364
|
-
throw new ModernInstallRecoveryError(
|
|
7365
|
-
if (Date.parse(record.challengeExpiresAt) <= deps.now())
|
|
7366
|
-
throw new ModernInstallRecoveryError(
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7564
|
+
throw new ModernInstallRecoveryError(`The previous signup response is unresolved. Preserve proof and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} before restarting.`);
|
|
7565
|
+
if (isProtectedModernRecord(record) && Date.parse(record.challengeExpiresAt) <= deps.now())
|
|
7566
|
+
throw new ModernInstallRecoveryError(`The saved verification challenge expired. Preserve proof and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
|
|
7567
|
+
// One pass per challenge. Everything challenge-local resets only when a new
|
|
7568
|
+
// challenge begins; the loop leaves it on a verified code or by throwing.
|
|
7569
|
+
let verifiedToken;
|
|
7570
|
+
for (;;) {
|
|
7571
|
+
const handle = record.challengeHandle;
|
|
7572
|
+
logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
|
|
7573
|
+
let rejections = 0;
|
|
7574
|
+
let consecutiveEmpty = 0;
|
|
7575
|
+
let emptyGuidanceShown = false;
|
|
7576
|
+
// Unknown until a resend in THIS session reports it, including on a resumed
|
|
7577
|
+
// challenge: the first `r` then makes a real request.
|
|
7578
|
+
let resendsRemaining;
|
|
7579
|
+
let capReached = false;
|
|
7580
|
+
// Count a rejection; true when the shared cap was just reached.
|
|
7581
|
+
const countRejection = () => {
|
|
7582
|
+
deps.errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED);
|
|
7583
|
+
rejections += 1;
|
|
7584
|
+
capReached = rejections >= INSTALL_BRIDGE_MAX_CODE_REJECTIONS;
|
|
7585
|
+
return capReached;
|
|
7586
|
+
};
|
|
7587
|
+
while (!capReached && verifiedToken === undefined) {
|
|
7588
|
+
const prompted = await promptForVerificationCode(deps, deps.log, 3, { allowResend: true });
|
|
7589
|
+
if (prompted.kind === "give-up")
|
|
7590
|
+
throw new ModernInstallRecoveryError(INSTALL_BRIDGE_VERIFICATION_INCOMPLETE);
|
|
7591
|
+
if (prompted.kind === "empty") {
|
|
7592
|
+
consecutiveEmpty += 1;
|
|
7593
|
+
if (consecutiveEmpty >= 2)
|
|
7594
|
+
throw new ModernInstallRecoveryError(INSTALL_BRIDGE_VERIFICATION_INCOMPLETE);
|
|
7595
|
+
if (!emptyGuidanceShown) {
|
|
7596
|
+
deps.log(INSTALL_BRIDGE_EMPTY_CODE_GUIDANCE);
|
|
7597
|
+
emptyGuidanceShown = true;
|
|
7598
|
+
}
|
|
7599
|
+
continue;
|
|
7600
|
+
}
|
|
7601
|
+
consecutiveEmpty = 0;
|
|
7602
|
+
if (prompted.kind === "resend") {
|
|
7603
|
+
if (resendsRemaining === 0) {
|
|
7604
|
+
deps.log(INSTALL_BRIDGE_NO_MORE_CODES);
|
|
7605
|
+
continue;
|
|
7606
|
+
}
|
|
7607
|
+
// The server enforces the cooldown and the resend budget; there is no
|
|
7608
|
+
// client timer or cap. The stored expiry is never touched.
|
|
7609
|
+
const resent = await resendSelfServeChallenge(deps, baseUrl, handle);
|
|
7610
|
+
if (resent.ok) {
|
|
7611
|
+
deps.log(` ${INSTALL_BRIDGE_RESEND_SENT}`);
|
|
7612
|
+
resendsRemaining = resent.resendsRemaining;
|
|
7613
|
+
}
|
|
7614
|
+
else if (resent.category === "cooldown") {
|
|
7615
|
+
deps.log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);
|
|
7616
|
+
}
|
|
7617
|
+
else if (resent.category === "rejected") {
|
|
7618
|
+
// The uniform 401 also covers "no resends left", so the challenge may
|
|
7619
|
+
// still be alive: count it, but do not end the run on its own.
|
|
7620
|
+
countRejection();
|
|
7621
|
+
}
|
|
7622
|
+
else {
|
|
7623
|
+
deps.log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
|
|
7624
|
+
}
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
const verified = await verifySelfServeChallenge(deps, baseUrl, handle, prompted.code);
|
|
7628
|
+
if (verified.ok) {
|
|
7629
|
+
// BAPI-1173: only an EXPLICIT false prints.
|
|
7630
|
+
if (verified.trialGranted === false)
|
|
7631
|
+
deps.log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);
|
|
7632
|
+
verifiedToken = verified.token;
|
|
7633
|
+
break;
|
|
7634
|
+
}
|
|
7635
|
+
if (verified.category === "rejected") {
|
|
7636
|
+
if (!countRejection() && resendsRemaining !== 0)
|
|
7637
|
+
deps.log(INSTALL_BRIDGE_REJECTED_RESEND_HINT);
|
|
7638
|
+
continue;
|
|
7639
|
+
}
|
|
7640
|
+
// Every other category ends the run and keeps the saved record.
|
|
7641
|
+
throw new ModernInstallRecoveryError(formatSelfServeVerifyFailure(verified));
|
|
7642
|
+
}
|
|
7643
|
+
if (verifiedToken !== undefined)
|
|
7644
|
+
break;
|
|
7645
|
+
// Rejection cap: offer one fresh signup (S5), unless one was already taken.
|
|
7646
|
+
if (freshSignupAccepted) {
|
|
7647
|
+
throw new ModernInstallRecoveryError(`${INSTALL_BRIDGE_VERIFICATION_GIVING_UP}\n\nRun it again from this same folder.`);
|
|
7648
|
+
}
|
|
7649
|
+
await askFreshSignup(INSTALL_BRIDGE_RECOVERY_REASON_CAP);
|
|
7650
|
+
await replaceWithFreshSignup(record);
|
|
7651
|
+
}
|
|
7652
|
+
token = verifiedToken;
|
|
7374
7653
|
fingerprint = fingerprintBootstrapInvite(token, baseUrl);
|
|
7375
7654
|
const inviteScope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
|
|
7376
7655
|
const transitioned = await transitionModernBootstrapSignup(makeScope(signupLocator), inviteScope, fingerprint, token, record.challengeHandle, writeDeps);
|
|
@@ -7406,7 +7685,7 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
|
|
|
7406
7685
|
if (!exchange.ok && exchange.message.includes("Creation outcome is unknown"))
|
|
7407
7686
|
throw new ModernInstallRecoveryError("Creation outcome is unknown. Preserve recovery proof and rerun the same attempt from the original folder and invite/signup.");
|
|
7408
7687
|
if (!exchange.ok || !exchange.displayName)
|
|
7409
|
-
throw new ModernInstallRecoveryError(
|
|
7688
|
+
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 Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
|
|
7410
7689
|
const checkpoint = await checkpointModernBootstrapIdentity(scope, record, exchange.repoName, exchange.displayName, writeDeps);
|
|
7411
7690
|
if (!checkpoint.ok || !checkpoint.record)
|
|
7412
7691
|
throw new Error(checkpoint.ok ? "Checkpoint missing." : checkpoint.error);
|