@difflab/pi 0.5.0-rc.202609231305.97666c2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/agents/diffpi-reviewer.md +3 -3
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/extensions/index.js +804 -296
- package/dist/extensions/tuicrx.d.ts +8 -1
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +458 -197
- package/dist/review/github-review-backend.d.ts.map +1 -1
- package/dist/review/gitlab-review-backend.d.ts.map +1 -1
- package/dist/review/index.d.ts +6 -6
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/local-review-backend.d.ts +3 -0
- package/dist/review/local-review-backend.d.ts.map +1 -0
- package/dist/review/review-backend.d.ts +3 -2
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/review/review-markdown.d.ts +4 -1
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/review/review-state.d.ts +20 -0
- package/dist/review/review-state.d.ts.map +1 -0
- package/dist/review/types.d.ts +28 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.js +783 -185
- package/dist/tools/review.d.ts +30 -0
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/workflow.d.ts +0 -6
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/review/SKILL.md +8 -6
- package/skills/review/references/workflows/address.md +11 -0
- package/skills/review/references/workflows/auto.md +18 -0
- package/skills/review/references/workflows/complete.md +7 -0
- package/skills/review/references/workflows/edit.md +7 -0
- package/skills/review/references/workflows/help.md +30 -0
- package/skills/review/references/workflows/merge.md +6 -0
- package/skills/review/references/workflows/new.md +19 -0
- package/skills/review/references/workflows/open.md +3 -0
- package/skills/review/references/workflows/publish.md +8 -0
- package/dist/review/local-reviews.d.ts +0 -31
- package/dist/review/local-reviews.d.ts.map +0 -1
- package/workflows/review/address.md +0 -8
- package/workflows/review/auto.md +0 -8
- package/workflows/review/complete.md +0 -7
- package/workflows/review/edit.md +0 -7
- package/workflows/review/help.md +0 -14
- package/workflows/review/merge.md +0 -6
- package/workflows/review/new.md +0 -7
- package/workflows/review/publish.md +0 -6
package/dist/index.js
CHANGED
|
@@ -1301,6 +1301,9 @@ function yymmdd(date = new Date) {
|
|
|
1301
1301
|
function reviewRecordName(target, date = new Date) {
|
|
1302
1302
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
1303
1303
|
}
|
|
1304
|
+
function isLocalResponse(body) {
|
|
1305
|
+
return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
|
|
1306
|
+
}
|
|
1304
1307
|
function dedupeFindings(findings) {
|
|
1305
1308
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
1306
1309
|
const byKey = new Map;
|
|
@@ -1346,6 +1349,18 @@ function renderCommentBody(finding) {
|
|
|
1346
1349
|
return `${prefix}${finding.body}${reference}`;
|
|
1347
1350
|
}
|
|
1348
1351
|
// src/review/review-markdown.ts
|
|
1352
|
+
import { z as z4 } from "zod";
|
|
1353
|
+
var reviewThreadRecordSchema = z4.object({
|
|
1354
|
+
id: z4.string().min(1),
|
|
1355
|
+
file: z4.string().nullable().optional().transform((value) => value ?? undefined),
|
|
1356
|
+
line: z4.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
|
|
1357
|
+
body: z4.string(),
|
|
1358
|
+
author: z4.string().optional(),
|
|
1359
|
+
resolved: z4.boolean(),
|
|
1360
|
+
addressed: z4.boolean().optional(),
|
|
1361
|
+
question: z4.boolean(),
|
|
1362
|
+
replies: z4.array(z4.string()).optional()
|
|
1363
|
+
});
|
|
1349
1364
|
function renderReviewDoc(input) {
|
|
1350
1365
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
1351
1366
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -1387,6 +1402,88 @@ function renderReviewDoc(input) {
|
|
|
1387
1402
|
`).trimEnd()}
|
|
1388
1403
|
`;
|
|
1389
1404
|
}
|
|
1405
|
+
function renderThreadArtifact(title, target, threads, options = {}) {
|
|
1406
|
+
const records = threads.map((thread) => ({
|
|
1407
|
+
id: thread.id,
|
|
1408
|
+
file: thread.file,
|
|
1409
|
+
line: thread.line,
|
|
1410
|
+
body: thread.body,
|
|
1411
|
+
author: thread.author,
|
|
1412
|
+
resolved: thread.resolved,
|
|
1413
|
+
addressed: thread.addressed,
|
|
1414
|
+
question: thread.question,
|
|
1415
|
+
replies: thread.replies
|
|
1416
|
+
}));
|
|
1417
|
+
const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
|
|
1418
|
+
const lines = [
|
|
1419
|
+
`<!-- diffpi-threads:${payload} -->`,
|
|
1420
|
+
`# Review threads: ${title}`,
|
|
1421
|
+
"",
|
|
1422
|
+
"## Metadata",
|
|
1423
|
+
"",
|
|
1424
|
+
`- Target: ${target}`,
|
|
1425
|
+
`- Pulled: ${options.timestamp ?? new Date().toISOString()}`
|
|
1426
|
+
];
|
|
1427
|
+
if (options.number !== undefined)
|
|
1428
|
+
lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
|
|
1429
|
+
lines.push("", "## Replies", "");
|
|
1430
|
+
for (const thread of threads) {
|
|
1431
|
+
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1432
|
+
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
|
|
1433
|
+
}
|
|
1434
|
+
lines.push("## Source comments", "");
|
|
1435
|
+
for (const thread of threads) {
|
|
1436
|
+
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", thread.body, "", "---", "");
|
|
1437
|
+
}
|
|
1438
|
+
return `${lines.join(`
|
|
1439
|
+
`).trimEnd()}
|
|
1440
|
+
`;
|
|
1441
|
+
}
|
|
1442
|
+
function parseThreadArtifact(content) {
|
|
1443
|
+
const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
|
|
1444
|
+
if (!payload)
|
|
1445
|
+
throw new Error("This file is not a Diffpi thread artifact.");
|
|
1446
|
+
let threads;
|
|
1447
|
+
try {
|
|
1448
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1449
|
+
threads = z4.array(reviewThreadRecordSchema).parse(decoded);
|
|
1450
|
+
} catch {
|
|
1451
|
+
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
1452
|
+
}
|
|
1453
|
+
const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
|
|
1454
|
+
return threads.map((thread) => {
|
|
1455
|
+
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1456
|
+
const startMarker = `<!-- diffpi-reply-start:${id} -->
|
|
1457
|
+
`;
|
|
1458
|
+
const endMarker = `
|
|
1459
|
+
<!-- diffpi-reply-end:${id} -->`;
|
|
1460
|
+
const start = replies.indexOf(startMarker);
|
|
1461
|
+
const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
|
|
1462
|
+
const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
|
|
1463
|
+
return reply ? { ...thread, reply } : thread;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function upsertThreadReply(content, threadId, body, question, resolved) {
|
|
1467
|
+
const threads = parseThreadArtifact(content);
|
|
1468
|
+
const thread = threads.find((candidate) => candidate.id === threadId);
|
|
1469
|
+
if (!thread)
|
|
1470
|
+
throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
|
|
1471
|
+
thread.reply = body;
|
|
1472
|
+
thread.addressed = true;
|
|
1473
|
+
if (question !== undefined)
|
|
1474
|
+
thread.question = question;
|
|
1475
|
+
if (resolved !== undefined)
|
|
1476
|
+
thread.resolved = resolved;
|
|
1477
|
+
const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
|
|
1478
|
+
const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
|
|
1479
|
+
const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
|
|
1480
|
+
const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
|
|
1481
|
+
return renderThreadArtifact(title, target, threads, {
|
|
1482
|
+
timestamp,
|
|
1483
|
+
number: pr ? Number.parseInt(pr[1], 10) : undefined,
|
|
1484
|
+
url: pr?.[2]
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1390
1487
|
// src/review/github-review-backend.ts
|
|
1391
1488
|
function GitHubReviewBackend(vcs, number) {
|
|
1392
1489
|
const pendingReview = async () => {
|
|
@@ -1448,6 +1545,9 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1448
1545
|
id: thread.id,
|
|
1449
1546
|
file: thread.path ?? undefined,
|
|
1450
1547
|
line: thread.line ?? undefined,
|
|
1548
|
+
rootCommentId: comment?.id,
|
|
1549
|
+
commentIds: nodes.flatMap((node) => [node.id, node.databaseId?.toString()].filter((id) => Boolean(id))),
|
|
1550
|
+
commentNodeIds: nodes.flatMap((node) => node.id ? [node.id] : []),
|
|
1451
1551
|
body,
|
|
1452
1552
|
author: comment?.author?.login,
|
|
1453
1553
|
resolved: thread.isResolved,
|
|
@@ -1457,6 +1557,7 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1457
1557
|
});
|
|
1458
1558
|
};
|
|
1459
1559
|
return {
|
|
1560
|
+
kind: "remote",
|
|
1460
1561
|
async stage(draft) {
|
|
1461
1562
|
const pending = await pendingReview();
|
|
1462
1563
|
if (!pending) {
|
|
@@ -1514,6 +1615,33 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1514
1615
|
await ghChecked(args);
|
|
1515
1616
|
}
|
|
1516
1617
|
},
|
|
1618
|
+
async readDraft() {
|
|
1619
|
+
const pending = await pendingReview();
|
|
1620
|
+
if (!pending)
|
|
1621
|
+
return { comments: [], body: "" };
|
|
1622
|
+
const review = await ghChecked(["api", `/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}`]);
|
|
1623
|
+
const comments = await ghChecked([
|
|
1624
|
+
"api",
|
|
1625
|
+
`/repos/${vcs.owner}/${vcs.repo}/pulls/${number}/reviews/${pending.id}/comments`
|
|
1626
|
+
]);
|
|
1627
|
+
let reviewData;
|
|
1628
|
+
let commentData;
|
|
1629
|
+
try {
|
|
1630
|
+
reviewData = JSON.parse(review.stdout);
|
|
1631
|
+
commentData = JSON.parse(comments.stdout);
|
|
1632
|
+
} catch {
|
|
1633
|
+
throw new Error("Cannot parse the pending GitHub review as JSON.");
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
body: reviewData.body ?? "",
|
|
1637
|
+
comments: commentData.map((comment) => ({
|
|
1638
|
+
file: comment.path,
|
|
1639
|
+
line: comment.line ?? comment.original_line ?? undefined,
|
|
1640
|
+
side: comment.side ?? undefined,
|
|
1641
|
+
body: comment.body
|
|
1642
|
+
}))
|
|
1643
|
+
};
|
|
1644
|
+
},
|
|
1517
1645
|
listThreads,
|
|
1518
1646
|
async reply(input) {
|
|
1519
1647
|
const threads = await listThreads();
|
|
@@ -1530,14 +1658,32 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1530
1658
|
`body=${input.body}`
|
|
1531
1659
|
]);
|
|
1532
1660
|
}
|
|
1533
|
-
if (input.resolve)
|
|
1661
|
+
if (input.resolve)
|
|
1662
|
+
await this.setResolved?.(input.threadId, true);
|
|
1663
|
+
},
|
|
1664
|
+
async setResolved(threadId, resolved) {
|
|
1665
|
+
await ghChecked([
|
|
1666
|
+
"api",
|
|
1667
|
+
"graphql",
|
|
1668
|
+
"-f",
|
|
1669
|
+
`query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
|
|
1670
|
+
"-f",
|
|
1671
|
+
`threadId=${threadId}`
|
|
1672
|
+
]);
|
|
1673
|
+
},
|
|
1674
|
+
async deleteThread(threadId) {
|
|
1675
|
+
const thread = (await listThreads()).find((candidate) => candidate.id === threadId);
|
|
1676
|
+
const commentNodeIds = thread?.commentNodeIds ?? (thread?.rootCommentId ? [thread.rootCommentId] : []);
|
|
1677
|
+
if (!commentNodeIds.length)
|
|
1678
|
+
throw new Error(`Cannot delete GitHub thread ${threadId}: comment node IDs are missing.`);
|
|
1679
|
+
for (const commentId of commentNodeIds) {
|
|
1534
1680
|
await ghChecked([
|
|
1535
1681
|
"api",
|
|
1536
1682
|
"graphql",
|
|
1537
1683
|
"-f",
|
|
1538
|
-
`query=${
|
|
1684
|
+
`query=${GITHUB_DELETE_COMMENT_MUTATION}`,
|
|
1539
1685
|
"-f",
|
|
1540
|
-
`
|
|
1686
|
+
`commentId=${commentId}`
|
|
1541
1687
|
]);
|
|
1542
1688
|
}
|
|
1543
1689
|
},
|
|
@@ -1556,11 +1702,13 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1556
1702
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
1557
1703
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
1558
1704
|
}
|
|
1559
|
-
var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
|
|
1705
|
+
var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$after:String){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100,after:$after){nodes{id,isResolved,path,line,comments(first:100){nodes{id,databaseId,body,author{login}}}}pageInfo{hasNextPage,endCursor}}}}}`;
|
|
1560
1706
|
var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
|
|
1561
1707
|
var GITHUB_ADD_FILE_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,subjectType:FILE}){thread{id}}}`;
|
|
1562
1708
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
1563
1709
|
var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
1710
|
+
var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
|
|
1711
|
+
var GITHUB_DELETE_COMMENT_MUTATION = `mutation($commentId:ID!){deletePullRequestReviewComment(input:{id:$commentId}){clientMutationId}}`;
|
|
1564
1712
|
|
|
1565
1713
|
// src/review/gitlab-review-backend.ts
|
|
1566
1714
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -1597,6 +1745,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1597
1745
|
});
|
|
1598
1746
|
};
|
|
1599
1747
|
return {
|
|
1748
|
+
kind: "remote",
|
|
1600
1749
|
async stage(draft) {
|
|
1601
1750
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
1602
1751
|
if (draft.body.trim()) {
|
|
@@ -1625,6 +1774,36 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1625
1774
|
});
|
|
1626
1775
|
}
|
|
1627
1776
|
},
|
|
1777
|
+
async readDraft() {
|
|
1778
|
+
const result = await glabChecked(["api", `${mergeRequestEndpoint()}/draft_notes`]);
|
|
1779
|
+
let notes;
|
|
1780
|
+
try {
|
|
1781
|
+
notes = JSON.parse(result.stdout);
|
|
1782
|
+
} catch {
|
|
1783
|
+
throw new Error("Cannot parse GitLab draft notes as JSON.");
|
|
1784
|
+
}
|
|
1785
|
+
const comments = [];
|
|
1786
|
+
const body = [];
|
|
1787
|
+
for (const note of notes) {
|
|
1788
|
+
if (!note.note)
|
|
1789
|
+
continue;
|
|
1790
|
+
const file = note.position?.new_path ?? note.position?.old_path;
|
|
1791
|
+
const line = note.position?.new_line ?? note.position?.old_line;
|
|
1792
|
+
if (file && line) {
|
|
1793
|
+
comments.push({
|
|
1794
|
+
file,
|
|
1795
|
+
line,
|
|
1796
|
+
side: note.position?.new_line ? "RIGHT" : "LEFT",
|
|
1797
|
+
body: note.note
|
|
1798
|
+
});
|
|
1799
|
+
} else {
|
|
1800
|
+
body.push(note.note);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return { comments, body: body.join(`
|
|
1804
|
+
|
|
1805
|
+
`) };
|
|
1806
|
+
},
|
|
1628
1807
|
listThreads,
|
|
1629
1808
|
async reply(input) {
|
|
1630
1809
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -1636,7 +1815,15 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1636
1815
|
});
|
|
1637
1816
|
}
|
|
1638
1817
|
if (input.resolve)
|
|
1639
|
-
await
|
|
1818
|
+
await this.setResolved?.(input.threadId, true);
|
|
1819
|
+
},
|
|
1820
|
+
async setResolved(threadId, resolved) {
|
|
1821
|
+
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
1822
|
+
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
|
|
1823
|
+
},
|
|
1824
|
+
async deleteThread(threadId) {
|
|
1825
|
+
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
|
|
1826
|
+
await glabChecked(["api", "--method", "DELETE", endpoint]);
|
|
1640
1827
|
},
|
|
1641
1828
|
async publish(event) {
|
|
1642
1829
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -1676,105 +1863,12 @@ function hasGitlabDraftNotes(input) {
|
|
|
1676
1863
|
}
|
|
1677
1864
|
}
|
|
1678
1865
|
|
|
1679
|
-
// src/review/review-backend.ts
|
|
1680
|
-
|
|
1681
|
-
if (vcs.provider === "github")
|
|
1682
|
-
return GitHubReviewBackend(vcs, number);
|
|
1683
|
-
if (vcs.provider === "gitlab")
|
|
1684
|
-
return GitLabReviewBackend(vcs, number);
|
|
1685
|
-
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
1686
|
-
}
|
|
1687
|
-
// src/review/local-reviews.ts
|
|
1688
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile6, unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1689
|
-
import { join as join9 } from "node:path";
|
|
1690
|
-
|
|
1691
|
-
// src/store.ts
|
|
1692
|
-
import { createHash } from "node:crypto";
|
|
1693
|
-
import { lstat, mkdir as mkdir3, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
|
|
1694
|
-
import { homedir as homedir4 } from "node:os";
|
|
1695
|
-
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
|
|
1696
|
-
var STORE_LINK = ".diffpi";
|
|
1697
|
-
var LEGACY_STORE_LINK = join8(".pi", "diffpi");
|
|
1698
|
-
async function computeProjectSlug(cwd) {
|
|
1699
|
-
return projectSlug(await inspectGitRepository(cwd));
|
|
1700
|
-
}
|
|
1701
|
-
function storeGlobalRoot(homeDir = homedir4()) {
|
|
1702
|
-
return join8(homeDir, ".difflab", "diffpi", "projects");
|
|
1703
|
-
}
|
|
1704
|
-
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
1705
|
-
const repository = await inspectGitRepository(cwd);
|
|
1706
|
-
const root = repository.root;
|
|
1707
|
-
const slug = projectSlug(repository);
|
|
1708
|
-
const dest = join8(storeGlobalRoot(homeDir), slug);
|
|
1709
|
-
const link = join8(root, STORE_LINK);
|
|
1710
|
-
await mkdir3(dest, { recursive: true });
|
|
1711
|
-
try {
|
|
1712
|
-
await assertStoreLink(link, dest);
|
|
1713
|
-
} catch (error) {
|
|
1714
|
-
if (error.code !== "ENOENT")
|
|
1715
|
-
throw error;
|
|
1716
|
-
await symlink(dest, link);
|
|
1717
|
-
}
|
|
1718
|
-
await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
|
|
1719
|
-
return { slug, root, dest, link, linked: true };
|
|
1720
|
-
}
|
|
1721
|
-
async function storeDir(cwd, homeDir = homedir4()) {
|
|
1722
|
-
const store = await ensureStore(cwd, homeDir);
|
|
1723
|
-
return store.dest;
|
|
1724
|
-
}
|
|
1725
|
-
async function plansDir(cwd, homeDir = homedir4()) {
|
|
1726
|
-
const store = await ensureStore(cwd, homeDir);
|
|
1727
|
-
const dir = join8(store.link, "plan");
|
|
1728
|
-
await mkdir3(dir, { recursive: true });
|
|
1729
|
-
return dir;
|
|
1730
|
-
}
|
|
1731
|
-
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
1732
|
-
const store = await ensureStore(cwd, homeDir);
|
|
1733
|
-
const dir = join8(store.link, "review");
|
|
1734
|
-
await mkdir3(dir, { recursive: true });
|
|
1735
|
-
return dir;
|
|
1736
|
-
}
|
|
1737
|
-
async function assertStoreLink(path, dest) {
|
|
1738
|
-
const entry = await lstat(path);
|
|
1739
|
-
if (!entry.isSymbolicLink())
|
|
1740
|
-
throw new Error(`${path} exists and is not a symlink.`);
|
|
1741
|
-
const target = await symlinkTarget(path);
|
|
1742
|
-
if (target !== await canonicalPath2(dest))
|
|
1743
|
-
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
1744
|
-
}
|
|
1745
|
-
async function removeLegacyStoreLink(path, dest) {
|
|
1746
|
-
try {
|
|
1747
|
-
const entry = await lstat(path);
|
|
1748
|
-
if (!entry.isSymbolicLink())
|
|
1749
|
-
return;
|
|
1750
|
-
const target = await symlinkTarget(path);
|
|
1751
|
-
if (target === await canonicalPath2(dest))
|
|
1752
|
-
await unlink(path);
|
|
1753
|
-
} catch (error) {
|
|
1754
|
-
if (error.code !== "ENOENT")
|
|
1755
|
-
throw error;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
async function symlinkTarget(path) {
|
|
1759
|
-
const target = await readlink(path);
|
|
1760
|
-
return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname4(path), target));
|
|
1761
|
-
}
|
|
1762
|
-
async function canonicalPath2(path) {
|
|
1763
|
-
try {
|
|
1764
|
-
return await realpath2(path);
|
|
1765
|
-
} catch {
|
|
1766
|
-
return resolve2(path);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
function projectSlug(repository) {
|
|
1770
|
-
const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
1771
|
-
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
1772
|
-
return `${readable}-${digest}`;
|
|
1773
|
-
}
|
|
1866
|
+
// src/review/local-review-backend.ts
|
|
1867
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
1774
1868
|
|
|
1775
1869
|
// src/extensions/tuicrx.ts
|
|
1776
|
-
import { readFile as readFile5, realpath as
|
|
1777
|
-
import { resolve as
|
|
1870
|
+
import { readFile as readFile5, realpath as realpath2 } from "node:fs/promises";
|
|
1871
|
+
import { resolve as resolve2 } from "node:path";
|
|
1778
1872
|
async function tuicrAvailable() {
|
|
1779
1873
|
return Boolean(await findExecutable("tuicr"));
|
|
1780
1874
|
}
|
|
@@ -1850,7 +1944,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
|
|
|
1850
1944
|
});
|
|
1851
1945
|
}
|
|
1852
1946
|
async function findMatchingSession(sessions, cwd, branch) {
|
|
1853
|
-
const repository = await
|
|
1947
|
+
const repository = await canonicalPath2(await gitToplevel(cwd));
|
|
1854
1948
|
for (const session of sessions) {
|
|
1855
1949
|
if (session.kind !== "local")
|
|
1856
1950
|
continue;
|
|
@@ -1858,7 +1952,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
1858
1952
|
const data = await readSession(session.path);
|
|
1859
1953
|
if (data.branch_name !== branch || !data.repo_path)
|
|
1860
1954
|
continue;
|
|
1861
|
-
if (await
|
|
1955
|
+
if (await canonicalPath2(data.repo_path) === repository)
|
|
1862
1956
|
return session;
|
|
1863
1957
|
} catch {}
|
|
1864
1958
|
}
|
|
@@ -1872,33 +1966,53 @@ async function readSession(path) {
|
|
|
1872
1966
|
throw new Error(`Cannot parse tuicr session JSON: ${path}`);
|
|
1873
1967
|
}
|
|
1874
1968
|
}
|
|
1875
|
-
function
|
|
1876
|
-
const
|
|
1877
|
-
const
|
|
1969
|
+
function toFindings(session, options = {}) {
|
|
1970
|
+
const comments = [];
|
|
1971
|
+
const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
|
|
1972
|
+
const publishableBody = (comment) => {
|
|
1878
1973
|
const author = commentAuthor(comment);
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
...location,
|
|
1882
|
-
body: comment.content,
|
|
1883
|
-
...author ? { author } : {},
|
|
1884
|
-
resolved: false,
|
|
1885
|
-
question: /\?\s*$/.test(comment.content.trim())
|
|
1886
|
-
});
|
|
1974
|
+
const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
|
|
1975
|
+
return route ? withRemoteProvenance(comment.content, route) : comment.content;
|
|
1887
1976
|
};
|
|
1888
|
-
|
|
1889
|
-
add(comment);
|
|
1977
|
+
const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
|
|
1890
1978
|
for (const [file, entry] of Object.entries(session.files ?? {})) {
|
|
1891
|
-
for (const
|
|
1892
|
-
|
|
1979
|
+
for (const fileComment of entry.file_comments ?? []) {
|
|
1980
|
+
if (!include(fileComment))
|
|
1981
|
+
continue;
|
|
1982
|
+
const comment = {
|
|
1983
|
+
file,
|
|
1984
|
+
body: fileComment.content,
|
|
1985
|
+
...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
|
|
1986
|
+
};
|
|
1987
|
+
const author = commentAuthor(fileComment);
|
|
1988
|
+
if (author)
|
|
1989
|
+
comment.author = author;
|
|
1990
|
+
comments.push(comment);
|
|
1991
|
+
}
|
|
1893
1992
|
for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
|
|
1894
1993
|
const line = Number.parseInt(lineKey, 10);
|
|
1895
1994
|
if (!Number.isFinite(line))
|
|
1896
1995
|
continue;
|
|
1897
|
-
for (const
|
|
1898
|
-
|
|
1996
|
+
for (const lineComment of lineComments) {
|
|
1997
|
+
if (!include(lineComment))
|
|
1998
|
+
continue;
|
|
1999
|
+
const comment = {
|
|
2000
|
+
file,
|
|
2001
|
+
line,
|
|
2002
|
+
side: lineComment.side === "old" ? "LEFT" : "RIGHT",
|
|
2003
|
+
body: lineComment.content,
|
|
2004
|
+
...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
|
|
2005
|
+
};
|
|
2006
|
+
const author = commentAuthor(lineComment);
|
|
2007
|
+
if (author)
|
|
2008
|
+
comment.author = author;
|
|
2009
|
+
comments.push(comment);
|
|
2010
|
+
}
|
|
1899
2011
|
}
|
|
1900
2012
|
}
|
|
1901
|
-
return
|
|
2013
|
+
return { comments, body: bodyParts.join(`
|
|
2014
|
+
|
|
2015
|
+
`) };
|
|
1902
2016
|
}
|
|
1903
2017
|
async function requireTuicr() {
|
|
1904
2018
|
if (!await tuicrAvailable())
|
|
@@ -1907,70 +2021,207 @@ async function requireTuicr() {
|
|
|
1907
2021
|
function commentAuthor(comment) {
|
|
1908
2022
|
return comment.username ?? comment.author;
|
|
1909
2023
|
}
|
|
1910
|
-
async function
|
|
2024
|
+
async function canonicalPath2(path) {
|
|
1911
2025
|
try {
|
|
1912
|
-
return await
|
|
2026
|
+
return await realpath2(path);
|
|
1913
2027
|
} catch {
|
|
1914
|
-
return
|
|
2028
|
+
return resolve2(path);
|
|
1915
2029
|
}
|
|
1916
2030
|
}
|
|
1917
2031
|
|
|
1918
|
-
// src/review/local-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2032
|
+
// src/review/local-review-backend.ts
|
|
2033
|
+
function LocalReviewBackend(options) {
|
|
2034
|
+
return {
|
|
2035
|
+
kind: "local",
|
|
2036
|
+
async stage(draft) {
|
|
2037
|
+
for (const comment of draft.comments) {
|
|
2038
|
+
await addComment(options.session, comment.body, {
|
|
2039
|
+
targetFile: comment.file,
|
|
2040
|
+
line: comment.line,
|
|
2041
|
+
side: comment.side === "LEFT" ? "old" : "new",
|
|
2042
|
+
username: options.author
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
if (draft.body.trim())
|
|
2046
|
+
await addComment(options.session, draft.body, { username: options.author });
|
|
2047
|
+
},
|
|
2048
|
+
async readDraft() {
|
|
2049
|
+
return toFindings(await readSession(options.session), { excludeLocalResponses: true });
|
|
2050
|
+
},
|
|
2051
|
+
async listThreads() {
|
|
2052
|
+
try {
|
|
2053
|
+
return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
|
|
2054
|
+
} catch (error) {
|
|
2055
|
+
if (error.code === "ENOENT")
|
|
2056
|
+
return [];
|
|
2057
|
+
throw error;
|
|
2058
|
+
}
|
|
2059
|
+
},
|
|
2060
|
+
async reply(input) {
|
|
2061
|
+
const content = await readFile6(options.artifactPath, "utf8");
|
|
2062
|
+
await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
2063
|
+
},
|
|
2064
|
+
async publish() {
|
|
2065
|
+
throw new Error("Promote a local draft through a remote review backend before publishing it.");
|
|
2066
|
+
}
|
|
1936
2067
|
};
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/review/review-backend.ts
|
|
2071
|
+
function createRemoteReviewBackend(vcs, number) {
|
|
2072
|
+
if (vcs.provider === "github")
|
|
2073
|
+
return GitHubReviewBackend(vcs, number);
|
|
2074
|
+
if (vcs.provider === "gitlab")
|
|
2075
|
+
return GitLabReviewBackend(vcs, number);
|
|
2076
|
+
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
2077
|
+
}
|
|
2078
|
+
function createLocalReviewBackend(options) {
|
|
2079
|
+
return LocalReviewBackend(options);
|
|
2080
|
+
}
|
|
2081
|
+
// src/review/review-state.ts
|
|
2082
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2083
|
+
import { readFile as readFile7, rename, writeFile as writeFile4 } from "node:fs/promises";
|
|
2084
|
+
import { join as join9 } from "node:path";
|
|
2085
|
+
import { z as z5 } from "zod";
|
|
2086
|
+
|
|
2087
|
+
// src/store.ts
|
|
2088
|
+
import { createHash } from "node:crypto";
|
|
2089
|
+
import { lstat, mkdir as mkdir3, readlink, realpath as realpath3, symlink, unlink } from "node:fs/promises";
|
|
2090
|
+
import { homedir as homedir4 } from "node:os";
|
|
2091
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
|
|
2092
|
+
var STORE_LINK = ".diffpi";
|
|
2093
|
+
var LEGACY_STORE_LINK = join8(".pi", "diffpi");
|
|
2094
|
+
async function computeProjectSlug(cwd) {
|
|
2095
|
+
return projectSlug(await inspectGitRepository(cwd));
|
|
2096
|
+
}
|
|
2097
|
+
function storeGlobalRoot(homeDir = homedir4()) {
|
|
2098
|
+
return join8(homeDir, ".difflab", "diffpi", "projects");
|
|
2099
|
+
}
|
|
2100
|
+
async function ensureStore(cwd, homeDir = homedir4()) {
|
|
2101
|
+
const repository = await inspectGitRepository(cwd);
|
|
2102
|
+
const root = repository.root;
|
|
2103
|
+
const slug = projectSlug(repository);
|
|
2104
|
+
const dest = join8(storeGlobalRoot(homeDir), slug);
|
|
2105
|
+
const link = join8(root, STORE_LINK);
|
|
2106
|
+
await mkdir3(dest, { recursive: true });
|
|
1949
2107
|
try {
|
|
1950
|
-
|
|
2108
|
+
await assertStoreLink(link, dest);
|
|
1951
2109
|
} catch (error) {
|
|
1952
|
-
if (error.code
|
|
2110
|
+
if (error.code !== "ENOENT")
|
|
2111
|
+
throw error;
|
|
2112
|
+
await symlink(dest, link);
|
|
2113
|
+
}
|
|
2114
|
+
await removeLegacyStoreLink(join8(root, LEGACY_STORE_LINK), dest);
|
|
2115
|
+
return { slug, root, dest, link, linked: true };
|
|
2116
|
+
}
|
|
2117
|
+
async function storeDir(cwd, homeDir = homedir4()) {
|
|
2118
|
+
const store = await ensureStore(cwd, homeDir);
|
|
2119
|
+
return store.dest;
|
|
2120
|
+
}
|
|
2121
|
+
async function plansDir(cwd, homeDir = homedir4()) {
|
|
2122
|
+
const store = await ensureStore(cwd, homeDir);
|
|
2123
|
+
const dir = join8(store.link, "plan");
|
|
2124
|
+
await mkdir3(dir, { recursive: true });
|
|
2125
|
+
return dir;
|
|
2126
|
+
}
|
|
2127
|
+
async function reviewsDir(cwd, homeDir = homedir4()) {
|
|
2128
|
+
const store = await ensureStore(cwd, homeDir);
|
|
2129
|
+
const dir = join8(store.link, "review");
|
|
2130
|
+
await mkdir3(dir, { recursive: true });
|
|
2131
|
+
return dir;
|
|
2132
|
+
}
|
|
2133
|
+
async function sessionsDir(cwd, homeDir = homedir4()) {
|
|
2134
|
+
const store = await ensureStore(cwd, homeDir);
|
|
2135
|
+
const dir = join8(store.link, "sessions");
|
|
2136
|
+
await mkdir3(dir, { recursive: true });
|
|
2137
|
+
return dir;
|
|
2138
|
+
}
|
|
2139
|
+
async function assertStoreLink(path, dest) {
|
|
2140
|
+
const entry = await lstat(path);
|
|
2141
|
+
if (!entry.isSymbolicLink())
|
|
2142
|
+
throw new Error(`${path} exists and is not a symlink.`);
|
|
2143
|
+
const target = await symlinkTarget(path);
|
|
2144
|
+
if (target !== await canonicalPath3(dest))
|
|
2145
|
+
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
2146
|
+
}
|
|
2147
|
+
async function removeLegacyStoreLink(path, dest) {
|
|
2148
|
+
try {
|
|
2149
|
+
const entry = await lstat(path);
|
|
2150
|
+
if (!entry.isSymbolicLink())
|
|
1953
2151
|
return;
|
|
1954
|
-
|
|
2152
|
+
const target = await symlinkTarget(path);
|
|
2153
|
+
if (target === await canonicalPath3(dest))
|
|
2154
|
+
await unlink(path);
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
if (error.code !== "ENOENT")
|
|
2157
|
+
throw error;
|
|
1955
2158
|
}
|
|
1956
2159
|
}
|
|
1957
|
-
async function
|
|
1958
|
-
|
|
2160
|
+
async function symlinkTarget(path) {
|
|
2161
|
+
const target = await readlink(path);
|
|
2162
|
+
return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname4(path), target));
|
|
1959
2163
|
}
|
|
1960
|
-
async function
|
|
2164
|
+
async function canonicalPath3(path) {
|
|
1961
2165
|
try {
|
|
1962
|
-
return
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2166
|
+
return await realpath3(path);
|
|
2167
|
+
} catch {
|
|
2168
|
+
return resolve3(path);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function projectSlug(repository) {
|
|
2172
|
+
const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
|
|
2173
|
+
const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
|
|
2174
|
+
return `${readable}-${digest}`;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/review/review-state.ts
|
|
2178
|
+
var reviewPublicationStateSchema = z5.object({
|
|
2179
|
+
target: z5.string().optional(),
|
|
2180
|
+
bodies: z5.array(z5.string()).default([]),
|
|
2181
|
+
comments: z5.array(z5.string()).default([]),
|
|
2182
|
+
stagedComments: z5.array(z5.string()).default([]),
|
|
2183
|
+
replies: z5.array(z5.string()).default([]),
|
|
2184
|
+
overlayPath: z5.string().optional()
|
|
2185
|
+
});
|
|
2186
|
+
function reviewBodyFingerprint(body) {
|
|
2187
|
+
return digest(body);
|
|
2188
|
+
}
|
|
2189
|
+
function reviewCommentFingerprint(comment) {
|
|
2190
|
+
return digest([comment.file, String(comment.line), comment.side ?? "RIGHT", comment.body].join("\x00"));
|
|
2191
|
+
}
|
|
2192
|
+
function reviewReplyFingerprint(threadId, body) {
|
|
2193
|
+
return digest(`${threadId}\x00${body}`);
|
|
2194
|
+
}
|
|
2195
|
+
function unpublishedReviewComments(comments, knownFingerprints) {
|
|
2196
|
+
return comments.filter((comment) => !knownFingerprints.has(reviewCommentFingerprint(comment)));
|
|
2197
|
+
}
|
|
2198
|
+
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
2199
|
+
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
2200
|
+
const path = join9(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
2201
|
+
try {
|
|
2202
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile7(path, "utf8")));
|
|
2203
|
+
return { path, state: { ...parsed, target } };
|
|
1966
2204
|
} catch (error) {
|
|
1967
|
-
if (error.code === "ENOENT")
|
|
1968
|
-
return
|
|
2205
|
+
if (error.code === "ENOENT") {
|
|
2206
|
+
return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
|
|
2207
|
+
}
|
|
2208
|
+
if (error instanceof SyntaxError || error instanceof z5.ZodError) {
|
|
2209
|
+
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
2210
|
+
}
|
|
1969
2211
|
throw error;
|
|
1970
2212
|
}
|
|
1971
2213
|
}
|
|
2214
|
+
async function saveReviewPublicationState(path, state) {
|
|
2215
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
2216
|
+
await writeFile4(temp, `${JSON.stringify(state, null, 2)}
|
|
2217
|
+
`, "utf8");
|
|
2218
|
+
await rename(temp, path);
|
|
2219
|
+
}
|
|
2220
|
+
function digest(value) {
|
|
2221
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
2222
|
+
}
|
|
1972
2223
|
// src/mcp.ts
|
|
1973
|
-
import { mkdir as
|
|
2224
|
+
import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
|
|
1974
2225
|
import { homedir as homedir5 } from "node:os";
|
|
1975
2226
|
import { dirname as dirname5, join as join10 } from "node:path";
|
|
1976
2227
|
var mcp = {
|
|
@@ -1988,8 +2239,8 @@ var mcp = {
|
|
|
1988
2239
|
const next = { ...current, mcpServers: nextServers };
|
|
1989
2240
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
1990
2241
|
if (changed && !options.dryRun) {
|
|
1991
|
-
await
|
|
1992
|
-
await
|
|
2242
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2243
|
+
await writeFile5(path, `${JSON.stringify(next, null, 2)}
|
|
1993
2244
|
`, "utf8");
|
|
1994
2245
|
}
|
|
1995
2246
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -2018,7 +2269,7 @@ function getParsedConfig(content, path) {
|
|
|
2018
2269
|
}
|
|
2019
2270
|
async function getOptionalFile2(path) {
|
|
2020
2271
|
try {
|
|
2021
|
-
return await
|
|
2272
|
+
return await readFile8(path, "utf8");
|
|
2022
2273
|
} catch (error) {
|
|
2023
2274
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2024
2275
|
return;
|
|
@@ -2033,7 +2284,7 @@ import {
|
|
|
2033
2284
|
getAgentDir,
|
|
2034
2285
|
parseFrontmatter as parseFrontmatter2
|
|
2035
2286
|
} from "@earendil-works/pi-coding-agent";
|
|
2036
|
-
import { readFile as
|
|
2287
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
2037
2288
|
import { homedir as homedir6 } from "node:os";
|
|
2038
2289
|
import { basename as basename4, extname, join as join11 } from "node:path";
|
|
2039
2290
|
async function discoverAgentModes(options) {
|
|
@@ -2232,7 +2483,7 @@ async function loadAgentModes(directory, source, modes, diagnostics, skillName)
|
|
|
2232
2483
|
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2233
2484
|
const path = join11(directory, entry.name);
|
|
2234
2485
|
try {
|
|
2235
|
-
const content = await
|
|
2486
|
+
const content = await readFile9(path, "utf8");
|
|
2236
2487
|
const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
|
|
2237
2488
|
if (frontmatter.enabled === false || frontmatter.inline === false)
|
|
2238
2489
|
continue;
|
|
@@ -2288,7 +2539,7 @@ function isModeBaseline(value) {
|
|
|
2288
2539
|
return (model === undefined || typeof model.provider === "string" && typeof model.id === "string") && candidate.thinkingLevel !== undefined && THINKING_LEVELS.has(candidate.thinkingLevel) && Array.isArray(candidate.tools) && candidate.tools.every((tool) => typeof tool === "string");
|
|
2289
2540
|
}
|
|
2290
2541
|
// src/pi.ts
|
|
2291
|
-
import { mkdir as
|
|
2542
|
+
import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
|
|
2292
2543
|
import { homedir as homedir7 } from "node:os";
|
|
2293
2544
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
2294
2545
|
var pi = {
|
|
@@ -2307,8 +2558,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
2307
2558
|
const currentText = await readTextIfExists(path);
|
|
2308
2559
|
const changed = currentText !== content;
|
|
2309
2560
|
if (changed && !dryRun) {
|
|
2310
|
-
await
|
|
2311
|
-
await
|
|
2561
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2562
|
+
await writeFile6(path, content, "utf8");
|
|
2312
2563
|
}
|
|
2313
2564
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
2314
2565
|
}
|
|
@@ -2344,8 +2595,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
2344
2595
|
const next = update(current);
|
|
2345
2596
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2346
2597
|
if (changed && !dryRun) {
|
|
2347
|
-
await
|
|
2348
|
-
await
|
|
2598
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2599
|
+
await writeFile6(path, `${JSON.stringify(next, null, 2)}
|
|
2349
2600
|
`, "utf8");
|
|
2350
2601
|
}
|
|
2351
2602
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -2378,16 +2629,16 @@ function parseJsonObject(content, path) {
|
|
|
2378
2629
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2379
2630
|
}
|
|
2380
2631
|
// src/plan/store.ts
|
|
2381
|
-
import { mkdir as
|
|
2632
|
+
import { mkdir as mkdir7, readdir as readdir2, readFile as readFile12, realpath as realpath4, rm as rm2 } from "node:fs/promises";
|
|
2382
2633
|
import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
|
|
2383
2634
|
|
|
2384
2635
|
// src/extensions/fsx.ts
|
|
2385
|
-
import { mkdir as
|
|
2636
|
+
import { mkdir as mkdir6, rename as rename2, rm, writeFile as writeFile7 } from "node:fs/promises";
|
|
2386
2637
|
import { basename as basename5, dirname as dirname7, join as join13 } from "node:path";
|
|
2387
2638
|
async function atomicWrite(path, content) {
|
|
2388
2639
|
const temp = join13(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
2389
|
-
await
|
|
2390
|
-
await
|
|
2640
|
+
await writeFile7(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
2641
|
+
await rename2(temp, path);
|
|
2391
2642
|
}
|
|
2392
2643
|
async function withDirectoryLock(lockDir, operation, options = {}) {
|
|
2393
2644
|
const waitMs = options.waitMs ?? 5000;
|
|
@@ -2395,7 +2646,7 @@ async function withDirectoryLock(lockDir, operation, options = {}) {
|
|
|
2395
2646
|
const started = Date.now();
|
|
2396
2647
|
while (true) {
|
|
2397
2648
|
try {
|
|
2398
|
-
await
|
|
2649
|
+
await mkdir6(lockDir);
|
|
2399
2650
|
break;
|
|
2400
2651
|
} catch (error) {
|
|
2401
2652
|
if (error.code !== "EEXIST")
|
|
@@ -2417,7 +2668,7 @@ function sleep(ms) {
|
|
|
2417
2668
|
|
|
2418
2669
|
// src/log.ts
|
|
2419
2670
|
import { randomUUID } from "node:crypto";
|
|
2420
|
-
import { appendFile, readFile as
|
|
2671
|
+
import { appendFile, readFile as readFile10 } from "node:fs/promises";
|
|
2421
2672
|
async function appendLogEntry(logPath, event) {
|
|
2422
2673
|
const entry = {
|
|
2423
2674
|
...event,
|
|
@@ -2431,7 +2682,7 @@ async function appendLogEntry(logPath, event) {
|
|
|
2431
2682
|
}
|
|
2432
2683
|
|
|
2433
2684
|
// src/templates.ts
|
|
2434
|
-
import { readFile as
|
|
2685
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
2435
2686
|
import { homedir as homedir8 } from "node:os";
|
|
2436
2687
|
import { join as join14, normalize } from "node:path";
|
|
2437
2688
|
async function loadTemplate(name, options = {}) {
|
|
@@ -2458,7 +2709,7 @@ function templateRelativePath(name) {
|
|
|
2458
2709
|
}
|
|
2459
2710
|
async function readOptionalFile(path) {
|
|
2460
2711
|
try {
|
|
2461
|
-
return await
|
|
2712
|
+
return await readFile11(path, "utf8");
|
|
2462
2713
|
} catch (error) {
|
|
2463
2714
|
if (error.code === "ENOENT")
|
|
2464
2715
|
return;
|
|
@@ -7542,7 +7793,7 @@ function planRecordName(shortSlug, date = new Date) {
|
|
|
7542
7793
|
}
|
|
7543
7794
|
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
7544
7795
|
const root = await plansDir(cwd, options.homeDir);
|
|
7545
|
-
const entries = await
|
|
7796
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
7546
7797
|
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
7547
7798
|
const records = [];
|
|
7548
7799
|
for (const entry of entries) {
|
|
@@ -7579,7 +7830,7 @@ function createPlanStore(options = {}) {
|
|
|
7579
7830
|
const dir = join16(root, id);
|
|
7580
7831
|
await assertContained(root, dirname8(dir));
|
|
7581
7832
|
try {
|
|
7582
|
-
await
|
|
7833
|
+
await mkdir7(dir);
|
|
7583
7834
|
} catch (error) {
|
|
7584
7835
|
if (error.code === "EEXIST")
|
|
7585
7836
|
throw new Error(`Plan ${id} already exists.`);
|
|
@@ -7654,7 +7905,7 @@ function createPlanStore(options = {}) {
|
|
|
7654
7905
|
async function readRecord(dir) {
|
|
7655
7906
|
const planPath = join16(dir, "PLAN.md");
|
|
7656
7907
|
const logPath = join16(dir, "logs.txt");
|
|
7657
|
-
const source = await
|
|
7908
|
+
const source = await readFile12(planPath, "utf8");
|
|
7658
7909
|
const document = parsePlanDocument(source, planPath);
|
|
7659
7910
|
return {
|
|
7660
7911
|
id: basename6(dir),
|
|
@@ -7668,7 +7919,7 @@ async function readRecord(dir) {
|
|
|
7668
7919
|
}
|
|
7669
7920
|
async function ensureImplementationFiles(dir, document, options) {
|
|
7670
7921
|
const implementationDir = join16(dir, "implementation");
|
|
7671
|
-
await
|
|
7922
|
+
await mkdir7(implementationDir, { recursive: true });
|
|
7672
7923
|
const template = await loadTemplate("plan/implementation", {
|
|
7673
7924
|
homeDir: options.homeDir,
|
|
7674
7925
|
bundledDir: options.bundledTemplatesDir
|
|
@@ -7676,7 +7927,7 @@ async function ensureImplementationFiles(dir, document, options) {
|
|
|
7676
7927
|
for (const phase of document.phases) {
|
|
7677
7928
|
const path = join16(implementationDir, `phase-${phase.id}.md`);
|
|
7678
7929
|
try {
|
|
7679
|
-
await
|
|
7930
|
+
await readFile12(path, "utf8");
|
|
7680
7931
|
} catch (error) {
|
|
7681
7932
|
if (error.code !== "ENOENT")
|
|
7682
7933
|
throw error;
|
|
@@ -7725,7 +7976,7 @@ function titleFromSlug(value) {
|
|
|
7725
7976
|
}
|
|
7726
7977
|
// src/setup.ts
|
|
7727
7978
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
7728
|
-
import { readdir as
|
|
7979
|
+
import { readdir as readdir3, readFile as readFile13 } from "node:fs/promises";
|
|
7729
7980
|
import { homedir as homedir9 } from "node:os";
|
|
7730
7981
|
import { basename as basename7, join as join17 } from "node:path";
|
|
7731
7982
|
var MISE_DEPENDENCIES = [
|
|
@@ -7828,11 +8079,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
7828
8079
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
7829
8080
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
|
|
7830
8081
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
7831
|
-
const entries = (await
|
|
8082
|
+
const entries = (await readdir3(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
7832
8083
|
const actions = [];
|
|
7833
8084
|
for (const entry of entries) {
|
|
7834
8085
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
7835
|
-
const source = await
|
|
8086
|
+
const source = await readFile13(join17(bundledAgentsDir, entry.name), "utf8");
|
|
7836
8087
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
7837
8088
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
7838
8089
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -8037,7 +8288,7 @@ function reportProgress(options, message) {
|
|
|
8037
8288
|
options.onProgress?.(message);
|
|
8038
8289
|
}
|
|
8039
8290
|
async function packageVersion2() {
|
|
8040
|
-
const source = await
|
|
8291
|
+
const source = await readFile13(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
8041
8292
|
let value;
|
|
8042
8293
|
try {
|
|
8043
8294
|
value = JSON.parse(source);
|
|
@@ -8062,11 +8313,11 @@ export {
|
|
|
8062
8313
|
addComment,
|
|
8063
8314
|
assertGitHubMergeReady,
|
|
8064
8315
|
assertReviewEventSupported,
|
|
8065
|
-
captureLocalReview,
|
|
8066
8316
|
checkConventionalSubject,
|
|
8067
8317
|
computeProjectSlug,
|
|
8068
8318
|
createForge,
|
|
8069
8319
|
createForgeBackend,
|
|
8320
|
+
createLocalReviewBackend,
|
|
8070
8321
|
createModeController,
|
|
8071
8322
|
createPlanStore,
|
|
8072
8323
|
createRemoteReviewBackend,
|
|
@@ -8099,6 +8350,7 @@ export {
|
|
|
8099
8350
|
launch,
|
|
8100
8351
|
listSessions,
|
|
8101
8352
|
loadDiffpiConfig,
|
|
8353
|
+
loadReviewPublicationState,
|
|
8102
8354
|
loadTemplate,
|
|
8103
8355
|
localReviewAuthor,
|
|
8104
8356
|
mcp,
|
|
@@ -8107,28 +8359,37 @@ export {
|
|
|
8107
8359
|
openInNewTab,
|
|
8108
8360
|
parseGitlabDiffRefs,
|
|
8109
8361
|
parseRemote,
|
|
8362
|
+
parseThreadArtifact,
|
|
8110
8363
|
pi,
|
|
8111
8364
|
plansDir,
|
|
8112
|
-
readLocalReview,
|
|
8113
8365
|
readSession,
|
|
8114
8366
|
renderReviewDoc,
|
|
8115
8367
|
renderTemplate,
|
|
8368
|
+
renderThreadArtifact,
|
|
8116
8369
|
resolveAgentMode,
|
|
8117
8370
|
resolveAgentModelPreferences,
|
|
8118
8371
|
resolvePrSession,
|
|
8119
8372
|
resolveReviewSession,
|
|
8120
8373
|
resolveSession,
|
|
8374
|
+
reviewBodyFingerprint,
|
|
8375
|
+
reviewCommentFingerprint,
|
|
8121
8376
|
reviewRecordName,
|
|
8377
|
+
reviewReplyFingerprint,
|
|
8122
8378
|
reviewSlug,
|
|
8123
8379
|
reviewsDir,
|
|
8124
8380
|
runMiseGates,
|
|
8381
|
+
saveReviewPublicationState,
|
|
8382
|
+
sessionsDir,
|
|
8125
8383
|
setupPi,
|
|
8126
8384
|
severitySchema,
|
|
8127
8385
|
storeDir,
|
|
8128
8386
|
storeGlobalRoot,
|
|
8129
8387
|
templateRelativePath,
|
|
8388
|
+
toFindings,
|
|
8130
8389
|
toReviewComments,
|
|
8131
8390
|
tuicrAvailable,
|
|
8391
|
+
unpublishedReviewComments,
|
|
8392
|
+
upsertThreadReply,
|
|
8132
8393
|
withRemoteProvenance,
|
|
8133
8394
|
yymmdd,
|
|
8134
8395
|
zedKeymapPath,
|