@bridge_gpt/mcp-server 0.2.68 → 0.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -41
- package/build/bridge-permissions.js +627 -94
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +5 -5
- package/build/connect-github-api.js +33 -14
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +109 -6
- package/build/epic-implementer-bin.js +80 -85
- package/build/executor/completion-contract.js +2 -0
- package/build/executor/http-client.js +43 -4
- package/build/index.js +464 -246
- package/build/install-bridge.js +260 -297
- 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/pipelines.generated.js +4 -4
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/start-tickets.js +15 -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 +347 -20
- package/build/version.generated.js +3 -3
- package/package.json +1 -1
package/build/install-bridge.js
CHANGED
|
@@ -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
|
|
@@ -155,7 +155,7 @@ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform
|
|
|
155
155
|
import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
|
|
156
156
|
import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
|
|
157
157
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
|
|
158
|
-
import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
|
|
158
|
+
import { DUPLICATE_REGISTRATION_GUIDANCE, discoverClaudeCodeEffectivePin } from "./launcher-config-inspection.js";
|
|
159
159
|
import { codexHome, configureBridgeCodexProfile, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
|
|
160
160
|
import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
161
161
|
// BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
|
|
@@ -193,7 +193,7 @@ import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
|
193
193
|
import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
|
|
194
194
|
// BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
|
|
195
195
|
// API primitives verbatim — no duplicated polling, browser, or picker logic here.
|
|
196
|
-
import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
196
|
+
import { fetchGithubConfigurationDetails, fetchGithubConfigurationState, } from "./connect-github-api.js";
|
|
197
197
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
198
198
|
import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
|
|
199
199
|
import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
|
|
@@ -274,7 +274,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
274
274
|
"'Needs setup' — and each item in them carries display_name, description, how_to_use, and (in the " +
|
|
275
275
|
"'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
|
|
276
276
|
"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, " +
|
|
277
|
+
"verbatim as the setup guidance for a 'Needs setup' item — except when that section carries a non-empty string heading: print it verbatim as the section's sub-heading instead of its name and print NO item's availability_text; with no usable heading, render each availability_text as usual — and show NO availability caveat, " +
|
|
278
278
|
"asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
|
|
279
279
|
"contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
|
|
280
280
|
"and never branch on it. Keep an empty section visible with a short neutral line rather than " +
|
|
@@ -303,7 +303,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
303
303
|
"short, plain-language sentence covering what is happening and why it matters to the person running " +
|
|
304
304
|
"this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
|
|
305
305
|
"internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
|
|
306
|
-
"get_install_manifest
|
|
306
|
+
"get_install_manifest or apply_install_manifest), or a manifest field key (like " +
|
|
307
307
|
"command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
|
|
308
308
|
"This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
|
|
309
309
|
"name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
|
|
@@ -317,11 +317,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
|
|
|
317
317
|
"duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
|
|
318
318
|
"FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
|
|
319
319
|
"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. " +
|
|
320
|
+
"The install-spawn skip set is exactly Stage 8 and Stage 9. " +
|
|
325
321
|
"Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
|
|
326
322
|
"indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
|
|
327
323
|
"funnel with no visibility from this session, so say nothing about it at all. " +
|
|
@@ -394,9 +390,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
394
390
|
"the next step the server returned — when it returns one.",
|
|
395
391
|
"If the project is already configured it instead",
|
|
396
392
|
"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 —",
|
|
393
|
+
"concise capability report plus that same conditional next step.",
|
|
394
|
+
"So not every run applies config fields. Indexing is never asked about —",
|
|
400
395
|
"it starts automatically once the repository reaches full parse readiness.",
|
|
401
396
|
"",
|
|
402
397
|
"Your API key is written to a project MCP config only when that file is safe: a",
|
|
@@ -406,17 +401,15 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
406
401
|
"resolves your key from the credential store at runtime. A config file that",
|
|
407
402
|
"cannot be parsed is left untouched, with manual-merge instructions printed.",
|
|
408
403
|
"",
|
|
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",
|
|
404
|
+
"Run it bare — `install` with no flags — in a terminal and it asks one question:",
|
|
405
|
+
`\`${INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT.trim()}\``,
|
|
406
|
+
"Type your email to create a brand-new Bridge workspace for you (the self-serve",
|
|
407
|
+
"flow). Press Enter if you already have a token: a single hidden token prompt",
|
|
408
|
+
"opens, and whatever you paste has its SHAPE pick the route — a bootstrap invite",
|
|
409
|
+
"(bapi_inv_…) is redeemed to CREATE a new project and mint your first admin key,",
|
|
410
|
+
"while a Bridge API key connects to the project that already exists. One",
|
|
411
|
+
"malformed answer re-prompts once, then exits with guidance. You are",
|
|
412
|
+
"never asked which kind of token you hold. The question is asked ONLY for a",
|
|
420
413
|
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
421
414
|
"terminal keeps the existing deterministic behavior and no prompt.",
|
|
422
415
|
"",
|
|
@@ -532,9 +525,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
532
525
|
" --tools claude-code,codex are accepted. On an",
|
|
533
526
|
" interactive terminal WITHOUT this flag you get a",
|
|
534
527
|
" numbered picker: enter one or more tool numbers,",
|
|
535
|
-
" comma-separated (e.g. 1,3), and press Enter once
|
|
536
|
-
"
|
|
537
|
-
"
|
|
528
|
+
" comma-separated (e.g. 1,3), and press Enter once.",
|
|
529
|
+
" Tools Bridge detects on this machine are pre-ticked:",
|
|
530
|
+
" a bare Enter accepts them, and typed numbers",
|
|
531
|
+
" replace them. With none detected you must select",
|
|
532
|
+
" at least one. A non-interactive",
|
|
538
533
|
" (non-TTY) run without",
|
|
539
534
|
" --tools writes the legacy automatic set (Claude",
|
|
540
535
|
" Code plus any detected Cursor / Copilot VS Code)",
|
|
@@ -563,6 +558,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
563
558
|
" session — it always wins, and there is NO Claude",
|
|
564
559
|
" default. Without it the launch tool is derived",
|
|
565
560
|
" from your selection: Claude Code opens `claude`.",
|
|
561
|
+
" With more than one launchable tool selected, the",
|
|
562
|
+
" first one is used and no question is asked.",
|
|
566
563
|
" Cursor is NOT auto-opened (its cursor-agent CLI",
|
|
567
564
|
" first-run workspace-trust prompt collapses a spawned",
|
|
568
565
|
" session); selecting Cursor writes .cursor/mcp.json",
|
|
@@ -1147,12 +1144,6 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
|
|
|
1147
1144
|
`Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
|
|
1148
1145
|
}
|
|
1149
1146
|
}
|
|
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
1147
|
/**
|
|
1157
1148
|
* Describe the selected notes platforms for the offer copy: a naturally joined
|
|
1158
1149
|
* subject and whether it is plural (so a verb can agree). `platformLabels` are
|
|
@@ -1175,68 +1166,30 @@ function joinLabelsWithAnd(labels) {
|
|
|
1175
1166
|
return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
|
|
1176
1167
|
}
|
|
1177
1168
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
* the agent how to use Bridge's MCP tools — and NAMES the selected platform(s)
|
|
1181
|
-
* rather than the old generic "cross-platform" phrasing.
|
|
1182
|
-
*/
|
|
1183
|
-
export function buildAgentNotesPrompt(platformLabels) {
|
|
1184
|
-
const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
|
|
1185
|
-
return `${INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX}${subject} know${plural ? "" : "s"} how to use the Bridge MCP? Enter adds guidance for ${subject}. [Y/n]: `;
|
|
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).
|
|
1169
|
+
* Decide whether to create or refresh the cross-platform agent notes, if the
|
|
1170
|
+
* selected platforms resolve to at least one writable target (BAPI-1207).
|
|
1214
1171
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1172
|
+
* BAPI-1448: an interactive run consents by default — the question is gone, with
|
|
1173
|
+
* the product sign-off recorded in the install notes. The write is still bounded to
|
|
1174
|
+
* Bridge's marked region (see {@link writeAgentNotes}) and reported afterwards with
|
|
1175
|
+
* a removal hint. A non-interactive run keeps `headless-skip`: it never writes
|
|
1176
|
+
* guidance on its own.
|
|
1177
|
+
*
|
|
1178
|
+
* Entirely best-effort and non-destructive: by the time this runs the install
|
|
1179
|
+
* itself is already complete and durable, so nothing here may fail the run. This
|
|
1180
|
+
* function only resolves the CONSENT decision — the caller performs the actual
|
|
1181
|
+
* write via {@link writeAgentNotes}, so a `--dry-run` (which never reaches this
|
|
1182
|
+
* call) and the live path share exactly one write path.
|
|
1221
1183
|
*/
|
|
1222
|
-
export
|
|
1184
|
+
export function offerAgentGuidanceNotes(deps, targets) {
|
|
1223
1185
|
if (targets.length === 0)
|
|
1224
1186
|
return { kind: "not-applicable" };
|
|
1225
|
-
|
|
1226
|
-
if (!deps.isTTY || !deps.promptGuidanceLine)
|
|
1187
|
+
if (!deps.isTTY)
|
|
1227
1188
|
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
|
-
}
|
|
1189
|
+
return { kind: "consented" };
|
|
1239
1190
|
}
|
|
1191
|
+
/** The one-line removal hint printed after guidance was actually written (BAPI-1448). */
|
|
1192
|
+
export const INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT = "To remove it, delete the lines from <!-- BEGIN BRIDGE MCP GUIDANCE --> to <!-- END BRIDGE MCP GUIDANCE -->.";
|
|
1240
1193
|
/**
|
|
1241
1194
|
* One secret-free summary line for the final install output (BAPI-1207). Never
|
|
1242
1195
|
* reveals guidance content, file bytes, or a credential — only which files
|
|
@@ -1273,6 +1226,47 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
|
|
|
1273
1226
|
parts.push(`failed for ${failed.join(", ")}`);
|
|
1274
1227
|
return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
|
|
1275
1228
|
}
|
|
1229
|
+
const OPTIMIZATION_POSTURE_SUMMARY_LABELS = {
|
|
1230
|
+
quality: "Quality",
|
|
1231
|
+
speed: "Speed",
|
|
1232
|
+
maximize_subscription: "Token Efficiency",
|
|
1233
|
+
};
|
|
1234
|
+
/**
|
|
1235
|
+
* The aligned "defaults Bridge chose" block printed once, right before the launch
|
|
1236
|
+
* consent question. Pure: it formats what the run already knows and performs no I/O.
|
|
1237
|
+
*
|
|
1238
|
+
* Layout: a header, then two rows of two cells. Both rows are indented two spaces and
|
|
1239
|
+
* the left cells are padded to the longer left cell plus three spaces, so the right
|
|
1240
|
+
* cells line up. The Ticket keys cell is omitted entirely when the server supplied no
|
|
1241
|
+
* valid prefix; the prefix is printed as given and never derived locally.
|
|
1242
|
+
*/
|
|
1243
|
+
export function buildInstallBridgeDefaultsSummary(input) {
|
|
1244
|
+
const optimization = input.persistedPosture
|
|
1245
|
+
? OPTIMIZATION_POSTURE_SUMMARY_LABELS[input.persistedPosture]
|
|
1246
|
+
: input.isNewProject
|
|
1247
|
+
? "Quality"
|
|
1248
|
+
: "unchanged";
|
|
1249
|
+
const guidanceLabel = input.claudeCodeSelected ? "Claude Code guidance" : "Agent guidance";
|
|
1250
|
+
const guidanceValue = input.guidance.written.length > 0
|
|
1251
|
+
? `added to ${input.guidance.written.join(", ")}`
|
|
1252
|
+
: input.guidance.unchanged.length > 0
|
|
1253
|
+
? "already up to date"
|
|
1254
|
+
: "not written";
|
|
1255
|
+
const leftTop = `Project name: ${input.displayName}`;
|
|
1256
|
+
const leftBottom = `Optimization: ${optimization}`;
|
|
1257
|
+
const rightTop = input.ticketPrefix
|
|
1258
|
+
? `Ticket keys: ${input.ticketPrefix}-1, ${input.ticketPrefix}-2, …`
|
|
1259
|
+
: undefined;
|
|
1260
|
+
const rightBottom = `${guidanceLabel}: ${guidanceValue}`;
|
|
1261
|
+
const width = Math.max(leftTop.length, leftBottom.length) + 3;
|
|
1262
|
+
const rowTop = rightTop === undefined ? ` ${leftTop}` : ` ${leftTop.padEnd(width)}${rightTop}`;
|
|
1263
|
+
const rowBottom = ` ${leftBottom.padEnd(width)}${rightBottom}`;
|
|
1264
|
+
return [
|
|
1265
|
+
"Bridge chose these defaults — change them later on the Setup page, or ask your agent:",
|
|
1266
|
+
rowTop,
|
|
1267
|
+
rowBottom,
|
|
1268
|
+
].join("\n");
|
|
1269
|
+
}
|
|
1276
1270
|
/**
|
|
1277
1271
|
* Echoed single-line prompt on stderr (used for repo confirmation / value).
|
|
1278
1272
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
@@ -1296,56 +1290,6 @@ export function promptLineViaReadline(promptText, input = process.stdin, output
|
|
|
1296
1290
|
});
|
|
1297
1291
|
});
|
|
1298
1292
|
}
|
|
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
1293
|
/**
|
|
1350
1294
|
* Default pre-warm spawner (BAPI-451 W3). Spawns array-based (`shell: false`) so
|
|
1351
1295
|
* no argument is ever shell-interpreted, ignores all child stdio (node-gyp /
|
|
@@ -1461,8 +1405,9 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
1461
1405
|
randomBytes: (size) => cryptoRandomBytes(size),
|
|
1462
1406
|
promptSecret: isTTY ? promptSecretViaReadline : undefined,
|
|
1463
1407
|
promptLine: isTTY ? promptLineViaReadline : undefined,
|
|
1464
|
-
|
|
1465
|
-
|
|
1408
|
+
promptMultiSelect: isTTY
|
|
1409
|
+
? (promptText, options, preselected) => promptMultiSelectViaReadline(promptText, options, process.stdin, process.stderr, preselected)
|
|
1410
|
+
: undefined,
|
|
1466
1411
|
vendor: createDefaultVendorProcessDeps(spawn),
|
|
1467
1412
|
fetch: productionFetch,
|
|
1468
1413
|
resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
|
|
@@ -2148,7 +2093,7 @@ export function buildInstallBridgeMaintenanceHint() {
|
|
|
2148
2093
|
"Persistent host registrations remain pinned until an explicit upgrade; the editor may need to reconnect to use the new server.",
|
|
2149
2094
|
];
|
|
2150
2095
|
}
|
|
2151
|
-
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2096
|
+
export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL, onboarding) {
|
|
2152
2097
|
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
2153
2098
|
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
2154
2099
|
// so an unattended run fails before any request is made, no verification email
|
|
@@ -2159,6 +2104,13 @@ export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_B
|
|
|
2159
2104
|
if (!deps.isTTY || !deps.promptLine) {
|
|
2160
2105
|
return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
|
|
2161
2106
|
}
|
|
2107
|
+
// BAPI-1448: an email already answered at the merged entry prompt is consumed the
|
|
2108
|
+
// way `--email` is, with no second question and no second privacy notice.
|
|
2109
|
+
if (onboarding) {
|
|
2110
|
+
if (!onboarding.privacyNoticeShown)
|
|
2111
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2112
|
+
return { ok: true, value: onboarding.email };
|
|
2113
|
+
}
|
|
2162
2114
|
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2163
2115
|
if (typeof options.email === "string" && options.email.trim().length > 0) {
|
|
2164
2116
|
return { ok: true, value: options.email.trim() };
|
|
@@ -2222,49 +2174,24 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
2222
2174
|
return { kind: "have-key" };
|
|
2223
2175
|
}
|
|
2224
2176
|
/**
|
|
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.
|
|
2239
|
-
*/
|
|
2240
|
-
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
|
|
2241
|
-
/**
|
|
2242
|
-
* The question's numbered options, in render order. Index + 1 is the accepted
|
|
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.
|
|
2177
|
+
* The bare-TTY entry prompt (BAPI-1448). One question replaces the BAPI-708 "do you
|
|
2178
|
+
* have a token yet? 1/2" chooser: a typed email selects self-serve signup, and a
|
|
2179
|
+
* blank answer selects the have-key branch, whose value is read ONLY through the
|
|
2180
|
+
* hidden `Paste your Bridge token (input hidden): ` prompt downstream.
|
|
2249
2181
|
*/
|
|
2250
|
-
export const
|
|
2251
|
-
|
|
2252
|
-
|
|
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.";
|
|
2182
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT = "Email (or press Enter if you already have a token): ";
|
|
2183
|
+
/** The one hint line printed after a non-blank malformed answer, before the single re-prompt. */
|
|
2184
|
+
export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT = "Enter an email address, or press Enter if you already have a token.";
|
|
2258
2185
|
/**
|
|
2259
2186
|
* The one place the "no usable credential" copy lives (BAPI-667, U1-3).
|
|
2260
2187
|
*
|
|
2261
|
-
* Centralized because the
|
|
2188
|
+
* Centralized because the entry-prompt failure and {@link resolveApiKey}'s two failure
|
|
2262
2189
|
* branches previously drifted: only one of them mentioned self-serve, and one
|
|
2263
2190
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
2264
2191
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
2265
2192
|
*/
|
|
2266
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run
|
|
2267
|
-
"already have
|
|
2193
|
+
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run, type your email to sign up, or press Enter if you " +
|
|
2194
|
+
"already have a token (an invite or an API key — either one is accepted at the hidden prompt). " +
|
|
2268
2195
|
"Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
2269
2196
|
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
2270
2197
|
/**
|
|
@@ -2312,7 +2239,7 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare int
|
|
|
2312
2239
|
* key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
|
|
2313
2240
|
* two.
|
|
2314
2241
|
*/
|
|
2315
|
-
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
|
|
2242
|
+
export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
2316
2243
|
const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
|
|
2317
2244
|
// An explicit need-key intent is already unambiguous — never re-ask it.
|
|
2318
2245
|
if (branch.kind === "need-key")
|
|
@@ -2324,38 +2251,35 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
2324
2251
|
}
|
|
2325
2252
|
const promptLine = deps.promptLine;
|
|
2326
2253
|
try {
|
|
2327
|
-
//
|
|
2328
|
-
|
|
2329
|
-
deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);
|
|
2330
|
-
for (const choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)
|
|
2331
|
-
deps.log(choice);
|
|
2254
|
+
// The answer may be an email, so the privacy notice comes first and prints once.
|
|
2255
|
+
deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
|
|
2332
2256
|
// Bounded to two reads total: the first answer, then ONE re-prompt after a
|
|
2333
2257
|
// single hint line. EOF resolves as an empty answer (promptLineViaReadline
|
|
2334
|
-
// resolves "" on close),
|
|
2335
|
-
// it can never hang or silently select a branch.
|
|
2258
|
+
// resolves "" on close), which selects the have-key branch rather than hanging.
|
|
2336
2259
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2337
|
-
const answer = (await promptLine(
|
|
2338
|
-
//
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
|
|
2260
|
+
const answer = String((await promptLine(INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT)) ?? "").trim();
|
|
2261
|
+
// Blank (or EOF): the user has a token; it is read only by the hidden prompt.
|
|
2262
|
+
if (answer.length === 0) {
|
|
2263
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2264
|
+
}
|
|
2265
|
+
// A typed value is accepted ONLY as a signup email. It is never classified as a
|
|
2266
|
+
// credential, logged, or persisted here; a token-shaped value fails the shape
|
|
2267
|
+
// check and gets the hint.
|
|
2268
|
+
if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(answer)) {
|
|
2345
2269
|
return {
|
|
2346
2270
|
ok: true,
|
|
2347
2271
|
branch: { kind: "need-key", method: "self-serve" },
|
|
2348
2272
|
credentialIntent: "inferred",
|
|
2273
|
+
onboardingEmail: answer,
|
|
2274
|
+
privacyNoticeShown: true,
|
|
2349
2275
|
};
|
|
2350
2276
|
}
|
|
2351
|
-
if (answer === "2")
|
|
2352
|
-
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
2353
2277
|
if (attempt === 0)
|
|
2354
|
-
deps.log(
|
|
2278
|
+
deps.log(INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT);
|
|
2355
2279
|
}
|
|
2356
2280
|
return {
|
|
2357
2281
|
ok: false,
|
|
2358
|
-
error: `No
|
|
2282
|
+
error: `No email or token choice was made. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
|
|
2359
2283
|
};
|
|
2360
2284
|
}
|
|
2361
2285
|
catch {
|
|
@@ -2464,22 +2388,31 @@ const SELECTION_TOKEN_PATTERN = /^[1-9][0-9]*$/;
|
|
|
2464
2388
|
* Interactive single-shot numbered picker on stderr (TTY only). Renders the
|
|
2465
2389
|
* heading and each option exactly once, then accepts ONE strict
|
|
2466
2390
|
* comma-separated line of one-based numbers and resolves immediately — there
|
|
2467
|
-
* is no
|
|
2391
|
+
* is no toggle state and no separate confirmation step (BAPI-663).
|
|
2392
|
+
*
|
|
2393
|
+
* BAPI-1448: when `preselected` names at least one offered option, those rows
|
|
2394
|
+
* render as `[x]` (the rest as `[ ]`) and a blank line accepts the ticked set.
|
|
2395
|
+
* Typed numbers REPLACE the ticked set; they never toggle it. With nothing
|
|
2396
|
+
* pre-ticked the original behaviour is unchanged: a blank line is rejected with
|
|
2397
|
+
* "Select at least one tool."
|
|
2468
2398
|
*
|
|
2469
|
-
* A
|
|
2470
|
-
*
|
|
2471
|
-
*
|
|
2472
|
-
* re-
|
|
2473
|
-
* deduplicated and returned in `options` order regardless of entry order.
|
|
2399
|
+
* A line containing any invalid token (non-numeric, zero, signed,
|
|
2400
|
+
* leading-zero, decimal, out-of-range) is rejected atomically — the whole line
|
|
2401
|
+
* is discarded, one error line is printed, and the picker re-prompts without
|
|
2402
|
+
* re-rendering the option list, so the ticks stay as first shown. A valid line
|
|
2403
|
+
* is deduplicated and returned in `options` order regardless of entry order.
|
|
2474
2404
|
*
|
|
2475
2405
|
* Resolves to `[]` on EOF / synchronous close before any line has been
|
|
2476
2406
|
* committed, so a readline regression can never hang. Returns `[]`
|
|
2477
2407
|
* immediately, without creating a readline interface, when `options` is
|
|
2478
2408
|
* empty.
|
|
2479
2409
|
*/
|
|
2480
|
-
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr) {
|
|
2410
|
+
export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr, preselected = []) {
|
|
2481
2411
|
if (options.length === 0)
|
|
2482
2412
|
return Promise.resolve([]);
|
|
2413
|
+
// Canonical option order, restricted to options actually offered.
|
|
2414
|
+
const ticked = options.filter((o) => preselected.includes(o.id)).map((o) => o.id);
|
|
2415
|
+
const hasTicks = ticked.length > 0;
|
|
2483
2416
|
return new Promise((resolve) => {
|
|
2484
2417
|
const rl = readline.createInterface({ input, output });
|
|
2485
2418
|
let settled = false;
|
|
@@ -2496,13 +2429,20 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
2496
2429
|
rl.on("close", () => finish([]));
|
|
2497
2430
|
output.write(`\n${promptText}\n`);
|
|
2498
2431
|
options.forEach((opt, idx) => {
|
|
2499
|
-
|
|
2432
|
+
const box = hasTicks ? `${ticked.includes(opt.id) ? "[x]" : "[ ]"} ` : "";
|
|
2433
|
+
output.write(` ${idx + 1}. ${box}${opt.label}\n`);
|
|
2500
2434
|
});
|
|
2501
|
-
output.write(
|
|
2435
|
+
output.write(hasTicks
|
|
2436
|
+
? "Press Enter to use the ticked tools, or type the numbers you use (e.g. 1,3).\n"
|
|
2437
|
+
: "Enter the number(s) of the tools you use (e.g. 1,3), then Enter.\n");
|
|
2502
2438
|
const prompt = () => {
|
|
2503
2439
|
rl.question("> ", (answer) => {
|
|
2504
2440
|
const trimmed = answer.trim();
|
|
2505
2441
|
if (trimmed.length === 0) {
|
|
2442
|
+
if (hasTicks) {
|
|
2443
|
+
finish(ticked);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2506
2446
|
output.write("Select at least one tool.\n");
|
|
2507
2447
|
prompt();
|
|
2508
2448
|
return;
|
|
@@ -2553,10 +2493,6 @@ export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
|
|
|
2553
2493
|
const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
|
|
2554
2494
|
return mapped ? `${mapped} subscription` : "coding subscription";
|
|
2555
2495
|
}
|
|
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
2496
|
/** The neutral single-line correction reprompted for any malformed answer. */
|
|
2561
2497
|
const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
2562
2498
|
/**
|
|
@@ -2572,20 +2508,21 @@ const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
|
|
|
2572
2508
|
const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
|
|
2573
2509
|
/**
|
|
2574
2510
|
* Render the one-time posture menu and its input instruction. Exactly three
|
|
2575
|
-
* vertically ordered numbered options; option three
|
|
2576
|
-
* wrap-safe text
|
|
2511
|
+
* vertically ordered numbered options; option three names the user's own
|
|
2512
|
+
* subscription. Plain, wrap-safe text (every line within 80 columns) — no
|
|
2513
|
+
* color-only state, icons, or decorative framing.
|
|
2577
2514
|
*/
|
|
2578
2515
|
function buildOptimizationPostureMenu(primaryPlatform) {
|
|
2579
|
-
return ("How should Bridge
|
|
2580
|
-
" 1. Quality —
|
|
2581
|
-
"
|
|
2582
|
-
|
|
2583
|
-
"
|
|
2584
|
-
"
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
"
|
|
2588
|
-
|
|
2516
|
+
return ("How should Bridge balance quality and cost on this project?\n" +
|
|
2517
|
+
" 1. Quality — Bridge plans and reviews each ticket in full depth; slowest and\n" +
|
|
2518
|
+
" uses the most credits, but catches the most problems.\n" +
|
|
2519
|
+
" 2. Speed — Bridge plans in one pass and reviews once; faster and cheaper, but\n" +
|
|
2520
|
+
" may miss things on some tickets.\n" +
|
|
2521
|
+
" 3. Token Efficiency — Bridge will use Speed settings and hand simpler tickets\n" +
|
|
2522
|
+
" to smaller models more often, which stretches your\n" +
|
|
2523
|
+
` ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} the furthest.\n` +
|
|
2524
|
+
"Press Enter to keep Quality (or your saved choice).\n" +
|
|
2525
|
+
"Enter 1, 2, or 3: ");
|
|
2589
2526
|
}
|
|
2590
2527
|
/**
|
|
2591
2528
|
* The purpose-built, strict single-select optimization-posture picker
|
|
@@ -2675,7 +2612,7 @@ function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
|
|
|
2675
2612
|
}
|
|
2676
2613
|
/** Printed immediately after a successful posture write, on its own line. */
|
|
2677
2614
|
const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
|
|
2678
|
-
const OPTIMIZATION_POSTURE_REMEDIATION = "
|
|
2615
|
+
const OPTIMIZATION_POSTURE_REMEDIATION = "Change it later by running /teach-bridge in your agent.";
|
|
2679
2616
|
/**
|
|
2680
2617
|
* The generic, secret-free fail-open message shared by every non-success,
|
|
2681
2618
|
* non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
|
|
@@ -2709,8 +2646,9 @@ function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
|
|
|
2709
2646
|
* Resolve the selected host platforms with strict precedence (BAPI-635):
|
|
2710
2647
|
* 1. explicit `--tools` (including an explicit EMPTY selection — never falls
|
|
2711
2648
|
* back to detection),
|
|
2712
|
-
* 2. interactive multi-select on a TTY
|
|
2713
|
-
*
|
|
2649
|
+
* 2. interactive multi-select on a TTY, pre-ticked with the detected tools
|
|
2650
|
+
* (BAPI-1448 reverses the BAPI-635 AC-2 empty picker; with nothing detected
|
|
2651
|
+
* the picker still requires an explicit ≥1 selection),
|
|
2714
2652
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
2715
2653
|
* detected Copilot VS Code and Cursor automatic targets (never Codex just
|
|
2716
2654
|
* because a global directory exists).
|
|
@@ -2720,11 +2658,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
|
|
|
2720
2658
|
if (options.tools !== undefined) {
|
|
2721
2659
|
return options.tools;
|
|
2722
2660
|
}
|
|
2723
|
-
// 2. Interactive single-shot numbered picker on a TTY
|
|
2724
|
-
//
|
|
2661
|
+
// 2. Interactive single-shot numbered picker on a TTY, seeded with the locally
|
|
2662
|
+
// detected tools (BAPI-1448): Enter accepts them, typed numbers replace them.
|
|
2725
2663
|
if (deps.isTTY && deps.promptMultiSelect) {
|
|
2726
2664
|
const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
|
|
2727
|
-
const
|
|
2665
|
+
const detected = detectDefaultPlatforms(await buildDetectionContext(deps));
|
|
2666
|
+
const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, detected);
|
|
2728
2667
|
return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
|
|
2729
2668
|
}
|
|
2730
2669
|
// 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
|
|
@@ -2842,42 +2781,6 @@ export function toolLabelForLaunchAgent(agent) {
|
|
|
2842
2781
|
const target = allHostTargets().find((t) => t.launchAgent === agent);
|
|
2843
2782
|
return target?.label ?? UNKNOWN_LAUNCH_TOOL_LABEL;
|
|
2844
2783
|
}
|
|
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
2784
|
/**
|
|
2882
2785
|
* Shared, copy-pasteable "we didn't open a session for you" guidance so every
|
|
2883
2786
|
* no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
|
|
@@ -4128,6 +4031,27 @@ export function buildSelfServeMintUrl(baseUrl) {
|
|
|
4128
4031
|
export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
|
|
4129
4032
|
/** The one prompt label. The E2E harness and the static contract both pin it. */
|
|
4130
4033
|
export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
|
|
4034
|
+
/**
|
|
4035
|
+
* The address verification codes are sent from. Mirrors the server's
|
|
4036
|
+
* `DEFAULT_FROM_ADDRESS` in `api/library/email/provider.py`; keep the two in step.
|
|
4037
|
+
*/
|
|
4038
|
+
export const INSTALL_BRIDGE_VERIFICATION_SENDER = "noreply@bridgegpt-api.com";
|
|
4039
|
+
/**
|
|
4040
|
+
* The shared lead-in printed immediately before every self-serve
|
|
4041
|
+
* {@link promptForVerificationCode} call (modern and legacy paths, fresh or resumed):
|
|
4042
|
+
* three unindented lines, then a blank line. It prints neither the prompt nor any
|
|
4043
|
+
* expiry or time-left text.
|
|
4044
|
+
*
|
|
4045
|
+
* BAPI-1449 (verification recovery) builds on this lead-in instead of adding its
|
|
4046
|
+
* own, and may later return the sender from the request endpoint rather than using
|
|
4047
|
+
* {@link INSTALL_BRIDGE_VERIFICATION_SENDER}.
|
|
4048
|
+
*/
|
|
4049
|
+
export function logVerificationCodeLeadIn(log, email) {
|
|
4050
|
+
log(`We emailed a ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to ${maskSignupEmail(email)}.`);
|
|
4051
|
+
log(`It usually arrives within two minutes. It comes from ${INSTALL_BRIDGE_VERIFICATION_SENDER} — check your`);
|
|
4052
|
+
log("spam or junk folder if you don't see it.");
|
|
4053
|
+
log("");
|
|
4054
|
+
}
|
|
4131
4055
|
/** Shown when a stored, unexpired challenge is picked up on a later run. */
|
|
4132
4056
|
export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
|
|
4133
4057
|
/**
|
|
@@ -4898,8 +4822,8 @@ function buildDeferredLaunchStepPreview() {
|
|
|
4898
4822
|
/**
|
|
4899
4823
|
* The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
|
|
4900
4824
|
* prompts or writes; when the selection resolves to at least one writable
|
|
4901
|
-
* notes target this states plainly that a real interactive run
|
|
4902
|
-
*
|
|
4825
|
+
* notes target this states plainly that a real interactive run writes it
|
|
4826
|
+
* without asking. Empty when the selection resolves to
|
|
4903
4827
|
* no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
|
|
4904
4828
|
* lines are unconditional but this offer's applicability is not.
|
|
4905
4829
|
*/
|
|
@@ -4914,10 +4838,10 @@ function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
|
|
|
4914
4838
|
"Step 4c — reached only after the Step 4b VCS connect offer has completed or been",
|
|
4915
4839
|
" explicitly deferred (a declined or incomplete connect is announced first,",
|
|
4916
4840
|
" 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
|
-
"
|
|
4841
|
+
`Step 4c — Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
|
|
4842
|
+
` terminal is interactive, a real run writes guidance so ${subject} know${plural ? "" : "s"} when to reach`,
|
|
4843
|
+
` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}, without asking,`,
|
|
4844
|
+
" then prints how to remove it. Existing content outside that region is preserved. No notes files are",
|
|
4921
4845
|
" written in --dry-run.",
|
|
4922
4846
|
];
|
|
4923
4847
|
}
|
|
@@ -5395,10 +5319,6 @@ export function isMinttyNonInteractiveInstall(platform, env, isTTY) {
|
|
|
5395
5319
|
}
|
|
5396
5320
|
export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
5397
5321
|
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
5322
|
// When the caller overrode only `fetch` (the common test seam), bind the
|
|
5403
5323
|
// default resolver to the FINAL merged fetch so server resolution still routes
|
|
5404
5324
|
// through the override. An explicit `resolveRepoViaServer` override is honored.
|
|
@@ -5749,7 +5669,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5749
5669
|
// The selection is pure and deterministic for every explicit, env-driven, and
|
|
5750
5670
|
// non-TTY invocation; only a BARE interactive run is asked which branch it wants
|
|
5751
5671
|
// (BAPI-626 — otherwise a first-time user can never reach self-serve).
|
|
5752
|
-
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
|
|
5672
|
+
const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl);
|
|
5753
5673
|
if (!branchResult.ok) {
|
|
5754
5674
|
errorLog(`Error: ${branchResult.error}`);
|
|
5755
5675
|
return 1;
|
|
@@ -5791,7 +5711,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5791
5711
|
// key-authentication ping, so there is no source to attribute.
|
|
5792
5712
|
let apiKeySource;
|
|
5793
5713
|
if (selfServeSignupMode) {
|
|
5794
|
-
const emailResult = await resolveSignupEmail(options, deps, baseUrl
|
|
5714
|
+
const emailResult = await resolveSignupEmail(options, deps, baseUrl, branchResult.onboardingEmail
|
|
5715
|
+
? {
|
|
5716
|
+
email: branchResult.onboardingEmail,
|
|
5717
|
+
privacyNoticeShown: branchResult.privacyNoticeShown === true,
|
|
5718
|
+
}
|
|
5719
|
+
: undefined);
|
|
5795
5720
|
if (!emailResult.ok) {
|
|
5796
5721
|
errorLog(`Error: ${emailResult.error}`);
|
|
5797
5722
|
return 1;
|
|
@@ -6137,10 +6062,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6137
6062
|
return 0;
|
|
6138
6063
|
}
|
|
6139
6064
|
// ---- Finalize the launch agent for spawn-capable decisions ----
|
|
6140
|
-
// choose-one:
|
|
6141
|
-
//
|
|
6142
|
-
//
|
|
6143
|
-
//
|
|
6065
|
+
// choose-one: on a TTY, take the first launchable agent the user selected and say so
|
|
6066
|
+
// (BAPI-1448 retired the launch-agent chooser; `--agent` still wins
|
|
6067
|
+
// upstream in the pure launch decision). A non-TTY run degrades to the manual
|
|
6068
|
+
// continuation (finalAgentName stays null).
|
|
6144
6069
|
let finalAgentName = null;
|
|
6145
6070
|
let finalSpawnCommand = null;
|
|
6146
6071
|
if (planLaunch.kind === "spawn") {
|
|
@@ -6148,8 +6073,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6148
6073
|
finalSpawnCommand = planLaunch.spawnCommand;
|
|
6149
6074
|
}
|
|
6150
6075
|
else if (planLaunch.kind === "choose-one") {
|
|
6151
|
-
const chosen =
|
|
6076
|
+
const chosen = deps.isTTY ? (planLaunch.agents[0] ?? null) : null;
|
|
6152
6077
|
if (chosen) {
|
|
6078
|
+
log(`Using ${toolLabelForLaunchAgent(chosen)} for the setup session (the first tool you selected). Pass --agent to choose another.`);
|
|
6153
6079
|
const spec = resolveAgentSpec(chosen);
|
|
6154
6080
|
if (!spec) {
|
|
6155
6081
|
errorLog(`Error: no launch agent is registered for '${chosen}'.`);
|
|
@@ -6159,7 +6085,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6159
6085
|
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
6160
6086
|
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
|
|
6161
6087
|
}
|
|
6162
|
-
// chosen === null → non-TTY
|
|
6088
|
+
// chosen === null → non-TTY → manual continuation below.
|
|
6163
6089
|
}
|
|
6164
6090
|
// planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
|
|
6165
6091
|
// ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
|
|
@@ -6348,7 +6274,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6348
6274
|
challengeExpiresAt = liveChallenge.expiresAt;
|
|
6349
6275
|
challengeEmail = liveChallenge.email;
|
|
6350
6276
|
log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
|
|
6351
|
-
log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6352
6277
|
}
|
|
6353
6278
|
else {
|
|
6354
6279
|
const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
|
|
@@ -6408,12 +6333,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6408
6333
|
"No workspace has been set up — fix the problem and re-run.");
|
|
6409
6334
|
return false;
|
|
6410
6335
|
}
|
|
6411
|
-
log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
|
|
6412
6336
|
}
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
if (remaining)
|
|
6416
|
-
log(` ${remaining}`);
|
|
6337
|
+
// BAPI-1448: the shared lead-in, fresh or resumed, directly before code entry.
|
|
6338
|
+
logVerificationCodeLeadIn(log, challengeEmail);
|
|
6417
6339
|
// Phase 2, second half, and phase 3. One resend is offered on the recovery
|
|
6418
6340
|
// path only — never proactively — so the budget is spent on the user's
|
|
6419
6341
|
// explicit request rather than by the CLI's own retry logic.
|
|
@@ -6820,9 +6742,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
6820
6742
|
// existing value unconditionally — the old value is never read first. A
|
|
6821
6743
|
// no-answer (or a prompt/stream exception the picker already absorbed)
|
|
6822
6744
|
// reports the same fail-open remediation without ever attempting a write.
|
|
6745
|
+
let persistedPosture;
|
|
6823
6746
|
if (optimizationPostureSelection) {
|
|
6824
6747
|
const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
|
|
6825
6748
|
reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
|
|
6749
|
+
if (postureResult.kind === "success")
|
|
6750
|
+
persistedPosture = optimizationPostureSelection;
|
|
6826
6751
|
}
|
|
6827
6752
|
else if (optimizationPostureEligible) {
|
|
6828
6753
|
log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
|
|
@@ -7106,8 +7031,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7106
7031
|
// selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
|
|
7107
7032
|
// other persisted install-state contract — consent is re-evaluated every run.
|
|
7108
7033
|
const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
|
|
7109
|
-
const
|
|
7110
|
-
const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets, notesPlatformLabels);
|
|
7034
|
+
const notesOffer = offerAgentGuidanceNotes(deps, notesPlan.targets);
|
|
7111
7035
|
let notesResult;
|
|
7112
7036
|
let notesWriteFailed = false;
|
|
7113
7037
|
if (notesOffer.kind === "consented") {
|
|
@@ -7137,10 +7061,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7137
7061
|
const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
|
|
7138
7062
|
if (notesSummaryLine)
|
|
7139
7063
|
log(notesSummaryLine);
|
|
7064
|
+
// The removal hint only when something was actually written (BAPI-1448).
|
|
7065
|
+
const notesWrittenTargets = (notesResult?.files ?? [])
|
|
7066
|
+
.filter((f) => f.status === "written")
|
|
7067
|
+
.map((f) => f.target);
|
|
7068
|
+
if (notesWrittenTargets.length > 0)
|
|
7069
|
+
log(INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT);
|
|
7070
|
+
const notesUnchangedTargets = (notesResult?.files ?? [])
|
|
7071
|
+
.filter((f) => f.status === "skipped")
|
|
7072
|
+
.map((f) => f.target);
|
|
7140
7073
|
// ---- BAPI-1398: Bridge permission rules for /drive-epic-run (Claude Code) ----
|
|
7141
7074
|
// 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
|
|
7075
|
+
// opens. Project rules are written as part of the normal install. The user
|
|
7076
|
+
// settings change is NOT part of install (BAPI-1448): it lives in the
|
|
7077
|
+
// `bridge-permissions` subcommand, offered in-session by /drive-epic-run. Every
|
|
7144
7078
|
// failure here is advisory: a malformed settings file is reported and skipped,
|
|
7145
7079
|
// and the install still completes.
|
|
7146
7080
|
if (selectedPlatforms.includes("claude-code")) {
|
|
@@ -7149,13 +7083,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7149
7083
|
cwd: deps.cwd,
|
|
7150
7084
|
homeDir: deps.homedir(),
|
|
7151
7085
|
env: deps.env,
|
|
7152
|
-
promptLine: deps.isTTY ? deps.promptLine : undefined,
|
|
7153
7086
|
readFile: deps.readFile,
|
|
7154
7087
|
writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
|
|
7155
7088
|
mkdir: (p, o) => deps.mkdir(p, o),
|
|
7156
7089
|
log,
|
|
7157
7090
|
errorLog,
|
|
7158
7091
|
ensureGitignored: (relPath) => ensureGitignoredShared(deps.cwd, relPath, gitignoreDeps),
|
|
7092
|
+
// BAPI-1446: pin the SessionStart upgrade-notice hook to the effective Claude Code pin.
|
|
7093
|
+
sessionStartHook: { pinnedVersion: await discoverClaudeCodeEffectivePin({ cwd: deps.cwd, readFile: deps.readFile, targetVersion: VERSION }) },
|
|
7159
7094
|
});
|
|
7160
7095
|
}
|
|
7161
7096
|
catch {
|
|
@@ -7187,6 +7122,27 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
7187
7122
|
for (const line of buildInstallBridgeMaintenanceHint())
|
|
7188
7123
|
log(line);
|
|
7189
7124
|
await showFirstBuildAdvisory(finalAgentName);
|
|
7125
|
+
// ---- BAPI-1448: the defaults summary, once, right before the launch consent ----
|
|
7126
|
+
// Every input is already in memory except the optional ticket prefix, which is one
|
|
7127
|
+
// bounded read of the manifest the GitHub offer already uses. Any failure there just
|
|
7128
|
+
// omits the cell: the summary never blocks or changes the install's outcome.
|
|
7129
|
+
let ticketPrefix;
|
|
7130
|
+
try {
|
|
7131
|
+
const details = await fetchGithubConfigurationDetails({ fetch: deps.fetch, baseUrl, apiKey }, repoName);
|
|
7132
|
+
ticketPrefix = details.ticketPrefix;
|
|
7133
|
+
}
|
|
7134
|
+
catch {
|
|
7135
|
+
ticketPrefix = undefined;
|
|
7136
|
+
}
|
|
7137
|
+
log("");
|
|
7138
|
+
log(buildInstallBridgeDefaultsSummary({
|
|
7139
|
+
displayName: modernAttempt?.displayName ?? repoName,
|
|
7140
|
+
...(ticketPrefix ? { ticketPrefix } : {}),
|
|
7141
|
+
isNewProject: Boolean(modernAttempt) || bootstrapExchangeSucceeded,
|
|
7142
|
+
...(persistedPosture ? { persistedPosture } : {}),
|
|
7143
|
+
claudeCodeSelected: selectedPlatforms.includes("claude-code"),
|
|
7144
|
+
guidance: { written: notesWrittenTargets, unchanged: notesUnchangedTargets },
|
|
7145
|
+
}));
|
|
7190
7146
|
// ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
|
|
7191
7147
|
// AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
|
|
7192
7148
|
// resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
|
|
@@ -7326,13 +7282,19 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
|
|
|
7326
7282
|
if (label === undefined) {
|
|
7327
7283
|
if (!deps.isTTY || !deps.promptLine)
|
|
7328
7284
|
throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
|
|
7285
|
+
// BAPI-1448: a valid folder name is the label, with no question. The prompt
|
|
7286
|
+
// survives only for a folder name the validator rejects.
|
|
7329
7287
|
let suggestion = "";
|
|
7330
7288
|
try {
|
|
7331
7289
|
suggestion = validateBootstrapDisplayName(path.basename(folder));
|
|
7332
7290
|
}
|
|
7333
7291
|
catch { /* explicit label required */ }
|
|
7334
|
-
|
|
7335
|
-
|
|
7292
|
+
if (suggestion) {
|
|
7293
|
+
label = suggestion;
|
|
7294
|
+
}
|
|
7295
|
+
else {
|
|
7296
|
+
label = validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
|
|
7297
|
+
}
|
|
7336
7298
|
}
|
|
7337
7299
|
if (selfServe) {
|
|
7338
7300
|
if (!deps.isTTY || !deps.promptLine)
|
|
@@ -7364,6 +7326,7 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
|
|
|
7364
7326
|
throw new ModernInstallRecoveryError("The previous signup response is unresolved. Preserve proof and contact installation support before restarting.");
|
|
7365
7327
|
if (Date.parse(record.challengeExpiresAt) <= deps.now())
|
|
7366
7328
|
throw new ModernInstallRecoveryError("The saved verification challenge expired. Preserve proof and contact installation support.");
|
|
7329
|
+
logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
|
|
7367
7330
|
const code = await promptForVerificationCode(deps, deps.log);
|
|
7368
7331
|
if (!code)
|
|
7369
7332
|
throw new ModernInstallRecoveryError("Verification incomplete. Resume the same signup from this folder.");
|