diffprism 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +17 -9
- package/dist/{chunk-4VXA6GCO.js → chunk-L2D5SDUV.js} +130 -13
- package/dist/mcp-server.js +3 -3
- package/package.json +1 -1
- package/ui-dist/assets/{index-BfIxxwO-.js → index-C_k3uWX-.js} +69 -64
- package/ui-dist/assets/index-CkUXOfXP.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-BcuyMNeU.css +0 -1
package/dist/bin.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
startGlobalServer,
|
|
7
7
|
startReview,
|
|
8
8
|
startWatch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-L2D5SDUV.js";
|
|
10
10
|
|
|
11
11
|
// cli/src/index.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -144,6 +144,12 @@ You can also check for feedback without blocking by calling \`get_review_result\
|
|
|
144
144
|
`;
|
|
145
145
|
|
|
146
146
|
// cli/src/commands/setup.ts
|
|
147
|
+
var GITIGNORE_ENTRIES = [
|
|
148
|
+
".diffprism",
|
|
149
|
+
".mcp.json",
|
|
150
|
+
".claude/settings.json",
|
|
151
|
+
".claude/skills/review/"
|
|
152
|
+
];
|
|
147
153
|
function findGitRoot(from) {
|
|
148
154
|
let dir = path.resolve(from);
|
|
149
155
|
while (true) {
|
|
@@ -306,27 +312,28 @@ async function promptUser(question) {
|
|
|
306
312
|
}
|
|
307
313
|
async function setupGitignore(gitRoot) {
|
|
308
314
|
const filePath = path.join(gitRoot, ".gitignore");
|
|
309
|
-
const entry = ".diffprism";
|
|
310
315
|
if (fs.existsSync(filePath)) {
|
|
311
316
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
312
317
|
const lines = content.split("\n").map((l) => l.trim());
|
|
313
|
-
|
|
318
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
|
|
319
|
+
if (missing.length === 0) {
|
|
314
320
|
return { action: "skipped", filePath };
|
|
315
321
|
}
|
|
316
|
-
const
|
|
322
|
+
const suffix = missing.map((e) => e + "\n").join("");
|
|
323
|
+
const newContent = content.endsWith("\n") ? content + suffix : content + "\n" + suffix;
|
|
317
324
|
fs.writeFileSync(filePath, newContent);
|
|
318
325
|
return { action: "updated", filePath };
|
|
319
326
|
}
|
|
320
327
|
const confirmed = await promptUser(
|
|
321
|
-
"No .gitignore found. Create one with
|
|
328
|
+
"No .gitignore found. Create one with DiffPrism entries? (Y/n) "
|
|
322
329
|
);
|
|
323
330
|
if (!confirmed) {
|
|
324
331
|
console.log(
|
|
325
|
-
" Warning:
|
|
332
|
+
" Warning: DiffPrism files will appear in git status and may be accidentally committed."
|
|
326
333
|
);
|
|
327
334
|
return { action: "skipped", filePath };
|
|
328
335
|
}
|
|
329
|
-
fs.writeFileSync(filePath,
|
|
336
|
+
fs.writeFileSync(filePath, GITIGNORE_ENTRIES.map((e) => e + "\n").join(""));
|
|
330
337
|
return { action: "created", filePath };
|
|
331
338
|
}
|
|
332
339
|
async function setup(flags) {
|
|
@@ -534,7 +541,8 @@ function teardownGitignore(gitRoot) {
|
|
|
534
541
|
}
|
|
535
542
|
const content = fs2.readFileSync(filePath, "utf-8");
|
|
536
543
|
const lines = content.split("\n");
|
|
537
|
-
const
|
|
544
|
+
const entrySet = new Set(GITIGNORE_ENTRIES);
|
|
545
|
+
const filtered = lines.filter((l) => !entrySet.has(l.trim()));
|
|
538
546
|
if (filtered.length === lines.length) {
|
|
539
547
|
return { action: "skipped", filePath };
|
|
540
548
|
}
|
|
@@ -824,7 +832,7 @@ async function serverStop() {
|
|
|
824
832
|
|
|
825
833
|
// cli/src/index.ts
|
|
826
834
|
var program = new Command();
|
|
827
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.
|
|
835
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.20.1" : "0.0.0-dev");
|
|
828
836
|
program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
|
|
829
837
|
program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
|
|
830
838
|
program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
|
|
@@ -1132,7 +1132,6 @@ async function isServerAlive() {
|
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
1134
|
// packages/core/src/watch.ts
|
|
1135
|
-
import { createHash } from "crypto";
|
|
1136
1135
|
import getPort2 from "get-port";
|
|
1137
1136
|
import open2 from "open";
|
|
1138
1137
|
|
|
@@ -1265,7 +1264,8 @@ function createWatchBridge(port, callbacks) {
|
|
|
1265
1264
|
});
|
|
1266
1265
|
}
|
|
1267
1266
|
|
|
1268
|
-
// packages/core/src/
|
|
1267
|
+
// packages/core/src/diff-utils.ts
|
|
1268
|
+
import { createHash } from "crypto";
|
|
1269
1269
|
function hashDiff(rawDiff) {
|
|
1270
1270
|
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1271
1271
|
}
|
|
@@ -1296,6 +1296,8 @@ function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
|
1296
1296
|
}
|
|
1297
1297
|
return changed;
|
|
1298
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
// packages/core/src/watch.ts
|
|
1299
1301
|
async function startWatch(options) {
|
|
1300
1302
|
const {
|
|
1301
1303
|
diffRef,
|
|
@@ -1447,6 +1449,8 @@ import open3 from "open";
|
|
|
1447
1449
|
import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
|
|
1448
1450
|
var sessions2 = /* @__PURE__ */ new Map();
|
|
1449
1451
|
var clientSessions = /* @__PURE__ */ new Map();
|
|
1452
|
+
var sessionWatchers = /* @__PURE__ */ new Map();
|
|
1453
|
+
var serverPollInterval = 2e3;
|
|
1450
1454
|
var reopenBrowserIfNeeded = null;
|
|
1451
1455
|
function toSummary(session) {
|
|
1452
1456
|
const { payload } = session;
|
|
@@ -1466,7 +1470,8 @@ function toSummary(session) {
|
|
|
1466
1470
|
additions,
|
|
1467
1471
|
deletions,
|
|
1468
1472
|
status: session.status,
|
|
1469
|
-
createdAt: session.createdAt
|
|
1473
|
+
createdAt: session.createdAt,
|
|
1474
|
+
hasNewChanges: session.hasNewChanges
|
|
1470
1475
|
};
|
|
1471
1476
|
}
|
|
1472
1477
|
function readBody(req) {
|
|
@@ -1517,6 +1522,93 @@ function sendToSessionClients(sessionId, msg) {
|
|
|
1517
1522
|
}
|
|
1518
1523
|
}
|
|
1519
1524
|
}
|
|
1525
|
+
function hasViewersForSession(sessionId) {
|
|
1526
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1527
|
+
if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
function startSessionWatcher(sessionId) {
|
|
1534
|
+
if (sessionWatchers.has(sessionId)) return;
|
|
1535
|
+
const session = sessions2.get(sessionId);
|
|
1536
|
+
if (!session?.diffRef) return;
|
|
1537
|
+
const interval = setInterval(() => {
|
|
1538
|
+
const s = sessions2.get(sessionId);
|
|
1539
|
+
if (!s?.diffRef) {
|
|
1540
|
+
stopSessionWatcher(sessionId);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(s.diffRef, {
|
|
1545
|
+
cwd: s.projectPath
|
|
1546
|
+
});
|
|
1547
|
+
const newHash = hashDiff(newRawDiff);
|
|
1548
|
+
if (newHash !== s.lastDiffHash) {
|
|
1549
|
+
const newBriefing = analyze(newDiffSet);
|
|
1550
|
+
const changedFiles = detectChangedFiles(s.lastDiffSet ?? null, newDiffSet);
|
|
1551
|
+
s.payload = {
|
|
1552
|
+
...s.payload,
|
|
1553
|
+
diffSet: newDiffSet,
|
|
1554
|
+
rawDiff: newRawDiff,
|
|
1555
|
+
briefing: newBriefing
|
|
1556
|
+
};
|
|
1557
|
+
s.lastDiffHash = newHash;
|
|
1558
|
+
s.lastDiffSet = newDiffSet;
|
|
1559
|
+
if (hasViewersForSession(sessionId)) {
|
|
1560
|
+
sendToSessionClients(sessionId, {
|
|
1561
|
+
type: "diff:update",
|
|
1562
|
+
payload: {
|
|
1563
|
+
diffSet: newDiffSet,
|
|
1564
|
+
rawDiff: newRawDiff,
|
|
1565
|
+
briefing: newBriefing,
|
|
1566
|
+
changedFiles,
|
|
1567
|
+
timestamp: Date.now()
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
s.hasNewChanges = false;
|
|
1571
|
+
} else {
|
|
1572
|
+
s.hasNewChanges = true;
|
|
1573
|
+
broadcastSessionList();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
}, serverPollInterval);
|
|
1579
|
+
sessionWatchers.set(sessionId, interval);
|
|
1580
|
+
}
|
|
1581
|
+
function stopSessionWatcher(sessionId) {
|
|
1582
|
+
const interval = sessionWatchers.get(sessionId);
|
|
1583
|
+
if (interval) {
|
|
1584
|
+
clearInterval(interval);
|
|
1585
|
+
sessionWatchers.delete(sessionId);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function startAllWatchers() {
|
|
1589
|
+
for (const [id, session] of sessions2.entries()) {
|
|
1590
|
+
if (session.diffRef && !sessionWatchers.has(id)) {
|
|
1591
|
+
startSessionWatcher(id);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function stopAllWatchers() {
|
|
1596
|
+
for (const [id, interval] of sessionWatchers.entries()) {
|
|
1597
|
+
clearInterval(interval);
|
|
1598
|
+
}
|
|
1599
|
+
sessionWatchers.clear();
|
|
1600
|
+
}
|
|
1601
|
+
function hasConnectedClients() {
|
|
1602
|
+
if (!wss) return false;
|
|
1603
|
+
for (const client of wss.clients) {
|
|
1604
|
+
if (client.readyState === WebSocket3.OPEN) return true;
|
|
1605
|
+
}
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
function broadcastSessionList() {
|
|
1609
|
+
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
1610
|
+
broadcastToAll({ type: "session:list", payload: summaries });
|
|
1611
|
+
}
|
|
1520
1612
|
async function handleApiRequest(req, res) {
|
|
1521
1613
|
const method = req.method ?? "GET";
|
|
1522
1614
|
const url = (req.url ?? "/").split("?")[0];
|
|
@@ -1543,18 +1635,28 @@ async function handleApiRequest(req, res) {
|
|
|
1543
1635
|
if (method === "POST" && url === "/api/reviews") {
|
|
1544
1636
|
try {
|
|
1545
1637
|
const body = await readBody(req);
|
|
1546
|
-
const { payload, projectPath } = JSON.parse(body);
|
|
1638
|
+
const { payload, projectPath, diffRef } = JSON.parse(body);
|
|
1547
1639
|
const sessionId = `session-${randomUUID().slice(0, 8)}`;
|
|
1548
1640
|
payload.reviewId = sessionId;
|
|
1641
|
+
if (diffRef) {
|
|
1642
|
+
payload.watchMode = true;
|
|
1643
|
+
}
|
|
1549
1644
|
const session = {
|
|
1550
1645
|
id: sessionId,
|
|
1551
1646
|
payload,
|
|
1552
1647
|
projectPath,
|
|
1553
1648
|
status: "pending",
|
|
1554
1649
|
createdAt: Date.now(),
|
|
1555
|
-
result: null
|
|
1650
|
+
result: null,
|
|
1651
|
+
diffRef,
|
|
1652
|
+
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
1653
|
+
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
1654
|
+
hasNewChanges: false
|
|
1556
1655
|
};
|
|
1557
1656
|
sessions2.set(sessionId, session);
|
|
1657
|
+
if (diffRef && hasConnectedClients()) {
|
|
1658
|
+
startSessionWatcher(sessionId);
|
|
1659
|
+
}
|
|
1558
1660
|
broadcastToAll({
|
|
1559
1661
|
type: "session:added",
|
|
1560
1662
|
payload: toSummary(session)
|
|
@@ -1644,6 +1746,7 @@ async function handleApiRequest(req, res) {
|
|
|
1644
1746
|
}
|
|
1645
1747
|
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
1646
1748
|
if (deleteParams) {
|
|
1749
|
+
stopSessionWatcher(deleteParams.id);
|
|
1647
1750
|
if (sessions2.delete(deleteParams.id)) {
|
|
1648
1751
|
jsonResponse(res, 200, { ok: true });
|
|
1649
1752
|
} else {
|
|
@@ -1659,8 +1762,10 @@ async function startGlobalServer(options = {}) {
|
|
|
1659
1762
|
httpPort: preferredHttpPort = 24680,
|
|
1660
1763
|
wsPort: preferredWsPort = 24681,
|
|
1661
1764
|
silent = false,
|
|
1662
|
-
dev = false
|
|
1765
|
+
dev = false,
|
|
1766
|
+
pollInterval = 2e3
|
|
1663
1767
|
} = options;
|
|
1768
|
+
serverPollInterval = pollInterval;
|
|
1664
1769
|
const [httpPort, wsPort] = await Promise.all([
|
|
1665
1770
|
getPort3({ port: preferredHttpPort }),
|
|
1666
1771
|
getPort3({ port: preferredWsPort })
|
|
@@ -1686,6 +1791,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1686
1791
|
});
|
|
1687
1792
|
wss = new WebSocketServer3({ port: wsPort });
|
|
1688
1793
|
wss.on("connection", (ws, req) => {
|
|
1794
|
+
startAllWatchers();
|
|
1689
1795
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
1690
1796
|
const sessionId = url.searchParams.get("sessionId");
|
|
1691
1797
|
if (sessionId) {
|
|
@@ -1693,6 +1799,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1693
1799
|
const session = sessions2.get(sessionId);
|
|
1694
1800
|
if (session) {
|
|
1695
1801
|
session.status = "in_review";
|
|
1802
|
+
session.hasNewChanges = false;
|
|
1696
1803
|
const msg = {
|
|
1697
1804
|
type: "review:init",
|
|
1698
1805
|
payload: session.payload
|
|
@@ -1711,6 +1818,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1711
1818
|
if (session) {
|
|
1712
1819
|
clientSessions.set(ws, session.id);
|
|
1713
1820
|
session.status = "in_review";
|
|
1821
|
+
session.hasNewChanges = false;
|
|
1714
1822
|
ws.send(JSON.stringify({
|
|
1715
1823
|
type: "review:init",
|
|
1716
1824
|
payload: session.payload
|
|
@@ -1735,17 +1843,32 @@ async function startGlobalServer(options = {}) {
|
|
|
1735
1843
|
if (session) {
|
|
1736
1844
|
clientSessions.set(ws, session.id);
|
|
1737
1845
|
session.status = "in_review";
|
|
1846
|
+
session.hasNewChanges = false;
|
|
1847
|
+
startSessionWatcher(session.id);
|
|
1738
1848
|
ws.send(JSON.stringify({
|
|
1739
1849
|
type: "review:init",
|
|
1740
1850
|
payload: session.payload
|
|
1741
1851
|
}));
|
|
1742
1852
|
}
|
|
1853
|
+
} else if (msg.type === "session:close") {
|
|
1854
|
+
const closedId = msg.payload.sessionId;
|
|
1855
|
+
stopSessionWatcher(closedId);
|
|
1856
|
+
sessions2.delete(closedId);
|
|
1857
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1858
|
+
if (sid === closedId) {
|
|
1859
|
+
clientSessions.delete(client);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
broadcastSessionList();
|
|
1743
1863
|
}
|
|
1744
1864
|
} catch {
|
|
1745
1865
|
}
|
|
1746
1866
|
});
|
|
1747
1867
|
ws.on("close", () => {
|
|
1748
1868
|
clientSessions.delete(ws);
|
|
1869
|
+
if (!hasConnectedClients()) {
|
|
1870
|
+
stopAllWatchers();
|
|
1871
|
+
}
|
|
1749
1872
|
});
|
|
1750
1873
|
});
|
|
1751
1874
|
await new Promise((resolve, reject) => {
|
|
@@ -1772,19 +1895,13 @@ Waiting for reviews...
|
|
|
1772
1895
|
}
|
|
1773
1896
|
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
|
|
1774
1897
|
await open3(uiUrl);
|
|
1775
|
-
function hasConnectedClients() {
|
|
1776
|
-
if (!wss) return false;
|
|
1777
|
-
for (const client of wss.clients) {
|
|
1778
|
-
if (client.readyState === WebSocket3.OPEN) return true;
|
|
1779
|
-
}
|
|
1780
|
-
return false;
|
|
1781
|
-
}
|
|
1782
1898
|
reopenBrowserIfNeeded = () => {
|
|
1783
1899
|
if (!hasConnectedClients()) {
|
|
1784
1900
|
open3(uiUrl);
|
|
1785
1901
|
}
|
|
1786
1902
|
};
|
|
1787
1903
|
async function stop() {
|
|
1904
|
+
stopAllWatchers();
|
|
1788
1905
|
if (wss) {
|
|
1789
1906
|
for (const client of wss.clients) {
|
|
1790
1907
|
client.close();
|
package/dist/mcp-server.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readReviewResult,
|
|
8
8
|
readWatchFile,
|
|
9
9
|
startReview
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-L2D5SDUV.js";
|
|
11
11
|
|
|
12
12
|
// packages/mcp-server/src/index.ts
|
|
13
13
|
import fs from "fs";
|
|
@@ -47,7 +47,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
|
|
|
47
47
|
{
|
|
48
48
|
method: "POST",
|
|
49
49
|
headers: { "Content-Type": "application/json" },
|
|
50
|
-
body: JSON.stringify({ payload, projectPath: cwd })
|
|
50
|
+
body: JSON.stringify({ payload, projectPath: cwd, diffRef })
|
|
51
51
|
}
|
|
52
52
|
);
|
|
53
53
|
if (!createResponse.ok) {
|
|
@@ -76,7 +76,7 @@ async function reviewViaGlobalServer(serverInfo, diffRef, options) {
|
|
|
76
76
|
async function startMcpServer() {
|
|
77
77
|
const server = new McpServer({
|
|
78
78
|
name: "diffprism",
|
|
79
|
-
version: true ? "0.20.
|
|
79
|
+
version: true ? "0.20.1" : "0.0.0-dev"
|
|
80
80
|
});
|
|
81
81
|
server.tool(
|
|
82
82
|
"open_review",
|