@difflab/pi 0.5.0 → 0.6.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-copilot.md +6 -3
- package/agents/diffpi-reviewer.md +3 -3
- package/dist/commands/review.d.ts +0 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/extensions/index.js +344 -810
- package/dist/extensions/tuicrx.d.ts +1 -8
- package/dist/extensions/tuicrx.d.ts.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +197 -458
- 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-reviews.d.ts +31 -0
- package/dist/review/local-reviews.d.ts.map +1 -0
- package/dist/review/review-backend.d.ts +2 -3
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/review/review-markdown.d.ts +1 -4
- package/dist/review/review-markdown.d.ts.map +1 -1
- package/dist/review/types.d.ts +0 -28
- package/dist/review/types.d.ts.map +1 -1
- package/dist/store.d.ts +0 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/tools/index.js +185 -783
- package/dist/tools/review.d.ts +0 -30
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/workflow.d.ts +47 -0
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/review/SKILL.md +6 -8
- package/workflows/review/address.md +8 -0
- package/workflows/review/auto.md +8 -0
- package/workflows/review/complete.md +7 -0
- package/workflows/review/edit.md +7 -0
- package/workflows/review/help.md +14 -0
- package/workflows/review/merge.md +6 -0
- package/workflows/review/new.md +7 -0
- package/workflows/review/publish.md +6 -0
- package/dist/review/local-review-backend.d.ts +0 -3
- package/dist/review/local-review-backend.d.ts.map +0 -1
- package/dist/review/review-state.d.ts +0 -20
- package/dist/review/review-state.d.ts.map +0 -1
- package/skills/review/references/workflows/address.md +0 -11
- package/skills/review/references/workflows/auto.md +0 -18
- package/skills/review/references/workflows/complete.md +0 -7
- package/skills/review/references/workflows/edit.md +0 -7
- package/skills/review/references/workflows/help.md +0 -30
- package/skills/review/references/workflows/merge.md +0 -6
- package/skills/review/references/workflows/new.md +0 -19
- package/skills/review/references/workflows/open.md +0 -3
- package/skills/review/references/workflows/publish.md +0 -8
package/dist/index.js
CHANGED
|
@@ -1301,9 +1301,6 @@ 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
|
-
}
|
|
1307
1304
|
function dedupeFindings(findings) {
|
|
1308
1305
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
1309
1306
|
const byKey = new Map;
|
|
@@ -1349,18 +1346,6 @@ function renderCommentBody(finding) {
|
|
|
1349
1346
|
return `${prefix}${finding.body}${reference}`;
|
|
1350
1347
|
}
|
|
1351
1348
|
// 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
|
-
});
|
|
1364
1349
|
function renderReviewDoc(input) {
|
|
1365
1350
|
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
1366
1351
|
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
@@ -1402,88 +1387,6 @@ function renderReviewDoc(input) {
|
|
|
1402
1387
|
`).trimEnd()}
|
|
1403
1388
|
`;
|
|
1404
1389
|
}
|
|
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
|
-
}
|
|
1487
1390
|
// src/review/github-review-backend.ts
|
|
1488
1391
|
function GitHubReviewBackend(vcs, number) {
|
|
1489
1392
|
const pendingReview = async () => {
|
|
@@ -1545,9 +1448,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1545
1448
|
id: thread.id,
|
|
1546
1449
|
file: thread.path ?? undefined,
|
|
1547
1450
|
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] : []),
|
|
1551
1451
|
body,
|
|
1552
1452
|
author: comment?.author?.login,
|
|
1553
1453
|
resolved: thread.isResolved,
|
|
@@ -1557,7 +1457,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1557
1457
|
});
|
|
1558
1458
|
};
|
|
1559
1459
|
return {
|
|
1560
|
-
kind: "remote",
|
|
1561
1460
|
async stage(draft) {
|
|
1562
1461
|
const pending = await pendingReview();
|
|
1563
1462
|
if (!pending) {
|
|
@@ -1615,33 +1514,6 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1615
1514
|
await ghChecked(args);
|
|
1616
1515
|
}
|
|
1617
1516
|
},
|
|
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
|
-
},
|
|
1645
1517
|
listThreads,
|
|
1646
1518
|
async reply(input) {
|
|
1647
1519
|
const threads = await listThreads();
|
|
@@ -1658,32 +1530,14 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1658
1530
|
`body=${input.body}`
|
|
1659
1531
|
]);
|
|
1660
1532
|
}
|
|
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) {
|
|
1533
|
+
if (input.resolve) {
|
|
1680
1534
|
await ghChecked([
|
|
1681
1535
|
"api",
|
|
1682
1536
|
"graphql",
|
|
1683
1537
|
"-f",
|
|
1684
|
-
`query=${
|
|
1538
|
+
`query=${GITHUB_RESOLVE_MUTATION}`,
|
|
1685
1539
|
"-f",
|
|
1686
|
-
`
|
|
1540
|
+
`threadId=${input.threadId}`
|
|
1687
1541
|
]);
|
|
1688
1542
|
}
|
|
1689
1543
|
},
|
|
@@ -1702,13 +1556,11 @@ function GitHubReviewBackend(vcs, number) {
|
|
|
1702
1556
|
function githubReviewSubmissionEndpoint(owner, repo, id, pendingReviewId) {
|
|
1703
1557
|
return pendingReviewId ? `/repos/${owner}/${repo}/pulls/${id}/reviews/${pendingReviewId}/events` : `/repos/${owner}/${repo}/pulls/${id}/reviews`;
|
|
1704
1558
|
}
|
|
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{
|
|
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}}}}}`;
|
|
1706
1560
|
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}}}`;
|
|
1707
1561
|
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}}}`;
|
|
1708
1562
|
var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
|
|
1709
1563
|
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}}`;
|
|
1712
1564
|
|
|
1713
1565
|
// src/review/gitlab-review-backend.ts
|
|
1714
1566
|
function GitLabReviewBackend(vcs, number) {
|
|
@@ -1745,7 +1597,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1745
1597
|
});
|
|
1746
1598
|
};
|
|
1747
1599
|
return {
|
|
1748
|
-
kind: "remote",
|
|
1749
1600
|
async stage(draft) {
|
|
1750
1601
|
const endpoint = `${mergeRequestEndpoint()}/draft_notes`;
|
|
1751
1602
|
if (draft.body.trim()) {
|
|
@@ -1774,36 +1625,6 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1774
1625
|
});
|
|
1775
1626
|
}
|
|
1776
1627
|
},
|
|
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
|
-
},
|
|
1807
1628
|
listThreads,
|
|
1808
1629
|
async reply(input) {
|
|
1809
1630
|
const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(input.threadId)}`;
|
|
@@ -1815,15 +1636,7 @@ function GitLabReviewBackend(vcs, number) {
|
|
|
1815
1636
|
});
|
|
1816
1637
|
}
|
|
1817
1638
|
if (input.resolve)
|
|
1818
|
-
await
|
|
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]);
|
|
1639
|
+
await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
|
|
1827
1640
|
},
|
|
1828
1641
|
async publish(event) {
|
|
1829
1642
|
assertReviewEventSupported(vcs.provider, event);
|
|
@@ -1863,12 +1676,105 @@ function hasGitlabDraftNotes(input) {
|
|
|
1863
1676
|
}
|
|
1864
1677
|
}
|
|
1865
1678
|
|
|
1866
|
-
// src/review/
|
|
1867
|
-
|
|
1679
|
+
// src/review/review-backend.ts
|
|
1680
|
+
function createRemoteReviewBackend(vcs, number) {
|
|
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
|
+
}
|
|
1868
1774
|
|
|
1869
1775
|
// src/extensions/tuicrx.ts
|
|
1870
|
-
import { readFile as readFile5, realpath as
|
|
1871
|
-
import { resolve as
|
|
1776
|
+
import { readFile as readFile5, realpath as realpath3 } from "node:fs/promises";
|
|
1777
|
+
import { resolve as resolve3 } from "node:path";
|
|
1872
1778
|
async function tuicrAvailable() {
|
|
1873
1779
|
return Boolean(await findExecutable("tuicr"));
|
|
1874
1780
|
}
|
|
@@ -1944,7 +1850,7 @@ async function resolvePrSession(cwd, owner, repo, number) {
|
|
|
1944
1850
|
});
|
|
1945
1851
|
}
|
|
1946
1852
|
async function findMatchingSession(sessions, cwd, branch) {
|
|
1947
|
-
const repository = await
|
|
1853
|
+
const repository = await canonicalPath3(await gitToplevel(cwd));
|
|
1948
1854
|
for (const session of sessions) {
|
|
1949
1855
|
if (session.kind !== "local")
|
|
1950
1856
|
continue;
|
|
@@ -1952,7 +1858,7 @@ async function findMatchingSession(sessions, cwd, branch) {
|
|
|
1952
1858
|
const data = await readSession(session.path);
|
|
1953
1859
|
if (data.branch_name !== branch || !data.repo_path)
|
|
1954
1860
|
continue;
|
|
1955
|
-
if (await
|
|
1861
|
+
if (await canonicalPath3(data.repo_path) === repository)
|
|
1956
1862
|
return session;
|
|
1957
1863
|
} catch {}
|
|
1958
1864
|
}
|
|
@@ -1966,53 +1872,33 @@ async function readSession(path) {
|
|
|
1966
1872
|
throw new Error(`Cannot parse tuicr session JSON: ${path}`);
|
|
1967
1873
|
}
|
|
1968
1874
|
}
|
|
1969
|
-
function
|
|
1970
|
-
const
|
|
1971
|
-
const
|
|
1972
|
-
const publishableBody = (comment) => {
|
|
1875
|
+
function toLocalReviewThreads(session) {
|
|
1876
|
+
const threads = [];
|
|
1877
|
+
const add = (comment, location = {}) => {
|
|
1973
1878
|
const author = commentAuthor(comment);
|
|
1974
|
-
|
|
1975
|
-
|
|
1879
|
+
threads.push({
|
|
1880
|
+
id: comment.id ?? `local-${threads.length + 1}`,
|
|
1881
|
+
...location,
|
|
1882
|
+
body: comment.content,
|
|
1883
|
+
...author ? { author } : {},
|
|
1884
|
+
resolved: false,
|
|
1885
|
+
question: /\?\s*$/.test(comment.content.trim())
|
|
1886
|
+
});
|
|
1976
1887
|
};
|
|
1977
|
-
const
|
|
1888
|
+
for (const comment of session.review_comments ?? [])
|
|
1889
|
+
add(comment);
|
|
1978
1890
|
for (const [file, entry] of Object.entries(session.files ?? {})) {
|
|
1979
|
-
for (const
|
|
1980
|
-
|
|
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
|
-
}
|
|
1891
|
+
for (const comment of entry.file_comments ?? [])
|
|
1892
|
+
add(comment, { file });
|
|
1992
1893
|
for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
|
|
1993
1894
|
const line = Number.parseInt(lineKey, 10);
|
|
1994
1895
|
if (!Number.isFinite(line))
|
|
1995
1896
|
continue;
|
|
1996
|
-
for (const
|
|
1997
|
-
|
|
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
|
-
}
|
|
1897
|
+
for (const comment of lineComments)
|
|
1898
|
+
add(comment, { file, line });
|
|
2011
1899
|
}
|
|
2012
1900
|
}
|
|
2013
|
-
return
|
|
2014
|
-
|
|
2015
|
-
`) };
|
|
1901
|
+
return threads;
|
|
2016
1902
|
}
|
|
2017
1903
|
async function requireTuicr() {
|
|
2018
1904
|
if (!await tuicrAvailable())
|
|
@@ -2021,207 +1907,70 @@ async function requireTuicr() {
|
|
|
2021
1907
|
function commentAuthor(comment) {
|
|
2022
1908
|
return comment.username ?? comment.author;
|
|
2023
1909
|
}
|
|
2024
|
-
async function
|
|
1910
|
+
async function canonicalPath3(path) {
|
|
2025
1911
|
try {
|
|
2026
|
-
return await
|
|
1912
|
+
return await realpath3(path);
|
|
2027
1913
|
} catch {
|
|
2028
|
-
return
|
|
1914
|
+
return resolve3(path);
|
|
2029
1915
|
}
|
|
2030
1916
|
}
|
|
2031
1917
|
|
|
2032
|
-
// src/review/local-
|
|
2033
|
-
function
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
-
}
|
|
1918
|
+
// src/review/local-reviews.ts
|
|
1919
|
+
async function captureLocalReview(input) {
|
|
1920
|
+
const id = reviewSlug(input.branch) || "local-review";
|
|
1921
|
+
const dir = join9(await reviewsDir(input.cwd, input.homeDir), id);
|
|
1922
|
+
await mkdir4(dir, { recursive: true });
|
|
1923
|
+
const revision = await nextRevision(dir);
|
|
1924
|
+
const output = await readFile6(input.session.path, "utf8");
|
|
1925
|
+
const comments = toLocalReviewThreads(await readSession(input.session.path));
|
|
1926
|
+
const dump = {
|
|
1927
|
+
schemaVersion: 1,
|
|
1928
|
+
id,
|
|
1929
|
+
revision,
|
|
1930
|
+
branch: input.branch,
|
|
1931
|
+
base: input.base,
|
|
1932
|
+
createdAt: (input.now ?? (() => new Date))().toISOString(),
|
|
1933
|
+
diff: input.diff,
|
|
1934
|
+
comments,
|
|
1935
|
+
tuicr: { slug: input.session.slug, output }
|
|
2067
1936
|
};
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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 });
|
|
1937
|
+
const path = join9(dir, `${revision}.json`);
|
|
1938
|
+
await writeFile3(path, `${JSON.stringify(dump, null, 2)}
|
|
1939
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
1940
|
+
await unlink2(input.session.path);
|
|
1941
|
+
return { path, dump };
|
|
1942
|
+
}
|
|
1943
|
+
async function readLocalReview(cwd, branch, revision, homeDir) {
|
|
1944
|
+
const id = reviewSlug(branch) || "local-review";
|
|
1945
|
+
const dir = join9(await reviewsDir(cwd, homeDir), id);
|
|
1946
|
+
const selected = revision ?? await latestRevision(dir);
|
|
1947
|
+
if (!selected)
|
|
1948
|
+
return;
|
|
2107
1949
|
try {
|
|
2108
|
-
await
|
|
1950
|
+
return JSON.parse(await readFile6(join9(dir, `${selected}.json`), "utf8"));
|
|
2109
1951
|
} catch (error) {
|
|
2110
|
-
if (error.code
|
|
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())
|
|
1952
|
+
if (error.code === "ENOENT")
|
|
2151
1953
|
return;
|
|
2152
|
-
|
|
2153
|
-
if (target === await canonicalPath3(dest))
|
|
2154
|
-
await unlink(path);
|
|
2155
|
-
} catch (error) {
|
|
2156
|
-
if (error.code !== "ENOENT")
|
|
2157
|
-
throw error;
|
|
1954
|
+
throw error;
|
|
2158
1955
|
}
|
|
2159
1956
|
}
|
|
2160
|
-
async function
|
|
2161
|
-
|
|
2162
|
-
return canonicalPath3(isAbsolute2(target) ? target : resolve3(dirname4(path), target));
|
|
1957
|
+
async function nextRevision(dir) {
|
|
1958
|
+
return await latestRevision(dir) + 1;
|
|
2163
1959
|
}
|
|
2164
|
-
async function
|
|
1960
|
+
async function latestRevision(dir) {
|
|
2165
1961
|
try {
|
|
2166
|
-
return await
|
|
2167
|
-
|
|
2168
|
-
|
|
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 } };
|
|
1962
|
+
return (await readdir2(dir)).reduce((latest, name) => {
|
|
1963
|
+
const match = name.match(/^(\d+)\.json$/);
|
|
1964
|
+
return match ? Math.max(latest, Number(match[1])) : latest;
|
|
1965
|
+
}, 0);
|
|
2204
1966
|
} catch (error) {
|
|
2205
|
-
if (error.code === "ENOENT")
|
|
2206
|
-
return
|
|
2207
|
-
}
|
|
2208
|
-
if (error instanceof SyntaxError || error instanceof z5.ZodError) {
|
|
2209
|
-
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
2210
|
-
}
|
|
1967
|
+
if (error.code === "ENOENT")
|
|
1968
|
+
return 0;
|
|
2211
1969
|
throw error;
|
|
2212
1970
|
}
|
|
2213
1971
|
}
|
|
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
|
-
}
|
|
2223
1972
|
// src/mcp.ts
|
|
2224
|
-
import { mkdir as
|
|
1973
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
|
|
2225
1974
|
import { homedir as homedir5 } from "node:os";
|
|
2226
1975
|
import { dirname as dirname5, join as join10 } from "node:path";
|
|
2227
1976
|
var mcp = {
|
|
@@ -2239,8 +1988,8 @@ var mcp = {
|
|
|
2239
1988
|
const next = { ...current, mcpServers: nextServers };
|
|
2240
1989
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2241
1990
|
if (changed && !options.dryRun) {
|
|
2242
|
-
await
|
|
2243
|
-
await
|
|
1991
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
1992
|
+
await writeFile4(path, `${JSON.stringify(next, null, 2)}
|
|
2244
1993
|
`, "utf8");
|
|
2245
1994
|
}
|
|
2246
1995
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -2269,7 +2018,7 @@ function getParsedConfig(content, path) {
|
|
|
2269
2018
|
}
|
|
2270
2019
|
async function getOptionalFile2(path) {
|
|
2271
2020
|
try {
|
|
2272
|
-
return await
|
|
2021
|
+
return await readFile7(path, "utf8");
|
|
2273
2022
|
} catch (error) {
|
|
2274
2023
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2275
2024
|
return;
|
|
@@ -2284,7 +2033,7 @@ import {
|
|
|
2284
2033
|
getAgentDir,
|
|
2285
2034
|
parseFrontmatter as parseFrontmatter2
|
|
2286
2035
|
} from "@earendil-works/pi-coding-agent";
|
|
2287
|
-
import { readFile as
|
|
2036
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2288
2037
|
import { homedir as homedir6 } from "node:os";
|
|
2289
2038
|
import { basename as basename4, extname, join as join11 } from "node:path";
|
|
2290
2039
|
async function discoverAgentModes(options) {
|
|
@@ -2483,7 +2232,7 @@ async function loadAgentModes(directory, source, modes, diagnostics, skillName)
|
|
|
2483
2232
|
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2484
2233
|
const path = join11(directory, entry.name);
|
|
2485
2234
|
try {
|
|
2486
|
-
const content = await
|
|
2235
|
+
const content = await readFile8(path, "utf8");
|
|
2487
2236
|
const { frontmatter, body } = parseFrontmatter2(content.startsWith("\uFEFF") ? content.slice(1) : content);
|
|
2488
2237
|
if (frontmatter.enabled === false || frontmatter.inline === false)
|
|
2489
2238
|
continue;
|
|
@@ -2539,7 +2288,7 @@ function isModeBaseline(value) {
|
|
|
2539
2288
|
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");
|
|
2540
2289
|
}
|
|
2541
2290
|
// src/pi.ts
|
|
2542
|
-
import { mkdir as
|
|
2291
|
+
import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
|
|
2543
2292
|
import { homedir as homedir7 } from "node:os";
|
|
2544
2293
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
2545
2294
|
var pi = {
|
|
@@ -2558,8 +2307,8 @@ async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(),
|
|
|
2558
2307
|
const currentText = await readTextIfExists(path);
|
|
2559
2308
|
const changed = currentText !== content;
|
|
2560
2309
|
if (changed && !dryRun) {
|
|
2561
|
-
await
|
|
2562
|
-
await
|
|
2310
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
2311
|
+
await writeFile5(path, content, "utf8");
|
|
2563
2312
|
}
|
|
2564
2313
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
2565
2314
|
}
|
|
@@ -2595,8 +2344,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
2595
2344
|
const next = update(current);
|
|
2596
2345
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2597
2346
|
if (changed && !dryRun) {
|
|
2598
|
-
await
|
|
2599
|
-
await
|
|
2347
|
+
await mkdir6(dirname6(path), { recursive: true });
|
|
2348
|
+
await writeFile5(path, `${JSON.stringify(next, null, 2)}
|
|
2600
2349
|
`, "utf8");
|
|
2601
2350
|
}
|
|
2602
2351
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -2629,16 +2378,16 @@ function parseJsonObject(content, path) {
|
|
|
2629
2378
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2630
2379
|
}
|
|
2631
2380
|
// src/plan/store.ts
|
|
2632
|
-
import { mkdir as
|
|
2381
|
+
import { mkdir as mkdir8, readdir as readdir3, readFile as readFile11, realpath as realpath4, rm as rm2 } from "node:fs/promises";
|
|
2633
2382
|
import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve4, sep } from "node:path";
|
|
2634
2383
|
|
|
2635
2384
|
// src/extensions/fsx.ts
|
|
2636
|
-
import { mkdir as
|
|
2385
|
+
import { mkdir as mkdir7, rename, rm, writeFile as writeFile6 } from "node:fs/promises";
|
|
2637
2386
|
import { basename as basename5, dirname as dirname7, join as join13 } from "node:path";
|
|
2638
2387
|
async function atomicWrite(path, content) {
|
|
2639
2388
|
const temp = join13(dirname7(path), `.${basename5(path)}.${crypto.randomUUID()}.tmp`);
|
|
2640
|
-
await
|
|
2641
|
-
await
|
|
2389
|
+
await writeFile6(temp, content, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
2390
|
+
await rename(temp, path);
|
|
2642
2391
|
}
|
|
2643
2392
|
async function withDirectoryLock(lockDir, operation, options = {}) {
|
|
2644
2393
|
const waitMs = options.waitMs ?? 5000;
|
|
@@ -2646,7 +2395,7 @@ async function withDirectoryLock(lockDir, operation, options = {}) {
|
|
|
2646
2395
|
const started = Date.now();
|
|
2647
2396
|
while (true) {
|
|
2648
2397
|
try {
|
|
2649
|
-
await
|
|
2398
|
+
await mkdir7(lockDir);
|
|
2650
2399
|
break;
|
|
2651
2400
|
} catch (error) {
|
|
2652
2401
|
if (error.code !== "EEXIST")
|
|
@@ -2668,7 +2417,7 @@ function sleep(ms) {
|
|
|
2668
2417
|
|
|
2669
2418
|
// src/log.ts
|
|
2670
2419
|
import { randomUUID } from "node:crypto";
|
|
2671
|
-
import { appendFile, readFile as
|
|
2420
|
+
import { appendFile, readFile as readFile9 } from "node:fs/promises";
|
|
2672
2421
|
async function appendLogEntry(logPath, event) {
|
|
2673
2422
|
const entry = {
|
|
2674
2423
|
...event,
|
|
@@ -2682,7 +2431,7 @@ async function appendLogEntry(logPath, event) {
|
|
|
2682
2431
|
}
|
|
2683
2432
|
|
|
2684
2433
|
// src/templates.ts
|
|
2685
|
-
import { readFile as
|
|
2434
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
2686
2435
|
import { homedir as homedir8 } from "node:os";
|
|
2687
2436
|
import { join as join14, normalize } from "node:path";
|
|
2688
2437
|
async function loadTemplate(name, options = {}) {
|
|
@@ -2709,7 +2458,7 @@ function templateRelativePath(name) {
|
|
|
2709
2458
|
}
|
|
2710
2459
|
async function readOptionalFile(path) {
|
|
2711
2460
|
try {
|
|
2712
|
-
return await
|
|
2461
|
+
return await readFile10(path, "utf8");
|
|
2713
2462
|
} catch (error) {
|
|
2714
2463
|
if (error.code === "ENOENT")
|
|
2715
2464
|
return;
|
|
@@ -7793,7 +7542,7 @@ function planRecordName(shortSlug, date = new Date) {
|
|
|
7793
7542
|
}
|
|
7794
7543
|
async function resolvePlan(cwd, query, filters = {}, options = {}) {
|
|
7795
7544
|
const root = await plansDir(cwd, options.homeDir);
|
|
7796
|
-
const entries = await
|
|
7545
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
7797
7546
|
const normalizedQuery = query ? normalizeQuery(query) : undefined;
|
|
7798
7547
|
const records = [];
|
|
7799
7548
|
for (const entry of entries) {
|
|
@@ -7830,7 +7579,7 @@ function createPlanStore(options = {}) {
|
|
|
7830
7579
|
const dir = join16(root, id);
|
|
7831
7580
|
await assertContained(root, dirname8(dir));
|
|
7832
7581
|
try {
|
|
7833
|
-
await
|
|
7582
|
+
await mkdir8(dir);
|
|
7834
7583
|
} catch (error) {
|
|
7835
7584
|
if (error.code === "EEXIST")
|
|
7836
7585
|
throw new Error(`Plan ${id} already exists.`);
|
|
@@ -7905,7 +7654,7 @@ function createPlanStore(options = {}) {
|
|
|
7905
7654
|
async function readRecord(dir) {
|
|
7906
7655
|
const planPath = join16(dir, "PLAN.md");
|
|
7907
7656
|
const logPath = join16(dir, "logs.txt");
|
|
7908
|
-
const source = await
|
|
7657
|
+
const source = await readFile11(planPath, "utf8");
|
|
7909
7658
|
const document = parsePlanDocument(source, planPath);
|
|
7910
7659
|
return {
|
|
7911
7660
|
id: basename6(dir),
|
|
@@ -7919,7 +7668,7 @@ async function readRecord(dir) {
|
|
|
7919
7668
|
}
|
|
7920
7669
|
async function ensureImplementationFiles(dir, document, options) {
|
|
7921
7670
|
const implementationDir = join16(dir, "implementation");
|
|
7922
|
-
await
|
|
7671
|
+
await mkdir8(implementationDir, { recursive: true });
|
|
7923
7672
|
const template = await loadTemplate("plan/implementation", {
|
|
7924
7673
|
homeDir: options.homeDir,
|
|
7925
7674
|
bundledDir: options.bundledTemplatesDir
|
|
@@ -7927,7 +7676,7 @@ async function ensureImplementationFiles(dir, document, options) {
|
|
|
7927
7676
|
for (const phase of document.phases) {
|
|
7928
7677
|
const path = join16(implementationDir, `phase-${phase.id}.md`);
|
|
7929
7678
|
try {
|
|
7930
|
-
await
|
|
7679
|
+
await readFile11(path, "utf8");
|
|
7931
7680
|
} catch (error) {
|
|
7932
7681
|
if (error.code !== "ENOENT")
|
|
7933
7682
|
throw error;
|
|
@@ -7976,7 +7725,7 @@ function titleFromSlug(value) {
|
|
|
7976
7725
|
}
|
|
7977
7726
|
// src/setup.ts
|
|
7978
7727
|
import { parseFrontmatter as parseFrontmatter3 } from "@earendil-works/pi-coding-agent";
|
|
7979
|
-
import { readdir as
|
|
7728
|
+
import { readdir as readdir4, readFile as readFile12 } from "node:fs/promises";
|
|
7980
7729
|
import { homedir as homedir9 } from "node:os";
|
|
7981
7730
|
import { basename as basename7, join as join17 } from "node:path";
|
|
7982
7731
|
var MISE_DEPENDENCIES = [
|
|
@@ -8079,11 +7828,11 @@ async function ensurePiAgents(options = {}) {
|
|
|
8079
7828
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
8080
7829
|
const bundledAgentsDir = options.bundledAgentsDir ?? BUNDLED_AGENTS_DIR2;
|
|
8081
7830
|
const userConfig = await loadDiffpiConfig({ homeDir: options.homeDir });
|
|
8082
|
-
const entries = (await
|
|
7831
|
+
const entries = (await readdir4(bundledAgentsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.startsWith("diffpi-") && entry.name.endsWith(".md")).sort((left, right) => left.name.localeCompare(right.name));
|
|
8083
7832
|
const actions = [];
|
|
8084
7833
|
for (const entry of entries) {
|
|
8085
7834
|
const id = basename7(entry.name, ".md").replace(/^diffpi-/, "");
|
|
8086
|
-
const source = await
|
|
7835
|
+
const source = await readFile12(join17(bundledAgentsDir, entry.name), "utf8");
|
|
8087
7836
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
8088
7837
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
8089
7838
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -8288,7 +8037,7 @@ function reportProgress(options, message) {
|
|
|
8288
8037
|
options.onProgress?.(message);
|
|
8289
8038
|
}
|
|
8290
8039
|
async function packageVersion2() {
|
|
8291
|
-
const source = await
|
|
8040
|
+
const source = await readFile12(join17(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
8292
8041
|
let value;
|
|
8293
8042
|
try {
|
|
8294
8043
|
value = JSON.parse(source);
|
|
@@ -8313,11 +8062,11 @@ export {
|
|
|
8313
8062
|
addComment,
|
|
8314
8063
|
assertGitHubMergeReady,
|
|
8315
8064
|
assertReviewEventSupported,
|
|
8065
|
+
captureLocalReview,
|
|
8316
8066
|
checkConventionalSubject,
|
|
8317
8067
|
computeProjectSlug,
|
|
8318
8068
|
createForge,
|
|
8319
8069
|
createForgeBackend,
|
|
8320
|
-
createLocalReviewBackend,
|
|
8321
8070
|
createModeController,
|
|
8322
8071
|
createPlanStore,
|
|
8323
8072
|
createRemoteReviewBackend,
|
|
@@ -8350,7 +8099,6 @@ export {
|
|
|
8350
8099
|
launch,
|
|
8351
8100
|
listSessions,
|
|
8352
8101
|
loadDiffpiConfig,
|
|
8353
|
-
loadReviewPublicationState,
|
|
8354
8102
|
loadTemplate,
|
|
8355
8103
|
localReviewAuthor,
|
|
8356
8104
|
mcp,
|
|
@@ -8359,37 +8107,28 @@ export {
|
|
|
8359
8107
|
openInNewTab,
|
|
8360
8108
|
parseGitlabDiffRefs,
|
|
8361
8109
|
parseRemote,
|
|
8362
|
-
parseThreadArtifact,
|
|
8363
8110
|
pi,
|
|
8364
8111
|
plansDir,
|
|
8112
|
+
readLocalReview,
|
|
8365
8113
|
readSession,
|
|
8366
8114
|
renderReviewDoc,
|
|
8367
8115
|
renderTemplate,
|
|
8368
|
-
renderThreadArtifact,
|
|
8369
8116
|
resolveAgentMode,
|
|
8370
8117
|
resolveAgentModelPreferences,
|
|
8371
8118
|
resolvePrSession,
|
|
8372
8119
|
resolveReviewSession,
|
|
8373
8120
|
resolveSession,
|
|
8374
|
-
reviewBodyFingerprint,
|
|
8375
|
-
reviewCommentFingerprint,
|
|
8376
8121
|
reviewRecordName,
|
|
8377
|
-
reviewReplyFingerprint,
|
|
8378
8122
|
reviewSlug,
|
|
8379
8123
|
reviewsDir,
|
|
8380
8124
|
runMiseGates,
|
|
8381
|
-
saveReviewPublicationState,
|
|
8382
|
-
sessionsDir,
|
|
8383
8125
|
setupPi,
|
|
8384
8126
|
severitySchema,
|
|
8385
8127
|
storeDir,
|
|
8386
8128
|
storeGlobalRoot,
|
|
8387
8129
|
templateRelativePath,
|
|
8388
|
-
toFindings,
|
|
8389
8130
|
toReviewComments,
|
|
8390
8131
|
tuicrAvailable,
|
|
8391
|
-
unpublishedReviewComments,
|
|
8392
|
-
upsertThreadReply,
|
|
8393
8132
|
withRemoteProvenance,
|
|
8394
8133
|
yymmdd,
|
|
8395
8134
|
zedKeymapPath,
|