diffprism 0.34.1 → 0.36.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 +47 -188
- package/dist/bin.js +213 -227
- package/dist/chunk-DHCVZGHE.js +501 -0
- package/dist/chunk-ITPHDFOS.js +1283 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/{chunk-VASCXEMN.js → chunk-OR6PCPZX.js} +22 -2791
- package/dist/chunk-QGWYCEJN.js +448 -0
- package/dist/chunk-UYZ3A2PB.js +231 -0
- package/dist/demo-JH5YOKTZ.js +10 -0
- package/dist/mcp-server.js +77 -280
- package/dist/src-AMCPIYDZ.js +19 -0
- package/dist/src-JMPTSU3P.js +27 -0
- package/package.json +1 -1
- package/ui-dist/assets/index-CNIXSkOg.js +325 -0
- package/ui-dist/index.html +1 -1
- package/ui-dist/assets/index-BfqEajZq.js +0 -325
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
-
mod
|
|
25
|
-
));
|
|
1
|
+
import {
|
|
2
|
+
parseDiff
|
|
3
|
+
} from "./chunk-QGWYCEJN.js";
|
|
4
|
+
import {
|
|
5
|
+
analyze
|
|
6
|
+
} from "./chunk-DHCVZGHE.js";
|
|
7
|
+
import {
|
|
8
|
+
__commonJS,
|
|
9
|
+
__toESM
|
|
10
|
+
} from "./chunk-JSBRDJBE.js";
|
|
26
11
|
|
|
27
12
|
// node_modules/.pnpm/fast-content-type-parse@2.0.1/node_modules/fast-content-type-parse/index.js
|
|
28
13
|
var require_fast_content_type_parse = __commonJS({
|
|
@@ -120,2760 +105,18 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
120
105
|
}
|
|
121
106
|
});
|
|
122
107
|
|
|
123
|
-
// packages/
|
|
108
|
+
// packages/github/src/auth.ts
|
|
124
109
|
import { execSync } from "child_process";
|
|
125
|
-
import { readFileSync } from "fs";
|
|
126
|
-
import path from "path";
|
|
127
|
-
function getGitDiff(ref, options) {
|
|
128
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
129
|
-
try {
|
|
130
|
-
execSync("git --version", { cwd, stdio: "pipe" });
|
|
131
|
-
} catch {
|
|
132
|
-
throw new Error(
|
|
133
|
-
"git is not available. Please install git and make sure it is on your PATH."
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
138
|
-
} catch {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`The directory "${cwd}" is not inside a git repository.`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
let command;
|
|
144
|
-
let includeUntracked = false;
|
|
145
|
-
switch (ref) {
|
|
146
|
-
case "staged":
|
|
147
|
-
command = "git diff --staged --no-color";
|
|
148
|
-
break;
|
|
149
|
-
case "unstaged":
|
|
150
|
-
command = "git diff --no-color";
|
|
151
|
-
includeUntracked = true;
|
|
152
|
-
break;
|
|
153
|
-
case "all":
|
|
154
|
-
command = "git diff HEAD --no-color";
|
|
155
|
-
includeUntracked = true;
|
|
156
|
-
break;
|
|
157
|
-
default:
|
|
158
|
-
command = `git diff --no-color ${ref}`;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
let output;
|
|
162
|
-
try {
|
|
163
|
-
output = execSync(command, {
|
|
164
|
-
cwd,
|
|
165
|
-
encoding: "utf-8",
|
|
166
|
-
maxBuffer: 50 * 1024 * 1024
|
|
167
|
-
// 50 MB
|
|
168
|
-
});
|
|
169
|
-
} catch (err) {
|
|
170
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
171
|
-
throw new Error(`git diff failed: ${message}`);
|
|
172
|
-
}
|
|
173
|
-
if (includeUntracked) {
|
|
174
|
-
output += getUntrackedDiffs(cwd);
|
|
175
|
-
}
|
|
176
|
-
return output;
|
|
177
|
-
}
|
|
178
|
-
function getCurrentBranch(options) {
|
|
179
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
180
|
-
try {
|
|
181
|
-
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
182
|
-
cwd,
|
|
183
|
-
encoding: "utf-8",
|
|
184
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
185
|
-
}).trim();
|
|
186
|
-
} catch {
|
|
187
|
-
return "unknown";
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function listBranches(options) {
|
|
191
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
192
|
-
try {
|
|
193
|
-
const output = execSync(
|
|
194
|
-
"git branch -a --format='%(refname:short)' --sort=-committerdate",
|
|
195
|
-
{ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
196
|
-
).trim();
|
|
197
|
-
if (!output) return { local: [], remote: [] };
|
|
198
|
-
const local = [];
|
|
199
|
-
const remote = [];
|
|
200
|
-
for (const line of output.split("\n")) {
|
|
201
|
-
const name = line.trim();
|
|
202
|
-
if (!name) continue;
|
|
203
|
-
if (name.endsWith("/HEAD")) continue;
|
|
204
|
-
if (name.includes("/")) {
|
|
205
|
-
remote.push(name);
|
|
206
|
-
} else {
|
|
207
|
-
local.push(name);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return { local, remote };
|
|
211
|
-
} catch {
|
|
212
|
-
return { local: [], remote: [] };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function listCommits(options) {
|
|
216
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
217
|
-
const limit = options?.limit ?? 50;
|
|
218
|
-
try {
|
|
219
|
-
const output = execSync(
|
|
220
|
-
`git log --format='%H<<>>%h<<>>%s<<>>%an<<>>%aI' -n ${limit}`,
|
|
221
|
-
{ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
222
|
-
).trim();
|
|
223
|
-
if (!output) return [];
|
|
224
|
-
const commits = [];
|
|
225
|
-
for (const line of output.split("\n")) {
|
|
226
|
-
const parts = line.split("<<>>");
|
|
227
|
-
if (parts.length < 5) continue;
|
|
228
|
-
commits.push({
|
|
229
|
-
hash: parts[0],
|
|
230
|
-
shortHash: parts[1],
|
|
231
|
-
subject: parts[2],
|
|
232
|
-
author: parts[3],
|
|
233
|
-
date: parts[4]
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return commits;
|
|
237
|
-
} catch {
|
|
238
|
-
return [];
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function detectWorktree(options) {
|
|
242
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
243
|
-
try {
|
|
244
|
-
const gitDir = execSync("git rev-parse --git-dir", {
|
|
245
|
-
cwd,
|
|
246
|
-
encoding: "utf-8",
|
|
247
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
248
|
-
}).trim();
|
|
249
|
-
const gitCommonDir = execSync("git rev-parse --git-common-dir", {
|
|
250
|
-
cwd,
|
|
251
|
-
encoding: "utf-8",
|
|
252
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
253
|
-
}).trim();
|
|
254
|
-
const resolvedGitDir = path.resolve(cwd, gitDir);
|
|
255
|
-
const resolvedCommonDir = path.resolve(cwd, gitCommonDir);
|
|
256
|
-
const isWorktree = resolvedGitDir !== resolvedCommonDir;
|
|
257
|
-
if (!isWorktree) {
|
|
258
|
-
return { isWorktree: false };
|
|
259
|
-
}
|
|
260
|
-
const worktreePath = execSync("git rev-parse --show-toplevel", {
|
|
261
|
-
cwd,
|
|
262
|
-
encoding: "utf-8",
|
|
263
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
264
|
-
}).trim();
|
|
265
|
-
const mainWorktreePath = path.dirname(resolvedCommonDir);
|
|
266
|
-
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
267
|
-
cwd,
|
|
268
|
-
encoding: "utf-8",
|
|
269
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
270
|
-
}).trim();
|
|
271
|
-
return {
|
|
272
|
-
isWorktree: true,
|
|
273
|
-
worktreePath,
|
|
274
|
-
mainWorktreePath,
|
|
275
|
-
branch: branch === "HEAD" ? void 0 : branch
|
|
276
|
-
};
|
|
277
|
-
} catch {
|
|
278
|
-
return { isWorktree: false };
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function getUntrackedDiffs(cwd) {
|
|
282
|
-
let untrackedList;
|
|
283
|
-
try {
|
|
284
|
-
untrackedList = execSync(
|
|
285
|
-
"git ls-files --others --exclude-standard",
|
|
286
|
-
{ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
287
|
-
).trim();
|
|
288
|
-
} catch {
|
|
289
|
-
return "";
|
|
290
|
-
}
|
|
291
|
-
if (!untrackedList) return "";
|
|
292
|
-
const files = untrackedList.split("\n");
|
|
293
|
-
let result = "";
|
|
294
|
-
for (const file of files) {
|
|
295
|
-
const absPath = path.resolve(cwd, file);
|
|
296
|
-
let content;
|
|
297
|
-
try {
|
|
298
|
-
content = readFileSync(absPath, "utf-8");
|
|
299
|
-
} catch {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const lines = content.split("\n");
|
|
303
|
-
const hasTrailingNewline = content.length > 0 && content[content.length - 1] === "\n";
|
|
304
|
-
const contentLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
305
|
-
result += `diff --git a/${file} b/${file}
|
|
306
|
-
`;
|
|
307
|
-
result += "new file mode 100644\n";
|
|
308
|
-
result += "--- /dev/null\n";
|
|
309
|
-
result += `+++ b/${file}
|
|
310
|
-
`;
|
|
311
|
-
result += `@@ -0,0 +1,${contentLines.length} @@
|
|
312
|
-
`;
|
|
313
|
-
for (const line of contentLines) {
|
|
314
|
-
result += `+${line}
|
|
315
|
-
`;
|
|
316
|
-
}
|
|
317
|
-
if (!hasTrailingNewline) {
|
|
318
|
-
result += "\\n";
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// packages/git/src/parser.ts
|
|
325
|
-
import path2 from "path";
|
|
326
|
-
var EXTENSION_MAP = {
|
|
327
|
-
ts: "typescript",
|
|
328
|
-
tsx: "typescript",
|
|
329
|
-
js: "javascript",
|
|
330
|
-
jsx: "javascript",
|
|
331
|
-
py: "python",
|
|
332
|
-
rb: "ruby",
|
|
333
|
-
go: "go",
|
|
334
|
-
rs: "rust",
|
|
335
|
-
java: "java",
|
|
336
|
-
c: "c",
|
|
337
|
-
h: "c",
|
|
338
|
-
cpp: "cpp",
|
|
339
|
-
hpp: "cpp",
|
|
340
|
-
cc: "cpp",
|
|
341
|
-
cs: "csharp",
|
|
342
|
-
md: "markdown",
|
|
343
|
-
json: "json",
|
|
344
|
-
yaml: "yaml",
|
|
345
|
-
yml: "yaml",
|
|
346
|
-
html: "html",
|
|
347
|
-
css: "css",
|
|
348
|
-
scss: "scss",
|
|
349
|
-
sql: "sql",
|
|
350
|
-
sh: "shell",
|
|
351
|
-
bash: "shell",
|
|
352
|
-
zsh: "shell"
|
|
353
|
-
};
|
|
354
|
-
var FILENAME_MAP = {
|
|
355
|
-
Dockerfile: "dockerfile",
|
|
356
|
-
Makefile: "makefile"
|
|
357
|
-
};
|
|
358
|
-
function detectLanguage(filePath) {
|
|
359
|
-
const basename = path2.basename(filePath);
|
|
360
|
-
if (FILENAME_MAP[basename]) {
|
|
361
|
-
return FILENAME_MAP[basename];
|
|
362
|
-
}
|
|
363
|
-
const ext = basename.includes(".") ? basename.slice(basename.lastIndexOf(".") + 1) : "";
|
|
364
|
-
return EXTENSION_MAP[ext] ?? "text";
|
|
365
|
-
}
|
|
366
|
-
function stripPrefix(raw) {
|
|
367
|
-
if (raw === "/dev/null") return raw;
|
|
368
|
-
return raw.replace(/^[ab]\//, "");
|
|
369
|
-
}
|
|
370
|
-
function parseDiff(rawDiff, baseRef, headRef) {
|
|
371
|
-
if (!rawDiff.trim()) {
|
|
372
|
-
return { baseRef, headRef, files: [] };
|
|
373
|
-
}
|
|
374
|
-
const files = [];
|
|
375
|
-
const lines = rawDiff.split("\n");
|
|
376
|
-
let i = 0;
|
|
377
|
-
while (i < lines.length) {
|
|
378
|
-
if (!lines[i].startsWith("diff --git ")) {
|
|
379
|
-
i++;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
let oldPath;
|
|
383
|
-
let newPath;
|
|
384
|
-
let status = "modified";
|
|
385
|
-
let binary = false;
|
|
386
|
-
let renameFrom;
|
|
387
|
-
let renameTo;
|
|
388
|
-
const diffLine = lines[i];
|
|
389
|
-
const gitPathMatch = diffLine.match(/^diff --git a\/(.*) b\/(.*)$/);
|
|
390
|
-
if (gitPathMatch) {
|
|
391
|
-
oldPath = gitPathMatch[1];
|
|
392
|
-
newPath = gitPathMatch[2];
|
|
393
|
-
}
|
|
394
|
-
i++;
|
|
395
|
-
while (i < lines.length && !lines[i].startsWith("diff --git ")) {
|
|
396
|
-
const line = lines[i];
|
|
397
|
-
if (line.startsWith("--- ")) {
|
|
398
|
-
const raw = line.slice(4);
|
|
399
|
-
oldPath = stripPrefix(raw);
|
|
400
|
-
if (raw === "/dev/null") {
|
|
401
|
-
status = "added";
|
|
402
|
-
}
|
|
403
|
-
} else if (line.startsWith("+++ ")) {
|
|
404
|
-
const raw = line.slice(4);
|
|
405
|
-
newPath = stripPrefix(raw);
|
|
406
|
-
if (raw === "/dev/null") {
|
|
407
|
-
status = "deleted";
|
|
408
|
-
}
|
|
409
|
-
} else if (line.startsWith("rename from ")) {
|
|
410
|
-
renameFrom = line.slice("rename from ".length);
|
|
411
|
-
status = "renamed";
|
|
412
|
-
} else if (line.startsWith("rename to ")) {
|
|
413
|
-
renameTo = line.slice("rename to ".length);
|
|
414
|
-
status = "renamed";
|
|
415
|
-
} else if (line.startsWith("new file mode")) {
|
|
416
|
-
status = "added";
|
|
417
|
-
} else if (line.startsWith("deleted file mode")) {
|
|
418
|
-
status = "deleted";
|
|
419
|
-
} else if (line.startsWith("Binary files") || line === "GIT binary patch") {
|
|
420
|
-
binary = true;
|
|
421
|
-
if (line.includes("/dev/null") && line.includes(" and b/")) {
|
|
422
|
-
status = "added";
|
|
423
|
-
} else if (line.includes("a/") && line.includes("/dev/null")) {
|
|
424
|
-
status = "deleted";
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (line.startsWith("@@ ")) {
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
i++;
|
|
431
|
-
}
|
|
432
|
-
const filePath = status === "deleted" ? oldPath ?? newPath ?? "unknown" : newPath ?? oldPath ?? "unknown";
|
|
433
|
-
const fileOldPath = status === "renamed" ? renameFrom ?? oldPath : oldPath;
|
|
434
|
-
const hunks = [];
|
|
435
|
-
let additions = 0;
|
|
436
|
-
let deletions = 0;
|
|
437
|
-
while (i < lines.length && !lines[i].startsWith("diff --git ")) {
|
|
438
|
-
const line = lines[i];
|
|
439
|
-
if (line.startsWith("@@ ")) {
|
|
440
|
-
const hunkMatch = line.match(
|
|
441
|
-
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
442
|
-
);
|
|
443
|
-
if (!hunkMatch) {
|
|
444
|
-
i++;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
const oldStart = parseInt(hunkMatch[1], 10);
|
|
448
|
-
const oldLines = hunkMatch[2] !== void 0 ? parseInt(hunkMatch[2], 10) : 1;
|
|
449
|
-
const newStart = parseInt(hunkMatch[3], 10);
|
|
450
|
-
const newLines = hunkMatch[4] !== void 0 ? parseInt(hunkMatch[4], 10) : 1;
|
|
451
|
-
const changes = [];
|
|
452
|
-
let oldLineNum = oldStart;
|
|
453
|
-
let newLineNum = newStart;
|
|
454
|
-
i++;
|
|
455
|
-
while (i < lines.length) {
|
|
456
|
-
const changeLine = lines[i];
|
|
457
|
-
if (changeLine.startsWith("@@ ") || changeLine.startsWith("diff --git ")) {
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
if (changeLine.startsWith("\")) {
|
|
461
|
-
i++;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
if (changeLine.startsWith("+")) {
|
|
465
|
-
changes.push({
|
|
466
|
-
type: "add",
|
|
467
|
-
lineNumber: newLineNum,
|
|
468
|
-
content: changeLine.slice(1)
|
|
469
|
-
});
|
|
470
|
-
newLineNum++;
|
|
471
|
-
additions++;
|
|
472
|
-
} else if (changeLine.startsWith("-")) {
|
|
473
|
-
changes.push({
|
|
474
|
-
type: "delete",
|
|
475
|
-
lineNumber: oldLineNum,
|
|
476
|
-
content: changeLine.slice(1)
|
|
477
|
-
});
|
|
478
|
-
oldLineNum++;
|
|
479
|
-
deletions++;
|
|
480
|
-
} else {
|
|
481
|
-
changes.push({
|
|
482
|
-
type: "context",
|
|
483
|
-
lineNumber: newLineNum,
|
|
484
|
-
content: changeLine.length > 0 ? changeLine.slice(1) : ""
|
|
485
|
-
});
|
|
486
|
-
oldLineNum++;
|
|
487
|
-
newLineNum++;
|
|
488
|
-
}
|
|
489
|
-
i++;
|
|
490
|
-
}
|
|
491
|
-
hunks.push({ oldStart, oldLines, newStart, newLines, changes });
|
|
492
|
-
} else {
|
|
493
|
-
i++;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
const diffFile = {
|
|
497
|
-
path: filePath,
|
|
498
|
-
status,
|
|
499
|
-
hunks,
|
|
500
|
-
language: detectLanguage(filePath),
|
|
501
|
-
binary,
|
|
502
|
-
additions,
|
|
503
|
-
deletions
|
|
504
|
-
};
|
|
505
|
-
if (status === "renamed" && fileOldPath) {
|
|
506
|
-
diffFile.oldPath = fileOldPath;
|
|
507
|
-
}
|
|
508
|
-
files.push(diffFile);
|
|
509
|
-
}
|
|
510
|
-
return { baseRef, headRef, files };
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// packages/git/src/index.ts
|
|
514
|
-
function getDiff(ref, options) {
|
|
515
|
-
if (ref === "working-copy") {
|
|
516
|
-
return getWorkingCopyDiff(options);
|
|
517
|
-
}
|
|
518
|
-
const rawDiff = getGitDiff(ref, options);
|
|
519
|
-
let baseRef;
|
|
520
|
-
let headRef;
|
|
521
|
-
if (ref === "staged") {
|
|
522
|
-
baseRef = "HEAD";
|
|
523
|
-
headRef = "staged";
|
|
524
|
-
} else if (ref === "unstaged") {
|
|
525
|
-
baseRef = "staged";
|
|
526
|
-
headRef = "working tree";
|
|
527
|
-
} else if (ref.includes("..")) {
|
|
528
|
-
const [base, head] = ref.split("..");
|
|
529
|
-
baseRef = base;
|
|
530
|
-
headRef = head;
|
|
531
|
-
} else {
|
|
532
|
-
baseRef = ref;
|
|
533
|
-
headRef = "HEAD";
|
|
534
|
-
}
|
|
535
|
-
const diffSet = parseDiff(rawDiff, baseRef, headRef);
|
|
536
|
-
return { diffSet, rawDiff };
|
|
537
|
-
}
|
|
538
|
-
function getWorkingCopyDiff(options) {
|
|
539
|
-
const stagedRaw = getGitDiff("staged", options);
|
|
540
|
-
const unstagedRaw = getGitDiff("unstaged", options);
|
|
541
|
-
const stagedDiffSet = parseDiff(stagedRaw, "HEAD", "staged");
|
|
542
|
-
const unstagedDiffSet = parseDiff(unstagedRaw, "staged", "working tree");
|
|
543
|
-
const stagedFiles = stagedDiffSet.files.map((f) => ({
|
|
544
|
-
...f,
|
|
545
|
-
stage: "staged"
|
|
546
|
-
}));
|
|
547
|
-
const unstagedFiles = unstagedDiffSet.files.map((f) => ({
|
|
548
|
-
...f,
|
|
549
|
-
stage: "unstaged"
|
|
550
|
-
}));
|
|
551
|
-
const rawDiff = [stagedRaw, unstagedRaw].filter(Boolean).join("");
|
|
552
|
-
return {
|
|
553
|
-
diffSet: {
|
|
554
|
-
baseRef: "HEAD",
|
|
555
|
-
headRef: "working tree",
|
|
556
|
-
files: [...stagedFiles, ...unstagedFiles]
|
|
557
|
-
},
|
|
558
|
-
rawDiff
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// packages/analysis/src/deterministic.ts
|
|
563
|
-
var MECHANICAL_CONFIG_PATTERNS = [
|
|
564
|
-
/\.config\./,
|
|
565
|
-
/\.eslintrc/,
|
|
566
|
-
/\.prettierrc/,
|
|
567
|
-
/tsconfig.*\.json$/,
|
|
568
|
-
/\.gitignore$/,
|
|
569
|
-
/\.lock$/
|
|
570
|
-
];
|
|
571
|
-
var API_SURFACE_PATTERNS = [
|
|
572
|
-
/\/api\//,
|
|
573
|
-
/\/routes\//
|
|
574
|
-
];
|
|
575
|
-
function isFormattingOnly(file) {
|
|
576
|
-
if (file.hunks.length === 0) return false;
|
|
577
|
-
for (const hunk of file.hunks) {
|
|
578
|
-
const adds = hunk.changes.filter((c) => c.type === "add").map((c) => c.content.replace(/\s/g, ""));
|
|
579
|
-
const deletes = hunk.changes.filter((c) => c.type === "delete").map((c) => c.content.replace(/\s/g, ""));
|
|
580
|
-
if (adds.length === 0 || deletes.length === 0) return false;
|
|
581
|
-
const deleteBag = [...deletes];
|
|
582
|
-
for (const add of adds) {
|
|
583
|
-
const idx = deleteBag.indexOf(add);
|
|
584
|
-
if (idx === -1) return false;
|
|
585
|
-
deleteBag.splice(idx, 1);
|
|
586
|
-
}
|
|
587
|
-
if (deleteBag.length > 0) return false;
|
|
588
|
-
}
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
function isImportOnly(file) {
|
|
592
|
-
if (file.hunks.length === 0) return false;
|
|
593
|
-
const importPattern = /^\s*(import\s|export\s.*from\s|const\s+\w+\s*=\s*require\(|require\()/;
|
|
594
|
-
for (const hunk of file.hunks) {
|
|
595
|
-
for (const change of hunk.changes) {
|
|
596
|
-
if (change.type === "context") continue;
|
|
597
|
-
const trimmed = change.content.trim();
|
|
598
|
-
if (trimmed === "") continue;
|
|
599
|
-
if (!importPattern.test(trimmed)) return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return true;
|
|
603
|
-
}
|
|
604
|
-
function isMechanicalConfigFile(path8) {
|
|
605
|
-
return MECHANICAL_CONFIG_PATTERNS.some((re) => re.test(path8));
|
|
606
|
-
}
|
|
607
|
-
function isApiSurface(file) {
|
|
608
|
-
if (API_SURFACE_PATTERNS.some((re) => re.test(file.path))) return true;
|
|
609
|
-
const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
610
|
-
if ((basename === "index.ts" || basename === "index.js") && file.additions >= 10) {
|
|
611
|
-
return true;
|
|
612
|
-
}
|
|
613
|
-
return false;
|
|
614
|
-
}
|
|
615
|
-
function categorizeFiles(files) {
|
|
616
|
-
const critical = [];
|
|
617
|
-
const notable = [];
|
|
618
|
-
const mechanical = [];
|
|
619
|
-
const securityFlags = detectSecurityPatterns(files);
|
|
620
|
-
const complexityScores = computeComplexityScores(files);
|
|
621
|
-
const securityByFile = /* @__PURE__ */ new Map();
|
|
622
|
-
for (const flag of securityFlags) {
|
|
623
|
-
const existing = securityByFile.get(flag.file) || [];
|
|
624
|
-
existing.push(flag);
|
|
625
|
-
securityByFile.set(flag.file, existing);
|
|
626
|
-
}
|
|
627
|
-
const complexityByFile = /* @__PURE__ */ new Map();
|
|
628
|
-
for (const score of complexityScores) {
|
|
629
|
-
complexityByFile.set(score.path, score);
|
|
630
|
-
}
|
|
631
|
-
for (const file of files) {
|
|
632
|
-
const description = `${file.status} (${file.language || "unknown"}) +${file.additions} -${file.deletions}`;
|
|
633
|
-
const fileSecurityFlags = securityByFile.get(file.path);
|
|
634
|
-
const fileComplexity = complexityByFile.get(file.path);
|
|
635
|
-
const criticalReasons = [];
|
|
636
|
-
if (fileSecurityFlags && fileSecurityFlags.length > 0) {
|
|
637
|
-
const patterns = fileSecurityFlags.map((f) => f.pattern);
|
|
638
|
-
const unique = [...new Set(patterns)];
|
|
639
|
-
criticalReasons.push(`security patterns detected: ${unique.join(", ")}`);
|
|
640
|
-
}
|
|
641
|
-
if (fileComplexity && fileComplexity.score >= 8) {
|
|
642
|
-
criticalReasons.push(`high complexity score (${fileComplexity.score}/10)`);
|
|
643
|
-
}
|
|
644
|
-
if (isApiSurface(file)) {
|
|
645
|
-
criticalReasons.push("modifies public API surface");
|
|
646
|
-
}
|
|
647
|
-
if (criticalReasons.length > 0) {
|
|
648
|
-
critical.push({
|
|
649
|
-
file: file.path,
|
|
650
|
-
description,
|
|
651
|
-
reason: `Critical: ${criticalReasons.join("; ")}`
|
|
652
|
-
});
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const isPureRename = file.status === "renamed" && file.additions === 0 && file.deletions === 0;
|
|
656
|
-
if (isPureRename) {
|
|
657
|
-
mechanical.push({
|
|
658
|
-
file: file.path,
|
|
659
|
-
description,
|
|
660
|
-
reason: "Mechanical: pure rename with no content changes"
|
|
661
|
-
});
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (isFormattingOnly(file)) {
|
|
665
|
-
mechanical.push({
|
|
666
|
-
file: file.path,
|
|
667
|
-
description,
|
|
668
|
-
reason: "Mechanical: formatting/whitespace-only changes"
|
|
669
|
-
});
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
if (isMechanicalConfigFile(file.path)) {
|
|
673
|
-
mechanical.push({
|
|
674
|
-
file: file.path,
|
|
675
|
-
description,
|
|
676
|
-
reason: "Mechanical: config file change"
|
|
677
|
-
});
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (file.hunks.length > 0 && isImportOnly(file)) {
|
|
681
|
-
mechanical.push({
|
|
682
|
-
file: file.path,
|
|
683
|
-
description,
|
|
684
|
-
reason: "Mechanical: import/require-only changes"
|
|
685
|
-
});
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
notable.push({
|
|
689
|
-
file: file.path,
|
|
690
|
-
description,
|
|
691
|
-
reason: "Notable: requires review"
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
return { critical, notable, mechanical };
|
|
695
|
-
}
|
|
696
|
-
function computeFileStats(files) {
|
|
697
|
-
return files.map((f) => ({
|
|
698
|
-
path: f.path,
|
|
699
|
-
language: f.language,
|
|
700
|
-
status: f.status,
|
|
701
|
-
additions: f.additions,
|
|
702
|
-
deletions: f.deletions
|
|
703
|
-
}));
|
|
704
|
-
}
|
|
705
|
-
function detectAffectedModules(files) {
|
|
706
|
-
const dirs = /* @__PURE__ */ new Set();
|
|
707
|
-
for (const f of files) {
|
|
708
|
-
const lastSlash = f.path.lastIndexOf("/");
|
|
709
|
-
if (lastSlash > 0) {
|
|
710
|
-
dirs.add(f.path.slice(0, lastSlash));
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return [...dirs].sort();
|
|
714
|
-
}
|
|
715
|
-
var TEST_PATTERNS = [
|
|
716
|
-
/\.test\./,
|
|
717
|
-
/\.spec\./,
|
|
718
|
-
/\/__tests__\//,
|
|
719
|
-
/\/test\//
|
|
720
|
-
];
|
|
721
|
-
function detectAffectedTests(files) {
|
|
722
|
-
return files.filter((f) => TEST_PATTERNS.some((re) => re.test(f.path))).map((f) => f.path);
|
|
723
|
-
}
|
|
724
|
-
var DEPENDENCY_FIELDS = [
|
|
725
|
-
'"dependencies"',
|
|
726
|
-
'"devDependencies"',
|
|
727
|
-
'"peerDependencies"',
|
|
728
|
-
'"optionalDependencies"'
|
|
729
|
-
];
|
|
730
|
-
function detectNewDependencies(files) {
|
|
731
|
-
const deps = /* @__PURE__ */ new Set();
|
|
732
|
-
const packageFiles = files.filter(
|
|
733
|
-
(f) => f.path.endsWith("package.json") && f.hunks.length > 0
|
|
734
|
-
);
|
|
735
|
-
for (const file of packageFiles) {
|
|
736
|
-
for (const hunk of file.hunks) {
|
|
737
|
-
let inDependencyBlock = false;
|
|
738
|
-
for (const change of hunk.changes) {
|
|
739
|
-
const line = change.content;
|
|
740
|
-
if (DEPENDENCY_FIELDS.some((field) => line.includes(field))) {
|
|
741
|
-
inDependencyBlock = true;
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
if (inDependencyBlock && line.trim().startsWith("}")) {
|
|
745
|
-
inDependencyBlock = false;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (change.type === "add" && inDependencyBlock) {
|
|
749
|
-
const match = line.match(/"([^"]+)"\s*:/);
|
|
750
|
-
if (match) {
|
|
751
|
-
deps.add(match[1]);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
return [...deps].sort();
|
|
758
|
-
}
|
|
759
|
-
function generateSummary(files) {
|
|
760
|
-
const totalFiles = files.length;
|
|
761
|
-
const counts = {
|
|
762
|
-
added: 0,
|
|
763
|
-
modified: 0,
|
|
764
|
-
deleted: 0,
|
|
765
|
-
renamed: 0
|
|
766
|
-
};
|
|
767
|
-
let totalAdditions = 0;
|
|
768
|
-
let totalDeletions = 0;
|
|
769
|
-
for (const f of files) {
|
|
770
|
-
counts[f.status]++;
|
|
771
|
-
totalAdditions += f.additions;
|
|
772
|
-
totalDeletions += f.deletions;
|
|
773
|
-
}
|
|
774
|
-
const parts = [];
|
|
775
|
-
if (counts.modified > 0) parts.push(`${counts.modified} modified`);
|
|
776
|
-
if (counts.added > 0) parts.push(`${counts.added} added`);
|
|
777
|
-
if (counts.deleted > 0) parts.push(`${counts.deleted} deleted`);
|
|
778
|
-
if (counts.renamed > 0) parts.push(`${counts.renamed} renamed`);
|
|
779
|
-
const breakdown = parts.length > 0 ? `: ${parts.join(", ")}` : "";
|
|
780
|
-
return `${totalFiles} files changed${breakdown} (+${totalAdditions} -${totalDeletions})`;
|
|
781
|
-
}
|
|
782
|
-
var BRANCH_PATTERN = /\b(if|else|switch|case|catch)\b|\?\s|&&|\|\|/;
|
|
783
|
-
function computeComplexityScores(files) {
|
|
784
|
-
const results = [];
|
|
785
|
-
for (const file of files) {
|
|
786
|
-
let score = 0;
|
|
787
|
-
const factors = [];
|
|
788
|
-
const totalChanges = file.additions + file.deletions;
|
|
789
|
-
if (totalChanges > 100) {
|
|
790
|
-
score += 3;
|
|
791
|
-
factors.push(`large diff (+${file.additions} -${file.deletions})`);
|
|
792
|
-
} else if (totalChanges > 50) {
|
|
793
|
-
score += 2;
|
|
794
|
-
factors.push(`medium diff (+${file.additions} -${file.deletions})`);
|
|
795
|
-
} else if (totalChanges > 20) {
|
|
796
|
-
score += 1;
|
|
797
|
-
factors.push(`moderate diff (+${file.additions} -${file.deletions})`);
|
|
798
|
-
}
|
|
799
|
-
const hunkCount = file.hunks.length;
|
|
800
|
-
if (hunkCount > 4) {
|
|
801
|
-
score += 2;
|
|
802
|
-
factors.push(`many hunks (${hunkCount})`);
|
|
803
|
-
} else if (hunkCount > 2) {
|
|
804
|
-
score += 1;
|
|
805
|
-
factors.push(`multiple hunks (${hunkCount})`);
|
|
806
|
-
}
|
|
807
|
-
let branchCount = 0;
|
|
808
|
-
let deepNestCount = 0;
|
|
809
|
-
for (const hunk of file.hunks) {
|
|
810
|
-
for (const change of hunk.changes) {
|
|
811
|
-
if (change.type !== "add") continue;
|
|
812
|
-
const line = change.content;
|
|
813
|
-
if (BRANCH_PATTERN.test(line)) {
|
|
814
|
-
branchCount++;
|
|
815
|
-
}
|
|
816
|
-
const leadingSpaces = line.match(/^(\s*)/);
|
|
817
|
-
if (leadingSpaces) {
|
|
818
|
-
const ws = leadingSpaces[1];
|
|
819
|
-
const tabCount = (ws.match(/\t/g) || []).length;
|
|
820
|
-
const spaceCount = ws.replace(/\t/g, "").length;
|
|
821
|
-
if (tabCount >= 4 || spaceCount >= 16) {
|
|
822
|
-
deepNestCount++;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
const branchScore = Math.floor(branchCount / 5);
|
|
828
|
-
if (branchScore > 0) {
|
|
829
|
-
score += branchScore;
|
|
830
|
-
factors.push(`${branchCount} logic branches`);
|
|
831
|
-
}
|
|
832
|
-
const nestScore = Math.floor(deepNestCount / 5);
|
|
833
|
-
if (nestScore > 0) {
|
|
834
|
-
score += nestScore;
|
|
835
|
-
factors.push(`${deepNestCount} deeply nested lines`);
|
|
836
|
-
}
|
|
837
|
-
score = Math.max(1, Math.min(10, score));
|
|
838
|
-
results.push({ path: file.path, score, factors });
|
|
839
|
-
}
|
|
840
|
-
results.sort((a, b) => b.score - a.score);
|
|
841
|
-
return results;
|
|
842
|
-
}
|
|
843
|
-
var NON_CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
844
|
-
".json",
|
|
845
|
-
".md",
|
|
846
|
-
".css",
|
|
847
|
-
".scss",
|
|
848
|
-
".less",
|
|
849
|
-
".svg",
|
|
850
|
-
".png",
|
|
851
|
-
".jpg",
|
|
852
|
-
".gif",
|
|
853
|
-
".ico",
|
|
854
|
-
".yaml",
|
|
855
|
-
".yml",
|
|
856
|
-
".toml",
|
|
857
|
-
".lock",
|
|
858
|
-
".html"
|
|
859
|
-
]);
|
|
860
|
-
var CONFIG_PATTERNS = [
|
|
861
|
-
/\.config\./,
|
|
862
|
-
/\.rc\./,
|
|
863
|
-
/eslint/,
|
|
864
|
-
/prettier/,
|
|
865
|
-
/tsconfig/,
|
|
866
|
-
/tailwind/,
|
|
867
|
-
/vite\.config/,
|
|
868
|
-
/vitest\.config/
|
|
869
|
-
];
|
|
870
|
-
function isTestFile(path8) {
|
|
871
|
-
return TEST_PATTERNS.some((re) => re.test(path8));
|
|
872
|
-
}
|
|
873
|
-
function isNonCodeFile(path8) {
|
|
874
|
-
const ext = path8.slice(path8.lastIndexOf("."));
|
|
875
|
-
return NON_CODE_EXTENSIONS.has(ext);
|
|
876
|
-
}
|
|
877
|
-
function isConfigFile(path8) {
|
|
878
|
-
return CONFIG_PATTERNS.some((re) => re.test(path8));
|
|
879
|
-
}
|
|
880
|
-
function detectTestCoverageGaps(files) {
|
|
881
|
-
const filePaths = new Set(files.map((f) => f.path));
|
|
882
|
-
const results = [];
|
|
883
|
-
for (const file of files) {
|
|
884
|
-
if (file.status !== "added" && file.status !== "modified") continue;
|
|
885
|
-
if (isTestFile(file.path)) continue;
|
|
886
|
-
if (isNonCodeFile(file.path)) continue;
|
|
887
|
-
if (isConfigFile(file.path)) continue;
|
|
888
|
-
const dir = file.path.slice(0, file.path.lastIndexOf("/") + 1);
|
|
889
|
-
const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
890
|
-
const extDot = basename.lastIndexOf(".");
|
|
891
|
-
const name = extDot > 0 ? basename.slice(0, extDot) : basename;
|
|
892
|
-
const ext = extDot > 0 ? basename.slice(extDot) : "";
|
|
893
|
-
const candidates = [
|
|
894
|
-
`${dir}${name}.test${ext}`,
|
|
895
|
-
`${dir}${name}.spec${ext}`,
|
|
896
|
-
`${dir}__tests__/${name}${ext}`,
|
|
897
|
-
`${dir}__tests__/${name}.test${ext}`,
|
|
898
|
-
`${dir}__tests__/${name}.spec${ext}`
|
|
899
|
-
];
|
|
900
|
-
const matchedTest = candidates.find((c) => filePaths.has(c));
|
|
901
|
-
results.push({
|
|
902
|
-
sourceFile: file.path,
|
|
903
|
-
testFile: matchedTest ?? null
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
return results;
|
|
907
|
-
}
|
|
908
|
-
var PATTERN_MATCHERS = [
|
|
909
|
-
{ pattern: "todo", test: (l) => /\btodo\b/i.test(l) },
|
|
910
|
-
{ pattern: "fixme", test: (l) => /\bfixme\b/i.test(l) },
|
|
911
|
-
{ pattern: "hack", test: (l) => /\bhack\b/i.test(l) },
|
|
912
|
-
{
|
|
913
|
-
pattern: "console",
|
|
914
|
-
test: (l) => /\bconsole\.(log|debug|warn|error)\b/.test(l)
|
|
915
|
-
},
|
|
916
|
-
{ pattern: "debug", test: (l) => /\bdebugger\b/.test(l) },
|
|
917
|
-
{
|
|
918
|
-
pattern: "disabled_test",
|
|
919
|
-
test: (l) => /\.(skip)\(|(\bxit|xdescribe|xtest)\(/.test(l)
|
|
920
|
-
}
|
|
921
|
-
];
|
|
922
|
-
function detectPatterns(files) {
|
|
923
|
-
const results = [];
|
|
924
|
-
for (const file of files) {
|
|
925
|
-
if (file.status === "added" && file.additions > 500) {
|
|
926
|
-
results.push({
|
|
927
|
-
file: file.path,
|
|
928
|
-
line: 0,
|
|
929
|
-
pattern: "large_file",
|
|
930
|
-
content: `Large added file: ${file.additions} lines`
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
for (const hunk of file.hunks) {
|
|
934
|
-
for (const change of hunk.changes) {
|
|
935
|
-
if (change.type !== "add") continue;
|
|
936
|
-
for (const matcher of PATTERN_MATCHERS) {
|
|
937
|
-
if (matcher.test(change.content)) {
|
|
938
|
-
results.push({
|
|
939
|
-
file: file.path,
|
|
940
|
-
line: change.lineNumber,
|
|
941
|
-
pattern: matcher.pattern,
|
|
942
|
-
content: change.content.trim()
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
results.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
950
|
-
return results;
|
|
951
|
-
}
|
|
952
|
-
var SECURITY_MATCHERS = [
|
|
953
|
-
{
|
|
954
|
-
pattern: "eval",
|
|
955
|
-
severity: "critical",
|
|
956
|
-
test: (l) => /\beval\s*\(/.test(l)
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
pattern: "inner_html",
|
|
960
|
-
severity: "warning",
|
|
961
|
-
test: (l) => /\.innerHTML\b|dangerouslySetInnerHTML/.test(l)
|
|
962
|
-
},
|
|
963
|
-
{
|
|
964
|
-
pattern: "sql_injection",
|
|
965
|
-
severity: "critical",
|
|
966
|
-
test: (l) => /`[^`]*\b(SELECT|INSERT|UPDATE|DELETE)\b/i.test(l) || /\b(SELECT|INSERT|UPDATE|DELETE)\b[^`]*\$\{/i.test(l)
|
|
967
|
-
},
|
|
968
|
-
{
|
|
969
|
-
pattern: "exec",
|
|
970
|
-
severity: "critical",
|
|
971
|
-
test: (l) => /child_process/.test(l) || /\bexec\s*\(/.test(l) || /\bexecSync\s*\(/.test(l)
|
|
972
|
-
},
|
|
973
|
-
{
|
|
974
|
-
pattern: "hardcoded_secret",
|
|
975
|
-
severity: "critical",
|
|
976
|
-
test: (l) => /\b(token|secret|api_key|apikey|password|passwd|credential)\s*=\s*["']/i.test(l)
|
|
977
|
-
},
|
|
978
|
-
{
|
|
979
|
-
pattern: "insecure_url",
|
|
980
|
-
severity: "warning",
|
|
981
|
-
test: (l) => /http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(l)
|
|
982
|
-
}
|
|
983
|
-
];
|
|
984
|
-
function detectSecurityPatterns(files) {
|
|
985
|
-
const results = [];
|
|
986
|
-
for (const file of files) {
|
|
987
|
-
for (const hunk of file.hunks) {
|
|
988
|
-
for (const change of hunk.changes) {
|
|
989
|
-
if (change.type !== "add") continue;
|
|
990
|
-
for (const matcher of SECURITY_MATCHERS) {
|
|
991
|
-
if (matcher.test(change.content)) {
|
|
992
|
-
results.push({
|
|
993
|
-
file: file.path,
|
|
994
|
-
line: change.lineNumber,
|
|
995
|
-
pattern: matcher.pattern,
|
|
996
|
-
content: change.content.trim(),
|
|
997
|
-
severity: matcher.severity
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
results.sort((a, b) => {
|
|
1005
|
-
const severityOrder = { critical: 0, warning: 1 };
|
|
1006
|
-
const aSev = severityOrder[a.severity];
|
|
1007
|
-
const bSev = severityOrder[b.severity];
|
|
1008
|
-
if (aSev !== bSev) return aSev - bSev;
|
|
1009
|
-
return a.file.localeCompare(b.file) || a.line - b.line;
|
|
1010
|
-
});
|
|
1011
|
-
return results;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// packages/analysis/src/index.ts
|
|
1015
|
-
function analyze(diffSet) {
|
|
1016
|
-
const { files } = diffSet;
|
|
1017
|
-
const triage = categorizeFiles(files);
|
|
1018
|
-
const fileStats = computeFileStats(files);
|
|
1019
|
-
const affectedModules = detectAffectedModules(files);
|
|
1020
|
-
const affectedTests = detectAffectedTests(files);
|
|
1021
|
-
const newDependencies = detectNewDependencies(files);
|
|
1022
|
-
const summary = generateSummary(files);
|
|
1023
|
-
const complexity = computeComplexityScores(files);
|
|
1024
|
-
const testCoverage = detectTestCoverageGaps(files);
|
|
1025
|
-
const codePatterns = detectPatterns(files);
|
|
1026
|
-
const securityPatterns = detectSecurityPatterns(files);
|
|
1027
|
-
const patterns = [...securityPatterns, ...codePatterns];
|
|
1028
|
-
return {
|
|
1029
|
-
summary,
|
|
1030
|
-
triage,
|
|
1031
|
-
impact: {
|
|
1032
|
-
affectedModules,
|
|
1033
|
-
affectedTests,
|
|
1034
|
-
publicApiChanges: false,
|
|
1035
|
-
breakingChanges: [],
|
|
1036
|
-
newDependencies
|
|
1037
|
-
},
|
|
1038
|
-
verification: {
|
|
1039
|
-
testsPass: null,
|
|
1040
|
-
typeCheck: null,
|
|
1041
|
-
lintClean: null
|
|
1042
|
-
},
|
|
1043
|
-
fileStats,
|
|
1044
|
-
complexity,
|
|
1045
|
-
testCoverage,
|
|
1046
|
-
patterns
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// packages/core/src/watch-file.ts
|
|
1051
110
|
import fs from "fs";
|
|
1052
|
-
import
|
|
1053
|
-
import { execSync as execSync2 } from "child_process";
|
|
1054
|
-
function findGitRoot(cwd) {
|
|
1055
|
-
const root = execSync2("git rev-parse --show-toplevel", {
|
|
1056
|
-
cwd: cwd ?? process.cwd(),
|
|
1057
|
-
encoding: "utf-8"
|
|
1058
|
-
}).trim();
|
|
1059
|
-
return root;
|
|
1060
|
-
}
|
|
1061
|
-
function watchFilePath(cwd) {
|
|
1062
|
-
const gitRoot = findGitRoot(cwd);
|
|
1063
|
-
return path3.join(gitRoot, ".diffprism", "watch.json");
|
|
1064
|
-
}
|
|
1065
|
-
function isPidAlive(pid) {
|
|
1066
|
-
try {
|
|
1067
|
-
process.kill(pid, 0);
|
|
1068
|
-
return true;
|
|
1069
|
-
} catch {
|
|
1070
|
-
return false;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
function writeWatchFile(cwd, info) {
|
|
1074
|
-
const filePath = watchFilePath(cwd);
|
|
1075
|
-
const dir = path3.dirname(filePath);
|
|
1076
|
-
if (!fs.existsSync(dir)) {
|
|
1077
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1078
|
-
}
|
|
1079
|
-
fs.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
|
|
1080
|
-
}
|
|
1081
|
-
function readWatchFile(cwd) {
|
|
1082
|
-
const filePath = watchFilePath(cwd);
|
|
1083
|
-
if (!fs.existsSync(filePath)) {
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1088
|
-
const info = JSON.parse(raw);
|
|
1089
|
-
if (!isPidAlive(info.pid)) {
|
|
1090
|
-
fs.unlinkSync(filePath);
|
|
1091
|
-
return null;
|
|
1092
|
-
}
|
|
1093
|
-
return info;
|
|
1094
|
-
} catch {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
function removeWatchFile(cwd) {
|
|
1099
|
-
try {
|
|
1100
|
-
const filePath = watchFilePath(cwd);
|
|
1101
|
-
if (fs.existsSync(filePath)) {
|
|
1102
|
-
fs.unlinkSync(filePath);
|
|
1103
|
-
}
|
|
1104
|
-
} catch {
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
function reviewResultPath(cwd) {
|
|
1108
|
-
const gitRoot = findGitRoot(cwd);
|
|
1109
|
-
return path3.join(gitRoot, ".diffprism", "last-review.json");
|
|
1110
|
-
}
|
|
1111
|
-
function writeReviewResult(cwd, result) {
|
|
1112
|
-
const filePath = reviewResultPath(cwd);
|
|
1113
|
-
const dir = path3.dirname(filePath);
|
|
1114
|
-
if (!fs.existsSync(dir)) {
|
|
1115
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1116
|
-
}
|
|
1117
|
-
const data = {
|
|
1118
|
-
result,
|
|
1119
|
-
timestamp: Date.now(),
|
|
1120
|
-
consumed: false
|
|
1121
|
-
};
|
|
1122
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1123
|
-
}
|
|
1124
|
-
function readReviewResult(cwd) {
|
|
1125
|
-
try {
|
|
1126
|
-
const filePath = reviewResultPath(cwd);
|
|
1127
|
-
if (!fs.existsSync(filePath)) {
|
|
1128
|
-
return null;
|
|
1129
|
-
}
|
|
1130
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1131
|
-
const data = JSON.parse(raw);
|
|
1132
|
-
if (data.consumed) {
|
|
1133
|
-
return null;
|
|
1134
|
-
}
|
|
1135
|
-
return data;
|
|
1136
|
-
} catch {
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function consumeReviewResult(cwd) {
|
|
1141
|
-
try {
|
|
1142
|
-
const filePath = reviewResultPath(cwd);
|
|
1143
|
-
if (!fs.existsSync(filePath)) {
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1147
|
-
const data = JSON.parse(raw);
|
|
1148
|
-
data.consumed = true;
|
|
1149
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1150
|
-
} catch {
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// packages/core/src/pipeline.ts
|
|
1155
|
-
import getPort from "get-port";
|
|
1156
|
-
import open from "open";
|
|
1157
|
-
|
|
1158
|
-
// packages/core/src/review-history.ts
|
|
1159
|
-
import fs2 from "fs";
|
|
1160
|
-
import path4 from "path";
|
|
1161
|
-
import { randomUUID } from "crypto";
|
|
1162
|
-
function generateEntryId() {
|
|
1163
|
-
return randomUUID();
|
|
1164
|
-
}
|
|
1165
|
-
function getHistoryPath(projectDir) {
|
|
1166
|
-
return path4.join(projectDir, ".diffprism", "history", "reviews.json");
|
|
1167
|
-
}
|
|
1168
|
-
function readHistory(projectDir) {
|
|
1169
|
-
const filePath = getHistoryPath(projectDir);
|
|
1170
|
-
if (!fs2.existsSync(filePath)) {
|
|
1171
|
-
return { version: 1, entries: [] };
|
|
1172
|
-
}
|
|
1173
|
-
try {
|
|
1174
|
-
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1175
|
-
const parsed = JSON.parse(raw);
|
|
1176
|
-
return parsed;
|
|
1177
|
-
} catch {
|
|
1178
|
-
return { version: 1, entries: [] };
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
function appendHistory(projectDir, entry) {
|
|
1182
|
-
const filePath = getHistoryPath(projectDir);
|
|
1183
|
-
const dir = path4.dirname(filePath);
|
|
1184
|
-
if (!fs2.existsSync(dir)) {
|
|
1185
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
1186
|
-
}
|
|
1187
|
-
const history = readHistory(projectDir);
|
|
1188
|
-
history.entries.push(entry);
|
|
1189
|
-
history.entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
1190
|
-
fs2.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
|
|
1191
|
-
}
|
|
1192
|
-
function getRecentHistory(projectDir, limit = 50) {
|
|
1193
|
-
const history = readHistory(projectDir);
|
|
1194
|
-
return history.entries.slice(-limit);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// packages/core/src/watch-bridge.ts
|
|
1198
|
-
import http from "http";
|
|
1199
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
1200
|
-
function createWatchBridge(port, callbacks) {
|
|
1201
|
-
let client = null;
|
|
1202
|
-
let initPayload = null;
|
|
1203
|
-
let pendingInit = null;
|
|
1204
|
-
let closeTimer = null;
|
|
1205
|
-
let submitCallback = null;
|
|
1206
|
-
let resultReject = null;
|
|
1207
|
-
const httpServer = http.createServer(async (req, res) => {
|
|
1208
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1209
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1210
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1211
|
-
if (req.method === "OPTIONS") {
|
|
1212
|
-
res.writeHead(204);
|
|
1213
|
-
res.end();
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
if (req.method === "GET" && req.url === "/api/status") {
|
|
1217
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1218
|
-
res.end(JSON.stringify({ running: true, pid: process.pid }));
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (req.method === "POST" && req.url === "/api/context") {
|
|
1222
|
-
let body = "";
|
|
1223
|
-
req.on("data", (chunk) => {
|
|
1224
|
-
body += chunk.toString();
|
|
1225
|
-
});
|
|
1226
|
-
req.on("end", () => {
|
|
1227
|
-
try {
|
|
1228
|
-
const payload = JSON.parse(body);
|
|
1229
|
-
callbacks.onContextUpdate(payload);
|
|
1230
|
-
sendToClient({ type: "context:update", payload });
|
|
1231
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1232
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1233
|
-
} catch {
|
|
1234
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1235
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1236
|
-
}
|
|
1237
|
-
});
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
if (req.method === "POST" && req.url === "/api/refresh") {
|
|
1241
|
-
callbacks.onRefreshRequest();
|
|
1242
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1243
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
const pathname = (req.url ?? "").split("?")[0];
|
|
1247
|
-
if (req.method === "GET" && (pathname === "/api/refs" || /^\/api\/reviews\/[^/]+\/refs$/.test(pathname))) {
|
|
1248
|
-
if (callbacks.onRefsRequest) {
|
|
1249
|
-
const refsPayload = await callbacks.onRefsRequest();
|
|
1250
|
-
if (refsPayload) {
|
|
1251
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1252
|
-
res.end(JSON.stringify(refsPayload));
|
|
1253
|
-
} else {
|
|
1254
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1255
|
-
res.end(JSON.stringify({ error: "Failed to list git refs" }));
|
|
1256
|
-
}
|
|
1257
|
-
} else {
|
|
1258
|
-
res.writeHead(404);
|
|
1259
|
-
res.end("Not found");
|
|
1260
|
-
}
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
if (req.method === "POST" && (pathname === "/api/compare" || /^\/api\/reviews\/[^/]+\/compare$/.test(pathname))) {
|
|
1264
|
-
if (callbacks.onCompareRequest) {
|
|
1265
|
-
let body = "";
|
|
1266
|
-
req.on("data", (chunk) => {
|
|
1267
|
-
body += chunk.toString();
|
|
1268
|
-
});
|
|
1269
|
-
req.on("end", async () => {
|
|
1270
|
-
try {
|
|
1271
|
-
const { ref } = JSON.parse(body);
|
|
1272
|
-
if (!ref) {
|
|
1273
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1274
|
-
res.end(JSON.stringify({ error: "Missing ref" }));
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
const success = await callbacks.onCompareRequest(ref);
|
|
1278
|
-
if (success) {
|
|
1279
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1280
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1281
|
-
} else {
|
|
1282
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1283
|
-
res.end(JSON.stringify({ error: "Failed to compute diff" }));
|
|
1284
|
-
}
|
|
1285
|
-
} catch {
|
|
1286
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1287
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
} else {
|
|
1291
|
-
res.writeHead(404);
|
|
1292
|
-
res.end("Not found");
|
|
1293
|
-
}
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
res.writeHead(404);
|
|
1297
|
-
res.end("Not found");
|
|
1298
|
-
});
|
|
1299
|
-
const wss2 = new WebSocketServer({ server: httpServer });
|
|
1300
|
-
function sendToClient(msg) {
|
|
1301
|
-
if (client && client.readyState === WebSocket.OPEN) {
|
|
1302
|
-
client.send(JSON.stringify(msg));
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
wss2.on("connection", (ws) => {
|
|
1306
|
-
if (closeTimer) {
|
|
1307
|
-
clearTimeout(closeTimer);
|
|
1308
|
-
closeTimer = null;
|
|
1309
|
-
}
|
|
1310
|
-
client = ws;
|
|
1311
|
-
const payload = pendingInit ?? initPayload;
|
|
1312
|
-
if (payload) {
|
|
1313
|
-
sendToClient({ type: "review:init", payload });
|
|
1314
|
-
pendingInit = null;
|
|
1315
|
-
}
|
|
1316
|
-
ws.on("message", (data) => {
|
|
1317
|
-
try {
|
|
1318
|
-
const msg = JSON.parse(data.toString());
|
|
1319
|
-
if (msg.type === "review:submit" && submitCallback) {
|
|
1320
|
-
submitCallback(msg.payload);
|
|
1321
|
-
} else if (msg.type === "diff:change_ref" && callbacks.onDiffRefChange) {
|
|
1322
|
-
callbacks.onDiffRefChange(msg.payload.diffRef);
|
|
1323
|
-
}
|
|
1324
|
-
} catch {
|
|
1325
|
-
}
|
|
1326
|
-
});
|
|
1327
|
-
ws.on("close", () => {
|
|
1328
|
-
client = null;
|
|
1329
|
-
if (resultReject) {
|
|
1330
|
-
closeTimer = setTimeout(() => {
|
|
1331
|
-
closeTimer = null;
|
|
1332
|
-
if (resultReject) {
|
|
1333
|
-
resultReject(new Error("Browser closed before review was submitted"));
|
|
1334
|
-
resultReject = null;
|
|
1335
|
-
submitCallback = null;
|
|
1336
|
-
}
|
|
1337
|
-
}, 2e3);
|
|
1338
|
-
} else {
|
|
1339
|
-
closeTimer = setTimeout(() => {
|
|
1340
|
-
closeTimer = null;
|
|
1341
|
-
}, 2e3);
|
|
1342
|
-
}
|
|
1343
|
-
});
|
|
1344
|
-
});
|
|
1345
|
-
return new Promise((resolve, reject) => {
|
|
1346
|
-
httpServer.on("error", reject);
|
|
1347
|
-
httpServer.listen(port, () => {
|
|
1348
|
-
resolve({
|
|
1349
|
-
port,
|
|
1350
|
-
sendInit(payload) {
|
|
1351
|
-
initPayload = payload;
|
|
1352
|
-
if (client && client.readyState === WebSocket.OPEN) {
|
|
1353
|
-
sendToClient({ type: "review:init", payload });
|
|
1354
|
-
} else {
|
|
1355
|
-
pendingInit = payload;
|
|
1356
|
-
}
|
|
1357
|
-
},
|
|
1358
|
-
storeInitPayload(payload) {
|
|
1359
|
-
initPayload = payload;
|
|
1360
|
-
},
|
|
1361
|
-
sendDiffUpdate(payload) {
|
|
1362
|
-
sendToClient({ type: "diff:update", payload });
|
|
1363
|
-
},
|
|
1364
|
-
sendContextUpdate(payload) {
|
|
1365
|
-
sendToClient({ type: "context:update", payload });
|
|
1366
|
-
},
|
|
1367
|
-
sendDiffError(payload) {
|
|
1368
|
-
sendToClient({ type: "diff:error", payload });
|
|
1369
|
-
},
|
|
1370
|
-
onSubmit(callback) {
|
|
1371
|
-
submitCallback = callback;
|
|
1372
|
-
},
|
|
1373
|
-
waitForResult() {
|
|
1374
|
-
return new Promise((resolve2, reject2) => {
|
|
1375
|
-
submitCallback = resolve2;
|
|
1376
|
-
resultReject = reject2;
|
|
1377
|
-
});
|
|
1378
|
-
},
|
|
1379
|
-
triggerRefresh() {
|
|
1380
|
-
callbacks.onRefreshRequest();
|
|
1381
|
-
},
|
|
1382
|
-
async close() {
|
|
1383
|
-
if (closeTimer) {
|
|
1384
|
-
clearTimeout(closeTimer);
|
|
1385
|
-
}
|
|
1386
|
-
for (const ws of wss2.clients) {
|
|
1387
|
-
ws.close();
|
|
1388
|
-
}
|
|
1389
|
-
wss2.close();
|
|
1390
|
-
await new Promise((resolve2) => {
|
|
1391
|
-
httpServer.close(() => resolve2());
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
});
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// packages/core/src/diff-utils.ts
|
|
1400
|
-
import { createHash } from "crypto";
|
|
1401
|
-
function hashDiff(rawDiff) {
|
|
1402
|
-
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1403
|
-
}
|
|
1404
|
-
function fileKey(file) {
|
|
1405
|
-
return file.stage ? `${file.stage}:${file.path}` : file.path;
|
|
1406
|
-
}
|
|
1407
|
-
function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
1408
|
-
if (!oldDiffSet) {
|
|
1409
|
-
return newDiffSet.files.map(fileKey);
|
|
1410
|
-
}
|
|
1411
|
-
const oldFiles = new Map(
|
|
1412
|
-
oldDiffSet.files.map((f) => [fileKey(f), f])
|
|
1413
|
-
);
|
|
1414
|
-
const changed = [];
|
|
1415
|
-
for (const newFile of newDiffSet.files) {
|
|
1416
|
-
const key = fileKey(newFile);
|
|
1417
|
-
const oldFile = oldFiles.get(key);
|
|
1418
|
-
if (!oldFile) {
|
|
1419
|
-
changed.push(key);
|
|
1420
|
-
} else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
|
|
1421
|
-
changed.push(key);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
for (const oldFile of oldDiffSet.files) {
|
|
1425
|
-
if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
|
|
1426
|
-
changed.push(fileKey(oldFile));
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
return changed;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
// packages/core/src/diff-poller.ts
|
|
1433
|
-
function createDiffPoller(options) {
|
|
1434
|
-
let { diffRef } = options;
|
|
1435
|
-
const { cwd, pollInterval, onDiffChanged, onError, silent } = options;
|
|
1436
|
-
let lastDiffHash = null;
|
|
1437
|
-
let lastDiffSet = null;
|
|
1438
|
-
let refreshRequested = false;
|
|
1439
|
-
let interval = null;
|
|
1440
|
-
let running = false;
|
|
1441
|
-
function poll() {
|
|
1442
|
-
if (!running) return;
|
|
1443
|
-
try {
|
|
1444
|
-
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
|
|
1445
|
-
const newHash = hashDiff(newRawDiff);
|
|
1446
|
-
if (newHash !== lastDiffHash || refreshRequested) {
|
|
1447
|
-
refreshRequested = false;
|
|
1448
|
-
const newBriefing = analyze(newDiffSet);
|
|
1449
|
-
const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
|
|
1450
|
-
lastDiffHash = newHash;
|
|
1451
|
-
lastDiffSet = newDiffSet;
|
|
1452
|
-
const updatePayload = {
|
|
1453
|
-
diffSet: newDiffSet,
|
|
1454
|
-
rawDiff: newRawDiff,
|
|
1455
|
-
briefing: newBriefing,
|
|
1456
|
-
changedFiles,
|
|
1457
|
-
timestamp: Date.now()
|
|
1458
|
-
};
|
|
1459
|
-
onDiffChanged(updatePayload);
|
|
1460
|
-
}
|
|
1461
|
-
} catch (err) {
|
|
1462
|
-
if (onError && err instanceof Error) {
|
|
1463
|
-
onError(err);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
start() {
|
|
1469
|
-
if (running) return;
|
|
1470
|
-
running = true;
|
|
1471
|
-
try {
|
|
1472
|
-
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1473
|
-
lastDiffHash = hashDiff(initialRawDiff);
|
|
1474
|
-
lastDiffSet = initialDiffSet;
|
|
1475
|
-
} catch {
|
|
1476
|
-
}
|
|
1477
|
-
interval = setInterval(poll, pollInterval);
|
|
1478
|
-
},
|
|
1479
|
-
stop() {
|
|
1480
|
-
running = false;
|
|
1481
|
-
if (interval) {
|
|
1482
|
-
clearInterval(interval);
|
|
1483
|
-
interval = null;
|
|
1484
|
-
}
|
|
1485
|
-
},
|
|
1486
|
-
setDiffRef(newRef) {
|
|
1487
|
-
diffRef = newRef;
|
|
1488
|
-
lastDiffHash = null;
|
|
1489
|
-
lastDiffSet = null;
|
|
1490
|
-
},
|
|
1491
|
-
refresh() {
|
|
1492
|
-
refreshRequested = true;
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// packages/core/src/review-manager.ts
|
|
1498
|
-
var sessions = /* @__PURE__ */ new Map();
|
|
1499
|
-
var idCounter = 0;
|
|
1500
|
-
function createSession(options) {
|
|
1501
|
-
const id = `review-${Date.now()}-${++idCounter}`;
|
|
1502
|
-
const session = {
|
|
1503
|
-
id,
|
|
1504
|
-
options,
|
|
1505
|
-
status: "pending",
|
|
1506
|
-
createdAt: Date.now()
|
|
1507
|
-
};
|
|
1508
|
-
sessions.set(id, session);
|
|
1509
|
-
return session;
|
|
1510
|
-
}
|
|
1511
|
-
function updateSession(id, update) {
|
|
1512
|
-
const session = sessions.get(id);
|
|
1513
|
-
if (session) {
|
|
1514
|
-
Object.assign(session, update);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// packages/core/src/ui-server.ts
|
|
1519
|
-
import http2 from "http";
|
|
1520
|
-
import fs3 from "fs";
|
|
1521
|
-
import path5 from "path";
|
|
1522
|
-
import { fileURLToPath } from "url";
|
|
1523
|
-
var MIME_TYPES = {
|
|
1524
|
-
".html": "text/html",
|
|
1525
|
-
".js": "application/javascript",
|
|
1526
|
-
".css": "text/css",
|
|
1527
|
-
".json": "application/json",
|
|
1528
|
-
".svg": "image/svg+xml",
|
|
1529
|
-
".png": "image/png",
|
|
1530
|
-
".ico": "image/x-icon",
|
|
1531
|
-
".woff": "font/woff",
|
|
1532
|
-
".woff2": "font/woff2"
|
|
1533
|
-
};
|
|
1534
|
-
function resolveUiDist() {
|
|
1535
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
1536
|
-
const thisDir = path5.dirname(thisFile);
|
|
1537
|
-
const publishedUiDist = path5.resolve(thisDir, "..", "ui-dist");
|
|
1538
|
-
if (fs3.existsSync(path5.join(publishedUiDist, "index.html"))) {
|
|
1539
|
-
return publishedUiDist;
|
|
1540
|
-
}
|
|
1541
|
-
const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
|
|
1542
|
-
const devUiDist = path5.join(workspaceRoot, "packages", "ui", "dist");
|
|
1543
|
-
if (fs3.existsSync(path5.join(devUiDist, "index.html"))) {
|
|
1544
|
-
return devUiDist;
|
|
1545
|
-
}
|
|
1546
|
-
throw new Error(
|
|
1547
|
-
"Could not find built UI. Run 'pnpm -F @diffprism/ui build' first."
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
function resolveUiRoot() {
|
|
1551
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
1552
|
-
const thisDir = path5.dirname(thisFile);
|
|
1553
|
-
const workspaceRoot = path5.resolve(thisDir, "..", "..", "..");
|
|
1554
|
-
const uiRoot = path5.join(workspaceRoot, "packages", "ui");
|
|
1555
|
-
if (fs3.existsSync(path5.join(uiRoot, "index.html"))) {
|
|
1556
|
-
return uiRoot;
|
|
1557
|
-
}
|
|
1558
|
-
throw new Error(
|
|
1559
|
-
"Could not find UI source directory. Dev mode requires the diffprism workspace."
|
|
1560
|
-
);
|
|
1561
|
-
}
|
|
1562
|
-
async function startViteDevServer(uiRoot, port, silent) {
|
|
1563
|
-
const { createServer } = await import("vite");
|
|
1564
|
-
const vite = await createServer({
|
|
1565
|
-
root: uiRoot,
|
|
1566
|
-
server: { port, strictPort: true, open: false },
|
|
1567
|
-
logLevel: silent ? "silent" : "warn"
|
|
1568
|
-
});
|
|
1569
|
-
await vite.listen();
|
|
1570
|
-
return vite;
|
|
1571
|
-
}
|
|
1572
|
-
function createStaticServer(distPath, port) {
|
|
1573
|
-
const server = http2.createServer((req, res) => {
|
|
1574
|
-
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
1575
|
-
let filePath = path5.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
1576
|
-
if (!fs3.existsSync(filePath)) {
|
|
1577
|
-
filePath = path5.join(distPath, "index.html");
|
|
1578
|
-
}
|
|
1579
|
-
const ext = path5.extname(filePath);
|
|
1580
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1581
|
-
try {
|
|
1582
|
-
const content = fs3.readFileSync(filePath);
|
|
1583
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
1584
|
-
res.end(content);
|
|
1585
|
-
} catch {
|
|
1586
|
-
res.writeHead(404);
|
|
1587
|
-
res.end("Not found");
|
|
1588
|
-
}
|
|
1589
|
-
});
|
|
1590
|
-
return new Promise((resolve, reject) => {
|
|
1591
|
-
server.on("error", reject);
|
|
1592
|
-
server.listen(port, () => resolve(server));
|
|
1593
|
-
});
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// packages/core/src/pipeline.ts
|
|
1597
|
-
async function startReview(options) {
|
|
1598
|
-
const { diffRef, title, description, reasoning, cwd, silent, dev, injectedPayload } = options;
|
|
1599
|
-
let diffSet;
|
|
1600
|
-
let rawDiff;
|
|
1601
|
-
let briefing;
|
|
1602
|
-
let metadata;
|
|
1603
|
-
if (injectedPayload) {
|
|
1604
|
-
diffSet = injectedPayload.diffSet;
|
|
1605
|
-
rawDiff = injectedPayload.rawDiff;
|
|
1606
|
-
briefing = injectedPayload.briefing;
|
|
1607
|
-
metadata = { ...injectedPayload.metadata };
|
|
1608
|
-
} else {
|
|
1609
|
-
const result = getDiff(diffRef, { cwd });
|
|
1610
|
-
diffSet = result.diffSet;
|
|
1611
|
-
rawDiff = result.rawDiff;
|
|
1612
|
-
const currentBranch = getCurrentBranch({ cwd });
|
|
1613
|
-
if (diffSet.files.length === 0) {
|
|
1614
|
-
if (!silent) {
|
|
1615
|
-
console.log("No changes to review.");
|
|
1616
|
-
}
|
|
1617
|
-
return {
|
|
1618
|
-
decision: "approved",
|
|
1619
|
-
comments: [],
|
|
1620
|
-
summary: "No changes to review."
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
briefing = analyze(diffSet);
|
|
1624
|
-
const worktreeInfo = detectWorktree({ cwd });
|
|
1625
|
-
metadata = {
|
|
1626
|
-
title,
|
|
1627
|
-
description,
|
|
1628
|
-
reasoning,
|
|
1629
|
-
currentBranch,
|
|
1630
|
-
worktree: worktreeInfo.isWorktree ? {
|
|
1631
|
-
isWorktree: true,
|
|
1632
|
-
worktreePath: worktreeInfo.worktreePath,
|
|
1633
|
-
mainWorktreePath: worktreeInfo.mainWorktreePath
|
|
1634
|
-
} : void 0
|
|
1635
|
-
};
|
|
1636
|
-
}
|
|
1637
|
-
if (diffSet.files.length === 0) {
|
|
1638
|
-
if (!silent) {
|
|
1639
|
-
console.log("No changes to review.");
|
|
1640
|
-
}
|
|
1641
|
-
return {
|
|
1642
|
-
decision: "approved",
|
|
1643
|
-
comments: [],
|
|
1644
|
-
summary: "No changes to review."
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
const session = createSession(options);
|
|
1648
|
-
updateSession(session.id, { status: "in_progress" });
|
|
1649
|
-
const isInjected = !!injectedPayload;
|
|
1650
|
-
let poller = null;
|
|
1651
|
-
const [bridgePort, httpPort] = await Promise.all([
|
|
1652
|
-
getPort(),
|
|
1653
|
-
getPort()
|
|
1654
|
-
]);
|
|
1655
|
-
function handleDiffRefChange(newRef) {
|
|
1656
|
-
if (isInjected) return;
|
|
1657
|
-
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1658
|
-
const newBriefing = analyze(newDiffSet);
|
|
1659
|
-
bridge.sendDiffUpdate({
|
|
1660
|
-
diffSet: newDiffSet,
|
|
1661
|
-
rawDiff: newRawDiff,
|
|
1662
|
-
briefing: newBriefing,
|
|
1663
|
-
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1664
|
-
timestamp: Date.now()
|
|
1665
|
-
});
|
|
1666
|
-
bridge.storeInitPayload({
|
|
1667
|
-
reviewId: session.id,
|
|
1668
|
-
diffSet: newDiffSet,
|
|
1669
|
-
rawDiff: newRawDiff,
|
|
1670
|
-
briefing: newBriefing,
|
|
1671
|
-
metadata,
|
|
1672
|
-
watchMode: true
|
|
1673
|
-
});
|
|
1674
|
-
poller?.setDiffRef(newRef);
|
|
1675
|
-
}
|
|
1676
|
-
const bridge = await createWatchBridge(bridgePort, {
|
|
1677
|
-
onRefreshRequest: () => {
|
|
1678
|
-
poller?.refresh();
|
|
1679
|
-
},
|
|
1680
|
-
onContextUpdate: (payload) => {
|
|
1681
|
-
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1682
|
-
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1683
|
-
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1684
|
-
},
|
|
1685
|
-
onDiffRefChange: (newRef) => {
|
|
1686
|
-
if (isInjected) return;
|
|
1687
|
-
try {
|
|
1688
|
-
handleDiffRefChange(newRef);
|
|
1689
|
-
} catch (err) {
|
|
1690
|
-
bridge.sendDiffError({
|
|
1691
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
},
|
|
1695
|
-
onRefsRequest: async () => {
|
|
1696
|
-
if (isInjected) return null;
|
|
1697
|
-
try {
|
|
1698
|
-
const resolvedCwd = cwd ?? process.cwd();
|
|
1699
|
-
const branches = listBranches({ cwd: resolvedCwd });
|
|
1700
|
-
const commits = listCommits({ cwd: resolvedCwd });
|
|
1701
|
-
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1702
|
-
return { branches, commits, currentBranch: branch };
|
|
1703
|
-
} catch {
|
|
1704
|
-
return null;
|
|
1705
|
-
}
|
|
1706
|
-
},
|
|
1707
|
-
onCompareRequest: async (ref) => {
|
|
1708
|
-
if (isInjected) return false;
|
|
1709
|
-
try {
|
|
1710
|
-
handleDiffRefChange(ref);
|
|
1711
|
-
return true;
|
|
1712
|
-
} catch {
|
|
1713
|
-
return false;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
});
|
|
1717
|
-
let httpServer = null;
|
|
1718
|
-
let viteServer = null;
|
|
1719
|
-
try {
|
|
1720
|
-
if (dev) {
|
|
1721
|
-
const uiRoot = resolveUiRoot();
|
|
1722
|
-
viteServer = await startViteDevServer(uiRoot, httpPort, !!silent);
|
|
1723
|
-
} else {
|
|
1724
|
-
const uiDist = resolveUiDist();
|
|
1725
|
-
httpServer = await createStaticServer(uiDist, httpPort);
|
|
1726
|
-
}
|
|
1727
|
-
writeWatchFile(cwd, {
|
|
1728
|
-
wsPort: bridgePort,
|
|
1729
|
-
uiPort: httpPort,
|
|
1730
|
-
pid: process.pid,
|
|
1731
|
-
cwd: cwd ?? process.cwd(),
|
|
1732
|
-
diffRef,
|
|
1733
|
-
startedAt: Date.now()
|
|
1734
|
-
});
|
|
1735
|
-
const url = `http://localhost:${httpPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${session.id}`;
|
|
1736
|
-
if (!silent) {
|
|
1737
|
-
console.log(`
|
|
1738
|
-
DiffPrism Review: ${title ?? briefing.summary}`);
|
|
1739
|
-
console.log(`Opening browser at ${url}
|
|
1740
|
-
`);
|
|
1741
|
-
}
|
|
1742
|
-
await open(url);
|
|
1743
|
-
const initPayload = {
|
|
1744
|
-
reviewId: session.id,
|
|
1745
|
-
diffSet,
|
|
1746
|
-
rawDiff,
|
|
1747
|
-
briefing,
|
|
1748
|
-
metadata,
|
|
1749
|
-
watchMode: !isInjected
|
|
1750
|
-
};
|
|
1751
|
-
bridge.sendInit(initPayload);
|
|
1752
|
-
if (!isInjected) {
|
|
1753
|
-
poller = createDiffPoller({
|
|
1754
|
-
diffRef,
|
|
1755
|
-
cwd: cwd ?? process.cwd(),
|
|
1756
|
-
pollInterval: 1e3,
|
|
1757
|
-
onDiffChanged: (updatePayload) => {
|
|
1758
|
-
bridge.storeInitPayload({
|
|
1759
|
-
reviewId: session.id,
|
|
1760
|
-
diffSet: updatePayload.diffSet,
|
|
1761
|
-
rawDiff: updatePayload.rawDiff,
|
|
1762
|
-
briefing: updatePayload.briefing,
|
|
1763
|
-
metadata,
|
|
1764
|
-
watchMode: true
|
|
1765
|
-
});
|
|
1766
|
-
bridge.sendDiffUpdate(updatePayload);
|
|
1767
|
-
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
1768
|
-
console.log(
|
|
1769
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
1770
|
-
);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
});
|
|
1774
|
-
poller.start();
|
|
1775
|
-
}
|
|
1776
|
-
const result = await bridge.waitForResult();
|
|
1777
|
-
poller?.stop();
|
|
1778
|
-
updateSession(session.id, { status: "completed", result });
|
|
1779
|
-
try {
|
|
1780
|
-
const projectDir = cwd ?? process.cwd();
|
|
1781
|
-
const entry = {
|
|
1782
|
-
id: generateEntryId(),
|
|
1783
|
-
timestamp: Date.now(),
|
|
1784
|
-
diffRef,
|
|
1785
|
-
decision: result.decision,
|
|
1786
|
-
filesReviewed: diffSet.files.length,
|
|
1787
|
-
additions: diffSet.files.reduce((sum, f) => sum + f.additions, 0),
|
|
1788
|
-
deletions: diffSet.files.reduce((sum, f) => sum + f.deletions, 0),
|
|
1789
|
-
commentCount: result.comments.length,
|
|
1790
|
-
branch: metadata.currentBranch,
|
|
1791
|
-
title: metadata.title,
|
|
1792
|
-
summary: result.summary ?? briefing.summary
|
|
1793
|
-
};
|
|
1794
|
-
appendHistory(projectDir, entry);
|
|
1795
|
-
} catch {
|
|
1796
|
-
}
|
|
1797
|
-
return result;
|
|
1798
|
-
} finally {
|
|
1799
|
-
poller?.stop();
|
|
1800
|
-
await bridge.close();
|
|
1801
|
-
removeWatchFile(cwd);
|
|
1802
|
-
if (viteServer) {
|
|
1803
|
-
await viteServer.close();
|
|
1804
|
-
}
|
|
1805
|
-
if (httpServer) {
|
|
1806
|
-
httpServer.close();
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// packages/core/src/server-file.ts
|
|
1812
|
-
import fs4 from "fs";
|
|
1813
|
-
import path6 from "path";
|
|
111
|
+
import path from "path";
|
|
1814
112
|
import os from "os";
|
|
1815
|
-
function serverDir() {
|
|
1816
|
-
return path6.join(os.homedir(), ".diffprism");
|
|
1817
|
-
}
|
|
1818
|
-
function serverFilePath() {
|
|
1819
|
-
return path6.join(serverDir(), "server.json");
|
|
1820
|
-
}
|
|
1821
|
-
function isPidAlive2(pid) {
|
|
1822
|
-
try {
|
|
1823
|
-
process.kill(pid, 0);
|
|
1824
|
-
return true;
|
|
1825
|
-
} catch {
|
|
1826
|
-
return false;
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
function writeServerFile(info) {
|
|
1830
|
-
const dir = serverDir();
|
|
1831
|
-
if (!fs4.existsSync(dir)) {
|
|
1832
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
1833
|
-
}
|
|
1834
|
-
fs4.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1835
|
-
}
|
|
1836
|
-
function readServerFile() {
|
|
1837
|
-
const filePath = serverFilePath();
|
|
1838
|
-
if (!fs4.existsSync(filePath)) {
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
try {
|
|
1842
|
-
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1843
|
-
const info = JSON.parse(raw);
|
|
1844
|
-
if (!isPidAlive2(info.pid)) {
|
|
1845
|
-
fs4.unlinkSync(filePath);
|
|
1846
|
-
return null;
|
|
1847
|
-
}
|
|
1848
|
-
return info;
|
|
1849
|
-
} catch {
|
|
1850
|
-
return null;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
function removeServerFile() {
|
|
1854
|
-
try {
|
|
1855
|
-
const filePath = serverFilePath();
|
|
1856
|
-
if (fs4.existsSync(filePath)) {
|
|
1857
|
-
fs4.unlinkSync(filePath);
|
|
1858
|
-
}
|
|
1859
|
-
} catch {
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
async function isServerAlive() {
|
|
1863
|
-
const info = readServerFile();
|
|
1864
|
-
if (!info) {
|
|
1865
|
-
return null;
|
|
1866
|
-
}
|
|
1867
|
-
try {
|
|
1868
|
-
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
1869
|
-
signal: AbortSignal.timeout(2e3)
|
|
1870
|
-
});
|
|
1871
|
-
if (response.ok) {
|
|
1872
|
-
return info;
|
|
1873
|
-
}
|
|
1874
|
-
return null;
|
|
1875
|
-
} catch {
|
|
1876
|
-
removeServerFile();
|
|
1877
|
-
return null;
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// packages/core/src/watch.ts
|
|
1882
|
-
import getPort2 from "get-port";
|
|
1883
|
-
import open2 from "open";
|
|
1884
|
-
async function startWatch(options) {
|
|
1885
|
-
const {
|
|
1886
|
-
diffRef,
|
|
1887
|
-
title,
|
|
1888
|
-
description,
|
|
1889
|
-
reasoning,
|
|
1890
|
-
cwd,
|
|
1891
|
-
silent,
|
|
1892
|
-
dev,
|
|
1893
|
-
pollInterval = 1e3
|
|
1894
|
-
} = options;
|
|
1895
|
-
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1896
|
-
const currentBranch = getCurrentBranch({ cwd });
|
|
1897
|
-
const initialBriefing = analyze(initialDiffSet);
|
|
1898
|
-
const metadata = {
|
|
1899
|
-
title,
|
|
1900
|
-
description,
|
|
1901
|
-
reasoning,
|
|
1902
|
-
currentBranch
|
|
1903
|
-
};
|
|
1904
|
-
const [bridgePort, uiPort] = await Promise.all([
|
|
1905
|
-
getPort2(),
|
|
1906
|
-
getPort2()
|
|
1907
|
-
]);
|
|
1908
|
-
const reviewId = "watch-session";
|
|
1909
|
-
function handleDiffRefChange(newRef) {
|
|
1910
|
-
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1911
|
-
const newBriefing = analyze(newDiffSet);
|
|
1912
|
-
bridge.sendDiffUpdate({
|
|
1913
|
-
diffSet: newDiffSet,
|
|
1914
|
-
rawDiff: newRawDiff,
|
|
1915
|
-
briefing: newBriefing,
|
|
1916
|
-
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1917
|
-
timestamp: Date.now()
|
|
1918
|
-
});
|
|
1919
|
-
bridge.storeInitPayload({
|
|
1920
|
-
reviewId,
|
|
1921
|
-
diffSet: newDiffSet,
|
|
1922
|
-
rawDiff: newRawDiff,
|
|
1923
|
-
briefing: newBriefing,
|
|
1924
|
-
metadata,
|
|
1925
|
-
watchMode: true
|
|
1926
|
-
});
|
|
1927
|
-
poller.setDiffRef(newRef);
|
|
1928
|
-
}
|
|
1929
|
-
const bridge = await createWatchBridge(bridgePort, {
|
|
1930
|
-
onRefreshRequest: () => {
|
|
1931
|
-
poller.refresh();
|
|
1932
|
-
},
|
|
1933
|
-
onContextUpdate: (payload) => {
|
|
1934
|
-
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1935
|
-
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1936
|
-
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1937
|
-
},
|
|
1938
|
-
onDiffRefChange: (newRef) => {
|
|
1939
|
-
try {
|
|
1940
|
-
handleDiffRefChange(newRef);
|
|
1941
|
-
} catch (err) {
|
|
1942
|
-
bridge.sendDiffError({
|
|
1943
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
},
|
|
1947
|
-
onRefsRequest: async () => {
|
|
1948
|
-
try {
|
|
1949
|
-
const resolvedCwd = cwd ?? process.cwd();
|
|
1950
|
-
const branches = listBranches({ cwd: resolvedCwd });
|
|
1951
|
-
const commits = listCommits({ cwd: resolvedCwd });
|
|
1952
|
-
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1953
|
-
return { branches, commits, currentBranch: branch };
|
|
1954
|
-
} catch {
|
|
1955
|
-
return null;
|
|
1956
|
-
}
|
|
1957
|
-
},
|
|
1958
|
-
onCompareRequest: async (ref) => {
|
|
1959
|
-
try {
|
|
1960
|
-
handleDiffRefChange(ref);
|
|
1961
|
-
return true;
|
|
1962
|
-
} catch {
|
|
1963
|
-
return false;
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
1967
|
-
let httpServer = null;
|
|
1968
|
-
let viteServer = null;
|
|
1969
|
-
if (dev) {
|
|
1970
|
-
const uiRoot = resolveUiRoot();
|
|
1971
|
-
viteServer = await startViteDevServer(uiRoot, uiPort, !!silent);
|
|
1972
|
-
} else {
|
|
1973
|
-
const uiDist = resolveUiDist();
|
|
1974
|
-
httpServer = await createStaticServer(uiDist, uiPort);
|
|
1975
|
-
}
|
|
1976
|
-
writeWatchFile(cwd, {
|
|
1977
|
-
wsPort: bridgePort,
|
|
1978
|
-
uiPort,
|
|
1979
|
-
pid: process.pid,
|
|
1980
|
-
cwd: cwd ?? process.cwd(),
|
|
1981
|
-
diffRef,
|
|
1982
|
-
startedAt: Date.now()
|
|
1983
|
-
});
|
|
1984
|
-
const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${reviewId}`;
|
|
1985
|
-
if (!silent) {
|
|
1986
|
-
console.log(`
|
|
1987
|
-
DiffPrism Watch: ${title ?? `watching ${diffRef}`}`);
|
|
1988
|
-
console.log(`Browser: ${url}`);
|
|
1989
|
-
console.log(`API: http://localhost:${bridgePort}`);
|
|
1990
|
-
console.log(`Polling every ${pollInterval}ms
|
|
1991
|
-
`);
|
|
1992
|
-
}
|
|
1993
|
-
await open2(url);
|
|
1994
|
-
const initPayload = {
|
|
1995
|
-
reviewId,
|
|
1996
|
-
diffSet: initialDiffSet,
|
|
1997
|
-
rawDiff: initialRawDiff,
|
|
1998
|
-
briefing: initialBriefing,
|
|
1999
|
-
metadata,
|
|
2000
|
-
watchMode: true
|
|
2001
|
-
};
|
|
2002
|
-
bridge.sendInit(initPayload);
|
|
2003
|
-
bridge.onSubmit((result) => {
|
|
2004
|
-
if (!silent) {
|
|
2005
|
-
console.log(`
|
|
2006
|
-
Review submitted: ${result.decision}`);
|
|
2007
|
-
if (result.comments.length > 0) {
|
|
2008
|
-
console.log(` ${result.comments.length} comment(s)`);
|
|
2009
|
-
}
|
|
2010
|
-
console.log("Continuing to watch...\n");
|
|
2011
|
-
}
|
|
2012
|
-
writeReviewResult(cwd, result);
|
|
2013
|
-
});
|
|
2014
|
-
const poller = createDiffPoller({
|
|
2015
|
-
diffRef,
|
|
2016
|
-
cwd: cwd ?? process.cwd(),
|
|
2017
|
-
pollInterval,
|
|
2018
|
-
onDiffChanged: (updatePayload) => {
|
|
2019
|
-
bridge.storeInitPayload({
|
|
2020
|
-
reviewId,
|
|
2021
|
-
diffSet: updatePayload.diffSet,
|
|
2022
|
-
rawDiff: updatePayload.rawDiff,
|
|
2023
|
-
briefing: updatePayload.briefing,
|
|
2024
|
-
metadata,
|
|
2025
|
-
watchMode: true
|
|
2026
|
-
});
|
|
2027
|
-
bridge.sendDiffUpdate(updatePayload);
|
|
2028
|
-
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
2029
|
-
console.log(
|
|
2030
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
2031
|
-
);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
});
|
|
2035
|
-
poller.start();
|
|
2036
|
-
async function stop() {
|
|
2037
|
-
poller.stop();
|
|
2038
|
-
await bridge.close();
|
|
2039
|
-
if (viteServer) {
|
|
2040
|
-
await viteServer.close();
|
|
2041
|
-
}
|
|
2042
|
-
if (httpServer) {
|
|
2043
|
-
httpServer.close();
|
|
2044
|
-
}
|
|
2045
|
-
removeWatchFile(cwd);
|
|
2046
|
-
}
|
|
2047
|
-
function updateContext(payload) {
|
|
2048
|
-
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
2049
|
-
if (payload.title !== void 0) metadata.title = payload.title;
|
|
2050
|
-
if (payload.description !== void 0) metadata.description = payload.description;
|
|
2051
|
-
bridge.sendContextUpdate(payload);
|
|
2052
|
-
}
|
|
2053
|
-
return { stop, updateContext };
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
// packages/core/src/global-server.ts
|
|
2057
|
-
import http3 from "http";
|
|
2058
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2059
|
-
import getPort3 from "get-port";
|
|
2060
|
-
import open3 from "open";
|
|
2061
|
-
import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
|
|
2062
|
-
var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
|
|
2063
|
-
var ABANDONED_TTL_MS = 60 * 60 * 1e3;
|
|
2064
|
-
var CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
2065
|
-
var sessions2 = /* @__PURE__ */ new Map();
|
|
2066
|
-
var clientSessions = /* @__PURE__ */ new Map();
|
|
2067
|
-
var sessionWatchers = /* @__PURE__ */ new Map();
|
|
2068
|
-
var serverPollInterval = 2e3;
|
|
2069
|
-
var reopenBrowserIfNeeded = null;
|
|
2070
|
-
function toSummary(session) {
|
|
2071
|
-
const { payload } = session;
|
|
2072
|
-
const fileCount = payload.diffSet.files.length;
|
|
2073
|
-
let additions = 0;
|
|
2074
|
-
let deletions = 0;
|
|
2075
|
-
for (const file of payload.diffSet.files) {
|
|
2076
|
-
additions += file.additions;
|
|
2077
|
-
deletions += file.deletions;
|
|
2078
|
-
}
|
|
2079
|
-
return {
|
|
2080
|
-
id: session.id,
|
|
2081
|
-
projectPath: session.projectPath,
|
|
2082
|
-
branch: payload.metadata.currentBranch,
|
|
2083
|
-
title: payload.metadata.title,
|
|
2084
|
-
fileCount,
|
|
2085
|
-
additions,
|
|
2086
|
-
deletions,
|
|
2087
|
-
status: session.status,
|
|
2088
|
-
decision: session.result?.decision,
|
|
2089
|
-
createdAt: session.createdAt,
|
|
2090
|
-
hasNewChanges: session.hasNewChanges
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
function readBody(req) {
|
|
2094
|
-
return new Promise((resolve, reject) => {
|
|
2095
|
-
let body = "";
|
|
2096
|
-
req.on("data", (chunk) => {
|
|
2097
|
-
body += chunk.toString();
|
|
2098
|
-
});
|
|
2099
|
-
req.on("end", () => resolve(body));
|
|
2100
|
-
req.on("error", reject);
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
function jsonResponse(res, status, data) {
|
|
2104
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2105
|
-
res.end(JSON.stringify(data));
|
|
2106
|
-
}
|
|
2107
|
-
function matchRoute(method, url, expectedMethod, pattern) {
|
|
2108
|
-
if (method !== expectedMethod) return null;
|
|
2109
|
-
const patternParts = pattern.split("/");
|
|
2110
|
-
const urlParts = url.split("/");
|
|
2111
|
-
if (patternParts.length !== urlParts.length) return null;
|
|
2112
|
-
const params = {};
|
|
2113
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
2114
|
-
if (patternParts[i].startsWith(":")) {
|
|
2115
|
-
params[patternParts[i].slice(1)] = urlParts[i];
|
|
2116
|
-
} else if (patternParts[i] !== urlParts[i]) {
|
|
2117
|
-
return null;
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
return params;
|
|
2121
|
-
}
|
|
2122
|
-
var wss = null;
|
|
2123
|
-
function broadcastToAll(msg) {
|
|
2124
|
-
if (!wss) return;
|
|
2125
|
-
const data = JSON.stringify(msg);
|
|
2126
|
-
for (const client of wss.clients) {
|
|
2127
|
-
if (client.readyState === WebSocket2.OPEN) {
|
|
2128
|
-
client.send(data);
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
function sendToSessionClients(sessionId, msg) {
|
|
2133
|
-
if (!wss) return;
|
|
2134
|
-
const data = JSON.stringify(msg);
|
|
2135
|
-
for (const [client, sid] of clientSessions.entries()) {
|
|
2136
|
-
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
2137
|
-
client.send(data);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
function broadcastSessionUpdate(session) {
|
|
2142
|
-
broadcastToAll({
|
|
2143
|
-
type: "session:updated",
|
|
2144
|
-
payload: toSummary(session)
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
function broadcastSessionRemoved(sessionId) {
|
|
2148
|
-
for (const [client, sid] of clientSessions.entries()) {
|
|
2149
|
-
if (sid === sessionId) {
|
|
2150
|
-
clientSessions.delete(client);
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
broadcastToAll({
|
|
2154
|
-
type: "session:removed",
|
|
2155
|
-
payload: { sessionId }
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
function hasViewersForSession(sessionId) {
|
|
2159
|
-
for (const [client, sid] of clientSessions.entries()) {
|
|
2160
|
-
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
2161
|
-
return true;
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
return false;
|
|
2165
|
-
}
|
|
2166
|
-
function startSessionWatcher(sessionId) {
|
|
2167
|
-
if (sessionWatchers.has(sessionId)) return;
|
|
2168
|
-
const session = sessions2.get(sessionId);
|
|
2169
|
-
if (!session?.diffRef) return;
|
|
2170
|
-
const poller = createDiffPoller({
|
|
2171
|
-
diffRef: session.diffRef,
|
|
2172
|
-
cwd: session.projectPath,
|
|
2173
|
-
pollInterval: serverPollInterval,
|
|
2174
|
-
onDiffChanged: (updatePayload) => {
|
|
2175
|
-
const s = sessions2.get(sessionId);
|
|
2176
|
-
if (!s) return;
|
|
2177
|
-
s.payload = {
|
|
2178
|
-
...s.payload,
|
|
2179
|
-
diffSet: updatePayload.diffSet,
|
|
2180
|
-
rawDiff: updatePayload.rawDiff,
|
|
2181
|
-
briefing: updatePayload.briefing
|
|
2182
|
-
};
|
|
2183
|
-
s.lastDiffHash = hashDiff(updatePayload.rawDiff);
|
|
2184
|
-
s.lastDiffSet = updatePayload.diffSet;
|
|
2185
|
-
if (hasViewersForSession(sessionId)) {
|
|
2186
|
-
sendToSessionClients(sessionId, {
|
|
2187
|
-
type: "diff:update",
|
|
2188
|
-
payload: updatePayload
|
|
2189
|
-
});
|
|
2190
|
-
s.hasNewChanges = false;
|
|
2191
|
-
} else {
|
|
2192
|
-
s.hasNewChanges = true;
|
|
2193
|
-
broadcastSessionList();
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
});
|
|
2197
|
-
poller.start();
|
|
2198
|
-
sessionWatchers.set(sessionId, poller);
|
|
2199
|
-
}
|
|
2200
|
-
function stopSessionWatcher(sessionId) {
|
|
2201
|
-
const poller = sessionWatchers.get(sessionId);
|
|
2202
|
-
if (poller) {
|
|
2203
|
-
poller.stop();
|
|
2204
|
-
sessionWatchers.delete(sessionId);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
function startAllWatchers() {
|
|
2208
|
-
for (const [id, session] of sessions2.entries()) {
|
|
2209
|
-
if (session.diffRef && !sessionWatchers.has(id)) {
|
|
2210
|
-
startSessionWatcher(id);
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
function stopAllWatchers() {
|
|
2215
|
-
for (const [, poller] of sessionWatchers.entries()) {
|
|
2216
|
-
poller.stop();
|
|
2217
|
-
}
|
|
2218
|
-
sessionWatchers.clear();
|
|
2219
|
-
}
|
|
2220
|
-
function hasConnectedClients() {
|
|
2221
|
-
if (!wss) return false;
|
|
2222
|
-
for (const client of wss.clients) {
|
|
2223
|
-
if (client.readyState === WebSocket2.OPEN) return true;
|
|
2224
|
-
}
|
|
2225
|
-
return false;
|
|
2226
|
-
}
|
|
2227
|
-
function broadcastSessionList() {
|
|
2228
|
-
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
2229
|
-
broadcastToAll({ type: "session:list", payload: summaries });
|
|
2230
|
-
}
|
|
2231
|
-
function recordReviewHistory(session, result) {
|
|
2232
|
-
if (session.projectPath.startsWith("github:")) return;
|
|
2233
|
-
try {
|
|
2234
|
-
const { payload } = session;
|
|
2235
|
-
const entry = {
|
|
2236
|
-
id: generateEntryId(),
|
|
2237
|
-
timestamp: Date.now(),
|
|
2238
|
-
diffRef: session.diffRef ?? "unknown",
|
|
2239
|
-
decision: result.decision,
|
|
2240
|
-
filesReviewed: payload.diffSet.files.length,
|
|
2241
|
-
additions: payload.diffSet.files.reduce((sum, f) => sum + f.additions, 0),
|
|
2242
|
-
deletions: payload.diffSet.files.reduce((sum, f) => sum + f.deletions, 0),
|
|
2243
|
-
commentCount: result.comments.length,
|
|
2244
|
-
branch: payload.metadata.currentBranch,
|
|
2245
|
-
title: payload.metadata.title,
|
|
2246
|
-
summary: result.summary ?? payload.briefing.summary
|
|
2247
|
-
};
|
|
2248
|
-
appendHistory(session.projectPath, entry);
|
|
2249
|
-
} catch {
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
async function handleApiRequest(req, res) {
|
|
2253
|
-
const method = req.method ?? "GET";
|
|
2254
|
-
const url = (req.url ?? "/").split("?")[0];
|
|
2255
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2256
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
2257
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
2258
|
-
if (method === "OPTIONS") {
|
|
2259
|
-
res.writeHead(204);
|
|
2260
|
-
res.end();
|
|
2261
|
-
return true;
|
|
2262
|
-
}
|
|
2263
|
-
if (!url.startsWith("/api/")) {
|
|
2264
|
-
return false;
|
|
2265
|
-
}
|
|
2266
|
-
if (method === "GET" && url === "/api/status") {
|
|
2267
|
-
jsonResponse(res, 200, {
|
|
2268
|
-
running: true,
|
|
2269
|
-
pid: process.pid,
|
|
2270
|
-
sessions: sessions2.size,
|
|
2271
|
-
uptime: process.uptime()
|
|
2272
|
-
});
|
|
2273
|
-
return true;
|
|
2274
|
-
}
|
|
2275
|
-
if (method === "POST" && url === "/api/reviews") {
|
|
2276
|
-
try {
|
|
2277
|
-
const body = await readBody(req);
|
|
2278
|
-
const { payload, projectPath, diffRef } = JSON.parse(body);
|
|
2279
|
-
let existingSession;
|
|
2280
|
-
for (const session of sessions2.values()) {
|
|
2281
|
-
if (session.projectPath === projectPath) {
|
|
2282
|
-
existingSession = session;
|
|
2283
|
-
break;
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
if (existingSession) {
|
|
2287
|
-
const sessionId = existingSession.id;
|
|
2288
|
-
payload.reviewId = sessionId;
|
|
2289
|
-
if (diffRef) {
|
|
2290
|
-
payload.watchMode = true;
|
|
2291
|
-
}
|
|
2292
|
-
stopSessionWatcher(sessionId);
|
|
2293
|
-
existingSession.payload = payload;
|
|
2294
|
-
existingSession.status = "pending";
|
|
2295
|
-
existingSession.result = null;
|
|
2296
|
-
existingSession.createdAt = Date.now();
|
|
2297
|
-
existingSession.diffRef = diffRef;
|
|
2298
|
-
existingSession.lastDiffHash = diffRef ? hashDiff(payload.rawDiff) : void 0;
|
|
2299
|
-
existingSession.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
2300
|
-
existingSession.hasNewChanges = false;
|
|
2301
|
-
existingSession.annotations = [];
|
|
2302
|
-
if (diffRef && hasConnectedClients()) {
|
|
2303
|
-
startSessionWatcher(sessionId);
|
|
2304
|
-
}
|
|
2305
|
-
if (hasViewersForSession(sessionId)) {
|
|
2306
|
-
sendToSessionClients(sessionId, {
|
|
2307
|
-
type: "review:init",
|
|
2308
|
-
payload
|
|
2309
|
-
});
|
|
2310
|
-
}
|
|
2311
|
-
broadcastSessionUpdate(existingSession);
|
|
2312
|
-
reopenBrowserIfNeeded?.();
|
|
2313
|
-
jsonResponse(res, 200, { sessionId });
|
|
2314
|
-
} else {
|
|
2315
|
-
const sessionId = `session-${randomUUID2().slice(0, 8)}`;
|
|
2316
|
-
payload.reviewId = sessionId;
|
|
2317
|
-
if (diffRef) {
|
|
2318
|
-
payload.watchMode = true;
|
|
2319
|
-
}
|
|
2320
|
-
const session = {
|
|
2321
|
-
id: sessionId,
|
|
2322
|
-
payload,
|
|
2323
|
-
projectPath,
|
|
2324
|
-
status: "pending",
|
|
2325
|
-
createdAt: Date.now(),
|
|
2326
|
-
result: null,
|
|
2327
|
-
diffRef,
|
|
2328
|
-
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
2329
|
-
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
2330
|
-
hasNewChanges: false,
|
|
2331
|
-
annotations: []
|
|
2332
|
-
};
|
|
2333
|
-
sessions2.set(sessionId, session);
|
|
2334
|
-
if (diffRef && hasConnectedClients()) {
|
|
2335
|
-
startSessionWatcher(sessionId);
|
|
2336
|
-
}
|
|
2337
|
-
broadcastToAll({
|
|
2338
|
-
type: "session:added",
|
|
2339
|
-
payload: toSummary(session)
|
|
2340
|
-
});
|
|
2341
|
-
reopenBrowserIfNeeded?.();
|
|
2342
|
-
jsonResponse(res, 201, { sessionId });
|
|
2343
|
-
}
|
|
2344
|
-
} catch {
|
|
2345
|
-
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2346
|
-
}
|
|
2347
|
-
return true;
|
|
2348
|
-
}
|
|
2349
|
-
if (method === "GET" && url === "/api/reviews") {
|
|
2350
|
-
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
2351
|
-
jsonResponse(res, 200, { sessions: summaries });
|
|
2352
|
-
return true;
|
|
2353
|
-
}
|
|
2354
|
-
const getReviewParams = matchRoute(method, url, "GET", "/api/reviews/:id");
|
|
2355
|
-
if (getReviewParams) {
|
|
2356
|
-
const session = sessions2.get(getReviewParams.id);
|
|
2357
|
-
if (!session) {
|
|
2358
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2359
|
-
return true;
|
|
2360
|
-
}
|
|
2361
|
-
jsonResponse(res, 200, toSummary(session));
|
|
2362
|
-
return true;
|
|
2363
|
-
}
|
|
2364
|
-
const postResultParams = matchRoute(method, url, "POST", "/api/reviews/:id/result");
|
|
2365
|
-
if (postResultParams) {
|
|
2366
|
-
const session = sessions2.get(postResultParams.id);
|
|
2367
|
-
if (!session) {
|
|
2368
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2369
|
-
return true;
|
|
2370
|
-
}
|
|
2371
|
-
try {
|
|
2372
|
-
const body = await readBody(req);
|
|
2373
|
-
const result = JSON.parse(body);
|
|
2374
|
-
session.result = result;
|
|
2375
|
-
session.status = "submitted";
|
|
2376
|
-
recordReviewHistory(session, result);
|
|
2377
|
-
if (result.decision === "dismissed") {
|
|
2378
|
-
broadcastSessionRemoved(postResultParams.id);
|
|
2379
|
-
} else {
|
|
2380
|
-
broadcastSessionUpdate(session);
|
|
2381
|
-
}
|
|
2382
|
-
jsonResponse(res, 200, { ok: true });
|
|
2383
|
-
} catch {
|
|
2384
|
-
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2385
|
-
}
|
|
2386
|
-
return true;
|
|
2387
|
-
}
|
|
2388
|
-
const getResultParams = matchRoute(method, url, "GET", "/api/reviews/:id/result");
|
|
2389
|
-
if (getResultParams) {
|
|
2390
|
-
const session = sessions2.get(getResultParams.id);
|
|
2391
|
-
if (!session) {
|
|
2392
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2393
|
-
return true;
|
|
2394
|
-
}
|
|
2395
|
-
if (session.result) {
|
|
2396
|
-
jsonResponse(res, 200, { result: session.result, status: "submitted" });
|
|
2397
|
-
} else {
|
|
2398
|
-
jsonResponse(res, 200, { result: null, status: session.status });
|
|
2399
|
-
}
|
|
2400
|
-
return true;
|
|
2401
|
-
}
|
|
2402
|
-
const postContextParams = matchRoute(method, url, "POST", "/api/reviews/:id/context");
|
|
2403
|
-
if (postContextParams) {
|
|
2404
|
-
const session = sessions2.get(postContextParams.id);
|
|
2405
|
-
if (!session) {
|
|
2406
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2407
|
-
return true;
|
|
2408
|
-
}
|
|
2409
|
-
try {
|
|
2410
|
-
const body = await readBody(req);
|
|
2411
|
-
const contextPayload = JSON.parse(body);
|
|
2412
|
-
if (contextPayload.reasoning !== void 0) {
|
|
2413
|
-
session.payload.metadata.reasoning = contextPayload.reasoning;
|
|
2414
|
-
}
|
|
2415
|
-
if (contextPayload.title !== void 0) {
|
|
2416
|
-
session.payload.metadata.title = contextPayload.title;
|
|
2417
|
-
}
|
|
2418
|
-
if (contextPayload.description !== void 0) {
|
|
2419
|
-
session.payload.metadata.description = contextPayload.description;
|
|
2420
|
-
}
|
|
2421
|
-
sendToSessionClients(session.id, {
|
|
2422
|
-
type: "context:update",
|
|
2423
|
-
payload: contextPayload
|
|
2424
|
-
});
|
|
2425
|
-
jsonResponse(res, 200, { ok: true });
|
|
2426
|
-
} catch {
|
|
2427
|
-
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2428
|
-
}
|
|
2429
|
-
return true;
|
|
2430
|
-
}
|
|
2431
|
-
const postAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations");
|
|
2432
|
-
if (postAnnotationParams) {
|
|
2433
|
-
const session = sessions2.get(postAnnotationParams.id);
|
|
2434
|
-
if (!session) {
|
|
2435
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2436
|
-
return true;
|
|
2437
|
-
}
|
|
2438
|
-
try {
|
|
2439
|
-
const body = await readBody(req);
|
|
2440
|
-
const { file, line, body: annotationBody, type, confidence, category, source } = JSON.parse(body);
|
|
2441
|
-
const annotation = {
|
|
2442
|
-
id: randomUUID2(),
|
|
2443
|
-
sessionId: session.id,
|
|
2444
|
-
file,
|
|
2445
|
-
line,
|
|
2446
|
-
body: annotationBody,
|
|
2447
|
-
type,
|
|
2448
|
-
confidence: confidence ?? 1,
|
|
2449
|
-
category: category ?? "other",
|
|
2450
|
-
source,
|
|
2451
|
-
createdAt: Date.now()
|
|
2452
|
-
};
|
|
2453
|
-
session.annotations.push(annotation);
|
|
2454
|
-
sendToSessionClients(session.id, {
|
|
2455
|
-
type: "annotation:added",
|
|
2456
|
-
payload: annotation
|
|
2457
|
-
});
|
|
2458
|
-
jsonResponse(res, 200, { annotationId: annotation.id });
|
|
2459
|
-
} catch {
|
|
2460
|
-
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2461
|
-
}
|
|
2462
|
-
return true;
|
|
2463
|
-
}
|
|
2464
|
-
const getAnnotationsParams = matchRoute(method, url, "GET", "/api/reviews/:id/annotations");
|
|
2465
|
-
if (getAnnotationsParams) {
|
|
2466
|
-
const session = sessions2.get(getAnnotationsParams.id);
|
|
2467
|
-
if (!session) {
|
|
2468
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2469
|
-
return true;
|
|
2470
|
-
}
|
|
2471
|
-
jsonResponse(res, 200, { annotations: session.annotations });
|
|
2472
|
-
return true;
|
|
2473
|
-
}
|
|
2474
|
-
const dismissAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/dismiss");
|
|
2475
|
-
if (dismissAnnotationParams) {
|
|
2476
|
-
const session = sessions2.get(dismissAnnotationParams.id);
|
|
2477
|
-
if (!session) {
|
|
2478
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2479
|
-
return true;
|
|
2480
|
-
}
|
|
2481
|
-
const annotation = session.annotations.find((a) => a.id === dismissAnnotationParams.annotationId);
|
|
2482
|
-
if (!annotation) {
|
|
2483
|
-
jsonResponse(res, 404, { error: "Annotation not found" });
|
|
2484
|
-
return true;
|
|
2485
|
-
}
|
|
2486
|
-
annotation.dismissed = true;
|
|
2487
|
-
sendToSessionClients(dismissAnnotationParams.id, {
|
|
2488
|
-
type: "annotation:dismissed",
|
|
2489
|
-
payload: { annotationId: dismissAnnotationParams.annotationId }
|
|
2490
|
-
});
|
|
2491
|
-
jsonResponse(res, 200, { ok: true });
|
|
2492
|
-
return true;
|
|
2493
|
-
}
|
|
2494
|
-
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
2495
|
-
if (deleteParams) {
|
|
2496
|
-
stopSessionWatcher(deleteParams.id);
|
|
2497
|
-
if (sessions2.delete(deleteParams.id)) {
|
|
2498
|
-
broadcastSessionRemoved(deleteParams.id);
|
|
2499
|
-
jsonResponse(res, 200, { ok: true });
|
|
2500
|
-
} else {
|
|
2501
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2502
|
-
}
|
|
2503
|
-
return true;
|
|
2504
|
-
}
|
|
2505
|
-
const getRefsParams = matchRoute(method, url, "GET", "/api/reviews/:id/refs");
|
|
2506
|
-
if (getRefsParams) {
|
|
2507
|
-
const session = sessions2.get(getRefsParams.id);
|
|
2508
|
-
if (!session) {
|
|
2509
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2510
|
-
return true;
|
|
2511
|
-
}
|
|
2512
|
-
if (session.projectPath.startsWith("github:")) {
|
|
2513
|
-
jsonResponse(res, 400, { error: "Ref listing not available for GitHub PRs" });
|
|
2514
|
-
return true;
|
|
2515
|
-
}
|
|
2516
|
-
try {
|
|
2517
|
-
const branches = listBranches({ cwd: session.projectPath });
|
|
2518
|
-
const commits = listCommits({ cwd: session.projectPath });
|
|
2519
|
-
const currentBranch = getCurrentBranch({ cwd: session.projectPath });
|
|
2520
|
-
jsonResponse(res, 200, { branches, commits, currentBranch });
|
|
2521
|
-
} catch {
|
|
2522
|
-
jsonResponse(res, 500, { error: "Failed to list git refs" });
|
|
2523
|
-
}
|
|
2524
|
-
return true;
|
|
2525
|
-
}
|
|
2526
|
-
const postCompareParams = matchRoute(method, url, "POST", "/api/reviews/:id/compare");
|
|
2527
|
-
if (postCompareParams) {
|
|
2528
|
-
const session = sessions2.get(postCompareParams.id);
|
|
2529
|
-
if (!session) {
|
|
2530
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2531
|
-
return true;
|
|
2532
|
-
}
|
|
2533
|
-
if (session.projectPath.startsWith("github:")) {
|
|
2534
|
-
jsonResponse(res, 400, { error: "Ref comparison not available for GitHub PRs" });
|
|
2535
|
-
return true;
|
|
2536
|
-
}
|
|
2537
|
-
try {
|
|
2538
|
-
const body = await readBody(req);
|
|
2539
|
-
const { ref } = JSON.parse(body);
|
|
2540
|
-
if (!ref) {
|
|
2541
|
-
jsonResponse(res, 400, { error: "Missing ref in request body" });
|
|
2542
|
-
return true;
|
|
2543
|
-
}
|
|
2544
|
-
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(ref, {
|
|
2545
|
-
cwd: session.projectPath
|
|
2546
|
-
});
|
|
2547
|
-
const newBriefing = analyze(newDiffSet);
|
|
2548
|
-
const changedFiles = detectChangedFiles(session.lastDiffSet ?? null, newDiffSet);
|
|
2549
|
-
session.payload = {
|
|
2550
|
-
...session.payload,
|
|
2551
|
-
diffSet: newDiffSet,
|
|
2552
|
-
rawDiff: newRawDiff,
|
|
2553
|
-
briefing: newBriefing
|
|
2554
|
-
};
|
|
2555
|
-
session.lastDiffHash = hashDiff(newRawDiff);
|
|
2556
|
-
session.lastDiffSet = newDiffSet;
|
|
2557
|
-
stopSessionWatcher(session.id);
|
|
2558
|
-
session.diffRef = ref;
|
|
2559
|
-
if (hasConnectedClients()) {
|
|
2560
|
-
startSessionWatcher(session.id);
|
|
2561
|
-
}
|
|
2562
|
-
sendToSessionClients(session.id, {
|
|
2563
|
-
type: "diff:update",
|
|
2564
|
-
payload: {
|
|
2565
|
-
diffSet: newDiffSet,
|
|
2566
|
-
rawDiff: newRawDiff,
|
|
2567
|
-
briefing: newBriefing,
|
|
2568
|
-
changedFiles,
|
|
2569
|
-
timestamp: Date.now()
|
|
2570
|
-
}
|
|
2571
|
-
});
|
|
2572
|
-
jsonResponse(res, 200, { ok: true, fileCount: newDiffSet.files.length });
|
|
2573
|
-
} catch {
|
|
2574
|
-
jsonResponse(res, 400, { error: "Failed to compute diff for the given ref" });
|
|
2575
|
-
}
|
|
2576
|
-
return true;
|
|
2577
|
-
}
|
|
2578
|
-
const getSessionHistoryParams = matchRoute(method, url, "GET", "/api/reviews/:id/history");
|
|
2579
|
-
if (getSessionHistoryParams) {
|
|
2580
|
-
const session = sessions2.get(getSessionHistoryParams.id);
|
|
2581
|
-
if (!session) {
|
|
2582
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
2583
|
-
return true;
|
|
2584
|
-
}
|
|
2585
|
-
if (session.projectPath.startsWith("github:")) {
|
|
2586
|
-
jsonResponse(res, 200, { history: [] });
|
|
2587
|
-
return true;
|
|
2588
|
-
}
|
|
2589
|
-
const history = getRecentHistory(session.projectPath);
|
|
2590
|
-
jsonResponse(res, 200, { history });
|
|
2591
|
-
return true;
|
|
2592
|
-
}
|
|
2593
|
-
if (method === "GET" && req.url) {
|
|
2594
|
-
const parsedUrl = new URL(req.url, "http://localhost");
|
|
2595
|
-
if (parsedUrl.pathname === "/api/history") {
|
|
2596
|
-
const projectPath = parsedUrl.searchParams.get("project");
|
|
2597
|
-
if (!projectPath) {
|
|
2598
|
-
jsonResponse(res, 400, { error: "Missing required query parameter: project" });
|
|
2599
|
-
return true;
|
|
2600
|
-
}
|
|
2601
|
-
if (projectPath.startsWith("github:")) {
|
|
2602
|
-
jsonResponse(res, 200, { history: [] });
|
|
2603
|
-
return true;
|
|
2604
|
-
}
|
|
2605
|
-
const history = getRecentHistory(projectPath);
|
|
2606
|
-
jsonResponse(res, 200, { history });
|
|
2607
|
-
return true;
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
jsonResponse(res, 404, { error: "Not found" });
|
|
2611
|
-
return true;
|
|
2612
|
-
}
|
|
2613
|
-
async function startGlobalServer(options = {}) {
|
|
2614
|
-
const {
|
|
2615
|
-
httpPort: preferredHttpPort = 24680,
|
|
2616
|
-
wsPort: preferredWsPort = 24681,
|
|
2617
|
-
silent = false,
|
|
2618
|
-
dev = false,
|
|
2619
|
-
pollInterval = 2e3
|
|
2620
|
-
} = options;
|
|
2621
|
-
serverPollInterval = pollInterval;
|
|
2622
|
-
const [httpPort, wsPort] = await Promise.all([
|
|
2623
|
-
getPort3({ port: preferredHttpPort }),
|
|
2624
|
-
getPort3({ port: preferredWsPort })
|
|
2625
|
-
]);
|
|
2626
|
-
let uiPort;
|
|
2627
|
-
let uiHttpServer = null;
|
|
2628
|
-
let viteServer = null;
|
|
2629
|
-
if (dev) {
|
|
2630
|
-
uiPort = await getPort3();
|
|
2631
|
-
const uiRoot = resolveUiRoot();
|
|
2632
|
-
viteServer = await startViteDevServer(uiRoot, uiPort, silent);
|
|
2633
|
-
} else {
|
|
2634
|
-
uiPort = await getPort3();
|
|
2635
|
-
const uiDist = resolveUiDist();
|
|
2636
|
-
uiHttpServer = await createStaticServer(uiDist, uiPort);
|
|
2637
|
-
}
|
|
2638
|
-
const httpServer = http3.createServer(async (req, res) => {
|
|
2639
|
-
const handled = await handleApiRequest(req, res);
|
|
2640
|
-
if (!handled) {
|
|
2641
|
-
res.writeHead(404);
|
|
2642
|
-
res.end("Not found");
|
|
2643
|
-
}
|
|
2644
|
-
});
|
|
2645
|
-
wss = new WebSocketServer2({ port: wsPort });
|
|
2646
|
-
wss.on("connection", (ws, req) => {
|
|
2647
|
-
startAllWatchers();
|
|
2648
|
-
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
2649
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
2650
|
-
if (sessionId) {
|
|
2651
|
-
clientSessions.set(ws, sessionId);
|
|
2652
|
-
const session = sessions2.get(sessionId);
|
|
2653
|
-
if (session) {
|
|
2654
|
-
session.status = "in_review";
|
|
2655
|
-
session.hasNewChanges = false;
|
|
2656
|
-
broadcastSessionUpdate(session);
|
|
2657
|
-
const msg = {
|
|
2658
|
-
type: "review:init",
|
|
2659
|
-
payload: session.payload
|
|
2660
|
-
};
|
|
2661
|
-
ws.send(JSON.stringify(msg));
|
|
2662
|
-
for (const annotation of session.annotations) {
|
|
2663
|
-
ws.send(JSON.stringify({
|
|
2664
|
-
type: "annotation:added",
|
|
2665
|
-
payload: annotation
|
|
2666
|
-
}));
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
} else {
|
|
2670
|
-
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
2671
|
-
const msg = {
|
|
2672
|
-
type: "session:list",
|
|
2673
|
-
payload: summaries
|
|
2674
|
-
};
|
|
2675
|
-
ws.send(JSON.stringify(msg));
|
|
2676
|
-
if (summaries.length === 1) {
|
|
2677
|
-
const session = sessions2.get(summaries[0].id);
|
|
2678
|
-
if (session) {
|
|
2679
|
-
clientSessions.set(ws, session.id);
|
|
2680
|
-
session.status = "in_review";
|
|
2681
|
-
session.hasNewChanges = false;
|
|
2682
|
-
broadcastSessionUpdate(session);
|
|
2683
|
-
ws.send(JSON.stringify({
|
|
2684
|
-
type: "review:init",
|
|
2685
|
-
payload: session.payload
|
|
2686
|
-
}));
|
|
2687
|
-
for (const annotation of session.annotations) {
|
|
2688
|
-
ws.send(JSON.stringify({
|
|
2689
|
-
type: "annotation:added",
|
|
2690
|
-
payload: annotation
|
|
2691
|
-
}));
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
ws.on("message", (data) => {
|
|
2697
|
-
try {
|
|
2698
|
-
const msg = JSON.parse(data.toString());
|
|
2699
|
-
if (msg.type === "review:submit") {
|
|
2700
|
-
const sid = clientSessions.get(ws);
|
|
2701
|
-
if (sid) {
|
|
2702
|
-
const session = sessions2.get(sid);
|
|
2703
|
-
if (session) {
|
|
2704
|
-
session.result = msg.payload;
|
|
2705
|
-
session.status = "submitted";
|
|
2706
|
-
recordReviewHistory(session, msg.payload);
|
|
2707
|
-
if (msg.payload.decision === "dismissed") {
|
|
2708
|
-
broadcastSessionRemoved(sid);
|
|
2709
|
-
} else {
|
|
2710
|
-
broadcastSessionUpdate(session);
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
} else if (msg.type === "session:select") {
|
|
2715
|
-
const session = sessions2.get(msg.payload.sessionId);
|
|
2716
|
-
if (session) {
|
|
2717
|
-
clientSessions.set(ws, session.id);
|
|
2718
|
-
session.status = "in_review";
|
|
2719
|
-
session.hasNewChanges = false;
|
|
2720
|
-
startSessionWatcher(session.id);
|
|
2721
|
-
broadcastSessionUpdate(session);
|
|
2722
|
-
ws.send(JSON.stringify({
|
|
2723
|
-
type: "review:init",
|
|
2724
|
-
payload: session.payload
|
|
2725
|
-
}));
|
|
2726
|
-
for (const annotation of session.annotations) {
|
|
2727
|
-
ws.send(JSON.stringify({
|
|
2728
|
-
type: "annotation:added",
|
|
2729
|
-
payload: annotation
|
|
2730
|
-
}));
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
} else if (msg.type === "session:close") {
|
|
2734
|
-
const closedId = msg.payload.sessionId;
|
|
2735
|
-
stopSessionWatcher(closedId);
|
|
2736
|
-
const closedSession = sessions2.get(closedId);
|
|
2737
|
-
if (closedSession && !closedSession.result) {
|
|
2738
|
-
closedSession.result = { decision: "dismissed", comments: [] };
|
|
2739
|
-
closedSession.status = "submitted";
|
|
2740
|
-
}
|
|
2741
|
-
broadcastSessionRemoved(closedId);
|
|
2742
|
-
} else if (msg.type === "diff:change_ref") {
|
|
2743
|
-
const sid = clientSessions.get(ws);
|
|
2744
|
-
if (sid) {
|
|
2745
|
-
const session = sessions2.get(sid);
|
|
2746
|
-
if (session) {
|
|
2747
|
-
const newRef = msg.payload.diffRef;
|
|
2748
|
-
try {
|
|
2749
|
-
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, {
|
|
2750
|
-
cwd: session.projectPath
|
|
2751
|
-
});
|
|
2752
|
-
const newBriefing = analyze(newDiffSet);
|
|
2753
|
-
session.payload = {
|
|
2754
|
-
...session.payload,
|
|
2755
|
-
diffSet: newDiffSet,
|
|
2756
|
-
rawDiff: newRawDiff,
|
|
2757
|
-
briefing: newBriefing
|
|
2758
|
-
};
|
|
2759
|
-
session.diffRef = newRef;
|
|
2760
|
-
session.lastDiffHash = hashDiff(newRawDiff);
|
|
2761
|
-
session.lastDiffSet = newDiffSet;
|
|
2762
|
-
stopSessionWatcher(sid);
|
|
2763
|
-
startSessionWatcher(sid);
|
|
2764
|
-
sendToSessionClients(sid, {
|
|
2765
|
-
type: "diff:update",
|
|
2766
|
-
payload: {
|
|
2767
|
-
diffSet: newDiffSet,
|
|
2768
|
-
rawDiff: newRawDiff,
|
|
2769
|
-
briefing: newBriefing,
|
|
2770
|
-
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
2771
|
-
timestamp: Date.now()
|
|
2772
|
-
}
|
|
2773
|
-
});
|
|
2774
|
-
} catch (err) {
|
|
2775
|
-
const errorMsg = {
|
|
2776
|
-
type: "diff:error",
|
|
2777
|
-
payload: {
|
|
2778
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2779
|
-
}
|
|
2780
|
-
};
|
|
2781
|
-
ws.send(JSON.stringify(errorMsg));
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
} catch {
|
|
2787
|
-
}
|
|
2788
|
-
});
|
|
2789
|
-
ws.on("close", () => {
|
|
2790
|
-
clientSessions.delete(ws);
|
|
2791
|
-
if (!hasConnectedClients()) {
|
|
2792
|
-
stopAllWatchers();
|
|
2793
|
-
}
|
|
2794
|
-
});
|
|
2795
|
-
});
|
|
2796
|
-
await new Promise((resolve, reject) => {
|
|
2797
|
-
httpServer.on("error", reject);
|
|
2798
|
-
httpServer.listen(httpPort, () => resolve());
|
|
2799
|
-
});
|
|
2800
|
-
function cleanupExpiredSessions() {
|
|
2801
|
-
const now = Date.now();
|
|
2802
|
-
for (const [id, session] of sessions2.entries()) {
|
|
2803
|
-
const age = now - session.createdAt;
|
|
2804
|
-
const expired = session.status === "submitted" && age > SUBMITTED_TTL_MS || session.status === "pending" && age > ABANDONED_TTL_MS;
|
|
2805
|
-
if (expired) {
|
|
2806
|
-
stopSessionWatcher(id);
|
|
2807
|
-
sessions2.delete(id);
|
|
2808
|
-
broadcastSessionRemoved(id);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
const cleanupTimer = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
|
|
2813
|
-
const serverInfo = {
|
|
2814
|
-
httpPort,
|
|
2815
|
-
wsPort,
|
|
2816
|
-
pid: process.pid,
|
|
2817
|
-
startedAt: Date.now()
|
|
2818
|
-
};
|
|
2819
|
-
writeServerFile(serverInfo);
|
|
2820
|
-
if (!silent) {
|
|
2821
|
-
console.log(`
|
|
2822
|
-
DiffPrism Global Server`);
|
|
2823
|
-
console.log(` API: http://localhost:${httpPort}`);
|
|
2824
|
-
console.log(` WS: ws://localhost:${wsPort}`);
|
|
2825
|
-
console.log(` UI: http://localhost:${uiPort}`);
|
|
2826
|
-
console.log(` PID: ${process.pid}`);
|
|
2827
|
-
console.log(`
|
|
2828
|
-
Waiting for reviews...
|
|
2829
|
-
`);
|
|
2830
|
-
}
|
|
2831
|
-
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&httpPort=${httpPort}&serverMode=true`;
|
|
2832
|
-
await open3(uiUrl);
|
|
2833
|
-
reopenBrowserIfNeeded = () => {
|
|
2834
|
-
if (!hasConnectedClients()) {
|
|
2835
|
-
open3(uiUrl);
|
|
2836
|
-
}
|
|
2837
|
-
};
|
|
2838
|
-
async function stop() {
|
|
2839
|
-
clearInterval(cleanupTimer);
|
|
2840
|
-
stopAllWatchers();
|
|
2841
|
-
if (wss) {
|
|
2842
|
-
for (const client of wss.clients) {
|
|
2843
|
-
client.close();
|
|
2844
|
-
}
|
|
2845
|
-
wss.close();
|
|
2846
|
-
wss = null;
|
|
2847
|
-
}
|
|
2848
|
-
clientSessions.clear();
|
|
2849
|
-
sessions2.clear();
|
|
2850
|
-
reopenBrowserIfNeeded = null;
|
|
2851
|
-
await new Promise((resolve) => {
|
|
2852
|
-
httpServer.close(() => resolve());
|
|
2853
|
-
});
|
|
2854
|
-
if (viteServer) {
|
|
2855
|
-
await viteServer.close();
|
|
2856
|
-
}
|
|
2857
|
-
if (uiHttpServer) {
|
|
2858
|
-
uiHttpServer.close();
|
|
2859
|
-
}
|
|
2860
|
-
removeServerFile();
|
|
2861
|
-
}
|
|
2862
|
-
return { httpPort, wsPort, stop };
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
|
-
// packages/github/src/auth.ts
|
|
2866
|
-
import { execSync as execSync3 } from "child_process";
|
|
2867
|
-
import fs5 from "fs";
|
|
2868
|
-
import path7 from "path";
|
|
2869
|
-
import os2 from "os";
|
|
2870
113
|
function resolveGitHubToken() {
|
|
2871
114
|
const envToken = process.env.GITHUB_TOKEN;
|
|
2872
115
|
if (envToken) {
|
|
2873
116
|
return envToken;
|
|
2874
117
|
}
|
|
2875
118
|
try {
|
|
2876
|
-
const token =
|
|
119
|
+
const token = execSync("gh auth token", {
|
|
2877
120
|
encoding: "utf-8",
|
|
2878
121
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2879
122
|
}).trim();
|
|
@@ -2882,10 +125,10 @@ function resolveGitHubToken() {
|
|
|
2882
125
|
}
|
|
2883
126
|
} catch {
|
|
2884
127
|
}
|
|
2885
|
-
const configPath =
|
|
128
|
+
const configPath = path.join(os.homedir(), ".diffprism", "config.json");
|
|
2886
129
|
try {
|
|
2887
|
-
if (
|
|
2888
|
-
const raw =
|
|
130
|
+
if (fs.existsSync(configPath)) {
|
|
131
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
2889
132
|
const config = JSON.parse(raw);
|
|
2890
133
|
const github = config.github;
|
|
2891
134
|
if (github?.token && typeof github.token === "string") {
|
|
@@ -3390,8 +633,8 @@ function isPlainObject2(value) {
|
|
|
3390
633
|
return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
|
|
3391
634
|
}
|
|
3392
635
|
async function fetchWrapper(requestOptions) {
|
|
3393
|
-
const
|
|
3394
|
-
if (!
|
|
636
|
+
const fetch = requestOptions.request?.fetch || globalThis.fetch;
|
|
637
|
+
if (!fetch) {
|
|
3395
638
|
throw new Error(
|
|
3396
639
|
"fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing"
|
|
3397
640
|
);
|
|
@@ -3407,7 +650,7 @@ async function fetchWrapper(requestOptions) {
|
|
|
3407
650
|
);
|
|
3408
651
|
let fetchResponse;
|
|
3409
652
|
try {
|
|
3410
|
-
fetchResponse = await
|
|
653
|
+
fetchResponse = await fetch(requestOptions.url, {
|
|
3411
654
|
method: requestOptions.method,
|
|
3412
655
|
body,
|
|
3413
656
|
redirect: requestOptions.request?.redirect,
|
|
@@ -3846,17 +1089,17 @@ function requestLog(octokit) {
|
|
|
3846
1089
|
octokit.log.debug("request", options);
|
|
3847
1090
|
const start = Date.now();
|
|
3848
1091
|
const requestOptions = octokit.request.endpoint.parse(options);
|
|
3849
|
-
const
|
|
1092
|
+
const path2 = requestOptions.url.replace(options.baseUrl, "");
|
|
3850
1093
|
return request2(options).then((response) => {
|
|
3851
1094
|
const requestId = response.headers["x-github-request-id"];
|
|
3852
1095
|
octokit.log.info(
|
|
3853
|
-
`${requestOptions.method} ${
|
|
1096
|
+
`${requestOptions.method} ${path2} - ${response.status} with id ${requestId} in ${Date.now() - start}ms`
|
|
3854
1097
|
);
|
|
3855
1098
|
return response;
|
|
3856
1099
|
}).catch((error) => {
|
|
3857
1100
|
const requestId = error.response?.headers["x-github-request-id"] || "UNKNOWN";
|
|
3858
1101
|
octokit.log.error(
|
|
3859
|
-
`${requestOptions.method} ${
|
|
1102
|
+
`${requestOptions.method} ${path2} - ${error.status} with id ${requestId} in ${Date.now() - start}ms`
|
|
3860
1103
|
);
|
|
3861
1104
|
throw error;
|
|
3862
1105
|
});
|
|
@@ -6595,18 +3838,6 @@ function buildReviewBody(result) {
|
|
|
6595
3838
|
}
|
|
6596
3839
|
|
|
6597
3840
|
export {
|
|
6598
|
-
getCurrentBranch,
|
|
6599
|
-
detectWorktree,
|
|
6600
|
-
getDiff,
|
|
6601
|
-
analyze,
|
|
6602
|
-
readWatchFile,
|
|
6603
|
-
readReviewResult,
|
|
6604
|
-
consumeReviewResult,
|
|
6605
|
-
startReview,
|
|
6606
|
-
startWatch,
|
|
6607
|
-
readServerFile,
|
|
6608
|
-
isServerAlive,
|
|
6609
|
-
startGlobalServer,
|
|
6610
3841
|
resolveGitHubToken,
|
|
6611
3842
|
createGitHubClient,
|
|
6612
3843
|
fetchPullRequest,
|