@dev.fast/whiteboard 0.2.5-preview.20261013.111 → 0.2.5-preview.20261013.113
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/dist/{agent-cli-BBPxwUOd.js → agent-cli-04xbDj23.js} +1 -1
- package/dist/agent-cli-BR_Jw5BL.js +2 -0
- package/dist/{authoring-tools-D8WXsm_g.js → authoring-tools-fdA0RPIT.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-runner-C1gY-HAI.js → cli-runner-C3men22A.js} +4 -4
- package/dist/cli.js +1 -1
- package/dist/{headless-host-BHD_Vg5F.js → headless-host-BLLJ7Dyu.js} +2 -2
- package/dist/{local-data-BlERtVm2.js → local-data-Dy1pNtLF.js} +125 -96
- package/dist/{mcp-t7VN9R3k.js → mcp-B8kuRSXM.js} +1 -1
- package/dist/{process-error-telemetry-C0swByfp.js → process-error-telemetry-BCBYp2t4.js} +2 -2
- package/dist/profile-aMSw1oCM.js +2 -0
- package/dist/{profile-BsYDBr7t.js → profile-tbGWuWHu.js} +1 -1
- package/dist/server/desktop-host.js +3 -3
- package/package.json +4 -4
- package/src/review-api/github-provider.ts +154 -0
- package/src/review-api/local-data.ts +17 -15
- package/src/review-api/origin.ts +3 -6
- package/src/review-api/provider.ts +44 -0
- package/src/review-api/pull-request.ts +15 -146
- package/dist/agent-cli-YSuhE5X8.js +0 -2
- package/dist/profile-j97aRTS7.js +0 -2
|
@@ -23,7 +23,7 @@ async function runReviewAgentCli(input) {
|
|
|
23
23
|
[REVIEW_AGENT_HEADER]: reviewSessionAgent(env)
|
|
24
24
|
};
|
|
25
25
|
if (mode === "mcp") {
|
|
26
|
-
const { serveReviewMcp } = await import("./mcp-
|
|
26
|
+
const { serveReviewMcp } = await import("./mcp-B8kuRSXM.js");
|
|
27
27
|
await serveReviewMcp(async (key, agentKind) => {
|
|
28
28
|
const connected = await connectReviewInstance(key ? {
|
|
29
29
|
...env,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as ReviewInputError } from "./src-DiHP11l3.js";
|
|
2
|
-
import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-
|
|
2
|
+
import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-Dy1pNtLF.js";
|
|
3
3
|
import { x as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-u4L_1VZa.js";
|
|
4
4
|
import { t as findReviewPackageRoot } from "./package-paths-B9PDoAl6.js";
|
|
5
5
|
import { z } from "zod";
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.2.5-preview.20261013.
|
|
1
|
+
{"version":"0.2.5-preview.20261013.113","commit":"c9ba2f7bc95523c617b751c760f4cb2a5fb0c5c3","dirty":true,"builtAt":"2026-10-13T14:45:33.958Z"}
|
|
@@ -12,7 +12,7 @@ import { t as ReviewTelemetry } from "./review-telemetry-CmM6tCbj.js";
|
|
|
12
12
|
import { a as isOwnedShim, f as windowsInstallerCommand, m as connectPrompts, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount } from "./account-alias-D0hF0sKk.js";
|
|
13
13
|
import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
|
|
14
14
|
import { n as readReviewServerDiscovery, o as reviewServerStateDir, r as readReviewServerHealth, s as serverNotReady } from "./server-discovery-ZVi8Ay8J.js";
|
|
15
|
-
import { t as reviewAgentCliHelp } from "./agent-cli-
|
|
15
|
+
import { t as reviewAgentCliHelp } from "./agent-cli-04xbDj23.js";
|
|
16
16
|
import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readSync, renameSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { realpath } from "node:fs/promises";
|
|
18
18
|
import path from "node:path";
|
|
@@ -4494,7 +4494,7 @@ async function runReviewCli(input) {
|
|
|
4494
4494
|
process.once("SIGINT", stop);
|
|
4495
4495
|
process.once("SIGTERM", stop);
|
|
4496
4496
|
try {
|
|
4497
|
-
const { runHeadlessServer } = await import("./headless-host-
|
|
4497
|
+
const { runHeadlessServer } = await import("./headless-host-BLLJ7Dyu.js");
|
|
4498
4498
|
await runHeadlessServer({
|
|
4499
4499
|
stateDir,
|
|
4500
4500
|
port,
|
|
@@ -4545,7 +4545,7 @@ async function runReviewCli(input) {
|
|
|
4545
4545
|
if (problem) throw new Error(`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`);
|
|
4546
4546
|
const inUse = () => /* @__PURE__ */ new Error(`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`);
|
|
4547
4547
|
if (desktops.instances.some(({ discovery }) => [discovery.appPid, discovery.serverPid].some((pid) => processIsAlive(pid)))) throw inUse();
|
|
4548
|
-
const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-
|
|
4548
|
+
const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-aMSw1oCM.js"), import("./headless-host-BLLJ7Dyu.js")]);
|
|
4549
4549
|
const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
|
|
4550
4550
|
const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
|
|
4551
4551
|
try {
|
|
@@ -4773,7 +4773,7 @@ async function runReviewCli(input) {
|
|
|
4773
4773
|
return commands;
|
|
4774
4774
|
} });
|
|
4775
4775
|
for (const [name, description] of [["api", "Call a JSON review authoring tool on the running server"], ["mcp", "Serve the JSON review authoring tools over stdio MCP"]]) configureOutput(program.command(name).description(description).argument("[args...]", "tool name and JSON input").allowUnknownOption().allowExcessArguments().helpOption(false).passThroughOptions().addHelpText("after", `\n${reviewAgentCliHelp}`), "plain").action(async (args) => {
|
|
4776
|
-
const { runReviewAgentCli } = await import("./agent-cli-
|
|
4776
|
+
const { runReviewAgentCli } = await import("./agent-cli-BR_Jw5BL.js");
|
|
4777
4777
|
state.exitCode = await runReviewAgentCli({
|
|
4778
4778
|
...input,
|
|
4779
4779
|
env: authoringEnv(),
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { p as isObjectValue } from "./src-X9phtB2j.js";
|
|
2
2
|
import "./review-home-paths-Y9mYWkCI.js";
|
|
3
3
|
import { O as withFileLock, P as writePrivateJsonAtomic } from "./s3-config-CAxhO9u_.js";
|
|
4
|
-
import { t as openReviewProfile } from "./profile-
|
|
5
|
-
import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-
|
|
4
|
+
import { t as openReviewProfile } from "./profile-tbGWuWHu.js";
|
|
5
|
+
import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-BCBYp2t4.js";
|
|
6
6
|
import { i as reviewServerDiscoveryPath, t as headlessServerLockPath } from "./server-discovery-ZVi8Ay8J.js";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
@@ -1231,8 +1231,6 @@ const mapInputSchema = z.strictObject({
|
|
|
1231
1231
|
});
|
|
1232
1232
|
//#endregion
|
|
1233
1233
|
//#region src/review-api/pull-request.ts
|
|
1234
|
-
const GH_TIMEOUT_MS = 2e4;
|
|
1235
|
-
const API_TIMEOUT_MS = 15e3;
|
|
1236
1234
|
const FETCH_TIMEOUT_MS = 12e4;
|
|
1237
1235
|
const LOCAL_TIMEOUT_MS = 3e4;
|
|
1238
1236
|
const defaultPullRequestDeps = {
|
|
@@ -1255,82 +1253,9 @@ const defaultPullRequestDeps = {
|
|
|
1255
1253
|
}),
|
|
1256
1254
|
fetch: (input, init) => fetch(input, init)
|
|
1257
1255
|
};
|
|
1258
|
-
/**
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const [, owner, repo, , number] = pathname.split("/");
|
|
1262
|
-
return {
|
|
1263
|
-
host: hostname,
|
|
1264
|
-
slug: `${owner}/${repo}`,
|
|
1265
|
-
number: Number(number)
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
const metadataSchema = z.object({
|
|
1269
|
-
number: z.number().int().positive(),
|
|
1270
|
-
title: z.string(),
|
|
1271
|
-
baseRefName: z.string().min(1),
|
|
1272
|
-
baseRefOid: z.string().optional()
|
|
1273
|
-
});
|
|
1274
|
-
/** gh first (it carries the user's auth), then, for github.com only, GitHub's
|
|
1275
|
-
* public REST API: Enterprise hosts require gh. */
|
|
1276
|
-
async function readPullRequest(url, deps) {
|
|
1277
|
-
const { host, slug, number } = pullRequestAddress(url);
|
|
1278
|
-
let ghFailure;
|
|
1279
|
-
try {
|
|
1280
|
-
const stdout = await deps.run("gh", [
|
|
1281
|
-
"pr",
|
|
1282
|
-
"view",
|
|
1283
|
-
String(number),
|
|
1284
|
-
"--repo",
|
|
1285
|
-
`${host}/${slug}`,
|
|
1286
|
-
"--json",
|
|
1287
|
-
"number,title,baseRefName,baseRefOid"
|
|
1288
|
-
], { timeoutMs: GH_TIMEOUT_MS });
|
|
1289
|
-
return {
|
|
1290
|
-
host,
|
|
1291
|
-
slug,
|
|
1292
|
-
...metadataSchema.parse(JSON.parse(stdout))
|
|
1293
|
-
};
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
ghFailure = firstLine(errorMessage(error));
|
|
1296
|
-
}
|
|
1297
|
-
if (host !== "github.com") throw new ReviewInputError(`Could not read ${url}: gh failed (${ghFailure}). Run \`gh auth login --hostname ${host}\` with an account that can read the repository, then retry.`, 409);
|
|
1298
|
-
let response;
|
|
1299
|
-
try {
|
|
1300
|
-
response = await deps.fetch(`https://api.github.com/repos/${slug}/pulls/${number}`, {
|
|
1301
|
-
headers: {
|
|
1302
|
-
Accept: "application/vnd.github+json",
|
|
1303
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
1304
|
-
},
|
|
1305
|
-
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
1306
|
-
});
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
throw unreadable(url, ghFailure, `the GitHub API is unreachable (${firstLine(errorMessage(error))})`);
|
|
1309
|
-
}
|
|
1310
|
-
if (response.status === 404) throw new ReviewInputError(`${url} was not found: it does not exist, or it is private and gh is not signed in to an account that can read it (run \`gh auth status\`). gh said: ${ghFailure}`, 404);
|
|
1311
|
-
if (response.status === 403 || response.status === 429) throw unreadable(url, ghFailure, response.headers.get("x-ratelimit-remaining") === "0" || response.status === 429 ? "the unauthenticated GitHub API rate limit is exhausted" : "the GitHub API refused the request");
|
|
1312
|
-
if (!response.ok) throw unreadable(url, ghFailure, `the GitHub API returned HTTP ${response.status}`);
|
|
1313
|
-
const parsed = z.object({
|
|
1314
|
-
number: z.number(),
|
|
1315
|
-
title: z.string(),
|
|
1316
|
-
base: z.object({
|
|
1317
|
-
ref: z.string().min(1),
|
|
1318
|
-
sha: z.string()
|
|
1319
|
-
})
|
|
1320
|
-
}).safeParse(await response.json().catch(() => void 0));
|
|
1321
|
-
if (!parsed.success) throw unreadable(url, ghFailure, "the GitHub API response was malformed");
|
|
1322
|
-
return {
|
|
1323
|
-
host,
|
|
1324
|
-
slug,
|
|
1325
|
-
number: parsed.data.number,
|
|
1326
|
-
title: parsed.data.title,
|
|
1327
|
-
baseRefName: parsed.data.base.ref,
|
|
1328
|
-
baseRefOid: parsed.data.base.sha
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
/** Remotes by the host and owner/repo their configured URL names. The
|
|
1332
|
-
* configured URL, not the insteadOf rewrite: a mirror still names its repo. */
|
|
1333
|
-
async function githubRemotes(gitDir, deps) {
|
|
1256
|
+
/** Remotes by their configured URL, not the insteadOf rewrite: a mirror
|
|
1257
|
+
* still names its repo. */
|
|
1258
|
+
async function remoteUrls(gitDir, deps) {
|
|
1334
1259
|
return (await deps.run("git", [
|
|
1335
1260
|
"--git-dir",
|
|
1336
1261
|
gitDir,
|
|
@@ -1339,17 +1264,15 @@ async function githubRemotes(gitDir, deps) {
|
|
|
1339
1264
|
String.raw`^remote\..*\.url$`
|
|
1340
1265
|
], { timeoutMs: LOCAL_TIMEOUT_MS }).catch(() => "")).split("\n").flatMap((line) => {
|
|
1341
1266
|
const match = /^remote\.(.+)\.url\s+(\S+)/.exec(line.trim());
|
|
1342
|
-
|
|
1343
|
-
return match && remote ? [{
|
|
1267
|
+
return match ? [{
|
|
1344
1268
|
name: match[1],
|
|
1345
|
-
|
|
1346
|
-
slug: remote.slug
|
|
1269
|
+
url: match[2]
|
|
1347
1270
|
}] : [];
|
|
1348
1271
|
});
|
|
1349
1272
|
}
|
|
1350
1273
|
/** Refs Review owns for one PR; the user's branches and bookmarks never move. */
|
|
1351
|
-
function pullRequestRefs(pr) {
|
|
1352
|
-
const prefix = `refs/review
|
|
1274
|
+
function pullRequestRefs(provider, pr) {
|
|
1275
|
+
const prefix = `refs/review/${provider}/${`${pr.host === "github.com" ? "" : `${pr.host}/`}${pr.slug}`.toLowerCase()}/pull/${pr.number}`;
|
|
1353
1276
|
return {
|
|
1354
1277
|
head: `${prefix}/head`,
|
|
1355
1278
|
base: `${prefix}/base`,
|
|
@@ -1369,7 +1292,7 @@ function pullRequestRefs(pr) {
|
|
|
1369
1292
|
*/
|
|
1370
1293
|
async function fetchPullRequest(input, deps) {
|
|
1371
1294
|
const { pullRequest: pr } = input;
|
|
1372
|
-
const refs = pullRequestRefs(pr);
|
|
1295
|
+
const refs = pullRequestRefs(input.provider, pr);
|
|
1373
1296
|
const git = (args, timeoutMs = LOCAL_TIMEOUT_MS) => deps.run("git", [
|
|
1374
1297
|
"--git-dir",
|
|
1375
1298
|
input.gitDir,
|
|
@@ -1401,11 +1324,11 @@ async function fetchPullRequest(input, deps) {
|
|
|
1401
1324
|
};
|
|
1402
1325
|
let baseTip;
|
|
1403
1326
|
try {
|
|
1404
|
-
await fetchRefs(
|
|
1327
|
+
await fetchRefs(`+${pr.headRef}:${refs.head}`, `+refs/heads/${pr.baseRefName}:${refs.base}`);
|
|
1405
1328
|
baseTip = await commit(refs.base);
|
|
1406
1329
|
} catch (error) {
|
|
1407
1330
|
try {
|
|
1408
|
-
await fetchRefs(
|
|
1331
|
+
await fetchRefs(`+${pr.headRef}:${refs.head}`);
|
|
1409
1332
|
} catch {
|
|
1410
1333
|
throw new ReviewInputError(`Could not fetch PR #${pr.number} from remote "${input.remote}" (${pr.slug}). Check \`git fetch ${input.remote}\` works with your Git credentials, then retry. Git said: ${firstLine(errorMessage(error))}`, 409);
|
|
1411
1334
|
}
|
|
@@ -1460,11 +1383,115 @@ async function indexForJj(rootPath, commits, git, deps) {
|
|
|
1460
1383
|
await jjImport().catch(() => void 0);
|
|
1461
1384
|
}
|
|
1462
1385
|
}
|
|
1386
|
+
function firstLine(text) {
|
|
1387
|
+
return text.split("\n")[0].slice(0, 300);
|
|
1388
|
+
}
|
|
1389
|
+
//#endregion
|
|
1390
|
+
//#region src/review-api/github-provider.ts
|
|
1391
|
+
const GH_TIMEOUT_MS = 2e4;
|
|
1392
|
+
const API_TIMEOUT_MS = 15e3;
|
|
1393
|
+
const GITHUB_PR_REGEX = /^https:\/\/([a-z0-9.-]+)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([1-9]\d*)$/;
|
|
1394
|
+
const metadataSchema = z.object({
|
|
1395
|
+
number: z.number().int().positive(),
|
|
1396
|
+
title: z.string(),
|
|
1397
|
+
baseRefName: z.string().min(1),
|
|
1398
|
+
baseRefOid: z.string().optional()
|
|
1399
|
+
});
|
|
1463
1400
|
function unreadable(url, ghFailure, fallback) {
|
|
1464
1401
|
return new ReviewInputError(`Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`, 409);
|
|
1465
1402
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1403
|
+
//#endregion
|
|
1404
|
+
//#region src/review-api/provider.ts
|
|
1405
|
+
const providers = [{
|
|
1406
|
+
id: "github",
|
|
1407
|
+
parseUrl(url) {
|
|
1408
|
+
const match = GITHUB_PR_REGEX.exec(url);
|
|
1409
|
+
const number = Number(match?.[4]);
|
|
1410
|
+
if (!match || !Number.isSafeInteger(number)) return void 0;
|
|
1411
|
+
return {
|
|
1412
|
+
host: match[1],
|
|
1413
|
+
slug: `${match[2]}/${match[3]}`,
|
|
1414
|
+
number
|
|
1415
|
+
};
|
|
1416
|
+
},
|
|
1417
|
+
/** gh first (it carries the user's auth), then, for github.com only, GitHub's
|
|
1418
|
+
* public REST API: Enterprise hosts require gh. */
|
|
1419
|
+
async readPullRequest({ host, slug, number }, deps) {
|
|
1420
|
+
const url = `https://${host}/${slug}/pull/${number}`;
|
|
1421
|
+
const headRef = `refs/pull/${number}/head`;
|
|
1422
|
+
let ghFailure;
|
|
1423
|
+
try {
|
|
1424
|
+
const stdout = await deps.run("gh", [
|
|
1425
|
+
"pr",
|
|
1426
|
+
"view",
|
|
1427
|
+
String(number),
|
|
1428
|
+
"--repo",
|
|
1429
|
+
`${host}/${slug}`,
|
|
1430
|
+
"--json",
|
|
1431
|
+
"number,title,baseRefName,baseRefOid"
|
|
1432
|
+
], { timeoutMs: GH_TIMEOUT_MS });
|
|
1433
|
+
return {
|
|
1434
|
+
host,
|
|
1435
|
+
slug,
|
|
1436
|
+
headRef,
|
|
1437
|
+
...metadataSchema.parse(JSON.parse(stdout))
|
|
1438
|
+
};
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
ghFailure = firstLine(errorMessage(error));
|
|
1441
|
+
}
|
|
1442
|
+
if (host !== "github.com") throw new ReviewInputError(`Could not read ${url}: gh failed (${ghFailure}). Run \`gh auth login --hostname ${host}\` with an account that can read the repository, then retry.`, 409);
|
|
1443
|
+
let response;
|
|
1444
|
+
try {
|
|
1445
|
+
response = await deps.fetch(`https://api.github.com/repos/${slug}/pulls/${number}`, {
|
|
1446
|
+
headers: {
|
|
1447
|
+
Accept: "application/vnd.github+json",
|
|
1448
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1449
|
+
},
|
|
1450
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
1451
|
+
});
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
throw unreadable(url, ghFailure, `the GitHub API is unreachable (${firstLine(errorMessage(error))})`);
|
|
1454
|
+
}
|
|
1455
|
+
if (response.status === 404) throw new ReviewInputError(`${url} was not found: it does not exist, or it is private and gh is not signed in to an account that can read it (run \`gh auth status\`). gh said: ${ghFailure}`, 404);
|
|
1456
|
+
if (response.status === 403 || response.status === 429) throw unreadable(url, ghFailure, response.headers.get("x-ratelimit-remaining") === "0" || response.status === 429 ? "the unauthenticated GitHub API rate limit is exhausted" : "the GitHub API refused the request");
|
|
1457
|
+
if (!response.ok) throw unreadable(url, ghFailure, `the GitHub API returned HTTP ${response.status}`);
|
|
1458
|
+
const parsed = z.object({
|
|
1459
|
+
number: z.number(),
|
|
1460
|
+
title: z.string(),
|
|
1461
|
+
base: z.object({
|
|
1462
|
+
ref: z.string().min(1),
|
|
1463
|
+
sha: z.string()
|
|
1464
|
+
})
|
|
1465
|
+
}).safeParse(await response.json().catch(() => void 0));
|
|
1466
|
+
if (!parsed.success) throw unreadable(url, ghFailure, "the GitHub API response was malformed");
|
|
1467
|
+
return {
|
|
1468
|
+
host,
|
|
1469
|
+
slug,
|
|
1470
|
+
headRef,
|
|
1471
|
+
number: parsed.data.number,
|
|
1472
|
+
title: parsed.data.title,
|
|
1473
|
+
baseRefName: parsed.data.base.ref,
|
|
1474
|
+
baseRefOid: parsed.data.base.sha
|
|
1475
|
+
};
|
|
1476
|
+
},
|
|
1477
|
+
matchesRemote(remoteUrl, address) {
|
|
1478
|
+
const remote = parseGitRemote(remoteUrl);
|
|
1479
|
+
return remote?.host === address.host && remote.slug.toLowerCase() === address.slug.toLowerCase();
|
|
1480
|
+
}
|
|
1481
|
+
}];
|
|
1482
|
+
function pullRequestProvider(url) {
|
|
1483
|
+
for (const provider of providers) {
|
|
1484
|
+
const address = provider.parseUrl(url);
|
|
1485
|
+
if (address) return {
|
|
1486
|
+
provider,
|
|
1487
|
+
address
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function resolvePullRequest(url) {
|
|
1492
|
+
const resolution = pullRequestProvider(url);
|
|
1493
|
+
if (!resolution) throw new ReviewInputError(`Unsupported pull request URL: ${url}`, 400);
|
|
1494
|
+
return resolution;
|
|
1468
1495
|
}
|
|
1469
1496
|
//#endregion
|
|
1470
1497
|
//#region src/review-api/activity.ts
|
|
@@ -2153,7 +2180,7 @@ var AskHistory = class {
|
|
|
2153
2180
|
};
|
|
2154
2181
|
//#endregion
|
|
2155
2182
|
//#region src/review-api/origin.ts
|
|
2156
|
-
const pullRequestUrl = z.string().
|
|
2183
|
+
const pullRequestUrl = z.string().refine((url) => pullRequestProvider(url) !== void 0, "Use a canonical GitHub PR URL: https://github.com/owner/repository/pull/123, or the same path on a GitHub Enterprise host.");
|
|
2157
2184
|
/** One key per PR: GitHub owner and repository names are case-insensitive,
|
|
2158
2185
|
* and the URL pattern admits only ASCII, so lowercasing is canonical. */
|
|
2159
2186
|
function pullRequestKey(url) {
|
|
@@ -4215,11 +4242,12 @@ var LocalReviewData = class {
|
|
|
4215
4242
|
/** The PR's current comparison, fetched into a registered checkout of its repository. */
|
|
4216
4243
|
async resolvePullRequest(url, repository) {
|
|
4217
4244
|
const deps = this.options.pullRequests ?? defaultPullRequestDeps;
|
|
4218
|
-
const {
|
|
4219
|
-
const checkout = await this.pullRequestCheckout(
|
|
4220
|
-
const pullRequest = await readPullRequest(
|
|
4245
|
+
const { provider, address } = resolvePullRequest(url);
|
|
4246
|
+
const checkout = await this.pullRequestCheckout(provider, address, repository, deps);
|
|
4247
|
+
const pullRequest = await provider.readPullRequest(address, deps);
|
|
4221
4248
|
const { head, base } = await fetchPullRequest({
|
|
4222
4249
|
...checkout,
|
|
4250
|
+
provider: provider.id,
|
|
4223
4251
|
pullRequest
|
|
4224
4252
|
}, deps);
|
|
4225
4253
|
const repositoryId = checkout.repositoryId;
|
|
@@ -4235,11 +4263,11 @@ var LocalReviewData = class {
|
|
|
4235
4263
|
base,
|
|
4236
4264
|
head
|
|
4237
4265
|
},
|
|
4238
|
-
title: pullRequest.title.trim() || `PR #${number}`
|
|
4266
|
+
title: pullRequest.title.trim() || `PR #${address.number}`
|
|
4239
4267
|
};
|
|
4240
4268
|
}
|
|
4241
4269
|
/** A registered checkout with a remote for host/owner/repo, and that remote. */
|
|
4242
|
-
async pullRequestCheckout(
|
|
4270
|
+
async pullRequestCheckout(provider, address, repository, deps) {
|
|
4243
4271
|
const registered = this.store.repositories();
|
|
4244
4272
|
const candidates = repository.id ? registered.filter((entry) => entry.id === repository.id) : [...registered.filter((entry) => entry.id === repository.preferred), ...registered.filter((entry) => entry.id !== repository.preferred)];
|
|
4245
4273
|
if (repository.id && candidates.length === 0) throw new ReviewInputError("Repository is not registered.", 404);
|
|
@@ -4248,7 +4276,7 @@ var LocalReviewData = class {
|
|
|
4248
4276
|
const vcs = await this.vcs(id);
|
|
4249
4277
|
const gitDir = vcs && await gitCommonDir(vcs.rootPath);
|
|
4250
4278
|
if (!vcs || !gitDir) continue;
|
|
4251
|
-
const remote = (await
|
|
4279
|
+
const remote = (await remoteUrls(gitDir, deps)).find((entry) => provider.matchesRemote(entry.url, address));
|
|
4252
4280
|
if (remote) return {
|
|
4253
4281
|
repositoryId: id,
|
|
4254
4282
|
rootPath: vcs.rootPath,
|
|
@@ -4257,6 +4285,7 @@ var LocalReviewData = class {
|
|
|
4257
4285
|
remote: remote.name
|
|
4258
4286
|
};
|
|
4259
4287
|
}
|
|
4288
|
+
const { host, slug } = address;
|
|
4260
4289
|
const name = host === "github.com" ? slug : `${host}/${slug}`;
|
|
4261
4290
|
throw new ReviewInputError(repository.id ? `That checkout has no GitHub remote for ${name}. Add one, or omit repositoryId.` : `No registered checkout has a GitHub remote for ${name}. Register a checkout of ${name} with review_register_repository first, or pass a target.`, 404);
|
|
4262
4291
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { C as ReviewInputError } from "./src-DiHP11l3.js";
|
|
2
2
|
import { a as toolResultText, s as ReviewApiError } from "./agent-client-P3J4y8Zn.js";
|
|
3
|
-
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-
|
|
3
|
+
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-fdA0RPIT.js";
|
|
4
4
|
import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CJHBCHMh.js";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { C as ReviewInputError } from "./src-DiHP11l3.js";
|
|
2
2
|
import { c as jsonString, m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
3
3
|
import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-DzcSt6pV.js";
|
|
4
|
-
import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-
|
|
4
|
+
import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-Dy1pNtLF.js";
|
|
5
5
|
import { d as REVIEW_CLIENT_REMOTE, i as parseReviewVerbRequest, r as parseReviewDesktopVerbResult, u as REVIEW_CLIENT_HEADER, v as reviewDiffrSummarizerInputSchema, y as decodeStructuralDiffEvent } from "./src-WCbabvdS.js";
|
|
6
6
|
import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as openUrlInBrowser, it as clearStoreAuth, ot as readStoreAuth, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, z as selectTraceStorage } from "./review-home-paths-Y9mYWkCI.js";
|
|
7
7
|
import { $ as unionIntervals, G as selectionKey, H as comparisonKey, I as fileLineRangeSchema, J as coverageSources, N as lensSelections, Q as subtractIntervals, T as selectionReferences, U as formatAnchor, V as anchorSelection, W as resolveDiffSelection, Y as emptyCoverage, Z as scopedCoverage, h as elements, k as UNCATEGORIZED_LENS_ID, u as anchorPins } from "./stored-document-migration-u4L_1VZa.js";
|
|
@@ -9,7 +9,7 @@ import { A as devReviewHome, h as TraceConfigurationError, i as isS3MockMode } f
|
|
|
9
9
|
import { t as errorMessage } from "./error-message-OtiDonty.js";
|
|
10
10
|
import { i as readReviewPackageVersion, n as readBuildCommit } from "./package-paths-B9PDoAl6.js";
|
|
11
11
|
import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-DA4K1qWY.js";
|
|
12
|
-
import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-
|
|
12
|
+
import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-fdA0RPIT.js";
|
|
13
13
|
import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import crypto$1, { createHash, randomUUID } from "node:crypto";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-
|
|
1
|
+
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-Dy1pNtLF.js";
|
|
2
2
|
import "./review-home-paths-Y9mYWkCI.js";
|
|
3
3
|
import { O as withFileLock } from "./s3-config-CAxhO9u_.js";
|
|
4
4
|
import { access, mkdir } from "node:fs/promises";
|
|
@@ -8,12 +8,12 @@ import { C as resourceReferences, M as lensSchema, p as documentSchema } from ".
|
|
|
8
8
|
import { A as devReviewHome, P as writePrivateJsonAtomic } from "../s3-config-CAxhO9u_.js";
|
|
9
9
|
import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-B9PDoAl6.js";
|
|
10
10
|
import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-Ba3dD5Cn.js";
|
|
11
|
-
import { t as openReviewProfile } from "../profile-
|
|
11
|
+
import { t as openReviewProfile } from "../profile-tbGWuWHu.js";
|
|
12
12
|
import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-CmM6tCbj.js";
|
|
13
13
|
import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-DA4K1qWY.js";
|
|
14
14
|
import { c as removeLegacyReviewSkills, d as skipCliInstall, i as finishCliInstallUpdate, l as resetCliInstall, n as applyCliInstall, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-D0hF0sKk.js";
|
|
15
|
-
import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-
|
|
16
|
-
import { c as readQuerySchemas } from "../authoring-tools-
|
|
15
|
+
import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-BCBYp2t4.js";
|
|
16
|
+
import { c as readQuerySchemas } from "../authoring-tools-fdA0RPIT.js";
|
|
17
17
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import fs, { existsSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.fast/whiteboard",
|
|
3
|
-
"version": "0.2.5-preview.20261013.
|
|
3
|
+
"version": "0.2.5-preview.20261013.113",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"typescript": "6.0.3",
|
|
74
74
|
"vitest": "4.1.11",
|
|
75
75
|
"@dev.fast/review-protocol": "0.0.0",
|
|
76
|
-
"@dev.fast/
|
|
76
|
+
"@dev.fast/json": "0.0.0",
|
|
77
77
|
"@dev.fast/review-share-protocol": "0.1.2",
|
|
78
78
|
"@dev.fast/trace-protocol": "0.5.0",
|
|
79
|
-
"@dev.fast/
|
|
79
|
+
"@dev.fast/trace-core": "0.0.0"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=24 <25"
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"repo": "devdotfast/review",
|
|
87
87
|
"docs": "packages/review/instructions/authoring.md"
|
|
88
88
|
},
|
|
89
|
-
"gitHead": "
|
|
89
|
+
"gitHead": "c9ba2f7bc95523c617b751c760f4cb2a5fb0c5c3",
|
|
90
90
|
"scripts": {
|
|
91
91
|
"prebuild": "pnpm run build:workspace-deps",
|
|
92
92
|
"build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { parseGitRemote } from "@dev.fast/local-vcs";
|
|
2
|
+
import { errorMessage } from "@dev.fast/trace-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { ReviewInputError } from "./document.js";
|
|
6
|
+
import type { PullRequestProvider } from "./provider.js";
|
|
7
|
+
import { firstLine } from "./pull-request.js";
|
|
8
|
+
|
|
9
|
+
const GH_TIMEOUT_MS = 20_000;
|
|
10
|
+
|
|
11
|
+
const API_TIMEOUT_MS = 15_000;
|
|
12
|
+
|
|
13
|
+
const GITHUB_PR_REGEX =
|
|
14
|
+
/^https:\/\/([a-z0-9.-]+)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([1-9]\d*)$/;
|
|
15
|
+
|
|
16
|
+
const metadataSchema = z.object({
|
|
17
|
+
number: z.number().int().positive(),
|
|
18
|
+
title: z.string(),
|
|
19
|
+
baseRefName: z.string().min(1),
|
|
20
|
+
baseRefOid: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function unreadable(url: string, ghFailure: string, fallback: string) {
|
|
24
|
+
return new ReviewInputError(
|
|
25
|
+
`Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
|
|
26
|
+
409,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const githubProvider: PullRequestProvider = {
|
|
31
|
+
id: "github",
|
|
32
|
+
|
|
33
|
+
parseUrl(url) {
|
|
34
|
+
const match = GITHUB_PR_REGEX.exec(url);
|
|
35
|
+
const number = Number(match?.[4]);
|
|
36
|
+
|
|
37
|
+
if (!match || !Number.isSafeInteger(number)) return undefined;
|
|
38
|
+
|
|
39
|
+
return { host: match[1]!, slug: `${match[2]}/${match[3]}`, number };
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** gh first (it carries the user's auth), then, for github.com only, GitHub's
|
|
43
|
+
* public REST API: Enterprise hosts require gh. */
|
|
44
|
+
async readPullRequest({ host, slug, number }, deps) {
|
|
45
|
+
const url = `https://${host}/${slug}/pull/${number}`;
|
|
46
|
+
const headRef = `refs/pull/${number}/head`;
|
|
47
|
+
let ghFailure: string;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const stdout = await deps.run(
|
|
51
|
+
"gh",
|
|
52
|
+
[
|
|
53
|
+
"pr",
|
|
54
|
+
"view",
|
|
55
|
+
String(number),
|
|
56
|
+
"--repo",
|
|
57
|
+
`${host}/${slug}`,
|
|
58
|
+
"--json",
|
|
59
|
+
"number,title,baseRefName,baseRefOid",
|
|
60
|
+
],
|
|
61
|
+
{ timeoutMs: GH_TIMEOUT_MS },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
host,
|
|
66
|
+
slug,
|
|
67
|
+
headRef,
|
|
68
|
+
...metadataSchema.parse(JSON.parse(stdout)),
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
ghFailure = firstLine(errorMessage(error));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (host !== "github.com")
|
|
75
|
+
throw new ReviewInputError(
|
|
76
|
+
`Could not read ${url}: gh failed (${ghFailure}). Run \`gh auth login --hostname ${host}\` with an account that can read the repository, then retry.`,
|
|
77
|
+
409,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let response: Response;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
response = await deps.fetch(
|
|
84
|
+
`https://api.github.com/repos/${slug}/pulls/${number}`,
|
|
85
|
+
{
|
|
86
|
+
headers: {
|
|
87
|
+
Accept: "application/vnd.github+json",
|
|
88
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
89
|
+
},
|
|
90
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS),
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw unreadable(
|
|
95
|
+
url,
|
|
96
|
+
ghFailure,
|
|
97
|
+
`the GitHub API is unreachable (${firstLine(errorMessage(error))})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (response.status === 404)
|
|
102
|
+
throw new ReviewInputError(
|
|
103
|
+
`${url} was not found: it does not exist, or it is private and gh is not signed in to an account that can read it (run \`gh auth status\`). gh said: ${ghFailure}`,
|
|
104
|
+
404,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (response.status === 403 || response.status === 429)
|
|
108
|
+
throw unreadable(
|
|
109
|
+
url,
|
|
110
|
+
ghFailure,
|
|
111
|
+
response.headers.get("x-ratelimit-remaining") === "0" ||
|
|
112
|
+
response.status === 429
|
|
113
|
+
? "the unauthenticated GitHub API rate limit is exhausted"
|
|
114
|
+
: "the GitHub API refused the request",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!response.ok)
|
|
118
|
+
throw unreadable(
|
|
119
|
+
url,
|
|
120
|
+
ghFailure,
|
|
121
|
+
`the GitHub API returned HTTP ${response.status}`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const parsed = z
|
|
125
|
+
.object({
|
|
126
|
+
number: z.number(),
|
|
127
|
+
title: z.string(),
|
|
128
|
+
base: z.object({ ref: z.string().min(1), sha: z.string() }),
|
|
129
|
+
})
|
|
130
|
+
.safeParse(await response.json().catch(() => undefined));
|
|
131
|
+
|
|
132
|
+
if (!parsed.success)
|
|
133
|
+
throw unreadable(url, ghFailure, "the GitHub API response was malformed");
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
host,
|
|
137
|
+
slug,
|
|
138
|
+
headRef,
|
|
139
|
+
number: parsed.data.number,
|
|
140
|
+
title: parsed.data.title,
|
|
141
|
+
baseRefName: parsed.data.base.ref,
|
|
142
|
+
baseRefOid: parsed.data.base.sha,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
matchesRemote(remoteUrl, address) {
|
|
147
|
+
const remote = parseGitRemote(remoteUrl);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
remote?.host === address.host &&
|
|
151
|
+
remote.slug.toLowerCase() === address.slug.toLowerCase()
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -77,13 +77,16 @@ import {
|
|
|
77
77
|
} from "./document.js";
|
|
78
78
|
import { decodeImage } from "./image-decode.js";
|
|
79
79
|
import { mapInputSchema } from "./map-input.js";
|
|
80
|
+
import {
|
|
81
|
+
type PullRequestAddress,
|
|
82
|
+
type PullRequestProvider,
|
|
83
|
+
resolvePullRequest,
|
|
84
|
+
} from "./provider.js";
|
|
80
85
|
import {
|
|
81
86
|
type PullRequestDeps,
|
|
82
87
|
defaultPullRequestDeps,
|
|
83
88
|
fetchPullRequest,
|
|
84
|
-
|
|
85
|
-
pullRequestAddress,
|
|
86
|
-
readPullRequest,
|
|
89
|
+
remoteUrls,
|
|
87
90
|
} from "./pull-request.js";
|
|
88
91
|
import {
|
|
89
92
|
type ResolvedPullRequest,
|
|
@@ -1145,20 +1148,20 @@ export class LocalReviewData {
|
|
|
1145
1148
|
repository: { id?: string; preferred?: string },
|
|
1146
1149
|
): Promise<ResolvedPullRequest> {
|
|
1147
1150
|
const deps = this.options.pullRequests ?? defaultPullRequestDeps;
|
|
1148
|
-
const {
|
|
1151
|
+
const { provider, address } = resolvePullRequest(url);
|
|
1149
1152
|
|
|
1150
1153
|
// Only a host a registered checkout already fetches from is contacted.
|
|
1151
1154
|
const checkout = await this.pullRequestCheckout(
|
|
1152
|
-
|
|
1153
|
-
|
|
1155
|
+
provider,
|
|
1156
|
+
address,
|
|
1154
1157
|
repository,
|
|
1155
1158
|
deps,
|
|
1156
1159
|
);
|
|
1157
1160
|
|
|
1158
|
-
const pullRequest = await readPullRequest(
|
|
1161
|
+
const pullRequest = await provider.readPullRequest(address, deps);
|
|
1159
1162
|
|
|
1160
1163
|
const { head, base } = await fetchPullRequest(
|
|
1161
|
-
{ ...checkout, pullRequest },
|
|
1164
|
+
{ ...checkout, provider: provider.id, pullRequest },
|
|
1162
1165
|
deps,
|
|
1163
1166
|
);
|
|
1164
1167
|
|
|
@@ -1167,13 +1170,13 @@ export class LocalReviewData {
|
|
|
1167
1170
|
return {
|
|
1168
1171
|
target: { kind: "commits", repositoryId, head, base },
|
|
1169
1172
|
pins: { repositoryId, base, head },
|
|
1170
|
-
title: pullRequest.title.trim() || `PR #${number}`,
|
|
1173
|
+
title: pullRequest.title.trim() || `PR #${address.number}`,
|
|
1171
1174
|
};
|
|
1172
1175
|
}
|
|
1173
1176
|
/** A registered checkout with a remote for host/owner/repo, and that remote. */
|
|
1174
1177
|
private async pullRequestCheckout(
|
|
1175
|
-
|
|
1176
|
-
|
|
1178
|
+
provider: PullRequestProvider,
|
|
1179
|
+
address: PullRequestAddress,
|
|
1177
1180
|
repository: { id?: string; preferred?: string },
|
|
1178
1181
|
deps: PullRequestDeps,
|
|
1179
1182
|
) {
|
|
@@ -1196,10 +1199,8 @@ export class LocalReviewData {
|
|
|
1196
1199
|
|
|
1197
1200
|
if (!vcs || !gitDir) continue;
|
|
1198
1201
|
|
|
1199
|
-
const remote = (await
|
|
1200
|
-
(entry)
|
|
1201
|
-
entry.host === host &&
|
|
1202
|
-
entry.slug.toLowerCase() === slug.toLowerCase(),
|
|
1202
|
+
const remote = (await remoteUrls(gitDir, deps)).find((entry) =>
|
|
1203
|
+
provider.matchesRemote(entry.url, address),
|
|
1203
1204
|
);
|
|
1204
1205
|
|
|
1205
1206
|
if (remote)
|
|
@@ -1212,6 +1213,7 @@ export class LocalReviewData {
|
|
|
1212
1213
|
};
|
|
1213
1214
|
}
|
|
1214
1215
|
|
|
1216
|
+
const { host, slug } = address;
|
|
1215
1217
|
const name = host === "github.com" ? slug : `${host}/${slug}`;
|
|
1216
1218
|
|
|
1217
1219
|
throw new ReviewInputError(
|
package/src/review-api/origin.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import { pullRequestProvider } from "./provider.js";
|
|
3
4
|
import type { Snapshot } from "./store.js";
|
|
4
5
|
|
|
5
6
|
export const pullRequestUrl = z
|
|
6
7
|
.string()
|
|
7
|
-
.regex(
|
|
8
|
-
/^https:\/\/[a-z0-9.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9]\d*$/,
|
|
9
|
-
"Use a canonical GitHub PR URL: https://github.com/owner/repository/pull/123, or the same path on a GitHub Enterprise host.",
|
|
10
|
-
)
|
|
11
8
|
.refine(
|
|
12
|
-
(url) =>
|
|
13
|
-
"PR
|
|
9
|
+
(url) => pullRequestProvider(url) !== undefined,
|
|
10
|
+
"Use a canonical GitHub PR URL: https://github.com/owner/repository/pull/123, or the same path on a GitHub Enterprise host.",
|
|
14
11
|
);
|
|
15
12
|
|
|
16
13
|
/** One key per PR: GitHub owner and repository names are case-insensitive,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ReviewInputError } from "./document.js";
|
|
2
|
+
import { githubProvider } from "./github-provider.js";
|
|
3
|
+
import type { PullRequestDeps, PullRequestRecord } from "./pull-request.js";
|
|
4
|
+
|
|
5
|
+
export interface PullRequestAddress {
|
|
6
|
+
host: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
number: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PullRequestProvider {
|
|
12
|
+
/** Names Review's ref namespace: refs/review/<id>/... */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Recognizes this forge's PR URLs and extracts host, repo and number. */
|
|
15
|
+
parseUrl(url: string): PullRequestAddress | undefined;
|
|
16
|
+
/** Asks the forge for the PR's title, base branch and head ref. */
|
|
17
|
+
readPullRequest(
|
|
18
|
+
address: PullRequestAddress,
|
|
19
|
+
deps: PullRequestDeps,
|
|
20
|
+
): Promise<PullRequestRecord>;
|
|
21
|
+
/** Picks the local remote to fetch the PR from. */
|
|
22
|
+
matchesRemote(remoteUrl: string, address: PullRequestAddress): boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const providers: PullRequestProvider[] = [githubProvider];
|
|
26
|
+
|
|
27
|
+
export function pullRequestProvider(url: string) {
|
|
28
|
+
for (const provider of providers) {
|
|
29
|
+
const address = provider.parseUrl(url);
|
|
30
|
+
|
|
31
|
+
if (address) return { provider, address };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolvePullRequest(url: string) {
|
|
38
|
+
const resolution = pullRequestProvider(url);
|
|
39
|
+
|
|
40
|
+
if (!resolution)
|
|
41
|
+
throw new ReviewInputError(`Unsupported pull request URL: ${url}`, 400);
|
|
42
|
+
|
|
43
|
+
return resolution;
|
|
44
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { LocalVcsKind } from "@dev.fast/local-vcs";
|
|
5
5
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
6
|
-
import { z } from "zod";
|
|
7
6
|
|
|
8
7
|
import { ReviewInputError } from "./document.js";
|
|
9
8
|
|
|
@@ -25,15 +24,13 @@ export interface PullRequestRecord {
|
|
|
25
24
|
slug: string;
|
|
26
25
|
number: number;
|
|
27
26
|
title: string;
|
|
27
|
+
/** e.g. refs/pull/7/head */
|
|
28
|
+
headRef: string;
|
|
28
29
|
baseRefName: string;
|
|
29
30
|
/** GitHub's base commit, frozen at the PR's last update. */
|
|
30
31
|
baseRefOid?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const GH_TIMEOUT_MS = 20_000;
|
|
34
|
-
|
|
35
|
-
const API_TIMEOUT_MS = 15_000;
|
|
36
|
-
|
|
37
34
|
const FETCH_TIMEOUT_MS = 120_000;
|
|
38
35
|
|
|
39
36
|
const LOCAL_TIMEOUT_MS = 30_000;
|
|
@@ -71,127 +68,12 @@ export const defaultPullRequestDeps: PullRequestDeps = {
|
|
|
71
68
|
fetch: (input, init) => fetch(input, init),
|
|
72
69
|
};
|
|
73
70
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const [, owner, repo, , number] = pathname.split("/");
|
|
78
|
-
|
|
79
|
-
return { host: hostname, slug: `${owner}/${repo}`, number: Number(number) };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const metadataSchema = z.object({
|
|
83
|
-
number: z.number().int().positive(),
|
|
84
|
-
title: z.string(),
|
|
85
|
-
baseRefName: z.string().min(1),
|
|
86
|
-
baseRefOid: z.string().optional(),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
/** gh first (it carries the user's auth), then, for github.com only, GitHub's
|
|
90
|
-
* public REST API: Enterprise hosts require gh. */
|
|
91
|
-
export async function readPullRequest(
|
|
92
|
-
url: string,
|
|
93
|
-
deps: PullRequestDeps,
|
|
94
|
-
): Promise<PullRequestRecord> {
|
|
95
|
-
const { host, slug, number } = pullRequestAddress(url);
|
|
96
|
-
let ghFailure: string;
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const stdout = await deps.run(
|
|
100
|
-
"gh",
|
|
101
|
-
[
|
|
102
|
-
"pr",
|
|
103
|
-
"view",
|
|
104
|
-
String(number),
|
|
105
|
-
"--repo",
|
|
106
|
-
`${host}/${slug}`,
|
|
107
|
-
"--json",
|
|
108
|
-
"number,title,baseRefName,baseRefOid",
|
|
109
|
-
],
|
|
110
|
-
{ timeoutMs: GH_TIMEOUT_MS },
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
return { host, slug, ...metadataSchema.parse(JSON.parse(stdout)) };
|
|
114
|
-
} catch (error) {
|
|
115
|
-
ghFailure = firstLine(errorMessage(error));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (host !== "github.com")
|
|
119
|
-
throw new ReviewInputError(
|
|
120
|
-
`Could not read ${url}: gh failed (${ghFailure}). Run \`gh auth login --hostname ${host}\` with an account that can read the repository, then retry.`,
|
|
121
|
-
409,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
let response: Response;
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
response = await deps.fetch(
|
|
128
|
-
`https://api.github.com/repos/${slug}/pulls/${number}`,
|
|
129
|
-
{
|
|
130
|
-
headers: {
|
|
131
|
-
Accept: "application/vnd.github+json",
|
|
132
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
133
|
-
},
|
|
134
|
-
signal: AbortSignal.timeout(API_TIMEOUT_MS),
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
throw unreadable(
|
|
139
|
-
url,
|
|
140
|
-
ghFailure,
|
|
141
|
-
`the GitHub API is unreachable (${firstLine(errorMessage(error))})`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (response.status === 404)
|
|
146
|
-
throw new ReviewInputError(
|
|
147
|
-
`${url} was not found: it does not exist, or it is private and gh is not signed in to an account that can read it (run \`gh auth status\`). gh said: ${ghFailure}`,
|
|
148
|
-
404,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
if (response.status === 403 || response.status === 429)
|
|
152
|
-
throw unreadable(
|
|
153
|
-
url,
|
|
154
|
-
ghFailure,
|
|
155
|
-
response.headers.get("x-ratelimit-remaining") === "0" ||
|
|
156
|
-
response.status === 429
|
|
157
|
-
? "the unauthenticated GitHub API rate limit is exhausted"
|
|
158
|
-
: "the GitHub API refused the request",
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
if (!response.ok)
|
|
162
|
-
throw unreadable(
|
|
163
|
-
url,
|
|
164
|
-
ghFailure,
|
|
165
|
-
`the GitHub API returned HTTP ${response.status}`,
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const parsed = z
|
|
169
|
-
.object({
|
|
170
|
-
number: z.number(),
|
|
171
|
-
title: z.string(),
|
|
172
|
-
base: z.object({ ref: z.string().min(1), sha: z.string() }),
|
|
173
|
-
})
|
|
174
|
-
.safeParse(await response.json().catch(() => undefined));
|
|
175
|
-
|
|
176
|
-
if (!parsed.success)
|
|
177
|
-
throw unreadable(url, ghFailure, "the GitHub API response was malformed");
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
host,
|
|
181
|
-
slug,
|
|
182
|
-
number: parsed.data.number,
|
|
183
|
-
title: parsed.data.title,
|
|
184
|
-
baseRefName: parsed.data.base.ref,
|
|
185
|
-
baseRefOid: parsed.data.base.sha,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Remotes by the host and owner/repo their configured URL names. The
|
|
190
|
-
* configured URL, not the insteadOf rewrite: a mirror still names its repo. */
|
|
191
|
-
export async function githubRemotes(
|
|
71
|
+
/** Remotes by their configured URL, not the insteadOf rewrite: a mirror
|
|
72
|
+
* still names its repo. */
|
|
73
|
+
export async function remoteUrls(
|
|
192
74
|
gitDir: string,
|
|
193
75
|
deps: PullRequestDeps,
|
|
194
|
-
): Promise<{ name: string;
|
|
76
|
+
): Promise<{ name: string; url: string }[]> {
|
|
195
77
|
const stdout = await deps
|
|
196
78
|
.run(
|
|
197
79
|
"git",
|
|
@@ -208,23 +90,16 @@ export async function githubRemotes(
|
|
|
208
90
|
|
|
209
91
|
return stdout.split("\n").flatMap((line) => {
|
|
210
92
|
const match = /^remote\.(.+)\.url\s+(\S+)/.exec(line.trim());
|
|
211
|
-
const remote = match && parseGitRemote(match[2]!);
|
|
212
93
|
|
|
213
|
-
return match
|
|
214
|
-
? [{ name: match[1]!, host: remote.host, slug: remote.slug }]
|
|
215
|
-
: [];
|
|
94
|
+
return match ? [{ name: match[1]!, url: match[2]! }] : [];
|
|
216
95
|
});
|
|
217
96
|
}
|
|
218
97
|
|
|
219
98
|
/** Refs Review owns for one PR; the user's branches and bookmarks never move. */
|
|
220
|
-
|
|
221
|
-
host: string;
|
|
222
|
-
slug: string;
|
|
223
|
-
number: number;
|
|
224
|
-
}) {
|
|
99
|
+
function pullRequestRefs(provider: string, pr: PullRequestRecord) {
|
|
225
100
|
// github.com keeps the host-less namespace existing checkouts already hold.
|
|
226
101
|
const repository = `${pr.host === "github.com" ? "" : `${pr.host}/`}${pr.slug}`;
|
|
227
|
-
const prefix = `refs/review
|
|
102
|
+
const prefix = `refs/review/${provider}/${repository.toLowerCase()}/pull/${pr.number}`;
|
|
228
103
|
|
|
229
104
|
return {
|
|
230
105
|
head: `${prefix}/head`,
|
|
@@ -250,12 +125,13 @@ export async function fetchPullRequest(
|
|
|
250
125
|
gitDir: string;
|
|
251
126
|
kind: LocalVcsKind;
|
|
252
127
|
remote: string;
|
|
128
|
+
provider: string;
|
|
253
129
|
pullRequest: PullRequestRecord;
|
|
254
130
|
},
|
|
255
131
|
deps: PullRequestDeps,
|
|
256
132
|
): Promise<{ head: string; base: string }> {
|
|
257
133
|
const { pullRequest: pr } = input;
|
|
258
|
-
const refs = pullRequestRefs(pr);
|
|
134
|
+
const refs = pullRequestRefs(input.provider, pr);
|
|
259
135
|
|
|
260
136
|
const git = (args: string[], timeoutMs = LOCAL_TIMEOUT_MS) =>
|
|
261
137
|
deps
|
|
@@ -298,14 +174,14 @@ export async function fetchPullRequest(
|
|
|
298
174
|
|
|
299
175
|
try {
|
|
300
176
|
await fetchRefs(
|
|
301
|
-
|
|
177
|
+
`+${pr.headRef}:${refs.head}`,
|
|
302
178
|
`+refs/heads/${pr.baseRefName}:${refs.base}`,
|
|
303
179
|
);
|
|
304
180
|
baseTip = await commit(refs.base);
|
|
305
181
|
} catch (error) {
|
|
306
182
|
// The base branch may be gone; the head and the frozen base suffice.
|
|
307
183
|
try {
|
|
308
|
-
await fetchRefs(
|
|
184
|
+
await fetchRefs(`+${pr.headRef}:${refs.head}`);
|
|
309
185
|
} catch {
|
|
310
186
|
throw new ReviewInputError(
|
|
311
187
|
`Could not fetch PR #${pr.number} from remote "${input.remote}" (${pr.slug}). Check \`git fetch ${input.remote}\` works with your Git credentials, then retry. Git said: ${firstLine(errorMessage(error))}`,
|
|
@@ -377,13 +253,6 @@ async function indexForJj(
|
|
|
377
253
|
}
|
|
378
254
|
}
|
|
379
255
|
|
|
380
|
-
function
|
|
381
|
-
return new ReviewInputError(
|
|
382
|
-
`Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
|
|
383
|
-
409,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function firstLine(text: string) {
|
|
256
|
+
export function firstLine(text: string) {
|
|
388
257
|
return text.split("\n")[0]!.slice(0, 300);
|
|
389
258
|
}
|
package/dist/profile-j97aRTS7.js
DELETED