diffprism 0.2.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 +124 -0
- package/dist/bin.js +47 -0
- package/dist/chunk-AUPKNXCS.js +597 -0
- package/dist/mcp-server.js +63 -0
- package/package.json +41 -0
- package/ui-dist/assets/index-Bs9RqYMb.css +1 -0
- package/ui-dist/assets/index-D1OHLP8P.js +134 -0
- package/ui-dist/index.html +13 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// packages/core/src/pipeline.ts
|
|
2
|
+
import http from "http";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path2 from "path";
|
|
5
|
+
import getPort from "get-port";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// packages/git/src/local.ts
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
function getGitDiff(ref, options) {
|
|
12
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
13
|
+
try {
|
|
14
|
+
execSync("git --version", { cwd, stdio: "pipe" });
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"git is not available. Please install git and make sure it is on your PATH."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`The directory "${cwd}" is not inside a git repository.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
let command;
|
|
28
|
+
switch (ref) {
|
|
29
|
+
case "staged":
|
|
30
|
+
command = "git diff --staged --no-color";
|
|
31
|
+
break;
|
|
32
|
+
case "unstaged":
|
|
33
|
+
command = "git diff --no-color";
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
command = `git diff --no-color ${ref}`;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const output = execSync(command, {
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
maxBuffer: 50 * 1024 * 1024
|
|
44
|
+
// 50 MB
|
|
45
|
+
});
|
|
46
|
+
return output;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
+
throw new Error(`git diff failed: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// packages/git/src/parser.ts
|
|
54
|
+
import path from "path";
|
|
55
|
+
var EXTENSION_MAP = {
|
|
56
|
+
ts: "typescript",
|
|
57
|
+
tsx: "typescript",
|
|
58
|
+
js: "javascript",
|
|
59
|
+
jsx: "javascript",
|
|
60
|
+
py: "python",
|
|
61
|
+
rb: "ruby",
|
|
62
|
+
go: "go",
|
|
63
|
+
rs: "rust",
|
|
64
|
+
java: "java",
|
|
65
|
+
c: "c",
|
|
66
|
+
h: "c",
|
|
67
|
+
cpp: "cpp",
|
|
68
|
+
hpp: "cpp",
|
|
69
|
+
cc: "cpp",
|
|
70
|
+
cs: "csharp",
|
|
71
|
+
md: "markdown",
|
|
72
|
+
json: "json",
|
|
73
|
+
yaml: "yaml",
|
|
74
|
+
yml: "yaml",
|
|
75
|
+
html: "html",
|
|
76
|
+
css: "css",
|
|
77
|
+
scss: "scss",
|
|
78
|
+
sql: "sql",
|
|
79
|
+
sh: "shell",
|
|
80
|
+
bash: "shell",
|
|
81
|
+
zsh: "shell"
|
|
82
|
+
};
|
|
83
|
+
var FILENAME_MAP = {
|
|
84
|
+
Dockerfile: "dockerfile",
|
|
85
|
+
Makefile: "makefile"
|
|
86
|
+
};
|
|
87
|
+
function detectLanguage(filePath) {
|
|
88
|
+
const basename = path.basename(filePath);
|
|
89
|
+
if (FILENAME_MAP[basename]) {
|
|
90
|
+
return FILENAME_MAP[basename];
|
|
91
|
+
}
|
|
92
|
+
const ext = basename.includes(".") ? basename.slice(basename.lastIndexOf(".") + 1) : "";
|
|
93
|
+
return EXTENSION_MAP[ext] ?? "text";
|
|
94
|
+
}
|
|
95
|
+
function stripPrefix(raw) {
|
|
96
|
+
if (raw === "/dev/null") return raw;
|
|
97
|
+
return raw.replace(/^[ab]\//, "");
|
|
98
|
+
}
|
|
99
|
+
function parseDiff(rawDiff, baseRef, headRef) {
|
|
100
|
+
if (!rawDiff.trim()) {
|
|
101
|
+
return { baseRef, headRef, files: [] };
|
|
102
|
+
}
|
|
103
|
+
const files = [];
|
|
104
|
+
const lines = rawDiff.split("\n");
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < lines.length) {
|
|
107
|
+
if (!lines[i].startsWith("diff --git ")) {
|
|
108
|
+
i++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let oldPath;
|
|
112
|
+
let newPath;
|
|
113
|
+
let status = "modified";
|
|
114
|
+
let binary = false;
|
|
115
|
+
let renameFrom;
|
|
116
|
+
let renameTo;
|
|
117
|
+
const diffLine = lines[i];
|
|
118
|
+
const gitPathMatch = diffLine.match(/^diff --git a\/(.*) b\/(.*)$/);
|
|
119
|
+
if (gitPathMatch) {
|
|
120
|
+
oldPath = gitPathMatch[1];
|
|
121
|
+
newPath = gitPathMatch[2];
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
while (i < lines.length && !lines[i].startsWith("diff --git ")) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
if (line.startsWith("--- ")) {
|
|
127
|
+
const raw = line.slice(4);
|
|
128
|
+
oldPath = stripPrefix(raw);
|
|
129
|
+
if (raw === "/dev/null") {
|
|
130
|
+
status = "added";
|
|
131
|
+
}
|
|
132
|
+
} else if (line.startsWith("+++ ")) {
|
|
133
|
+
const raw = line.slice(4);
|
|
134
|
+
newPath = stripPrefix(raw);
|
|
135
|
+
if (raw === "/dev/null") {
|
|
136
|
+
status = "deleted";
|
|
137
|
+
}
|
|
138
|
+
} else if (line.startsWith("rename from ")) {
|
|
139
|
+
renameFrom = line.slice("rename from ".length);
|
|
140
|
+
status = "renamed";
|
|
141
|
+
} else if (line.startsWith("rename to ")) {
|
|
142
|
+
renameTo = line.slice("rename to ".length);
|
|
143
|
+
status = "renamed";
|
|
144
|
+
} else if (line.startsWith("new file mode")) {
|
|
145
|
+
status = "added";
|
|
146
|
+
} else if (line.startsWith("deleted file mode")) {
|
|
147
|
+
status = "deleted";
|
|
148
|
+
} else if (line.startsWith("Binary files") || line === "GIT binary patch") {
|
|
149
|
+
binary = true;
|
|
150
|
+
if (line.includes("/dev/null") && line.includes(" and b/")) {
|
|
151
|
+
status = "added";
|
|
152
|
+
} else if (line.includes("a/") && line.includes("/dev/null")) {
|
|
153
|
+
status = "deleted";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (line.startsWith("@@ ")) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
const filePath = status === "deleted" ? oldPath ?? newPath ?? "unknown" : newPath ?? oldPath ?? "unknown";
|
|
162
|
+
const fileOldPath = status === "renamed" ? renameFrom ?? oldPath : oldPath;
|
|
163
|
+
const hunks = [];
|
|
164
|
+
let additions = 0;
|
|
165
|
+
let deletions = 0;
|
|
166
|
+
while (i < lines.length && !lines[i].startsWith("diff --git ")) {
|
|
167
|
+
const line = lines[i];
|
|
168
|
+
if (line.startsWith("@@ ")) {
|
|
169
|
+
const hunkMatch = line.match(
|
|
170
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
171
|
+
);
|
|
172
|
+
if (!hunkMatch) {
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const oldStart = parseInt(hunkMatch[1], 10);
|
|
177
|
+
const oldLines = hunkMatch[2] !== void 0 ? parseInt(hunkMatch[2], 10) : 1;
|
|
178
|
+
const newStart = parseInt(hunkMatch[3], 10);
|
|
179
|
+
const newLines = hunkMatch[4] !== void 0 ? parseInt(hunkMatch[4], 10) : 1;
|
|
180
|
+
const changes = [];
|
|
181
|
+
let oldLineNum = oldStart;
|
|
182
|
+
let newLineNum = newStart;
|
|
183
|
+
i++;
|
|
184
|
+
while (i < lines.length) {
|
|
185
|
+
const changeLine = lines[i];
|
|
186
|
+
if (changeLine.startsWith("@@ ") || changeLine.startsWith("diff --git ")) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (changeLine.startsWith("\")) {
|
|
190
|
+
i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (changeLine.startsWith("+")) {
|
|
194
|
+
changes.push({
|
|
195
|
+
type: "add",
|
|
196
|
+
lineNumber: newLineNum,
|
|
197
|
+
content: changeLine.slice(1)
|
|
198
|
+
});
|
|
199
|
+
newLineNum++;
|
|
200
|
+
additions++;
|
|
201
|
+
} else if (changeLine.startsWith("-")) {
|
|
202
|
+
changes.push({
|
|
203
|
+
type: "delete",
|
|
204
|
+
lineNumber: oldLineNum,
|
|
205
|
+
content: changeLine.slice(1)
|
|
206
|
+
});
|
|
207
|
+
oldLineNum++;
|
|
208
|
+
deletions++;
|
|
209
|
+
} else {
|
|
210
|
+
changes.push({
|
|
211
|
+
type: "context",
|
|
212
|
+
lineNumber: newLineNum,
|
|
213
|
+
content: changeLine.length > 0 ? changeLine.slice(1) : ""
|
|
214
|
+
});
|
|
215
|
+
oldLineNum++;
|
|
216
|
+
newLineNum++;
|
|
217
|
+
}
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
hunks.push({ oldStart, oldLines, newStart, newLines, changes });
|
|
221
|
+
} else {
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const diffFile = {
|
|
226
|
+
path: filePath,
|
|
227
|
+
status,
|
|
228
|
+
hunks,
|
|
229
|
+
language: detectLanguage(filePath),
|
|
230
|
+
binary,
|
|
231
|
+
additions,
|
|
232
|
+
deletions
|
|
233
|
+
};
|
|
234
|
+
if (status === "renamed" && fileOldPath) {
|
|
235
|
+
diffFile.oldPath = fileOldPath;
|
|
236
|
+
}
|
|
237
|
+
files.push(diffFile);
|
|
238
|
+
}
|
|
239
|
+
return { baseRef, headRef, files };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// packages/git/src/index.ts
|
|
243
|
+
function getDiff(ref, options) {
|
|
244
|
+
const rawDiff = getGitDiff(ref, options);
|
|
245
|
+
let baseRef;
|
|
246
|
+
let headRef;
|
|
247
|
+
if (ref === "staged") {
|
|
248
|
+
baseRef = "HEAD";
|
|
249
|
+
headRef = "staged";
|
|
250
|
+
} else if (ref === "unstaged") {
|
|
251
|
+
baseRef = "staged";
|
|
252
|
+
headRef = "working tree";
|
|
253
|
+
} else if (ref.includes("..")) {
|
|
254
|
+
const [base, head] = ref.split("..");
|
|
255
|
+
baseRef = base;
|
|
256
|
+
headRef = head;
|
|
257
|
+
} else {
|
|
258
|
+
baseRef = ref;
|
|
259
|
+
headRef = "HEAD";
|
|
260
|
+
}
|
|
261
|
+
const diffSet = parseDiff(rawDiff, baseRef, headRef);
|
|
262
|
+
return { diffSet, rawDiff };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// packages/analysis/src/deterministic.ts
|
|
266
|
+
function categorizeFiles(files) {
|
|
267
|
+
const notable = files.map((f) => ({
|
|
268
|
+
file: f.path,
|
|
269
|
+
description: `${f.status} (${f.language || "unknown"}) +${f.additions} -${f.deletions}`,
|
|
270
|
+
reason: "Uncategorized in M0 \u2014 placed in notable by default"
|
|
271
|
+
}));
|
|
272
|
+
return {
|
|
273
|
+
critical: [],
|
|
274
|
+
notable,
|
|
275
|
+
mechanical: []
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function computeFileStats(files) {
|
|
279
|
+
return files.map((f) => ({
|
|
280
|
+
path: f.path,
|
|
281
|
+
language: f.language,
|
|
282
|
+
status: f.status,
|
|
283
|
+
additions: f.additions,
|
|
284
|
+
deletions: f.deletions
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
function detectAffectedModules(files) {
|
|
288
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
289
|
+
for (const f of files) {
|
|
290
|
+
const lastSlash = f.path.lastIndexOf("/");
|
|
291
|
+
if (lastSlash > 0) {
|
|
292
|
+
dirs.add(f.path.slice(0, lastSlash));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return [...dirs].sort();
|
|
296
|
+
}
|
|
297
|
+
var TEST_PATTERNS = [
|
|
298
|
+
/\.test\./,
|
|
299
|
+
/\.spec\./,
|
|
300
|
+
/\/__tests__\//,
|
|
301
|
+
/\/test\//
|
|
302
|
+
];
|
|
303
|
+
function detectAffectedTests(files) {
|
|
304
|
+
return files.filter((f) => TEST_PATTERNS.some((re) => re.test(f.path))).map((f) => f.path);
|
|
305
|
+
}
|
|
306
|
+
var DEPENDENCY_FIELDS = [
|
|
307
|
+
'"dependencies"',
|
|
308
|
+
'"devDependencies"',
|
|
309
|
+
'"peerDependencies"',
|
|
310
|
+
'"optionalDependencies"'
|
|
311
|
+
];
|
|
312
|
+
function detectNewDependencies(files) {
|
|
313
|
+
const deps = /* @__PURE__ */ new Set();
|
|
314
|
+
const packageFiles = files.filter(
|
|
315
|
+
(f) => f.path.endsWith("package.json") && f.hunks.length > 0
|
|
316
|
+
);
|
|
317
|
+
for (const file of packageFiles) {
|
|
318
|
+
for (const hunk of file.hunks) {
|
|
319
|
+
let inDependencyBlock = false;
|
|
320
|
+
for (const change of hunk.changes) {
|
|
321
|
+
const line = change.content;
|
|
322
|
+
if (DEPENDENCY_FIELDS.some((field) => line.includes(field))) {
|
|
323
|
+
inDependencyBlock = true;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (inDependencyBlock && line.trim().startsWith("}")) {
|
|
327
|
+
inDependencyBlock = false;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (change.type === "add" && inDependencyBlock) {
|
|
331
|
+
const match = line.match(/"([^"]+)"\s*:/);
|
|
332
|
+
if (match) {
|
|
333
|
+
deps.add(match[1]);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return [...deps].sort();
|
|
340
|
+
}
|
|
341
|
+
function generateSummary(files) {
|
|
342
|
+
const totalFiles = files.length;
|
|
343
|
+
const counts = {
|
|
344
|
+
added: 0,
|
|
345
|
+
modified: 0,
|
|
346
|
+
deleted: 0,
|
|
347
|
+
renamed: 0
|
|
348
|
+
};
|
|
349
|
+
let totalAdditions = 0;
|
|
350
|
+
let totalDeletions = 0;
|
|
351
|
+
for (const f of files) {
|
|
352
|
+
counts[f.status]++;
|
|
353
|
+
totalAdditions += f.additions;
|
|
354
|
+
totalDeletions += f.deletions;
|
|
355
|
+
}
|
|
356
|
+
const parts = [];
|
|
357
|
+
if (counts.modified > 0) parts.push(`${counts.modified} modified`);
|
|
358
|
+
if (counts.added > 0) parts.push(`${counts.added} added`);
|
|
359
|
+
if (counts.deleted > 0) parts.push(`${counts.deleted} deleted`);
|
|
360
|
+
if (counts.renamed > 0) parts.push(`${counts.renamed} renamed`);
|
|
361
|
+
const breakdown = parts.length > 0 ? `: ${parts.join(", ")}` : "";
|
|
362
|
+
return `${totalFiles} files changed${breakdown} (+${totalAdditions} -${totalDeletions})`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// packages/analysis/src/index.ts
|
|
366
|
+
function analyze(diffSet) {
|
|
367
|
+
const { files } = diffSet;
|
|
368
|
+
const triage = categorizeFiles(files);
|
|
369
|
+
const fileStats = computeFileStats(files);
|
|
370
|
+
const affectedModules = detectAffectedModules(files);
|
|
371
|
+
const affectedTests = detectAffectedTests(files);
|
|
372
|
+
const newDependencies = detectNewDependencies(files);
|
|
373
|
+
const summary = generateSummary(files);
|
|
374
|
+
return {
|
|
375
|
+
summary,
|
|
376
|
+
triage,
|
|
377
|
+
impact: {
|
|
378
|
+
affectedModules,
|
|
379
|
+
affectedTests,
|
|
380
|
+
publicApiChanges: false,
|
|
381
|
+
breakingChanges: [],
|
|
382
|
+
newDependencies
|
|
383
|
+
},
|
|
384
|
+
verification: {
|
|
385
|
+
testsPass: null,
|
|
386
|
+
typeCheck: null,
|
|
387
|
+
lintClean: null
|
|
388
|
+
},
|
|
389
|
+
fileStats
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// packages/core/src/ws-bridge.ts
|
|
394
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
395
|
+
function createWsBridge(port) {
|
|
396
|
+
const wss = new WebSocketServer({ port });
|
|
397
|
+
let client = null;
|
|
398
|
+
let resultResolve = null;
|
|
399
|
+
let resultReject = null;
|
|
400
|
+
let pendingInit = null;
|
|
401
|
+
let initPayload = null;
|
|
402
|
+
let closeTimer = null;
|
|
403
|
+
wss.on("connection", (ws) => {
|
|
404
|
+
if (closeTimer) {
|
|
405
|
+
clearTimeout(closeTimer);
|
|
406
|
+
closeTimer = null;
|
|
407
|
+
}
|
|
408
|
+
client = ws;
|
|
409
|
+
const payload = pendingInit ?? initPayload;
|
|
410
|
+
if (payload) {
|
|
411
|
+
const msg = {
|
|
412
|
+
type: "review:init",
|
|
413
|
+
payload
|
|
414
|
+
};
|
|
415
|
+
ws.send(JSON.stringify(msg));
|
|
416
|
+
pendingInit = null;
|
|
417
|
+
}
|
|
418
|
+
ws.on("message", (data) => {
|
|
419
|
+
try {
|
|
420
|
+
const msg = JSON.parse(data.toString());
|
|
421
|
+
if (msg.type === "review:submit" && resultResolve) {
|
|
422
|
+
resultResolve(msg.payload);
|
|
423
|
+
resultResolve = null;
|
|
424
|
+
resultReject = null;
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
ws.on("close", () => {
|
|
430
|
+
client = null;
|
|
431
|
+
if (resultReject) {
|
|
432
|
+
closeTimer = setTimeout(() => {
|
|
433
|
+
if (resultReject) {
|
|
434
|
+
resultReject(new Error("Browser closed before review was submitted"));
|
|
435
|
+
resultResolve = null;
|
|
436
|
+
resultReject = null;
|
|
437
|
+
}
|
|
438
|
+
}, 2e3);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
port,
|
|
444
|
+
sendInit(payload) {
|
|
445
|
+
initPayload = payload;
|
|
446
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
447
|
+
const msg = {
|
|
448
|
+
type: "review:init",
|
|
449
|
+
payload
|
|
450
|
+
};
|
|
451
|
+
client.send(JSON.stringify(msg));
|
|
452
|
+
} else {
|
|
453
|
+
pendingInit = payload;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
waitForResult() {
|
|
457
|
+
return new Promise((resolve, reject) => {
|
|
458
|
+
resultResolve = resolve;
|
|
459
|
+
resultReject = reject;
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
close() {
|
|
463
|
+
for (const ws of wss.clients) {
|
|
464
|
+
ws.close();
|
|
465
|
+
}
|
|
466
|
+
wss.close();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// packages/core/src/review-manager.ts
|
|
472
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
473
|
+
var idCounter = 0;
|
|
474
|
+
function createSession(options) {
|
|
475
|
+
const id = `review-${Date.now()}-${++idCounter}`;
|
|
476
|
+
const session = {
|
|
477
|
+
id,
|
|
478
|
+
options,
|
|
479
|
+
status: "pending",
|
|
480
|
+
createdAt: Date.now()
|
|
481
|
+
};
|
|
482
|
+
sessions.set(id, session);
|
|
483
|
+
return session;
|
|
484
|
+
}
|
|
485
|
+
function updateSession(id, update) {
|
|
486
|
+
const session = sessions.get(id);
|
|
487
|
+
if (session) {
|
|
488
|
+
Object.assign(session, update);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// packages/core/src/pipeline.ts
|
|
493
|
+
var MIME_TYPES = {
|
|
494
|
+
".html": "text/html",
|
|
495
|
+
".js": "application/javascript",
|
|
496
|
+
".css": "text/css",
|
|
497
|
+
".json": "application/json",
|
|
498
|
+
".svg": "image/svg+xml",
|
|
499
|
+
".png": "image/png",
|
|
500
|
+
".ico": "image/x-icon",
|
|
501
|
+
".woff": "font/woff",
|
|
502
|
+
".woff2": "font/woff2"
|
|
503
|
+
};
|
|
504
|
+
function resolveUiDist() {
|
|
505
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
506
|
+
const thisDir = path2.dirname(thisFile);
|
|
507
|
+
const publishedUiDist = path2.resolve(thisDir, "..", "ui-dist");
|
|
508
|
+
if (fs.existsSync(path2.join(publishedUiDist, "index.html"))) {
|
|
509
|
+
return publishedUiDist;
|
|
510
|
+
}
|
|
511
|
+
const workspaceRoot = path2.resolve(thisDir, "..", "..", "..");
|
|
512
|
+
const devUiDist = path2.join(workspaceRoot, "packages", "ui", "dist");
|
|
513
|
+
if (fs.existsSync(path2.join(devUiDist, "index.html"))) {
|
|
514
|
+
return devUiDist;
|
|
515
|
+
}
|
|
516
|
+
throw new Error(
|
|
517
|
+
"Could not find built UI. Run 'pnpm -F @diffprism/ui build' first."
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
function createStaticServer(distPath, port) {
|
|
521
|
+
const server = http.createServer((req, res) => {
|
|
522
|
+
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
523
|
+
let filePath = path2.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
524
|
+
if (!fs.existsSync(filePath)) {
|
|
525
|
+
filePath = path2.join(distPath, "index.html");
|
|
526
|
+
}
|
|
527
|
+
const ext = path2.extname(filePath);
|
|
528
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
529
|
+
try {
|
|
530
|
+
const content = fs.readFileSync(filePath);
|
|
531
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
532
|
+
res.end(content);
|
|
533
|
+
} catch {
|
|
534
|
+
res.writeHead(404);
|
|
535
|
+
res.end("Not found");
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
return new Promise((resolve, reject) => {
|
|
539
|
+
server.on("error", reject);
|
|
540
|
+
server.listen(port, () => resolve(server));
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
async function startReview(options) {
|
|
544
|
+
const { diffRef, title, description, reasoning, cwd, silent } = options;
|
|
545
|
+
const { diffSet, rawDiff } = getDiff(diffRef, { cwd });
|
|
546
|
+
if (diffSet.files.length === 0) {
|
|
547
|
+
if (!silent) {
|
|
548
|
+
console.log("No changes to review.");
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
decision: "approved",
|
|
552
|
+
comments: [],
|
|
553
|
+
summary: "No changes to review."
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const briefing = analyze(diffSet);
|
|
557
|
+
const session = createSession(options);
|
|
558
|
+
updateSession(session.id, { status: "in_progress" });
|
|
559
|
+
const [wsPort, httpPort] = await Promise.all([
|
|
560
|
+
getPort(),
|
|
561
|
+
getPort()
|
|
562
|
+
]);
|
|
563
|
+
const bridge = createWsBridge(wsPort);
|
|
564
|
+
const uiDist = resolveUiDist();
|
|
565
|
+
let httpServer = null;
|
|
566
|
+
try {
|
|
567
|
+
httpServer = await createStaticServer(uiDist, httpPort);
|
|
568
|
+
const url = `http://localhost:${httpPort}?wsPort=${wsPort}&reviewId=${session.id}`;
|
|
569
|
+
if (!silent) {
|
|
570
|
+
console.log(`
|
|
571
|
+
DiffPrism Review: ${title ?? briefing.summary}`);
|
|
572
|
+
console.log(`Opening browser at ${url}
|
|
573
|
+
`);
|
|
574
|
+
}
|
|
575
|
+
await open(url);
|
|
576
|
+
const initPayload = {
|
|
577
|
+
reviewId: session.id,
|
|
578
|
+
diffSet,
|
|
579
|
+
rawDiff,
|
|
580
|
+
briefing,
|
|
581
|
+
metadata: { title, description, reasoning }
|
|
582
|
+
};
|
|
583
|
+
bridge.sendInit(initPayload);
|
|
584
|
+
const result = await bridge.waitForResult();
|
|
585
|
+
updateSession(session.id, { status: "completed", result });
|
|
586
|
+
return result;
|
|
587
|
+
} finally {
|
|
588
|
+
bridge.close();
|
|
589
|
+
if (httpServer) {
|
|
590
|
+
httpServer.close();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export {
|
|
596
|
+
startReview
|
|
597
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startReview
|
|
3
|
+
} from "./chunk-AUPKNXCS.js";
|
|
4
|
+
|
|
5
|
+
// packages/mcp-server/src/index.ts
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
async function startMcpServer() {
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: "diffprism",
|
|
12
|
+
version: "0.0.1"
|
|
13
|
+
});
|
|
14
|
+
server.tool(
|
|
15
|
+
"open_review",
|
|
16
|
+
"Open a browser-based code review for local git changes. Blocks until the engineer submits their review decision.",
|
|
17
|
+
{
|
|
18
|
+
diff_ref: z.string().describe(
|
|
19
|
+
'Git diff reference: "staged", "unstaged", or a ref range like "HEAD~3..HEAD"'
|
|
20
|
+
),
|
|
21
|
+
title: z.string().optional().describe("Title for the review"),
|
|
22
|
+
description: z.string().optional().describe("Description of the changes"),
|
|
23
|
+
reasoning: z.string().optional().describe("Agent reasoning about why these changes were made")
|
|
24
|
+
},
|
|
25
|
+
async ({ diff_ref, title, description, reasoning }) => {
|
|
26
|
+
try {
|
|
27
|
+
const result = await startReview({
|
|
28
|
+
diffRef: diff_ref,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
reasoning,
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
silent: true
|
|
34
|
+
// Suppress stdout — MCP uses stdio
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify(result, null, 2)
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Error: ${message}`
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
isError: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
const transport = new StdioServerTransport();
|
|
59
|
+
await server.connect(transport);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
startMcpServer
|
|
63
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diffprism",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Local-first code review tool for agent-generated code changes",
|
|
6
|
+
"bin": {
|
|
7
|
+
"diffprism": "dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"ui-dist/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "pnpm -r run build && pnpm run bundle",
|
|
15
|
+
"bundle": "tsup && rm -rf ui-dist && cp -r packages/ui/dist ui-dist",
|
|
16
|
+
"dev": "pnpm -r run dev",
|
|
17
|
+
"lint": "pnpm -r run lint",
|
|
18
|
+
"test": "pnpm -r run test",
|
|
19
|
+
"cli": "pnpm -F @diffprism/ui build && tsx cli/src/index.ts",
|
|
20
|
+
"prepublishOnly": "pnpm run build"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ws": "^8.18.0",
|
|
27
|
+
"open": "^10.1.0",
|
|
28
|
+
"get-port": "^7.1.0",
|
|
29
|
+
"commander": "^13.0.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.4.0",
|
|
31
|
+
"zod": "^3.24.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"tsup": "^8.5.1"
|
|
35
|
+
},
|
|
36
|
+
"pnpm": {
|
|
37
|
+
"onlyBuiltDependencies": [
|
|
38
|
+
"esbuild"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|