@bridge_gpt/mcp-server 0.2.67 → 0.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -51
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/http-client.js +43 -4
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1109 -695
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
|
@@ -87,6 +87,25 @@ function asString(value) {
|
|
|
87
87
|
function asNullableString(value) {
|
|
88
88
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
89
89
|
}
|
|
90
|
+
/** Same unambiguous alphabet the server draws its codes and check values from. */
|
|
91
|
+
const APPROVAL_CHECK_RE = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4,8}$/;
|
|
92
|
+
/**
|
|
93
|
+
* Decide the mint's mode defensively (BAPI-1401). Approval mode needs BOTH an explicit
|
|
94
|
+
* `approval_mode: "approve"` and a well-formed check value; any other combination is
|
|
95
|
+
* code mode, so a partial or unexpected response can never strand the user without the
|
|
96
|
+
* code they would need.
|
|
97
|
+
*/
|
|
98
|
+
function parseApprovalMode(body) {
|
|
99
|
+
const check = typeof body.approval_check === "string" ? body.approval_check.trim() : "";
|
|
100
|
+
if (body.approval_mode === "approve" && APPROVAL_CHECK_RE.test(check)) {
|
|
101
|
+
return { approvalMode: "approve", approvalCheck: check };
|
|
102
|
+
}
|
|
103
|
+
return { approvalMode: "code", approvalCheck: null };
|
|
104
|
+
}
|
|
105
|
+
/** Parse the optional, additive `reason` on a status body. Unknown → null. */
|
|
106
|
+
function parseStatusReason(value) {
|
|
107
|
+
return value === "declined" ? "declined" : null;
|
|
108
|
+
}
|
|
90
109
|
// ---------------------------------------------------------------------------
|
|
91
110
|
// Retry-After
|
|
92
111
|
// ---------------------------------------------------------------------------
|
|
@@ -121,13 +140,23 @@ export function parseRetryAfterMs(header, remainingMs, nowMs) {
|
|
|
121
140
|
// ---------------------------------------------------------------------------
|
|
122
141
|
// Endpoints
|
|
123
142
|
// ---------------------------------------------------------------------------
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Mint a CLI-origin connection code and its Forge install-page URL.
|
|
145
|
+
*
|
|
146
|
+
* BAPI-1401: when `workspaceSlug` is given, it is sent as the request's workspace claim,
|
|
147
|
+
* asking the server for an approval-mode connection request. Without it the request body
|
|
148
|
+
* is exactly the legacy shape. Either way the response decides the mode.
|
|
149
|
+
*/
|
|
150
|
+
export async function mintBitbucketConnection(deps, repoName, workspaceSlug) {
|
|
151
|
+
const payload = { repo_name: repoName };
|
|
152
|
+
if (workspaceSlug)
|
|
153
|
+
payload.workspace_slug = workspaceSlug;
|
|
154
|
+
const res = await postJson(deps, "/setup/bitbucket/cli/connection-code", payload);
|
|
129
155
|
if (!res.ok)
|
|
130
156
|
return res;
|
|
157
|
+
if (res.value.status === 429) {
|
|
158
|
+
return { ok: false, kind: "too-many-pending" };
|
|
159
|
+
}
|
|
131
160
|
if (res.value.status !== 200) {
|
|
132
161
|
return { ok: false, kind: classifyStatus(res.value.status) };
|
|
133
162
|
}
|
|
@@ -143,7 +172,11 @@ export async function mintBitbucketConnection(deps, repoName) {
|
|
|
143
172
|
// uses. Required-field strictness above is untouched — an optional field must not
|
|
144
173
|
// become a new way for a server change to fail a mint.
|
|
145
174
|
const redeemUrlTemplate = asNullableString(body.redeem_url_template);
|
|
146
|
-
|
|
175
|
+
const { approvalMode, approvalCheck } = parseApprovalMode(body);
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
value: { state, installUrl, ttlSeconds, redeemUrlTemplate, approvalMode, approvalCheck },
|
|
179
|
+
};
|
|
147
180
|
}
|
|
148
181
|
/** Strictly parse candidates. A candidate missing a usable identity fails the whole list. */
|
|
149
182
|
function parseCandidates(value) {
|
|
@@ -198,6 +231,7 @@ export async function fetchBitbucketConnectionStatus(deps, repoName, state, nowM
|
|
|
198
231
|
status: status,
|
|
199
232
|
candidates,
|
|
200
233
|
repoDisplayName: asNullableString(body.repo_display_name),
|
|
234
|
+
reason: parseStatusReason(body.reason),
|
|
201
235
|
// A single read has no deadline of its own to clamp against; the poller applies
|
|
202
236
|
// its own bound. POLL_DEADLINE_MS is the widest a caller could honor.
|
|
203
237
|
retryAfterMs: parseRetryAfterMs(res.value.retryAfter, POLL_DEADLINE_MS, nowMs),
|
|
@@ -348,6 +382,7 @@ export async function pollBitbucketConnection(deps, poll, repoName, state) {
|
|
|
348
382
|
status: typed,
|
|
349
383
|
candidates,
|
|
350
384
|
repoDisplayName: asNullableString(body.repo_display_name),
|
|
385
|
+
reason: parseStatusReason(body.reason),
|
|
351
386
|
retryAfterMs: null,
|
|
352
387
|
},
|
|
353
388
|
};
|
|
@@ -5,20 +5,28 @@
|
|
|
5
5
|
* The whole point of this command is that it needs no Bridge web session: a user who has
|
|
6
6
|
* a Bridge API key can connect Bitbucket without ever opening the setup UI. The flow is
|
|
7
7
|
*
|
|
8
|
-
* mint → browser (install the Forge app on Bitbucket,
|
|
9
|
-
*
|
|
8
|
+
* mint → browser (install the Forge app on Bitbucket, then approve the request on the
|
|
9
|
+
* app's workspace settings page) → poll → pick a repository → confirm
|
|
10
10
|
*
|
|
11
11
|
* and the browser half authenticates to *Bitbucket*, not to Bridge. Bridge never asks
|
|
12
12
|
* for, accepts, echoes, or transports a Bitbucket credential. The only thing crossing
|
|
13
13
|
* between the browser and this process is a Bridge-issued state nonce, held in memory.
|
|
14
14
|
*
|
|
15
|
+
* BAPI-1401 — two modes. **Approval mode** (the default when a Bitbucket workspace is
|
|
16
|
+
* known from `--workspace` or this project's git remote, and the server supports it):
|
|
17
|
+
* the mint is a connection request listed on that workspace's Forge settings page, and a
|
|
18
|
+
* workspace admin clicks Approve on the card showing the check value printed here. No
|
|
19
|
+
* code is printed. **Code mode** (`--use-code`, no workspace, or an older server): the
|
|
20
|
+
* legacy flow — the printed code is pasted into the same settings page.
|
|
21
|
+
*
|
|
15
22
|
* Two properties are load-bearing and easy to erode:
|
|
16
23
|
*
|
|
17
24
|
* 1. **The nonce and install URL are never printed to output the CLI narrates around.**
|
|
18
25
|
* The URL carries the state as a query parameter, so putting it in a log line or an
|
|
19
26
|
* error message would paste a live credential-equivalent into the user's scrollback.
|
|
20
|
-
*
|
|
21
|
-
* the one deliberate exception — see `runBitbucketConnectionFlow`
|
|
27
|
+
* In code mode the printed code shown to the user for pasting into Bitbucket's own
|
|
28
|
+
* settings page is the one deliberate exception — see `runBitbucketConnectionFlow`
|
|
29
|
+
* below. In approval mode it is never printed.
|
|
22
30
|
* 2. **Nothing binds without an explicit human choice.** Even a single-repository
|
|
23
31
|
* installation is confirmed by hand — the server stages candidates and this command
|
|
24
32
|
* asks. There is deliberately no `--yes`.
|
|
@@ -35,25 +43,30 @@ import { DEFAULT_BAPI_BASE_URL } from "./install-bridge.js";
|
|
|
35
43
|
import { validateRepoName } from "./bridge-config.js";
|
|
36
44
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
37
45
|
import { resolveBapiCredentials, } from "./credential-store.js";
|
|
46
|
+
import { defaultGitRemoteRunner, resolveBitbucketWorkspace, } from "./bitbucket-workspace-remote.js";
|
|
38
47
|
import { POLL_DEADLINE_MS, confirmBitbucketConnection, mintBitbucketConnection, pollBitbucketConnection, } from "./connect-bitbucket-api.js";
|
|
39
|
-
const USAGE = `Usage: connect-bitbucket [--repo <repo_name>] [--workspace <slug>]
|
|
48
|
+
const USAGE = `Usage: connect-bitbucket [--repo <repo_name>] [--workspace <slug>] [--use-code]
|
|
40
49
|
|
|
41
50
|
Connect a Bitbucket repository to a Bridge project from your terminal.
|
|
42
51
|
|
|
43
|
-
Opens the Bitbucket Forge app's install page in your browser
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
repository to connect.
|
|
52
|
+
Opens the Bitbucket Forge app's install page in your browser. Install the app,
|
|
53
|
+
then in Bitbucket open Workspace settings → Forge Apps → "Connect Bitbucket to
|
|
54
|
+
Bridge API" and click Approve on the request this command started (it prints a
|
|
55
|
+
short check value so you can tell it apart). That is how you authenticate to
|
|
56
|
+
Bitbucket; Bridge never asks for a Bitbucket token or app password. Once the
|
|
57
|
+
app reports back, this command asks which single repository to connect.
|
|
58
|
+
|
|
59
|
+
Without a known workspace, or with --use-code, it prints a connection code to
|
|
60
|
+
paste on that same page instead.
|
|
49
61
|
|
|
50
62
|
Options:
|
|
51
63
|
--repo <repo_name> Bridge project to connect (inferred from this directory
|
|
52
64
|
when omitted; you will be asked to confirm).
|
|
53
|
-
--workspace <slug> Bitbucket workspace slug.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
--workspace <slug> Bitbucket workspace slug. Inferred from this project's
|
|
66
|
+
origin git remote when omitted. Lists the request on that
|
|
67
|
+
workspace's settings page and prints a direct link to it.
|
|
68
|
+
--use-code Print a connection code to paste instead of approving a
|
|
69
|
+
request.
|
|
57
70
|
--help Show this message.`;
|
|
58
71
|
/**
|
|
59
72
|
* Bitbucket workspace slugs are NOT Bridge project names, so they do not go through
|
|
@@ -94,7 +107,7 @@ export function validateBitbucketWorkspace(raw) {
|
|
|
94
107
|
* resolve only through the shared credential-store seam).
|
|
95
108
|
*/
|
|
96
109
|
export function parseConnectBitbucketArgs(argv) {
|
|
97
|
-
const out = { help: false };
|
|
110
|
+
const out = { useCode: false, help: false };
|
|
98
111
|
for (let i = 0; i < argv.length; i += 1) {
|
|
99
112
|
const arg = argv[i];
|
|
100
113
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -137,6 +150,10 @@ export function parseConnectBitbucketArgs(argv) {
|
|
|
137
150
|
out.workspace = validated.value;
|
|
138
151
|
continue;
|
|
139
152
|
}
|
|
153
|
+
if (arg === "--use-code") {
|
|
154
|
+
out.useCode = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
140
157
|
if (arg === "--yes" || arg === "-y") {
|
|
141
158
|
return {
|
|
142
159
|
ok: false,
|
|
@@ -255,6 +272,7 @@ export function createDefaultConnectBitbucketDeps() {
|
|
|
255
272
|
openBrowser: (url) => defaultOpenBrowser(process.platform, url),
|
|
256
273
|
stdout: (message) => process.stdout.write(`${message}\n`),
|
|
257
274
|
stderr: (message) => process.stderr.write(`${message}\n`),
|
|
275
|
+
runGit: defaultGitRemoteRunner,
|
|
258
276
|
};
|
|
259
277
|
}
|
|
260
278
|
/**
|
|
@@ -313,7 +331,7 @@ export async function openBitbucketInstallPage(deps, installUrl) {
|
|
|
313
331
|
// ---------------------------------------------------------------------------
|
|
314
332
|
const STEPS = [
|
|
315
333
|
"Connect Bitbucket",
|
|
316
|
-
"
|
|
334
|
+
"Install the Bitbucket Forge app and connect it in Bitbucket",
|
|
317
335
|
"Verify connection",
|
|
318
336
|
"Choose repository",
|
|
319
337
|
"Confirm connection",
|
|
@@ -333,7 +351,7 @@ function renderStep(deps, index) {
|
|
|
333
351
|
* must never leave the user with nothing. The words are the app's own Bitbucket
|
|
334
352
|
* navigation, so they stay correct without any Bridge-side configuration at all.
|
|
335
353
|
*/
|
|
336
|
-
export const BITBUCKET_REDEEM_NAVIGATION_PATH = 'Workspace settings → Apps → "Connect Bitbucket to Bridge API"';
|
|
354
|
+
export const BITBUCKET_REDEEM_NAVIGATION_PATH = 'Workspace settings → Forge Apps → "Connect Bitbucket to Bridge API"';
|
|
337
355
|
/** The one `{workspace}` token a redemption template may carry. */
|
|
338
356
|
const WORKSPACE_TOKEN = "{workspace}";
|
|
339
357
|
/**
|
|
@@ -431,6 +449,24 @@ function renderRedemptionGuidance(deps, template, workspace) {
|
|
|
431
449
|
deps.stderr(" Replace {workspace} with your Bitbucket workspace slug.");
|
|
432
450
|
}
|
|
433
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Print what to do in approval mode (BAPI-1401): install, then Approve the request that
|
|
454
|
+
* shows this run's check value. The code is never printed. `firstRequest` is false on a
|
|
455
|
+
* renewal, where the browser is already open and only the new request's check changes.
|
|
456
|
+
*/
|
|
457
|
+
function renderApprovalGuidance(deps, repoName, approvalCheck, template, workspace, firstRequest) {
|
|
458
|
+
if (firstRequest) {
|
|
459
|
+
deps.stderr("Step 1: install the app in the browser window that just opened.");
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
deps.stderr("A new request was created. The app install page is already open in your browser.");
|
|
463
|
+
}
|
|
464
|
+
deps.stderr(`Step 2: in Bitbucket, go to ${BITBUCKET_REDEEM_NAVIGATION_PATH} and click Approve on ` +
|
|
465
|
+
`the request for ${repoName} showing check ${approvalCheck}.`);
|
|
466
|
+
const link = renderBitbucketRedeemLink(template, workspace);
|
|
467
|
+
if (link)
|
|
468
|
+
deps.stderr(` Direct link: ${link}`);
|
|
469
|
+
}
|
|
434
470
|
/** Display identity for a candidate: prefer the unambiguous workspace/repo slug form. */
|
|
435
471
|
function candidateLabel(c) {
|
|
436
472
|
const slugForm = `${c.workspace_slug}/${c.repository_slug}`;
|
|
@@ -443,6 +479,8 @@ function candidateLabel(c) {
|
|
|
443
479
|
const OUTCOME_MESSAGES = {
|
|
444
480
|
expired: "The connection request expired before the Forge app reported back. Run connect-bitbucket again.",
|
|
445
481
|
invalid: "This connection request is no longer valid. Run connect-bitbucket again.",
|
|
482
|
+
// BAPI-1401: an `invalid` status that carries reason "declined".
|
|
483
|
+
declined: "The connection request was declined in Bitbucket. Run connect-bitbucket again to start a new request.",
|
|
446
484
|
"no-repositories": "The Bitbucket Forge app installation did not include any repositories Bridge can " +
|
|
447
485
|
"access. Re-run connect-bitbucket and grant access to at least one repository.",
|
|
448
486
|
};
|
|
@@ -453,9 +491,13 @@ const FAILURE_MESSAGES = {
|
|
|
453
491
|
"not-found": "Bridge does not recognize this project. Check --repo matches your Bridge project name.",
|
|
454
492
|
server: "Bridge API returned an error. Run connect-bitbucket again shortly.",
|
|
455
493
|
malformed: "Bridge API returned an unexpected response. Run connect-bitbucket again shortly.",
|
|
456
|
-
deadline: "Timed out waiting for the Bitbucket Forge app. If you
|
|
457
|
-
"the code, run connect-bitbucket again to pick up the connection.",
|
|
494
|
+
deadline: "Timed out waiting for the Bitbucket Forge app. If you installed the app and approved " +
|
|
495
|
+
"the request (or pasted the code), run connect-bitbucket again to pick up the connection.",
|
|
496
|
+
"too-many-pending": "Too many Bitbucket connection requests are waiting. Approve or decline them in " +
|
|
497
|
+
"Bitbucket, or wait up to 15 minutes for them to expire, then run connect-bitbucket again.",
|
|
458
498
|
};
|
|
499
|
+
/** BAPI-1401: offered after the first approval-mode expiry or deadline. */
|
|
500
|
+
const USE_CODE_FALLBACK_HINT = "Can't see the request? Re-run with --use-code";
|
|
459
501
|
/** Only an explicit, case-insensitive `y`/`yes` approves. One definition, used everywhere. */
|
|
460
502
|
function isAffirmative(answer) {
|
|
461
503
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -548,7 +590,7 @@ async function chooseCandidate(deps, candidates) {
|
|
|
548
590
|
* a renewal can never confirm against a previous code.
|
|
549
591
|
*/
|
|
550
592
|
export async function runBitbucketConnectionFlow(deps, api, repoName, options = {}) {
|
|
551
|
-
const { workspace, initialMintApproved = false } = options;
|
|
593
|
+
const { workspace, useCode = false, initialMintApproved = false } = options;
|
|
552
594
|
renderStep(deps, 0);
|
|
553
595
|
if (!initialMintApproved) {
|
|
554
596
|
// A standalone run asks before it mints: minting is a side effect (a live code, a
|
|
@@ -559,8 +601,11 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
|
|
|
559
601
|
}
|
|
560
602
|
let browserOpened = false;
|
|
561
603
|
let expiryCount = 0;
|
|
604
|
+
let useCodeHintShown = false;
|
|
562
605
|
for (;;) {
|
|
563
|
-
|
|
606
|
+
// BAPI-1401: the workspace is sent as an approval-mode claim unless the user asked
|
|
607
|
+
// for the code flow. The server's response, not this request, decides the mode.
|
|
608
|
+
const minted = await mintBitbucketConnection(api, repoName, useCode ? undefined : workspace);
|
|
564
609
|
if (!minted.ok) {
|
|
565
610
|
// Includes a renewal mint: a failed fresh mint is terminal and resumable, never
|
|
566
611
|
// silently retried. An automatic retry here would spend codes the user cannot see.
|
|
@@ -568,6 +613,13 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
|
|
|
568
613
|
}
|
|
569
614
|
// Iteration-local. Nothing outside this block may read a previous code.
|
|
570
615
|
const connection = minted.value;
|
|
616
|
+
// Approval mode needs a workspace to have been claimed; a server that answers
|
|
617
|
+
// "approve" to a claimless mint is treated as code mode rather than trusted.
|
|
618
|
+
const approval = connection.approvalMode === "approve" &&
|
|
619
|
+
connection.approvalCheck !== null &&
|
|
620
|
+
!useCode &&
|
|
621
|
+
workspace !== undefined;
|
|
622
|
+
const firstRequest = !browserOpened;
|
|
571
623
|
if (!browserOpened) {
|
|
572
624
|
renderStep(deps, 1);
|
|
573
625
|
deps.stderr("Opening Bitbucket…");
|
|
@@ -581,13 +633,20 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
|
|
|
581
633
|
// both noise and a stale destination.
|
|
582
634
|
browserOpened = true;
|
|
583
635
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
636
|
+
if (approval) {
|
|
637
|
+
// BAPI-1401: nothing to paste. `connection.state` stays in memory, used only for
|
|
638
|
+
// polling and confirmation below.
|
|
639
|
+
renderApprovalGuidance(deps, repoName, connection.approvalCheck, connection.redeemUrlTemplate, workspace, firstRequest);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
renderRedemptionGuidance(deps, connection.redeemUrlTemplate, workspace);
|
|
643
|
+
// The code itself is not a bearer credential the way the URL's state parameter is
|
|
644
|
+
// treated elsewhere in this flow — it must be visible so the user can paste it into
|
|
645
|
+
// the installed Forge app's settings page, which is how redemption happens in code
|
|
646
|
+
// mode (there is no GitHub-style browser redirect back to Bridge). This is the
|
|
647
|
+
// single statement in the command that prints a code, and it runs once per mint.
|
|
648
|
+
deps.stderr(`Paste this code there: ${connection.state}`);
|
|
649
|
+
}
|
|
591
650
|
renderStep(deps, 2);
|
|
592
651
|
const minutes = Math.floor(POLL_DEADLINE_MS / 60_000);
|
|
593
652
|
deps.stderr(`Waiting for the Bitbucket Forge app… (up to ~${minutes} minutes)`);
|
|
@@ -599,14 +658,23 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
|
|
|
599
658
|
if (renewable) {
|
|
600
659
|
expiryCount += 1;
|
|
601
660
|
deps.stderr("");
|
|
602
|
-
deps.stderr(
|
|
661
|
+
deps.stderr(approval
|
|
662
|
+
? "That request expired and can no longer be approved."
|
|
663
|
+
: "That code expired and can no longer be used.");
|
|
664
|
+
if (approval && !useCodeHintShown) {
|
|
665
|
+
deps.stderr(USE_CODE_FALLBACK_HINT);
|
|
666
|
+
useCodeHintShown = true;
|
|
667
|
+
}
|
|
603
668
|
if (expiryCount > 1) {
|
|
604
669
|
// Said only after it has happened more than once — by then the likely cause is
|
|
605
670
|
// not slowness but a wrong page or an app that was never installed.
|
|
606
671
|
deps.stderr("If this keeps happening, check that the Forge app is installed and that you " +
|
|
607
672
|
"have the right workspace's settings page open.");
|
|
608
673
|
}
|
|
609
|
-
|
|
674
|
+
const renewQuestion = approval
|
|
675
|
+
? "Create a fresh request and keep waiting? [y/N]: "
|
|
676
|
+
: "Issue a fresh code and keep waiting? [y/N]: ";
|
|
677
|
+
if (await confirm(deps, renewQuestion)) {
|
|
610
678
|
continue;
|
|
611
679
|
}
|
|
612
680
|
return reportNoConnection(deps, polled.ok ? OUTCOME_MESSAGES.expired : FAILURE_MESSAGES.deadline, repoName, "incomplete");
|
|
@@ -624,7 +692,8 @@ export async function runBitbucketConnectionFlow(deps, api, repoName, options =
|
|
|
624
692
|
return { kind: "connected", displayName };
|
|
625
693
|
}
|
|
626
694
|
if (result.status !== "staged") {
|
|
627
|
-
|
|
695
|
+
const outcomeKey = result.status === "invalid" && result.reason === "declined" ? "declined" : result.status;
|
|
696
|
+
return reportNoConnection(deps, OUTCOME_MESSAGES[outcomeKey] ?? OUTCOME_MESSAGES.invalid, repoName);
|
|
628
697
|
}
|
|
629
698
|
const candidates = result.candidates;
|
|
630
699
|
if (candidates.length === 0) {
|
|
@@ -729,8 +798,12 @@ export async function runConnectBitbucketCli(argv, injected) {
|
|
|
729
798
|
baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
|
|
730
799
|
apiKey: cred.credentials.apiKey,
|
|
731
800
|
};
|
|
801
|
+
// BAPI-1401: explicit --workspace, else the origin remote. Inferred even with
|
|
802
|
+
// --use-code, where it still resolves the printed settings-page link.
|
|
803
|
+
const workspace = await resolveBitbucketWorkspace(parsed.value.workspace, deps.cwd, deps.runGit, deps.stderr);
|
|
732
804
|
const result = await runBitbucketConnectionFlow(deps, api, repo.value, {
|
|
733
|
-
workspace
|
|
805
|
+
workspace,
|
|
806
|
+
useCode: parsed.value.useCode,
|
|
734
807
|
// Standalone: nobody has asked yet, so the flow owns the initial confirmation.
|
|
735
808
|
initialMintApproved: false,
|
|
736
809
|
});
|
|
@@ -331,15 +331,21 @@ export async function revokeGithubHandoff(deps, repoName, state) {
|
|
|
331
331
|
}
|
|
332
332
|
return { ok: true, value: status };
|
|
333
333
|
}
|
|
334
|
+
/** A server-supplied ticket prefix is a short alphanumeric key, never free text. */
|
|
335
|
+
const TICKET_PREFIX_SHAPE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
336
|
+
function parseTicketPrefix(body) {
|
|
337
|
+
const raw = asRecord(body)?.ticket_prefix;
|
|
338
|
+
if (typeof raw !== "string")
|
|
339
|
+
return undefined;
|
|
340
|
+
const trimmed = raw.trim();
|
|
341
|
+
return TICKET_PREFIX_SHAPE.test(trimmed) ? trimmed : undefined;
|
|
342
|
+
}
|
|
334
343
|
/**
|
|
335
|
-
* Read
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* unrelated to any in-flight connection attempt. `unavailable` is a real answer and is
|
|
339
|
-
* deliberately distinct from `unconfigured`: callers must be able to skip rather than
|
|
340
|
-
* fabricate "not configured" from a probe that simply failed.
|
|
344
|
+
* Read the install manifest once and report GitHub's configured state and the
|
|
345
|
+
* optional ticket prefix. Same request, same deadline, and same GitHub parsing as
|
|
346
|
+
* {@link fetchGithubConfigurationState}, which delegates here.
|
|
341
347
|
*/
|
|
342
|
-
export async function
|
|
348
|
+
export async function fetchGithubConfigurationDetails(deps, repoName) {
|
|
343
349
|
let resp;
|
|
344
350
|
try {
|
|
345
351
|
resp = await deps.fetch(`${deps.baseUrl}/jira/config/install-manifest?repo_name=${encodeURIComponent(repoName)}`, {
|
|
@@ -348,31 +354,44 @@ export async function fetchGithubConfigurationState(deps, repoName) {
|
|
|
348
354
|
});
|
|
349
355
|
}
|
|
350
356
|
catch {
|
|
351
|
-
return "unavailable";
|
|
357
|
+
return { state: "unavailable" };
|
|
352
358
|
}
|
|
353
359
|
if (resp.status !== 200)
|
|
354
|
-
return "unavailable";
|
|
360
|
+
return { state: "unavailable" };
|
|
355
361
|
let body;
|
|
356
362
|
try {
|
|
357
363
|
body = await resp.json();
|
|
358
364
|
}
|
|
359
365
|
catch {
|
|
360
|
-
return "unavailable";
|
|
366
|
+
return { state: "unavailable" };
|
|
361
367
|
}
|
|
368
|
+
const ticketPrefix = parseTicketPrefix(body);
|
|
369
|
+
const withPrefix = (state) => ticketPrefix === undefined ? { state } : { state, ticketPrefix };
|
|
362
370
|
const integrations = asRecord(body)?.integrations;
|
|
363
371
|
if (!Array.isArray(integrations))
|
|
364
|
-
return "unavailable";
|
|
372
|
+
return withPrefix("unavailable");
|
|
365
373
|
for (const raw of integrations) {
|
|
366
374
|
const rec = asRecord(raw);
|
|
367
375
|
if (rec?.id !== "github_app")
|
|
368
376
|
continue;
|
|
369
377
|
if (typeof rec.is_configured !== "boolean")
|
|
370
|
-
return "unavailable";
|
|
371
|
-
return rec.is_configured ? "configured" : "unconfigured";
|
|
378
|
+
return withPrefix("unavailable");
|
|
379
|
+
return withPrefix(rec.is_configured ? "configured" : "unconfigured");
|
|
372
380
|
}
|
|
373
381
|
// The checklist is provider-scoped: GitHub is legitimately absent for a Bitbucket
|
|
374
382
|
// project. Absent is not "unconfigured" — there is nothing here to connect.
|
|
375
|
-
return "unavailable";
|
|
383
|
+
return withPrefix("unavailable");
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Read whether GitHub is already configured for a project.
|
|
387
|
+
*
|
|
388
|
+
* Uses the existing authenticated install-manifest/capability surface — read-only, and
|
|
389
|
+
* unrelated to any in-flight connection attempt. `unavailable` is a real answer and is
|
|
390
|
+
* deliberately distinct from `unconfigured`: callers must be able to skip rather than
|
|
391
|
+
* fabricate "not configured" from a probe that simply failed.
|
|
392
|
+
*/
|
|
393
|
+
export async function fetchGithubConfigurationState(deps, repoName) {
|
|
394
|
+
return (await fetchGithubConfigurationDetails(deps, repoName)).state;
|
|
376
395
|
}
|
|
377
396
|
/**
|
|
378
397
|
* Slightly longer than the server's 15-minute code TTL, so an expiry is reported by the
|