prreviewbuddy 0.93.0 → 0.95.2
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/CHANGELOG.md +80 -0
- package/dist/main.js +596 -135
- package/dist/{mcp_tools-DbFR9Z90.js → mcp_tools-DDbcXys2.js} +1022 -463
- package/dist/server.js +147 -371
- package/package.json +1 -1
- package/static/{chunk-CIJm20ER.js → chunk-CQIPqX7U.js} +1 -1
- package/static/chunk-Cs33-tV3.js +1 -0
- package/static/{chunk-Ahis0VaG.js → chunk-Cum6sHaC.js} +1 -1
- package/static/{chunk-Cg4cN8Vk.js → chunk-D1LBeOJc.js} +1 -1
- package/static/help.js +1 -1
- package/static/notifications.js +1 -1
- package/static/reviews.js +1 -1
- package/static/runs.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +11 -0
- package/static/workspace.js +1 -1
- package/static/chunk-DRtin0n-.js +0 -1
package/dist/main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import {
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { $a as TargetError, $r as findExistingReview, $t as statedWorkspacePort, An as BUILD_VERSION, Br as modelHelpLines, Bt as readServerRecord, Ci as driverGone, Di as followedRefName, Dn as recordTelemetryUploadConsent, Dt as endAskRequest, E as relativeTime, En as deliver$1, Eo as cleanName, Fi as loadWorkspace, Fo as STORE_ROOT, Ft as readIndexToken, Gt as stopServer, Hi as reviewedCommit, I as collectionHash, Ii as matchingWorkspaceIds, It as bootstrapUrl, Ja as WITHHELD_REASON, Jr as readMarker, Jt as DEFAULT_WORKSPACE_PORT, Ki as workspaceRevision, Kt as workspaceUrl, Na as findingStatus, Nt as readAskRequest, On as telemetryUploadConsent, Ot as forgetAskRequest, Pi as lineagePosition, Pt as recordAskAccepted, Qr as describeSubject, Ri as positionInLineage, Rt as ensureServer, Sa as explanationOf, Si as clearClaim, Sn as append, St as findingRef, T as runBatch, Tn as stamp, Tt as reviewTag, Ui as reviewedRepositories, Vi as recentWorkspaces, Vt as reviewsUrl, Wi as summarise, Wr as MANAGED_ROOT, Xa as resolveTarget, Xt as MIN_WORKSPACE_PORT, Ya as kissOutcome, Yr as removeWorktree, Yt as MAX_WORKSPACE_PORT, Zr as describeMovement, Zt as parseWorkspacePort, _a as reviewerDispositions, _n as prepareReview, _o as agentIdentityOf, _t as modelProvenanceOf, ai as forgeResolver, ao as git, bi as progressSteps, bo as parkedReason, ci as allJobs, co as resolveModel, do as readAgentPreference, dt as RUNNER_KINDS, en as writePortPreference, et as deliver, fa as isIssueOutstanding, fi as isTerminal$1, fn as UpdateAlreadyRunningError, fo as writeAgentPreference, gi as PHASES, gt as applyUpdate, hn as failureOf, ho as agentById, ht as spawnRunner, i as deleteReview$1, ja as findingAssessment, jn as PACKAGE_NAME, jt as isTerminalAsk, kn as telemetryUploadEnabled, kt as interruption, li as attemptIdOf, lo as agentIdOf, lt as queueForReview, mn as runningJobs, mo as AGENT_IDS, mt as runDetached, no as AmbiguousRefError, nt as missedDelivery, o as liveJobsFor, pa as isQuestionOutstanding, pi as loadJob, pn as liveUpdateFor, po as CONFIG_PATH, rt as openDestination, s as wasBlocked, sn as parkedUpdateRefusal, so as resolveAgent, t as MCP_SERVER_INFO_NAME, tn as COMMIT_REVIEW_CANNOT_UPDATE, tt as isDelivered, u as recordKissRun, va as verdictClosure, vn as settleReview, vo as detectAgents, wi as isClaimed, x as batchIdentity, xi as purposeOf, xn as startJob, xo as runnableAgents, yn as reanalyseReview, yo as isAgentId, zt as healthyPort } from "./mcp_tools-DDbcXys2.js";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
4
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
|
-
import {
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import process$1, { platform } from "node:process";
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
@@ -61,7 +61,9 @@ var VALUED = {
|
|
|
61
61
|
"--review",
|
|
62
62
|
"--audience",
|
|
63
63
|
"--lens",
|
|
64
|
-
"--concurrency"
|
|
64
|
+
"--concurrency",
|
|
65
|
+
"--collection",
|
|
66
|
+
"--new-collection"
|
|
65
67
|
],
|
|
66
68
|
open: [],
|
|
67
69
|
agents: [],
|
|
@@ -94,6 +96,7 @@ var BARE = {
|
|
|
94
96
|
"--resume",
|
|
95
97
|
"--yes",
|
|
96
98
|
"--queue",
|
|
99
|
+
"--also-queue",
|
|
97
100
|
"--json",
|
|
98
101
|
"--no-wait"
|
|
99
102
|
],
|
|
@@ -292,6 +295,11 @@ function parse$2(argv) {
|
|
|
292
295
|
};
|
|
293
296
|
const first = positional[0];
|
|
294
297
|
if (head === "review") {
|
|
298
|
+
const destination = refuseDestination(prs, values, flags);
|
|
299
|
+
if (destination) return {
|
|
300
|
+
name: "error",
|
|
301
|
+
message: destination
|
|
302
|
+
};
|
|
295
303
|
const batch = refuseBatch(prs, values, flags);
|
|
296
304
|
if (batch) return {
|
|
297
305
|
name: "error",
|
|
@@ -309,7 +317,9 @@ function parse$2(argv) {
|
|
|
309
317
|
...values["--agent"] ? { agentId: values["--agent"] } : {},
|
|
310
318
|
...values["--model"] ? { requestedModel: values["--model"] } : {},
|
|
311
319
|
...flags.has("--fresh") ? { operation: "fresh" } : {},
|
|
312
|
-
...flags.has("--queue") ? { queue: true } : {}
|
|
320
|
+
...flags.has("--queue") ? { queue: true } : {},
|
|
321
|
+
...collectionOf(values, flags),
|
|
322
|
+
...flags.has("--json") && (values["--collection"] !== void 0 || values["--new-collection"] !== void 0) ? { json: true } : {}
|
|
313
323
|
};
|
|
314
324
|
const target = Object.keys(TARGETS).find((flag) => values[flag]);
|
|
315
325
|
const operation = Object.keys(OPERATIONS).find((flag) => flags.has(flag));
|
|
@@ -324,6 +334,7 @@ function parse$2(argv) {
|
|
|
324
334
|
...values["--audience"] ? { audience: values["--audience"] } : {},
|
|
325
335
|
...flags.has("--yes") ? { yes: true } : {},
|
|
326
336
|
...flags.has("--queue") ? { queue: true } : {},
|
|
337
|
+
...collectionOf(values, flags),
|
|
327
338
|
...flags.has("--json") ? { json: true } : {},
|
|
328
339
|
...flags.has("--no-wait") ? { noWait: true } : {}
|
|
329
340
|
};
|
|
@@ -368,6 +379,48 @@ function parse$2(argv) {
|
|
|
368
379
|
...first ? { id: first } : {}
|
|
369
380
|
};
|
|
370
381
|
}
|
|
382
|
+
/** The collection fields, present only when a collection option was typed. */
|
|
383
|
+
function collectionOf(values, flags) {
|
|
384
|
+
const name = values["--collection"];
|
|
385
|
+
const create = values["--new-collection"];
|
|
386
|
+
if (name === void 0 && create === void 0) return {};
|
|
387
|
+
return {
|
|
388
|
+
collection: name !== void 0 ? { name: cleanName(name) } : { create: cleanName(create) },
|
|
389
|
+
...flags.has("--also-queue") ? { alsoQueue: true } : {}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* What a collection option cannot be asked with, or null. Everything that needs no store is
|
|
394
|
+
* checked here, so a refusal never follows a write: whether a name exists, or is taken, is the one
|
|
395
|
+
* thing left to `openDestination`, and it too runs before any pull request is touched.
|
|
396
|
+
*
|
|
397
|
+
* A collection holds the reviews a batch of pull requests starts or finds, so it goes with `--pr`
|
|
398
|
+
* and with nothing that acts on one review, asks a different question of it, or answers in JSON.
|
|
399
|
+
*/
|
|
400
|
+
function refuseDestination(prs, values, flags) {
|
|
401
|
+
const name = values["--collection"];
|
|
402
|
+
const create = values["--new-collection"];
|
|
403
|
+
const option = name !== void 0 ? "--collection" : create !== void 0 ? "--new-collection" : null;
|
|
404
|
+
if (name !== void 0 && create !== void 0) return "`--collection` adds the reviews to a collection you have, and `--new-collection` makes a new one for them. Keep the one you meant.";
|
|
405
|
+
if (!option) {
|
|
406
|
+
if (flags.has("--also-queue")) return "`--also-queue` puts the reviews on your review queue as well as in a collection, and no collection is named. Add `--collection <name>` or `--new-collection <name>`, or use `--queue` to put them on the queue alone.";
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const typed = name ?? create;
|
|
410
|
+
if (flags.has("--queue")) return `With \`${option}\`, the reviews go into the collection only, and the queue is asked for with \`--also-queue\`. Use \`--also-queue\` in place of \`--queue\`.`;
|
|
411
|
+
if (cleanName(typed) === "") return `\`${option}\` needs a collection name after it.`;
|
|
412
|
+
if (create !== void 0 && cleanName(create).length > 60) return `\`--new-collection\` takes a name of 60 characters at most, and this one has ${cleanName(create).length}.`;
|
|
413
|
+
if (prs.length === 0) return `\`${option}\` collects the reviews of pull requests named with \`--pr\`, and this names none. Name them: \`prreviewbuddy review --pr <url> <url> ${option} "${cleanName(typed)}"\`.`;
|
|
414
|
+
const operation = [
|
|
415
|
+
"--update",
|
|
416
|
+
"--reanalyse",
|
|
417
|
+
"--delete",
|
|
418
|
+
"--resume"
|
|
419
|
+
].find((flag) => flags.has(flag));
|
|
420
|
+
if (operation) return `\`${option}\` collects the reviews a command starts or finds, and \`${operation}\` acts on one review that already exists. Run them as separate commands.`;
|
|
421
|
+
if (values["--lens"] || values["--audience"]) return `\`${option}\` collects standard reviews, and \`${values["--lens"] ? "--lens" : "--audience"}\` asks for a different one. Drop one of them.`;
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
371
424
|
/**
|
|
372
425
|
* What several pull requests at once cannot be asked for, or null.
|
|
373
426
|
*
|
|
@@ -382,6 +435,7 @@ function refuseBatch(prs, values, flags) {
|
|
|
382
435
|
if (prs.length < 2) return "`--concurrency` says how many of several pull requests are reviewed at once, and this names one at most. Give `--pr` two or more URLs, or drop `--concurrency`.";
|
|
383
436
|
}
|
|
384
437
|
if (prs.length < 2) return null;
|
|
438
|
+
if (flags.has("--no-wait")) return "`--no-wait` hands one review to a background runner, and several pull requests are run here until every review has ended. Drop `--no-wait`, or run each pull request on its own with `prreviewbuddy review --pr <url> --no-wait`.";
|
|
385
439
|
const repeated = prs.find((url, index) => prs.indexOf(url) !== index);
|
|
386
440
|
if (repeated) return `\`${repeated}\` is named twice. Each pull request is reviewed once, so name it once.`;
|
|
387
441
|
const operation = [
|
|
@@ -461,9 +515,15 @@ function refuseCombination(values, flags) {
|
|
|
461
515
|
* date, and resume one that stopped. The rest are refused by name rather than half-supported: a
|
|
462
516
|
* lens and a batch answer in shapes the contract does not have yet, `--delete` asks a question, and
|
|
463
517
|
* `--reanalyse` replaces the analysis an agent's finding ids belong to.
|
|
518
|
+
*
|
|
519
|
+
* `--no-wait` says whether to wait and `--json` how to answer, so each goes without the other. It
|
|
520
|
+
* covers the same operations, because those are the ones the background runner drives.
|
|
464
521
|
*/
|
|
465
522
|
function refuseMachineFlags(values, flags) {
|
|
466
|
-
if (flags.has("--no-wait") && !flags.has("--json"))
|
|
523
|
+
if (flags.has("--no-wait") && !flags.has("--json")) {
|
|
524
|
+
if (values["--lens"]) return "`--no-wait` does not cover lens reviews yet. Run `--lens` without `--no-wait`.";
|
|
525
|
+
for (const flag of ["--delete", "--reanalyse"]) if (flags.has(flag)) return `\`--no-wait\` covers starting, updating and resuming a review, and not \`${flag}\`. Run \`${flag}\` without \`--no-wait\`.`;
|
|
526
|
+
}
|
|
467
527
|
if (flags.has("--resume") && !values["--review"]) return "`--resume` carries on one review whose analysis stopped, so it needs `--review <review>`. A plain `prreviewbuddy review` already resumes the review of the branch you are on.";
|
|
468
528
|
if (!flags.has("--json")) return null;
|
|
469
529
|
if (values["--lens"]) return "`--json` does not cover lens reviews yet. Run `--lens` without `--json`.";
|
|
@@ -1388,17 +1448,9 @@ async function statusNow(id, request, deps) {
|
|
|
1388
1448
|
...request.askId ? { ask: await askNow(workspace, request.askId, deps) } : {}
|
|
1389
1449
|
};
|
|
1390
1450
|
}
|
|
1391
|
-
|
|
1392
|
-
* Start a review, or resume one, and hand the work to a detached runner.
|
|
1393
|
-
*
|
|
1394
|
-
* Never a duplicate: a target that already has a healthy review gets it back with `started: false`,
|
|
1395
|
-
* which is the default operation's rule at the terminal too. `fresh` asks for a second one anyway.
|
|
1396
|
-
* Named by `reviewId`, it resumes that review's own stopped analysis and nothing else, so recovering
|
|
1397
|
-
* from a dead runner can never start an unrelated review.
|
|
1398
|
-
*/
|
|
1399
|
-
async function startReview(input, deps = defaultDeps) {
|
|
1451
|
+
async function startReview(input, deps = defaultDeps, hooks = {}) {
|
|
1400
1452
|
guard("run", deps.env);
|
|
1401
|
-
if (input.reviewId) return resumeReview(input.reviewId, input, deps);
|
|
1453
|
+
if (input.reviewId) return resumeReview(input.reviewId, input, deps, hooks);
|
|
1402
1454
|
let target;
|
|
1403
1455
|
try {
|
|
1404
1456
|
target = await resolveTarget({
|
|
@@ -1415,7 +1467,8 @@ async function startReview(input, deps = defaultDeps) {
|
|
|
1415
1467
|
if (!input.fresh) {
|
|
1416
1468
|
const existing = await findExistingReview(target);
|
|
1417
1469
|
if (existing) {
|
|
1418
|
-
if (failureOf(existing.id)) return resumeReview(existing.id, input, deps);
|
|
1470
|
+
if (failureOf(existing.id)) return resumeReview(existing.id, input, deps, hooks);
|
|
1471
|
+
hooks.ready?.();
|
|
1419
1472
|
return {
|
|
1420
1473
|
review: await getReview(existing.id, deps),
|
|
1421
1474
|
job: null,
|
|
@@ -1426,6 +1479,7 @@ async function startReview(input, deps = defaultDeps) {
|
|
|
1426
1479
|
}
|
|
1427
1480
|
const resolution = await resolveAgent({ flag: input.agent });
|
|
1428
1481
|
if (resolution.kind !== "resolved") throw new ApiError("refused", deps.explainAgent(resolution));
|
|
1482
|
+
hooks.ready?.();
|
|
1429
1483
|
const prepared = await prepareReview(target, resolution.agent.id, true, input.model, input.source);
|
|
1430
1484
|
return handOff(prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id, input.source, deps);
|
|
1431
1485
|
}
|
|
@@ -1438,10 +1492,11 @@ function declinedSelection(input, what, running = {}) {
|
|
|
1438
1492
|
if (!declined.length) return "";
|
|
1439
1493
|
return ` The ${declined.join(" and ")} you named ${declined.length > 1 ? "were" : "was"} not used: the ${what} keeps its own. Pass fresh to analyse it again with them.`;
|
|
1440
1494
|
}
|
|
1441
|
-
async function resumeReview(id, input, deps) {
|
|
1495
|
+
async function resumeReview(id, input, deps, hooks = {}) {
|
|
1442
1496
|
const workspace = findReview(id);
|
|
1443
1497
|
const failure = failureOf(workspace.id);
|
|
1444
1498
|
if (!failure) {
|
|
1499
|
+
hooks.ready?.();
|
|
1445
1500
|
const job = workspace.jobId ? loadJob(workspace.jobId) : null;
|
|
1446
1501
|
const running = job !== null && !isTerminal$1(job) && isClaimed(job.id);
|
|
1447
1502
|
return {
|
|
@@ -1456,6 +1511,7 @@ async function resumeReview(id, input, deps) {
|
|
|
1456
1511
|
} catch (error) {
|
|
1457
1512
|
throw new ApiError("refused", error instanceof Error ? error.message : String(error));
|
|
1458
1513
|
}
|
|
1514
|
+
hooks.ready?.();
|
|
1459
1515
|
const handed = await handOff(workspace.id, input.source, deps);
|
|
1460
1516
|
const note = declinedSelection(input, "resumed job", {
|
|
1461
1517
|
agent: failure.agentId,
|
|
@@ -1683,85 +1739,6 @@ function pidAlive(pid) {
|
|
|
1683
1739
|
}
|
|
1684
1740
|
}
|
|
1685
1741
|
//#endregion
|
|
1686
|
-
//#region src/exit.ts
|
|
1687
|
-
/**
|
|
1688
|
-
* What a command's exit status means, for every command.
|
|
1689
|
-
*
|
|
1690
|
-
* One table, the contract's own (`EXIT_CODES`), so a script reads the same number for the same
|
|
1691
|
-
* failure whether or not it asked for `--json`, and an MCP error and an exit status name the same
|
|
1692
|
-
* thing. A review that finds problems is a review that worked, and exits 0: these say whether the
|
|
1693
|
-
* command did what was asked, never what the code under review is like.
|
|
1694
|
-
*
|
|
1695
|
-
* The sentence printed beside a failure is unchanged by any of this. The number only sorts it.
|
|
1696
|
-
*/
|
|
1697
|
-
var EXIT = {
|
|
1698
|
-
ok: EXIT_CODES.ok,
|
|
1699
|
-
/** Something nobody anticipated. The message is still printed. */
|
|
1700
|
-
unexpected: EXIT_CODES.internal,
|
|
1701
|
-
/** The command, a flag or an argument was not one this can act on. */
|
|
1702
|
-
usage: EXIT_CODES.invalid_argument,
|
|
1703
|
-
/** No review, job or finding by that name, or a prefix that names more than one. */
|
|
1704
|
-
notFound: EXIT_CODES.not_found,
|
|
1705
|
-
/** Understood, and declined: already running, nothing to update, an agent that cannot run here, a question nobody answered. */
|
|
1706
|
-
refused: EXIT_CODES.refused,
|
|
1707
|
-
/** The analysis, update or question ran and failed. */
|
|
1708
|
-
failed: EXIT_CODES.analysis_failed,
|
|
1709
|
-
/** The local workspace server could not be started or reached. */
|
|
1710
|
-
unavailable: EXIT_CODES.service_unavailable,
|
|
1711
|
-
timeout: EXIT_CODES.timeout,
|
|
1712
|
-
/** Started from inside an agent run, which cannot start reviews. */
|
|
1713
|
-
recursion: EXIT_CODES.recursion_blocked
|
|
1714
|
-
};
|
|
1715
|
-
function exitFor(code) {
|
|
1716
|
-
return EXIT_CODES[code];
|
|
1717
|
-
}
|
|
1718
|
-
/** A failure that reached the top unclassified. Naming a target that does not resolve is a usage error, not a crash. */
|
|
1719
|
-
function exitForThrown(error) {
|
|
1720
|
-
if (error instanceof TargetError || error instanceof AmbiguousRefError) return EXIT.usage;
|
|
1721
|
-
return EXIT.unexpected;
|
|
1722
|
-
}
|
|
1723
|
-
//#endregion
|
|
1724
|
-
//#region src/write.ts
|
|
1725
|
-
/**
|
|
1726
|
-
* Where the two kinds of output go, decided once.
|
|
1727
|
-
*
|
|
1728
|
-
* Links and results to stdout, everything else to stderr, so `prreviewbuddy review | pbcopy`
|
|
1729
|
-
* copies a URL and progress still reaches the terminal it was meant for.
|
|
1730
|
-
*/
|
|
1731
|
-
function out(text) {
|
|
1732
|
-
process.stdout.write(`${text}\n`);
|
|
1733
|
-
}
|
|
1734
|
-
function err(text) {
|
|
1735
|
-
process.stderr.write(`${text}\n`);
|
|
1736
|
-
}
|
|
1737
|
-
//#endregion
|
|
1738
|
-
//#region src/json.ts
|
|
1739
|
-
/**
|
|
1740
|
-
* The one document a `--json` command writes, and how it answers a failure.
|
|
1741
|
-
*
|
|
1742
|
-
* Exactly one JSON value on stdout, success or not, so a caller can always parse what it gets and
|
|
1743
|
-
* branch on `error.code` rather than on a sentence. The sentence still goes to stderr, where a
|
|
1744
|
-
* person watching the command reads it.
|
|
1745
|
-
*/
|
|
1746
|
-
function emit(document) {
|
|
1747
|
-
out(JSON.stringify({
|
|
1748
|
-
schema_version: "1",
|
|
1749
|
-
...document
|
|
1750
|
-
}));
|
|
1751
|
-
}
|
|
1752
|
-
/** A failure, as a document on stdout and a sentence on stderr, and the exit status it maps to. */
|
|
1753
|
-
function emitError(error) {
|
|
1754
|
-
const document = errorDocument(error);
|
|
1755
|
-
out(JSON.stringify(document));
|
|
1756
|
-
err(document.error.message);
|
|
1757
|
-
return error instanceof ApiError ? exitFor(error.code) : exitForThrown(error);
|
|
1758
|
-
}
|
|
1759
|
-
/** The same failure without `--json`: the sentence alone, and the same exit status. */
|
|
1760
|
-
function reportError(error) {
|
|
1761
|
-
err(error instanceof Error ? error.message : String(error));
|
|
1762
|
-
return error instanceof ApiError ? exitFor(error.code) : exitForThrown(error);
|
|
1763
|
-
}
|
|
1764
|
-
//#endregion
|
|
1765
1742
|
//#region src/text.ts
|
|
1766
1743
|
/**
|
|
1767
1744
|
* The agent names, in the help, taken from the registry that actually has them.
|
|
@@ -1918,15 +1895,27 @@ Options
|
|
|
1918
1895
|
reviewer (somebody reviewing it). Required with --lens kiss
|
|
1919
1896
|
--concurrency <n> with several --pr URLs, how many are reviewed at once. Default 2
|
|
1920
1897
|
--queue put each review on your review queue as soon as it exists
|
|
1898
|
+
--collection <name> with --pr, put each review in a collection you have, and not on
|
|
1899
|
+
the queue. Add --also-queue for both
|
|
1900
|
+
--new-collection <name> with --pr, make a collection with that name and put each review in it
|
|
1901
|
+
--also-queue with a collection option, put each review on your review queue too
|
|
1921
1902
|
--json write one JSON document to stdout and nothing else: the review
|
|
1922
|
-
and its job, once the job ends. Never asks a question
|
|
1923
|
-
|
|
1924
|
-
|
|
1903
|
+
and its job, once the job ends. Never asks a question. With a
|
|
1904
|
+
collection option, also the collection and where each pull
|
|
1905
|
+
request went, for one --pr URL or several
|
|
1906
|
+
--no-wait answer as soon as the job has started, rather than waiting for
|
|
1907
|
+
the review. It carries on in the background after this exits;
|
|
1908
|
+
check on it with \`prreviewbuddy status <review>\`. With --json,
|
|
1909
|
+
the review and its job as a document
|
|
1925
1910
|
|
|
1926
1911
|
A target and an operation go together:
|
|
1927
1912
|
prreviewbuddy review --branch feat/foo --update
|
|
1928
1913
|
prreviewbuddy review --base origin/main
|
|
1929
1914
|
prreviewbuddy review --pr <url> <url> --queue
|
|
1915
|
+
prreviewbuddy review --pr <url> <url> --collection "Release review"
|
|
1916
|
+
prreviewbuddy review --pr <url> <url> --new-collection "Friday batch"
|
|
1917
|
+
prreviewbuddy review --pr <url> <url> --collection "Release review" --also-queue
|
|
1918
|
+
prreviewbuddy review --pr <url> --no-wait
|
|
1930
1919
|
prreviewbuddy review --lens kiss --audience author
|
|
1931
1920
|
prreviewbuddy review --review <review> --delete
|
|
1932
1921
|
|
|
@@ -2437,7 +2426,7 @@ var BATCH_QUEUED = "Queued";
|
|
|
2437
2426
|
var BATCH_LOOKING_UP = "Looking up the pull request";
|
|
2438
2427
|
var BATCH_STOPPING = "Stopping. Reviews already running are being stopped, and the rest will not start.";
|
|
2439
2428
|
/** The end of a batch: one line per pull request, with its link, and the index last. */
|
|
2440
|
-
function batchFinished(rows, index) {
|
|
2429
|
+
function batchFinished(rows, index, destination) {
|
|
2441
2430
|
const width = Math.max(...rows.map((row) => row.label.length));
|
|
2442
2431
|
const tally = Object.keys(BATCH_WORDS).map((outcome) => [outcome, rows.filter((row) => row.outcome === outcome).length]).filter(([, n]) => n > 0).map(([outcome, n]) => `${n} ${BATCH_WORDS[outcome].toLowerCase()}`);
|
|
2443
2432
|
const lines = ["", `${count(rows.length, "pull request")}: ${tally.join(", ")}`];
|
|
@@ -2447,7 +2436,8 @@ function batchFinished(rows, index) {
|
|
|
2447
2436
|
if (row.detail && row.url) lines.push(`${" ".repeat(lead.length)}${row.url}`);
|
|
2448
2437
|
}
|
|
2449
2438
|
const unfinished = rows.some((row) => row.outcome === "stopped" || row.outcome === "not-started" || row.outcome === "failed" && row.url);
|
|
2450
|
-
lines.push(...
|
|
2439
|
+
lines.push(...batchCollectionLines(rows, destination));
|
|
2440
|
+
lines.push(...batchQueueLines(rows, destination?.strict === true && destination.queue));
|
|
2451
2441
|
if (unfinished) lines.push("", "Run the same command again to carry on with the reviews that did not finish, from where each got to.");
|
|
2452
2442
|
const all = indexLinks(index);
|
|
2453
2443
|
if (all) lines.push("", all);
|
|
@@ -2460,18 +2450,129 @@ function batchFinished(rows, index) {
|
|
|
2460
2450
|
* ended, and the queue is where the reviewer goes next, so it is said once, together. What was left
|
|
2461
2451
|
* out is named with its reason, because each reason has a different way round it.
|
|
2462
2452
|
*/
|
|
2463
|
-
function batchQueueLines(rows) {
|
|
2453
|
+
function batchQueueLines(rows, strict = false) {
|
|
2464
2454
|
const asked = rows.filter((row) => row.queue !== void 0);
|
|
2465
|
-
|
|
2455
|
+
const never = strict ? rows.filter((row) => row.queue === void 0 && row.outcome !== "duplicate") : [];
|
|
2456
|
+
if (asked.length === 0 && never.length === 0) return [];
|
|
2466
2457
|
const lines = [""];
|
|
2467
2458
|
const queued = asked.filter((row) => row.queue === "added" || row.queue === "already");
|
|
2468
2459
|
if (queued.length > 0) lines.push(`In your review queue: ${queued.map((row) => row.label).join(", ")}`);
|
|
2469
|
-
for (const row of
|
|
2460
|
+
for (const row of rows) {
|
|
2461
|
+
if (row.queue === void 0) {
|
|
2462
|
+
if (never.includes(row)) lines.push(`Not added to your review queue: ${row.label}, because ${noReviewReason(row.outcome)}.`);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2470
2465
|
if (row.queue === "added" || row.queue === "already") continue;
|
|
2471
2466
|
lines.push(`Not added to your review queue: ${notQueuedBecause(row.label, row.queue)}`);
|
|
2472
2467
|
}
|
|
2473
2468
|
return lines;
|
|
2474
2469
|
}
|
|
2470
|
+
/** Why a pull request has no review to put anywhere: it ended before one existed. */
|
|
2471
|
+
function noReviewReason(outcome) {
|
|
2472
|
+
return outcome === "not-started" ? "it did not start" : "it failed before it had a review";
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* What the collection holds of a batch, what it is missing and why, and where to open it.
|
|
2476
|
+
*
|
|
2477
|
+
* A duplicate is never named: the pull request it repeats is the one in the collection. A collection
|
|
2478
|
+
* this command made is named with the flag that adds to it, because running the same command again
|
|
2479
|
+
* would refuse the name as taken, by this very command.
|
|
2480
|
+
*/
|
|
2481
|
+
function batchCollectionLines(rows, destination) {
|
|
2482
|
+
const collection = destination?.collection;
|
|
2483
|
+
if (!collection) return [];
|
|
2484
|
+
const lines = [""];
|
|
2485
|
+
const inside = rows.filter((row) => isDelivered(row.collection));
|
|
2486
|
+
if (inside.length > 0) lines.push(`In ${collection.name}: ${inside.map((row) => row.label).join(", ")}`);
|
|
2487
|
+
let missed = false;
|
|
2488
|
+
for (const row of rows) {
|
|
2489
|
+
if (row.outcome === "duplicate" || isDelivered(row.collection)) continue;
|
|
2490
|
+
missed = true;
|
|
2491
|
+
lines.push(row.collection === void 0 ? `Not added to ${collection.name}: ${row.label}, because ${noReviewReason(row.outcome)}.` : `Not added to ${collection.name}: ${row.label}, because ${collectReason(row.collection)}.`);
|
|
2492
|
+
}
|
|
2493
|
+
if (collection.url) lines.push(`Open collection: ${collection.url}`);
|
|
2494
|
+
if (collection.created) lines.push(createdCollectionLine(collection.name, missed || rows.some((row) => row.queue !== void 0 && !isDelivered(row.queue))));
|
|
2495
|
+
return lines;
|
|
2496
|
+
}
|
|
2497
|
+
/** Why a review is not in a collection, as the end of a sentence. */
|
|
2498
|
+
function collectReason(result) {
|
|
2499
|
+
switch (result) {
|
|
2500
|
+
case "missing": return "the collection no longer exists";
|
|
2501
|
+
case "full": return `the collection is full. It holds 500 reviews at most`;
|
|
2502
|
+
case "gone": return "its review could not be found";
|
|
2503
|
+
default: return "the collection could not be saved";
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* `review --no-wait` without `--json`: what was handed to the background runner, and the command
|
|
2508
|
+
* that follows it. The runner is detached, so the terminal can close; `status` takes the whole id.
|
|
2509
|
+
*
|
|
2510
|
+
* Said from the job's state, not from whether anything was asked to start: an update can end before
|
|
2511
|
+
* this answers, most often with nothing new to reassess, and then the service's `note` says how.
|
|
2512
|
+
* Only an update's note is shown. The others are written for MCP callers and name its tools, and
|
|
2513
|
+
* what they say about a declined agent or model is already said on stderr.
|
|
2514
|
+
*
|
|
2515
|
+
* A review that was found rather than started comes with no job, so whether it is still being made
|
|
2516
|
+
* is read from the review's own state.
|
|
2517
|
+
*/
|
|
2518
|
+
function reviewInBackground(record, operation) {
|
|
2519
|
+
const { review, job } = record;
|
|
2520
|
+
const repository = review.repository;
|
|
2521
|
+
const subject = repository.target_type === "pull-request" && repository.pr_url ? repository.pr_url : repository.target_type === "commit" ? `commit ${repository.head_sha.slice(0, 7)}` : repository.branch;
|
|
2522
|
+
const live = job !== null && (job.state === "queued" || job.state === "running");
|
|
2523
|
+
const updating = operation === "update" || job?.purpose === "update";
|
|
2524
|
+
let head;
|
|
2525
|
+
if (updating) head = live ? `Started updating the review of ${subject}.` : job ? `The update of the review of ${subject} ended before this could answer: ${job.state}.` : `The review of ${subject} was not updated.`;
|
|
2526
|
+
else if (!record.started) {
|
|
2527
|
+
const beingMade = review.state === "analysing";
|
|
2528
|
+
head = operation === "resume" ? beingMade ? `The review of ${subject} is being made right now, so there is nothing to resume.` : `The review of ${subject} has no stopped analysis to resume.` : beingMade ? `A review of ${subject} is already being made, so nothing new was started.` : `A review of ${subject} already exists, so nothing new was started.`;
|
|
2529
|
+
} else {
|
|
2530
|
+
const verb = operation === "resume" ? "Resumed" : "Started";
|
|
2531
|
+
head = live ? `${verb} the analysis of ${subject}.` : `${verb} the analysis of ${subject}, and it ended before this could answer: ${job ? job.state : "no job"}.`;
|
|
2532
|
+
}
|
|
2533
|
+
return [
|
|
2534
|
+
head,
|
|
2535
|
+
...updating && record.note && !live ? [record.note] : [],
|
|
2536
|
+
...live && (record.started || updating) ? ["It carries on in the background, so you can close this terminal."] : [],
|
|
2537
|
+
`Review ${review.id}`,
|
|
2538
|
+
`Job ${job ? job.state : "none running"}`,
|
|
2539
|
+
`Check prreviewbuddy status ${review.id}`,
|
|
2540
|
+
...review.workspace_url ? [`Open ${review.workspace_url}`] : []
|
|
2541
|
+
].join("\n");
|
|
2542
|
+
}
|
|
2543
|
+
/** A collection this command made before it failed, named so a retry goes to it. */
|
|
2544
|
+
function collectionMadeBeforeFailure(name) {
|
|
2545
|
+
return createdCollectionLine(name, true);
|
|
2546
|
+
}
|
|
2547
|
+
function createdCollectionLine(name, missed) {
|
|
2548
|
+
return missed ? `Created collection "${name}". To add what is missing, run the same command with --collection "${name}" in place of --new-collection.` : `Created collection "${name}".`;
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* What a collection did with the one review a single command acted on, and where to open it. To
|
|
2552
|
+
* stderr, like `reviewQueued`. `missed` says whether anything the command asked for was left out,
|
|
2553
|
+
* so a collection this command made is named with the flag that retries against it.
|
|
2554
|
+
*/
|
|
2555
|
+
function reviewCollected(collection, result, missed) {
|
|
2556
|
+
const lines = [result === "added" ? `Added to ${collection.name}.` : result === "already" ? `Already in ${collection.name}.` : `Not added to ${collection.name}: ${collectReason(result)}.`];
|
|
2557
|
+
if (collection.url) lines.push(`Open collection: ${collection.url}`);
|
|
2558
|
+
if (collection.created) lines.push(createdCollectionLine(collection.name, missed));
|
|
2559
|
+
return lines.join("\n");
|
|
2560
|
+
}
|
|
2561
|
+
/** Why a collection option could not be used, before anything was reviewed. */
|
|
2562
|
+
function destinationRefused(refusal) {
|
|
2563
|
+
const after = " Nothing was reviewed.";
|
|
2564
|
+
switch (refusal.reason) {
|
|
2565
|
+
case "missing": {
|
|
2566
|
+
const named = `"${refusal.requested}"`;
|
|
2567
|
+
return `No collection is named ${named}.${refusal.candidates.length > 0 ? ` Your collections: ${refusal.candidates.map((name) => `"${name}"`).join(", ")}.` : " You have no collections yet."} To make one, use --new-collection ${named}.${after}`;
|
|
2568
|
+
}
|
|
2569
|
+
case "duplicate": return `A collection named "${refusal.requested}" already exists. To add to it, use --collection "${refusal.requested}".${after}`;
|
|
2570
|
+
case "empty": return `--new-collection needs a collection name after it.${after}`;
|
|
2571
|
+
case "too-long": return `Use at most 60 characters for the collection name.${after}`;
|
|
2572
|
+
case "full": return `You have 100 collections, which is as many as there can be. Delete one in the workspace, or add to one you have with --collection.${after}`;
|
|
2573
|
+
default: return `The collection could not be saved.${after} Try again.`;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2475
2576
|
function notQueuedBecause(subject, result) {
|
|
2476
2577
|
switch (result) {
|
|
2477
2578
|
case "full": return `${subject}, because the review queue is full. It holds 50 reviews at most.`;
|
|
@@ -2832,9 +2933,165 @@ function askShown(ask) {
|
|
|
2832
2933
|
return `Question ${ask.ask_id}: ${ask.state}${ask.message ? `. ${ask.message}` : ""}`;
|
|
2833
2934
|
}
|
|
2834
2935
|
//#endregion
|
|
2936
|
+
//#region src/commands/destination.ts
|
|
2937
|
+
/**
|
|
2938
|
+
* Where a review command sends what it starts or finds, as the shared rule takes it. The parser has
|
|
2939
|
+
* already refused every combination that cannot be honoured; this only spells what was typed.
|
|
2940
|
+
*
|
|
2941
|
+
* A typed collection is always a name. The workspace sends ids; a person never does, and a name that
|
|
2942
|
+
* happens to look like one is still a name.
|
|
2943
|
+
*/
|
|
2944
|
+
function destinationRequest(command) {
|
|
2945
|
+
const queue = command.collection ? command.alsoQueue === true : command.queue === true;
|
|
2946
|
+
if (command.collection && "name" in command.collection) return {
|
|
2947
|
+
collectionName: command.collection.name,
|
|
2948
|
+
queue
|
|
2949
|
+
};
|
|
2950
|
+
if (command.collection) return {
|
|
2951
|
+
newCollection: command.collection.create,
|
|
2952
|
+
queue
|
|
2953
|
+
};
|
|
2954
|
+
return { queue };
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Settle the destination, or say why not and answer null. Called after every check that can refuse
|
|
2958
|
+
* without writing (the target, the agent), and before any review is prepared, so a refusal there
|
|
2959
|
+
* leaves no empty collection and a refusal here leaves no review.
|
|
2960
|
+
*/
|
|
2961
|
+
function openFor(command, say, open = openDestination) {
|
|
2962
|
+
const opened = open(destinationRequest(command));
|
|
2963
|
+
if (!opened.ok) {
|
|
2964
|
+
say(destinationRefused(opened));
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
return opened.destination;
|
|
2968
|
+
}
|
|
2969
|
+
/** The Reviews page showing one collection, from the index's own link. Null when there is no index. */
|
|
2970
|
+
function collectionUrl(index, id) {
|
|
2971
|
+
return index ? `${index}${collectionHash(id)}` : null;
|
|
2972
|
+
}
|
|
2973
|
+
//#endregion
|
|
2974
|
+
//#region src/commands/destination_json.ts
|
|
2975
|
+
/**
|
|
2976
|
+
* Where a `--collection` or `--new-collection` command with `--json` sent its reviews, as fields of
|
|
2977
|
+
* the one document it writes. The same two fields for one pull request and for several, so an agent
|
|
2978
|
+
* reads both the same way.
|
|
2979
|
+
*
|
|
2980
|
+
* Present only on collection commands. A command without one writes the document it always has.
|
|
2981
|
+
*/
|
|
2982
|
+
function destinationDocument(destination, index) {
|
|
2983
|
+
const { id, name, created } = destination.collection;
|
|
2984
|
+
return {
|
|
2985
|
+
collection: {
|
|
2986
|
+
id,
|
|
2987
|
+
name,
|
|
2988
|
+
created,
|
|
2989
|
+
url: collectionUrl(index, id)
|
|
2990
|
+
},
|
|
2991
|
+
queue: destination.queue
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
/** One pull request's deliveries. No review id means it never had a review to deliver. */
|
|
2995
|
+
function deliveryOf(pr, reviewId, delivered, destination) {
|
|
2996
|
+
const outcome = (result) => reviewId ? result ?? "unsaved" : "no_review";
|
|
2997
|
+
return {
|
|
2998
|
+
pr,
|
|
2999
|
+
review_id: reviewId ?? null,
|
|
3000
|
+
collection: outcome(delivered?.collection),
|
|
3001
|
+
...destination.queue ? { queue: outcome(delivered?.queue) } : {},
|
|
3002
|
+
delivered: reviewId !== void 0 && !missedDelivery(delivered, destination)
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
//#endregion
|
|
3006
|
+
//#region src/exit.ts
|
|
3007
|
+
/**
|
|
3008
|
+
* What a command's exit status means, for every command.
|
|
3009
|
+
*
|
|
3010
|
+
* One table, the contract's own (`EXIT_CODES`), so a script reads the same number for the same
|
|
3011
|
+
* failure whether or not it asked for `--json`, and an MCP error and an exit status name the same
|
|
3012
|
+
* thing. A review that finds problems is a review that worked, and exits 0: these say whether the
|
|
3013
|
+
* command did what was asked, never what the code under review is like.
|
|
3014
|
+
*
|
|
3015
|
+
* The sentence printed beside a failure is unchanged by any of this. The number only sorts it.
|
|
3016
|
+
*/
|
|
3017
|
+
var EXIT = {
|
|
3018
|
+
ok: EXIT_CODES.ok,
|
|
3019
|
+
/** Something nobody anticipated. The message is still printed. */
|
|
3020
|
+
unexpected: EXIT_CODES.internal,
|
|
3021
|
+
/** The command, a flag or an argument was not one this can act on. */
|
|
3022
|
+
usage: EXIT_CODES.invalid_argument,
|
|
3023
|
+
/** No review, job or finding by that name, or a prefix that names more than one. */
|
|
3024
|
+
notFound: EXIT_CODES.not_found,
|
|
3025
|
+
/** Understood, and declined: already running, nothing to update, an agent that cannot run here, a question nobody answered. */
|
|
3026
|
+
refused: EXIT_CODES.refused,
|
|
3027
|
+
/** The analysis, update or question ran and failed. */
|
|
3028
|
+
failed: EXIT_CODES.analysis_failed,
|
|
3029
|
+
/** The local workspace server could not be started or reached. */
|
|
3030
|
+
unavailable: EXIT_CODES.service_unavailable,
|
|
3031
|
+
timeout: EXIT_CODES.timeout,
|
|
3032
|
+
/** Started from inside an agent run, which cannot start reviews. */
|
|
3033
|
+
recursion: EXIT_CODES.recursion_blocked
|
|
3034
|
+
};
|
|
3035
|
+
function exitFor(code) {
|
|
3036
|
+
return EXIT_CODES[code];
|
|
3037
|
+
}
|
|
3038
|
+
/** A failure that reached the top unclassified. Naming a target that does not resolve is a usage error, not a crash. */
|
|
3039
|
+
function exitForThrown(error) {
|
|
3040
|
+
if (error instanceof TargetError || error instanceof AmbiguousRefError) return EXIT.usage;
|
|
3041
|
+
return EXIT.unexpected;
|
|
3042
|
+
}
|
|
3043
|
+
//#endregion
|
|
3044
|
+
//#region src/write.ts
|
|
3045
|
+
/**
|
|
3046
|
+
* Where the two kinds of output go, decided once.
|
|
3047
|
+
*
|
|
3048
|
+
* Links and results to stdout, everything else to stderr, so `prreviewbuddy review | pbcopy`
|
|
3049
|
+
* copies a URL and progress still reaches the terminal it was meant for.
|
|
3050
|
+
*/
|
|
3051
|
+
function out(text) {
|
|
3052
|
+
process.stdout.write(`${text}\n`);
|
|
3053
|
+
}
|
|
3054
|
+
function err(text) {
|
|
3055
|
+
process.stderr.write(`${text}\n`);
|
|
3056
|
+
}
|
|
3057
|
+
//#endregion
|
|
3058
|
+
//#region src/json.ts
|
|
3059
|
+
/**
|
|
3060
|
+
* The one document a `--json` command writes, and how it answers a failure.
|
|
3061
|
+
*
|
|
3062
|
+
* Exactly one JSON value on stdout, success or not, so a caller can always parse what it gets and
|
|
3063
|
+
* branch on `error.code` rather than on a sentence. The sentence still goes to stderr, where a
|
|
3064
|
+
* person watching the command reads it.
|
|
3065
|
+
*/
|
|
3066
|
+
function emit(document) {
|
|
3067
|
+
out(JSON.stringify({
|
|
3068
|
+
schema_version: "1",
|
|
3069
|
+
...document
|
|
3070
|
+
}));
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* A failure, as a document on stdout and a sentence on stderr, and the exit status it maps to.
|
|
3074
|
+
* `extra` adds what the command had already settled before it failed, such as a collection it made.
|
|
3075
|
+
*/
|
|
3076
|
+
function emitError(error, extra = {}) {
|
|
3077
|
+
const document = {
|
|
3078
|
+
...errorDocument(error),
|
|
3079
|
+
...extra
|
|
3080
|
+
};
|
|
3081
|
+
out(JSON.stringify(document));
|
|
3082
|
+
err(document.error.message);
|
|
3083
|
+
return error instanceof ApiError ? exitFor(error.code) : exitForThrown(error);
|
|
3084
|
+
}
|
|
3085
|
+
/** The same failure without `--json`: the sentence alone, and the same exit status. */
|
|
3086
|
+
function reportError(error) {
|
|
3087
|
+
err(error instanceof Error ? error.message : String(error));
|
|
3088
|
+
return error instanceof ApiError ? exitFor(error.code) : exitForThrown(error);
|
|
3089
|
+
}
|
|
3090
|
+
//#endregion
|
|
2835
3091
|
//#region src/commands/review_json.ts
|
|
2836
3092
|
/**
|
|
2837
|
-
* `prreviewbuddy review --json
|
|
3093
|
+
* `prreviewbuddy review --json`, and `--no-wait` with or without it: the same operations as the MCP
|
|
3094
|
+
* tools, and through the same calls.
|
|
2838
3095
|
*
|
|
2839
3096
|
* Start (or find) a review, bring one up to date, or resume one that stopped. Each goes through
|
|
2840
3097
|
* `api/service.ts`, which mints the job here, under this terminal's environment, and hands it to a
|
|
@@ -2842,6 +3099,11 @@ function askShown(ask) {
|
|
|
2842
3099
|
* follows the job to its end on the store and answers with how it ended, so a script that waits
|
|
2843
3100
|
* gets the finished review rather than a job to poll.
|
|
2844
3101
|
*
|
|
3102
|
+
* `--json` says how it answers and `--no-wait` whether it waits, and neither implies the other.
|
|
3103
|
+
* `--no-wait` alone comes here rather than to the terminal path because that path runs the job in
|
|
3104
|
+
* its own process, which ends with the terminal; the runner here is detached and outlives it. It
|
|
3105
|
+
* answers in sentences: what was started, its job's state, and the `status` command to check it.
|
|
3106
|
+
*
|
|
2845
3107
|
* Never a question, never a progress block on stdout, never a browser: an ambiguous agent is a
|
|
2846
3108
|
* refusal that names the agents, and everything a person would watch goes to stderr.
|
|
2847
3109
|
*/
|
|
@@ -2851,32 +3113,97 @@ function selectionOf$1(command) {
|
|
|
2851
3113
|
...command.requestedModel ? { model: command.requestedModel } : {}
|
|
2852
3114
|
};
|
|
2853
3115
|
}
|
|
2854
|
-
var
|
|
2855
|
-
|
|
2856
|
-
|
|
3116
|
+
var JSON_DEPS = {
|
|
3117
|
+
service: {
|
|
3118
|
+
...defaultDeps,
|
|
3119
|
+
explainAgent: unresolvedAgent
|
|
3120
|
+
},
|
|
3121
|
+
start: startReview,
|
|
3122
|
+
update: async (command, repoPath, deps) => requestUpdate(await reviewToUpdate(command, repoPath), "cli", deps),
|
|
3123
|
+
follow: (started, deps) => follow$1(started, deps),
|
|
3124
|
+
open: openDestination,
|
|
3125
|
+
deliver,
|
|
3126
|
+
index: async () => {
|
|
3127
|
+
try {
|
|
3128
|
+
return reviewsUrl(await ensureServer(), readIndexToken());
|
|
3129
|
+
} catch {
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
2857
3133
|
};
|
|
2858
|
-
async function
|
|
3134
|
+
async function reviewByService(command, repoPath, overrides = {}) {
|
|
3135
|
+
const jsonDeps = {
|
|
3136
|
+
...JSON_DEPS,
|
|
3137
|
+
...overrides
|
|
3138
|
+
};
|
|
3139
|
+
const deps = jsonDeps.service;
|
|
3140
|
+
/** Settled once the target and agent are, and before the review is written: see `StartHooks`. */
|
|
3141
|
+
let destination = null;
|
|
3142
|
+
const hooks = command.collection ? { ready: () => {
|
|
3143
|
+
const opened = jsonDeps.open(destinationRequest(command));
|
|
3144
|
+
if (!opened.ok) throw new ApiError("refused", destinationRefused(opened));
|
|
3145
|
+
if (opened.destination.collection) destination = {
|
|
3146
|
+
...opened.destination,
|
|
3147
|
+
collection: opened.destination.collection
|
|
3148
|
+
};
|
|
3149
|
+
} } : {};
|
|
3150
|
+
/** What was delivered and to which review, once it was: a failure after that still reports it. */
|
|
3151
|
+
let reached = null;
|
|
2859
3152
|
try {
|
|
2860
|
-
let started = await operate$1(command, repoPath, deps);
|
|
3153
|
+
let started = await operate$1(command, repoPath, deps, jsonDeps, hooks);
|
|
2861
3154
|
const continuation = command.operation === "update" ? "update" : command.operation === "resume" ? "resume" : started.started ? null : "existing";
|
|
2862
3155
|
if (continuation) for (const line of unusedSelection(continuation, selectionOf$1(command))) err(line);
|
|
2863
3156
|
if (command.queue) err(reviewQueued(queueForReview(started.review.id)));
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
3157
|
+
const sent = destination;
|
|
3158
|
+
const delivered = sent ? jsonDeps.deliver(started.review.id, sent) : void 0;
|
|
3159
|
+
if (delivered) reached = {
|
|
3160
|
+
reviewId: started.review.id,
|
|
3161
|
+
delivered
|
|
3162
|
+
};
|
|
3163
|
+
const index = sent ? await jsonDeps.index() : null;
|
|
3164
|
+
if (sent && delivered) {
|
|
3165
|
+
if (delivered.queue) err(reviewQueued(delivered.queue));
|
|
3166
|
+
const { name, created, id } = sent.collection;
|
|
3167
|
+
err(reviewCollected({
|
|
3168
|
+
name,
|
|
3169
|
+
created,
|
|
3170
|
+
url: collectionUrl(index, id)
|
|
3171
|
+
}, delivered.collection ?? "unsaved", missedDelivery(delivered, sent)));
|
|
3172
|
+
}
|
|
3173
|
+
if (!command.noWait && started.started && started.job) started = await jsonDeps.follow(started, deps);
|
|
3174
|
+
if (command.json) emit({
|
|
3175
|
+
...started,
|
|
3176
|
+
...sent ? {
|
|
3177
|
+
destination: destinationDocument(sent, index),
|
|
3178
|
+
pull_requests: [deliveryOf(command.prUrl, started.review.id, delivered, sent)]
|
|
3179
|
+
} : {}
|
|
3180
|
+
});
|
|
3181
|
+
else out(reviewInBackground(started, command.operation === "update" || command.operation === "resume" ? command.operation : "start"));
|
|
3182
|
+
const code = exitOf$1(started.job);
|
|
3183
|
+
return code === EXIT.ok && sent && missedDelivery(delivered, sent) ? EXIT.failed : code;
|
|
2867
3184
|
} catch (error) {
|
|
2868
|
-
|
|
3185
|
+
const sent = destination;
|
|
3186
|
+
if (!command.json) {
|
|
3187
|
+
if (sent?.collection.created && !reached) err(collectionMadeBeforeFailure(sent.collection.name));
|
|
3188
|
+
return reportError(error);
|
|
3189
|
+
}
|
|
3190
|
+
if (!sent) return emitError(error);
|
|
3191
|
+
const got = reached;
|
|
3192
|
+
return emitError(error, {
|
|
3193
|
+
destination: destinationDocument(sent, await jsonDeps.index()),
|
|
3194
|
+
pull_requests: [deliveryOf(command.prUrl, got?.reviewId, got?.delivered, sent)]
|
|
3195
|
+
});
|
|
2869
3196
|
}
|
|
2870
3197
|
}
|
|
2871
|
-
async function operate$1(command, repoPath, deps) {
|
|
3198
|
+
async function operate$1(command, repoPath, deps, { start, update }, hooks = {}) {
|
|
2872
3199
|
if (command.prUrls) throw new ApiError("invalid_argument", "`--json` reviews one target at a time. Run it once for each pull request.");
|
|
2873
|
-
if (command.operation === "resume") return
|
|
3200
|
+
if (command.operation === "resume") return start({
|
|
2874
3201
|
repoPath,
|
|
2875
3202
|
reviewId: command.reviewId,
|
|
2876
3203
|
source: "cli"
|
|
2877
|
-
}, deps);
|
|
2878
|
-
if (command.operation === "update") return
|
|
2879
|
-
return
|
|
3204
|
+
}, deps, {});
|
|
3205
|
+
if (command.operation === "update") return update(command, repoPath, deps);
|
|
3206
|
+
return start({
|
|
2880
3207
|
repoPath,
|
|
2881
3208
|
...command.branchRef ? { branch: command.branchRef } : {},
|
|
2882
3209
|
...command.prUrl ? { pr: command.prUrl } : {},
|
|
@@ -2886,7 +3213,7 @@ async function operate$1(command, repoPath, deps) {
|
|
|
2886
3213
|
...command.requestedModel ? { model: command.requestedModel } : {},
|
|
2887
3214
|
fresh: command.operation === "fresh",
|
|
2888
3215
|
source: "cli"
|
|
2889
|
-
}, deps);
|
|
3216
|
+
}, deps, hooks);
|
|
2890
3217
|
}
|
|
2891
3218
|
/** The review an update acts on: the one named, or the one this target already has. */
|
|
2892
3219
|
async function reviewToUpdate(command, repoPath) {
|
|
@@ -3223,8 +3550,24 @@ function labelsFor(urls) {
|
|
|
3223
3550
|
}
|
|
3224
3551
|
async function reviewBatch(command, repoPath, deps = realDeps()) {
|
|
3225
3552
|
const urls = command.prUrls ?? [];
|
|
3226
|
-
|
|
3553
|
+
/**
|
|
3554
|
+
* `--json`, which the parser lets through only beside a collection option: one document on stdout
|
|
3555
|
+
* at the end, refusals included, and no question asked of anybody.
|
|
3556
|
+
*/
|
|
3557
|
+
const json = command.json === true;
|
|
3558
|
+
let agent;
|
|
3559
|
+
try {
|
|
3560
|
+
agent = await deps.chooseAgent(command);
|
|
3561
|
+
} catch (error) {
|
|
3562
|
+
if (json) return refuseJson(deps, error);
|
|
3563
|
+
throw error;
|
|
3564
|
+
}
|
|
3227
3565
|
if (!agent) return EXIT.refused;
|
|
3566
|
+
let refusal = "";
|
|
3567
|
+
const destination = openFor(command, json ? (line) => refusal = line : deps.err, deps.open);
|
|
3568
|
+
if (!destination) return json ? refuseJson(deps, new ApiError("refused", refusal)) : EXIT.refused;
|
|
3569
|
+
/** A collection was asked for, so a review that missed a destination fails the command. */
|
|
3570
|
+
const strict = command.collection !== void 0;
|
|
3228
3571
|
const rows = labelsFor(urls).map((label) => ({
|
|
3229
3572
|
label,
|
|
3230
3573
|
state: { kind: "queued" }
|
|
@@ -3264,7 +3607,7 @@ async function reviewBatch(command, repoPath, deps = realDeps()) {
|
|
|
3264
3607
|
const prepared = await deps.prepare(target, agent.id, command.operation === "fresh", command.requestedModel);
|
|
3265
3608
|
const workspaceId = prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id;
|
|
3266
3609
|
row.workspaceId = workspaceId;
|
|
3267
|
-
if (
|
|
3610
|
+
if (destination.queue || destination.collection) row.delivered = deps.deliver(workspaceId, destination);
|
|
3268
3611
|
if (prepared.kind === "existing") {
|
|
3269
3612
|
row.state = {
|
|
3270
3613
|
kind: "existing",
|
|
@@ -3297,13 +3640,74 @@ async function reviewBatch(command, repoPath, deps = realDeps()) {
|
|
|
3297
3640
|
stopDrawing();
|
|
3298
3641
|
stopListening();
|
|
3299
3642
|
}
|
|
3300
|
-
|
|
3643
|
+
if (json && destination.collection) {
|
|
3644
|
+
const sent = {
|
|
3645
|
+
...destination,
|
|
3646
|
+
collection: destination.collection
|
|
3647
|
+
};
|
|
3648
|
+
deps.out(JSON.stringify({
|
|
3649
|
+
schema_version: "1",
|
|
3650
|
+
destination: destinationDocument(sent, links.index),
|
|
3651
|
+
pull_requests: rows.map((row, index) => pullRequestJson(rows, index, urls, sent, links.review))
|
|
3652
|
+
}));
|
|
3653
|
+
} else deps.out(batchFinished(rows.map((row) => summaryRow(row, links.review)), links.index, {
|
|
3654
|
+
queue: destination.queue,
|
|
3655
|
+
strict,
|
|
3656
|
+
...destination.collection ? { collection: {
|
|
3657
|
+
name: destination.collection.name,
|
|
3658
|
+
created: destination.collection.created,
|
|
3659
|
+
url: collectionUrl(links.index, destination.collection.id)
|
|
3660
|
+
} } : {}
|
|
3661
|
+
}));
|
|
3301
3662
|
if (signal.aborted) return 130;
|
|
3302
|
-
|
|
3663
|
+
if (rows.some((row) => row.state.kind === "failed")) return EXIT.failed;
|
|
3664
|
+
if (strict && rows.some((row) => row.state.kind !== "duplicate" && missedDelivery(row.delivered, destination))) return EXIT.failed;
|
|
3665
|
+
return EXIT.ok;
|
|
3666
|
+
}
|
|
3667
|
+
/** A refusal before anything ran, as the one document `--json` writes, and its exit status. */
|
|
3668
|
+
function refuseJson(deps, error) {
|
|
3669
|
+
const document = errorDocument(error);
|
|
3670
|
+
deps.out(JSON.stringify(document));
|
|
3671
|
+
deps.err(document.error.message);
|
|
3672
|
+
return error instanceof ApiError ? exitFor(error.code) : exitForThrown(error);
|
|
3673
|
+
}
|
|
3674
|
+
/** How each row ended, in the words a document uses. */
|
|
3675
|
+
var OUTCOMES = {
|
|
3676
|
+
queued: "not_started",
|
|
3677
|
+
"looking-up": "not_started",
|
|
3678
|
+
running: "not_started",
|
|
3679
|
+
"not-started": "not_started",
|
|
3680
|
+
ready: "ready",
|
|
3681
|
+
existing: "existing",
|
|
3682
|
+
duplicate: "duplicate",
|
|
3683
|
+
failed: "failed",
|
|
3684
|
+
stopped: "stopped"
|
|
3685
|
+
};
|
|
3686
|
+
/**
|
|
3687
|
+
* One pull request of a `--json` batch: how its review ended, and where it went. A duplicate reports
|
|
3688
|
+
* the review and the deliveries of the row it repeats, which are its own too.
|
|
3689
|
+
*/
|
|
3690
|
+
function pullRequestJson(rows, index, urls, destination, link) {
|
|
3691
|
+
const row = rows[index];
|
|
3692
|
+
const state = row.state;
|
|
3693
|
+
const first = state.kind === "duplicate" ? rows.findIndex((other) => other.label === state.of && other.state.kind !== "duplicate") : -1;
|
|
3694
|
+
const source = first >= 0 ? rows[first] : row;
|
|
3695
|
+
const url = source.workspaceId ? link(source.workspaceId) : void 0;
|
|
3696
|
+
const outcome = state.kind === "existing" && state.inProgress ? "in_progress" : OUTCOMES[state.kind];
|
|
3697
|
+
return {
|
|
3698
|
+
...deliveryOf(urls[index], source.workspaceId, source.delivered, destination),
|
|
3699
|
+
outcome,
|
|
3700
|
+
...url ? { review_url: url } : {},
|
|
3701
|
+
...state.kind === "failed" ? { detail: state.reason } : {},
|
|
3702
|
+
...first >= 0 ? { duplicate_of: urls[first] } : {}
|
|
3703
|
+
};
|
|
3303
3704
|
}
|
|
3304
3705
|
function summaryRow(row, link) {
|
|
3305
3706
|
const url = row.workspaceId ? link(row.workspaceId) : void 0;
|
|
3306
|
-
const queued =
|
|
3707
|
+
const queued = {
|
|
3708
|
+
...row.delivered?.queue ? { queue: row.delivered.queue } : {},
|
|
3709
|
+
...row.delivered?.collection ? { collection: row.delivered.collection } : {}
|
|
3710
|
+
};
|
|
3307
3711
|
const withUrl = {
|
|
3308
3712
|
...url ? { url } : {},
|
|
3309
3713
|
...queued
|
|
@@ -3342,9 +3746,18 @@ function summaryRow(row, link) {
|
|
|
3342
3746
|
};
|
|
3343
3747
|
}
|
|
3344
3748
|
}
|
|
3749
|
+
/**
|
|
3750
|
+
* The agent for a `--json` batch, never asked for: the one named, or the default, or a refusal that
|
|
3751
|
+
* says which agents there are. As the service resolves one for `review --json` on a single target.
|
|
3752
|
+
*/
|
|
3753
|
+
async function agentForJson(command) {
|
|
3754
|
+
const resolution = await resolveAgent({ flag: command.agentId });
|
|
3755
|
+
if (resolution.kind !== "resolved") throw new ApiError("refused", unresolvedAgent(resolution));
|
|
3756
|
+
return resolution.agent;
|
|
3757
|
+
}
|
|
3345
3758
|
function realDeps() {
|
|
3346
3759
|
return {
|
|
3347
|
-
chooseAgent: (command) => chooseFor(command),
|
|
3760
|
+
chooseAgent: (command) => command.json ? agentForJson(command) : chooseFor(command),
|
|
3348
3761
|
resolve: (prUrl, repoPath) => resolveTarget({
|
|
3349
3762
|
repoPath,
|
|
3350
3763
|
prUrl,
|
|
@@ -3353,7 +3766,9 @@ function realDeps() {
|
|
|
3353
3766
|
prepare: (target, agentId, fresh, requestedModel) => prepareReview(target, agentId, fresh, requestedModel, "cli"),
|
|
3354
3767
|
settle: (workspaceId, signal) => settleReview(workspaceId, "standard", signal, "cli"),
|
|
3355
3768
|
jobIdOf: (workspaceId) => loadWorkspace(workspaceId)?.jobId,
|
|
3356
|
-
|
|
3769
|
+
open: openDestination,
|
|
3770
|
+
deliver,
|
|
3771
|
+
err,
|
|
3357
3772
|
links: async () => {
|
|
3358
3773
|
try {
|
|
3359
3774
|
const port = await ensureServer();
|
|
@@ -3588,8 +4003,17 @@ function describeReview(id) {
|
|
|
3588
4003
|
createdAt: workspace?.createdAt ?? 0
|
|
3589
4004
|
};
|
|
3590
4005
|
}
|
|
4006
|
+
/**
|
|
4007
|
+
* Whether a command goes through the service rather than the terminal path. `--json` does, to
|
|
4008
|
+
* answer in a document. `--no-wait` does with or without it, because only the service's runner is
|
|
4009
|
+
* detached and outlives this process. Neither, and the terminal path shows progress and waits. A
|
|
4010
|
+
* batch into a collection answers `--json` itself.
|
|
4011
|
+
*/
|
|
4012
|
+
function answersThroughService(command) {
|
|
4013
|
+
return (command.json === true || command.noWait === true) && !(command.prUrls && command.collection);
|
|
4014
|
+
}
|
|
3591
4015
|
async function review(command, repoPath) {
|
|
3592
|
-
if (command
|
|
4016
|
+
if (answersThroughService(command)) return reviewByService(command, repoPath);
|
|
3593
4017
|
if (command.operation === "delete") return deleteReview(command);
|
|
3594
4018
|
if (command.prUrls) return reviewBatch(command, repoPath);
|
|
3595
4019
|
if (command.reviewId) {
|
|
@@ -3617,13 +4041,15 @@ async function review(command, repoPath) {
|
|
|
3617
4041
|
if (command.lens === "kiss") return kiss(command, target);
|
|
3618
4042
|
const agent = await chooseFor(command);
|
|
3619
4043
|
if (!agent) return EXIT.refused;
|
|
4044
|
+
const destination = openFor(command, err);
|
|
4045
|
+
if (!destination) return EXIT.refused;
|
|
3620
4046
|
const prepared = await prepareReview(target, agent.id, command.operation === "fresh", command.requestedModel, "cli");
|
|
3621
|
-
|
|
4047
|
+
const missed = await deliverTo(command, destination, prepared.kind === "new" ? prepared.workspaceId : prepared.existing.id);
|
|
3622
4048
|
if (prepared.kind === "existing") {
|
|
3623
4049
|
const url = linkTo(await ensureServer(), prepared.existing.id, prepared.existing.token);
|
|
3624
4050
|
out(existingReview(prepared.existing, url));
|
|
3625
4051
|
for (const line of unusedSelection("existing", selectionOf(command))) err(line);
|
|
3626
|
-
return 0;
|
|
4052
|
+
return missed ? EXIT.failed : 0;
|
|
3627
4053
|
}
|
|
3628
4054
|
if (prepared.kind === "resume") {
|
|
3629
4055
|
const { existing, failure } = prepared;
|
|
@@ -3634,12 +4060,47 @@ async function review(command, repoPath) {
|
|
|
3634
4060
|
...failure.requestedModel ? { model: failure.requestedModel } : {}
|
|
3635
4061
|
};
|
|
3636
4062
|
for (const line of unusedSelection("resume", selectionOf(command), running)) err(line);
|
|
3637
|
-
return drive(existing.id, url);
|
|
4063
|
+
return failedIfMissed(await drive(existing.id, url), missed);
|
|
3638
4064
|
}
|
|
3639
4065
|
const { workspaceId } = prepared;
|
|
3640
4066
|
const url = linkTo(await ensureServer(), workspaceId, loadWorkspace(workspaceId).token);
|
|
3641
4067
|
out(startedReview(url, target));
|
|
3642
|
-
return drive(workspaceId, url);
|
|
4068
|
+
return failedIfMissed(await drive(workspaceId, url), missed);
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* A review that ended well is still a failed command when a collection was asked for and it, or the
|
|
4072
|
+
* queue beside it, does not have the review. A review that failed keeps its own status, which says
|
|
4073
|
+
* more. `missed` is only ever true for a collection invocation: see `deliverTo`.
|
|
4074
|
+
*/
|
|
4075
|
+
function failedIfMissed(code, missed) {
|
|
4076
|
+
return code === 0 && missed ? EXIT.failed : code;
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Put the review where the command sends it, now, and say how that went. Answers whether a
|
|
4080
|
+
* destination that was asked for is missing it.
|
|
4081
|
+
*
|
|
4082
|
+
* Without a collection this is `enqueue`, exactly: `--queue` alone has never failed a command, and
|
|
4083
|
+
* does not start now. With one, both destinations are reported, the collection with its link.
|
|
4084
|
+
*/
|
|
4085
|
+
async function deliverTo(command, destination, workspaceId) {
|
|
4086
|
+
if (!command.collection || !destination.collection) {
|
|
4087
|
+
enqueue(command, workspaceId);
|
|
4088
|
+
return false;
|
|
4089
|
+
}
|
|
4090
|
+
const delivered = deliver(workspaceId, destination);
|
|
4091
|
+
if (delivered.queue) err(reviewQueued(delivered.queue));
|
|
4092
|
+
const missed = missedDelivery(delivered, destination);
|
|
4093
|
+
let index = null;
|
|
4094
|
+
try {
|
|
4095
|
+
index = reviewsUrl(await ensureServer(), readIndexToken());
|
|
4096
|
+
} catch {}
|
|
4097
|
+
const { id, name, created } = destination.collection;
|
|
4098
|
+
err(reviewCollected({
|
|
4099
|
+
name,
|
|
4100
|
+
created,
|
|
4101
|
+
url: collectionUrl(index, id)
|
|
4102
|
+
}, delivered.collection ?? "unsaved", missed));
|
|
4103
|
+
return missed;
|
|
3643
4104
|
}
|
|
3644
4105
|
/**
|
|
3645
4106
|
* `--lens kiss`: the other question, asked of the same change.
|
|
@@ -4724,7 +5185,7 @@ async function announceInstallation() {
|
|
|
4724
5185
|
if (!telemetryUploadEnabled()) return;
|
|
4725
5186
|
if (announced()) return;
|
|
4726
5187
|
const line = stamp({ event: "installation_started" });
|
|
4727
|
-
if (!line || !await deliver(line)) return;
|
|
5188
|
+
if (!line || !await deliver$1(line)) return;
|
|
4728
5189
|
writeMarker();
|
|
4729
5190
|
append(line);
|
|
4730
5191
|
} catch {}
|