diffprism 0.34.0 → 0.35.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/dist/bin.js +126 -224
- package/dist/chunk-DHCVZGHE.js +501 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/{chunk-NJKYNMAQ.js → chunk-LUUR6LNP.js} +317 -1802
- package/dist/chunk-QGWYCEJN.js +448 -0
- package/dist/mcp-server.js +75 -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-BfqEajZq.js +325 -0
- package/ui-dist/assets/{index-RCLz30rX.css → index-vG-fI3wH.css} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-1PUjTjRT.js +0 -324
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
getCurrentBranch,
|
|
3
|
+
getDiff,
|
|
4
|
+
listBranches,
|
|
5
|
+
listCommits,
|
|
6
|
+
parseDiff
|
|
7
|
+
} from "./chunk-QGWYCEJN.js";
|
|
8
|
+
import {
|
|
9
|
+
analyze
|
|
10
|
+
} from "./chunk-DHCVZGHE.js";
|
|
11
|
+
import {
|
|
12
|
+
__commonJS,
|
|
13
|
+
__toESM
|
|
14
|
+
} from "./chunk-JSBRDJBE.js";
|
|
26
15
|
|
|
27
16
|
// node_modules/.pnpm/fast-content-type-parse@2.0.1/node_modules/fast-content-type-parse/index.js
|
|
28
17
|
var require_fast_content_type_parse = __commonJS({
|
|
@@ -120,947 +109,15 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
120
109
|
}
|
|
121
110
|
});
|
|
122
111
|
|
|
123
|
-
// packages/
|
|
124
|
-
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
|
|
112
|
+
// packages/core/src/server-file.ts
|
|
1051
113
|
import fs from "fs";
|
|
1052
|
-
import
|
|
1053
|
-
import
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
cwd: cwd ?? process.cwd(),
|
|
1057
|
-
encoding: "utf-8"
|
|
1058
|
-
}).trim();
|
|
1059
|
-
return root;
|
|
114
|
+
import path from "path";
|
|
115
|
+
import os from "os";
|
|
116
|
+
function serverDir() {
|
|
117
|
+
return path.join(os.homedir(), ".diffprism");
|
|
1060
118
|
}
|
|
1061
|
-
function
|
|
1062
|
-
|
|
1063
|
-
return path3.join(gitRoot, ".diffprism", "watch.json");
|
|
119
|
+
function serverFilePath() {
|
|
120
|
+
return path.join(serverDir(), "server.json");
|
|
1064
121
|
}
|
|
1065
122
|
function isPidAlive(pid) {
|
|
1066
123
|
try {
|
|
@@ -1070,16 +127,15 @@ function isPidAlive(pid) {
|
|
|
1070
127
|
return false;
|
|
1071
128
|
}
|
|
1072
129
|
}
|
|
1073
|
-
function
|
|
1074
|
-
const
|
|
1075
|
-
const dir = path3.dirname(filePath);
|
|
130
|
+
function writeServerFile(info) {
|
|
131
|
+
const dir = serverDir();
|
|
1076
132
|
if (!fs.existsSync(dir)) {
|
|
1077
133
|
fs.mkdirSync(dir, { recursive: true });
|
|
1078
134
|
}
|
|
1079
|
-
fs.writeFileSync(
|
|
135
|
+
fs.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1080
136
|
}
|
|
1081
|
-
function
|
|
1082
|
-
const filePath =
|
|
137
|
+
function readServerFile() {
|
|
138
|
+
const filePath = serverFilePath();
|
|
1083
139
|
if (!fs.existsSync(filePath)) {
|
|
1084
140
|
return null;
|
|
1085
141
|
}
|
|
@@ -1095,305 +151,202 @@ function readWatchFile(cwd) {
|
|
|
1095
151
|
return null;
|
|
1096
152
|
}
|
|
1097
153
|
}
|
|
1098
|
-
function
|
|
154
|
+
function removeServerFile() {
|
|
1099
155
|
try {
|
|
1100
|
-
const filePath =
|
|
156
|
+
const filePath = serverFilePath();
|
|
1101
157
|
if (fs.existsSync(filePath)) {
|
|
1102
158
|
fs.unlinkSync(filePath);
|
|
1103
159
|
}
|
|
1104
160
|
} catch {
|
|
1105
161
|
}
|
|
1106
162
|
}
|
|
1107
|
-
function
|
|
1108
|
-
const
|
|
1109
|
-
|
|
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 {
|
|
163
|
+
async function isServerAlive() {
|
|
164
|
+
const info = readServerFile();
|
|
165
|
+
if (!info) {
|
|
1137
166
|
return null;
|
|
1138
167
|
}
|
|
1139
|
-
}
|
|
1140
|
-
function consumeReviewResult(cwd) {
|
|
1141
168
|
try {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
169
|
+
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
170
|
+
signal: AbortSignal.timeout(2e3)
|
|
171
|
+
});
|
|
172
|
+
if (response.ok) {
|
|
173
|
+
return info;
|
|
1145
174
|
}
|
|
1146
|
-
|
|
1147
|
-
const data = JSON.parse(raw);
|
|
1148
|
-
data.consumed = true;
|
|
1149
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
175
|
+
return null;
|
|
1150
176
|
} catch {
|
|
177
|
+
removeServerFile();
|
|
178
|
+
return null;
|
|
1151
179
|
}
|
|
1152
180
|
}
|
|
1153
181
|
|
|
1154
|
-
// packages/core/src/
|
|
1155
|
-
import
|
|
1156
|
-
import open from "open";
|
|
1157
|
-
|
|
1158
|
-
// packages/core/src/review-history.ts
|
|
182
|
+
// packages/core/src/server-client.ts
|
|
183
|
+
import { spawn } from "child_process";
|
|
1159
184
|
import fs2 from "fs";
|
|
1160
|
-
import
|
|
1161
|
-
import
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
if (!fs2.existsSync(
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
185
|
+
import path2 from "path";
|
|
186
|
+
import os2 from "os";
|
|
187
|
+
import { fileURLToPath } from "url";
|
|
188
|
+
async function ensureServer(options = {}) {
|
|
189
|
+
const existing = await isServerAlive();
|
|
190
|
+
if (existing) {
|
|
191
|
+
return existing;
|
|
192
|
+
}
|
|
193
|
+
const spawnArgs = options.spawnCommand ?? buildDefaultSpawnCommand(options);
|
|
194
|
+
const logDir = path2.join(os2.homedir(), ".diffprism");
|
|
195
|
+
if (!fs2.existsSync(logDir)) {
|
|
196
|
+
fs2.mkdirSync(logDir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
const logPath = path2.join(logDir, "server.log");
|
|
199
|
+
const logFd = fs2.openSync(logPath, "a");
|
|
200
|
+
const [cmd, ...args] = spawnArgs;
|
|
201
|
+
const child = spawn(cmd, args, {
|
|
202
|
+
detached: true,
|
|
203
|
+
stdio: ["ignore", logFd, logFd],
|
|
204
|
+
env: { ...process.env }
|
|
205
|
+
});
|
|
206
|
+
child.unref();
|
|
207
|
+
fs2.closeSync(logFd);
|
|
208
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
209
|
+
const startTime = Date.now();
|
|
210
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
212
|
+
const info = await isServerAlive();
|
|
213
|
+
if (info) {
|
|
214
|
+
return info;
|
|
215
|
+
}
|
|
1186
216
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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);
|
|
217
|
+
throw new Error(
|
|
218
|
+
`DiffPrism server failed to start within ${timeoutMs / 1e3}s. Check logs at ${logPath}`
|
|
219
|
+
);
|
|
1195
220
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
let
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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");
|
|
221
|
+
function buildDefaultSpawnCommand(options) {
|
|
222
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
223
|
+
const thisDir = path2.dirname(thisFile);
|
|
224
|
+
const workspaceRoot = path2.resolve(thisDir, "..", "..", "..");
|
|
225
|
+
const devBin = path2.join(workspaceRoot, "cli", "bin", "diffprism.mjs");
|
|
226
|
+
let binPath = "diffprism";
|
|
227
|
+
if (fs2.existsSync(devBin)) {
|
|
228
|
+
binPath = devBin;
|
|
229
|
+
} else {
|
|
230
|
+
let searchDir = thisDir;
|
|
231
|
+
while (searchDir !== path2.dirname(searchDir)) {
|
|
232
|
+
const candidate = path2.join(
|
|
233
|
+
searchDir,
|
|
234
|
+
"node_modules",
|
|
235
|
+
".bin",
|
|
236
|
+
"diffprism"
|
|
237
|
+
);
|
|
238
|
+
if (fs2.existsSync(candidate)) {
|
|
239
|
+
binPath = candidate;
|
|
240
|
+
break;
|
|
1293
241
|
}
|
|
1294
|
-
|
|
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));
|
|
242
|
+
searchDir = path2.dirname(searchDir);
|
|
1303
243
|
}
|
|
1304
244
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
await new Promise((resolve2) => {
|
|
1391
|
-
httpServer.close(() => resolve2());
|
|
1392
|
-
});
|
|
245
|
+
const args = [process.execPath, binPath, "server", "--_daemon"];
|
|
246
|
+
if (options.dev) {
|
|
247
|
+
args.push("--dev");
|
|
248
|
+
}
|
|
249
|
+
return args;
|
|
250
|
+
}
|
|
251
|
+
async function submitReviewToServer(serverInfo, diffRef, options = {}) {
|
|
252
|
+
const cwd = options.cwd ?? process.cwd();
|
|
253
|
+
const projectPath = options.projectPath ?? cwd;
|
|
254
|
+
let payload;
|
|
255
|
+
if (options.injectedPayload) {
|
|
256
|
+
payload = options.injectedPayload;
|
|
257
|
+
} else {
|
|
258
|
+
const { getDiff: getDiff2, getCurrentBranch: getCurrentBranch2, detectWorktree } = await import("./src-AMCPIYDZ.js");
|
|
259
|
+
const { analyze: analyze2 } = await import("./src-JMPTSU3P.js");
|
|
260
|
+
const { diffSet, rawDiff } = getDiff2(diffRef, { cwd });
|
|
261
|
+
if (diffSet.files.length === 0) {
|
|
262
|
+
return {
|
|
263
|
+
result: {
|
|
264
|
+
decision: "approved",
|
|
265
|
+
comments: [],
|
|
266
|
+
summary: "No changes to review."
|
|
267
|
+
},
|
|
268
|
+
sessionId: ""
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const briefing = analyze2(diffSet);
|
|
272
|
+
const currentBranch = getCurrentBranch2({ cwd });
|
|
273
|
+
const worktreeInfo = detectWorktree({ cwd });
|
|
274
|
+
payload = {
|
|
275
|
+
reviewId: "",
|
|
276
|
+
// Server assigns the real ID
|
|
277
|
+
diffSet,
|
|
278
|
+
rawDiff,
|
|
279
|
+
briefing,
|
|
280
|
+
metadata: {
|
|
281
|
+
title: options.title,
|
|
282
|
+
description: options.description,
|
|
283
|
+
reasoning: options.reasoning,
|
|
284
|
+
currentBranch,
|
|
285
|
+
worktree: worktreeInfo.isWorktree ? {
|
|
286
|
+
isWorktree: true,
|
|
287
|
+
worktreePath: worktreeInfo.worktreePath,
|
|
288
|
+
mainWorktreePath: worktreeInfo.mainWorktreePath
|
|
289
|
+
} : void 0
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const createResponse = await fetch(
|
|
294
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews`,
|
|
295
|
+
{
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "Content-Type": "application/json" },
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
payload,
|
|
300
|
+
projectPath,
|
|
301
|
+
diffRef: options.diffRef ?? diffRef
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
if (!createResponse.ok) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Global server returned ${createResponse.status} on create`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const { sessionId } = await createResponse.json();
|
|
311
|
+
if (options.annotations?.length) {
|
|
312
|
+
for (const ann of options.annotations) {
|
|
313
|
+
await fetch(
|
|
314
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations`,
|
|
315
|
+
{
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "Content-Type": "application/json" },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
file: ann.file,
|
|
320
|
+
line: ann.line,
|
|
321
|
+
body: ann.body,
|
|
322
|
+
type: ann.type,
|
|
323
|
+
confidence: ann.confidence ?? 1,
|
|
324
|
+
category: ann.category ?? "other",
|
|
325
|
+
source: {
|
|
326
|
+
agent: ann.source_agent ?? "unknown",
|
|
327
|
+
tool: "open_review"
|
|
328
|
+
}
|
|
329
|
+
})
|
|
1393
330
|
}
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const pollIntervalMs = 2e3;
|
|
335
|
+
const maxWaitMs = options.timeoutMs ?? 6e5;
|
|
336
|
+
const start = Date.now();
|
|
337
|
+
while (Date.now() - start < maxWaitMs) {
|
|
338
|
+
const resultResponse = await fetch(
|
|
339
|
+
`http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/result`
|
|
340
|
+
);
|
|
341
|
+
if (resultResponse.ok) {
|
|
342
|
+
const data = await resultResponse.json();
|
|
343
|
+
if (data.result) {
|
|
344
|
+
return { result: data.result, sessionId };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
348
|
+
}
|
|
349
|
+
throw new Error("Review timed out waiting for submission.");
|
|
1397
350
|
}
|
|
1398
351
|
|
|
1399
352
|
// packages/core/src/diff-utils.ts
|
|
@@ -1494,32 +447,18 @@ function createDiffPoller(options) {
|
|
|
1494
447
|
};
|
|
1495
448
|
}
|
|
1496
449
|
|
|
1497
|
-
// packages/core/src/
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
}
|
|
450
|
+
// packages/core/src/global-server.ts
|
|
451
|
+
import http2 from "http";
|
|
452
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
453
|
+
import getPort from "get-port";
|
|
454
|
+
import open from "open";
|
|
455
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1517
456
|
|
|
1518
457
|
// packages/core/src/ui-server.ts
|
|
1519
|
-
import
|
|
458
|
+
import http from "http";
|
|
1520
459
|
import fs3 from "fs";
|
|
1521
|
-
import
|
|
1522
|
-
import { fileURLToPath } from "url";
|
|
460
|
+
import path3 from "path";
|
|
461
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1523
462
|
var MIME_TYPES = {
|
|
1524
463
|
".html": "text/html",
|
|
1525
464
|
".js": "application/javascript",
|
|
@@ -1532,15 +471,15 @@ var MIME_TYPES = {
|
|
|
1532
471
|
".woff2": "font/woff2"
|
|
1533
472
|
};
|
|
1534
473
|
function resolveUiDist() {
|
|
1535
|
-
const thisFile =
|
|
1536
|
-
const thisDir =
|
|
1537
|
-
const publishedUiDist =
|
|
1538
|
-
if (fs3.existsSync(
|
|
474
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
475
|
+
const thisDir = path3.dirname(thisFile);
|
|
476
|
+
const publishedUiDist = path3.resolve(thisDir, "..", "ui-dist");
|
|
477
|
+
if (fs3.existsSync(path3.join(publishedUiDist, "index.html"))) {
|
|
1539
478
|
return publishedUiDist;
|
|
1540
479
|
}
|
|
1541
|
-
const workspaceRoot =
|
|
1542
|
-
const devUiDist =
|
|
1543
|
-
if (fs3.existsSync(
|
|
480
|
+
const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
|
|
481
|
+
const devUiDist = path3.join(workspaceRoot, "packages", "ui", "dist");
|
|
482
|
+
if (fs3.existsSync(path3.join(devUiDist, "index.html"))) {
|
|
1544
483
|
return devUiDist;
|
|
1545
484
|
}
|
|
1546
485
|
throw new Error(
|
|
@@ -1548,11 +487,11 @@ function resolveUiDist() {
|
|
|
1548
487
|
);
|
|
1549
488
|
}
|
|
1550
489
|
function resolveUiRoot() {
|
|
1551
|
-
const thisFile =
|
|
1552
|
-
const thisDir =
|
|
1553
|
-
const workspaceRoot =
|
|
1554
|
-
const uiRoot =
|
|
1555
|
-
if (fs3.existsSync(
|
|
490
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
491
|
+
const thisDir = path3.dirname(thisFile);
|
|
492
|
+
const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
|
|
493
|
+
const uiRoot = path3.join(workspaceRoot, "packages", "ui");
|
|
494
|
+
if (fs3.existsSync(path3.join(uiRoot, "index.html"))) {
|
|
1556
495
|
return uiRoot;
|
|
1557
496
|
}
|
|
1558
497
|
throw new Error(
|
|
@@ -1570,13 +509,13 @@ async function startViteDevServer(uiRoot, port, silent) {
|
|
|
1570
509
|
return vite;
|
|
1571
510
|
}
|
|
1572
511
|
function createStaticServer(distPath, port) {
|
|
1573
|
-
const server =
|
|
512
|
+
const server = http.createServer((req, res) => {
|
|
1574
513
|
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
1575
|
-
let filePath =
|
|
514
|
+
let filePath = path3.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
1576
515
|
if (!fs3.existsSync(filePath)) {
|
|
1577
|
-
filePath =
|
|
516
|
+
filePath = path3.join(distPath, "index.html");
|
|
1578
517
|
}
|
|
1579
|
-
const ext =
|
|
518
|
+
const ext = path3.extname(filePath);
|
|
1580
519
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1581
520
|
try {
|
|
1582
521
|
const content = fs3.readFileSync(filePath);
|
|
@@ -1593,476 +532,50 @@ function createStaticServer(distPath, port) {
|
|
|
1593
532
|
});
|
|
1594
533
|
}
|
|
1595
534
|
|
|
1596
|
-
// packages/core/src/
|
|
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
|
|
535
|
+
// packages/core/src/review-history.ts
|
|
1812
536
|
import fs4 from "fs";
|
|
1813
|
-
import
|
|
1814
|
-
import
|
|
1815
|
-
function
|
|
1816
|
-
return
|
|
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
|
-
}
|
|
537
|
+
import path4 from "path";
|
|
538
|
+
import { randomUUID } from "crypto";
|
|
539
|
+
function generateEntryId() {
|
|
540
|
+
return randomUUID();
|
|
1828
541
|
}
|
|
1829
|
-
function
|
|
1830
|
-
|
|
1831
|
-
if (!fs4.existsSync(dir)) {
|
|
1832
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
1833
|
-
}
|
|
1834
|
-
fs4.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
542
|
+
function getHistoryPath(projectDir) {
|
|
543
|
+
return path4.join(projectDir, ".diffprism", "history", "reviews.json");
|
|
1835
544
|
}
|
|
1836
|
-
function
|
|
1837
|
-
const filePath =
|
|
545
|
+
function readHistory(projectDir) {
|
|
546
|
+
const filePath = getHistoryPath(projectDir);
|
|
1838
547
|
if (!fs4.existsSync(filePath)) {
|
|
1839
|
-
return
|
|
548
|
+
return { version: 1, entries: [] };
|
|
1840
549
|
}
|
|
1841
550
|
try {
|
|
1842
551
|
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1843
|
-
const
|
|
1844
|
-
|
|
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
|
-
}
|
|
552
|
+
const parsed = JSON.parse(raw);
|
|
553
|
+
return parsed;
|
|
1859
554
|
} catch {
|
|
555
|
+
return { version: 1, entries: [] };
|
|
1860
556
|
}
|
|
1861
557
|
}
|
|
1862
|
-
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
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;
|
|
558
|
+
function appendHistory(projectDir, entry) {
|
|
559
|
+
const filePath = getHistoryPath(projectDir);
|
|
560
|
+
const dir = path4.dirname(filePath);
|
|
561
|
+
if (!fs4.existsSync(dir)) {
|
|
562
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1878
563
|
}
|
|
564
|
+
const history = readHistory(projectDir);
|
|
565
|
+
history.entries.push(entry);
|
|
566
|
+
history.entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
567
|
+
fs4.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
|
|
1879
568
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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 };
|
|
569
|
+
function getRecentHistory(projectDir, limit = 50) {
|
|
570
|
+
const history = readHistory(projectDir);
|
|
571
|
+
return history.entries.slice(-limit);
|
|
2054
572
|
}
|
|
2055
573
|
|
|
2056
574
|
// 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
575
|
var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
|
|
2063
576
|
var ABANDONED_TTL_MS = 60 * 60 * 1e3;
|
|
2064
577
|
var CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
2065
|
-
var
|
|
578
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
2066
579
|
var clientSessions = /* @__PURE__ */ new Map();
|
|
2067
580
|
var sessionWatchers = /* @__PURE__ */ new Map();
|
|
2068
581
|
var serverPollInterval = 2e3;
|
|
@@ -2124,7 +637,7 @@ function broadcastToAll(msg) {
|
|
|
2124
637
|
if (!wss) return;
|
|
2125
638
|
const data = JSON.stringify(msg);
|
|
2126
639
|
for (const client of wss.clients) {
|
|
2127
|
-
if (client.readyState ===
|
|
640
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2128
641
|
client.send(data);
|
|
2129
642
|
}
|
|
2130
643
|
}
|
|
@@ -2133,7 +646,7 @@ function sendToSessionClients(sessionId, msg) {
|
|
|
2133
646
|
if (!wss) return;
|
|
2134
647
|
const data = JSON.stringify(msg);
|
|
2135
648
|
for (const [client, sid] of clientSessions.entries()) {
|
|
2136
|
-
if (sid === sessionId && client.readyState ===
|
|
649
|
+
if (sid === sessionId && client.readyState === WebSocket.OPEN) {
|
|
2137
650
|
client.send(data);
|
|
2138
651
|
}
|
|
2139
652
|
}
|
|
@@ -2157,7 +670,7 @@ function broadcastSessionRemoved(sessionId) {
|
|
|
2157
670
|
}
|
|
2158
671
|
function hasViewersForSession(sessionId) {
|
|
2159
672
|
for (const [client, sid] of clientSessions.entries()) {
|
|
2160
|
-
if (sid === sessionId && client.readyState ===
|
|
673
|
+
if (sid === sessionId && client.readyState === WebSocket.OPEN) {
|
|
2161
674
|
return true;
|
|
2162
675
|
}
|
|
2163
676
|
}
|
|
@@ -2165,14 +678,14 @@ function hasViewersForSession(sessionId) {
|
|
|
2165
678
|
}
|
|
2166
679
|
function startSessionWatcher(sessionId) {
|
|
2167
680
|
if (sessionWatchers.has(sessionId)) return;
|
|
2168
|
-
const session =
|
|
681
|
+
const session = sessions.get(sessionId);
|
|
2169
682
|
if (!session?.diffRef) return;
|
|
2170
683
|
const poller = createDiffPoller({
|
|
2171
684
|
diffRef: session.diffRef,
|
|
2172
685
|
cwd: session.projectPath,
|
|
2173
686
|
pollInterval: serverPollInterval,
|
|
2174
687
|
onDiffChanged: (updatePayload) => {
|
|
2175
|
-
const s =
|
|
688
|
+
const s = sessions.get(sessionId);
|
|
2176
689
|
if (!s) return;
|
|
2177
690
|
s.payload = {
|
|
2178
691
|
...s.payload,
|
|
@@ -2205,7 +718,7 @@ function stopSessionWatcher(sessionId) {
|
|
|
2205
718
|
}
|
|
2206
719
|
}
|
|
2207
720
|
function startAllWatchers() {
|
|
2208
|
-
for (const [id, session] of
|
|
721
|
+
for (const [id, session] of sessions.entries()) {
|
|
2209
722
|
if (session.diffRef && !sessionWatchers.has(id)) {
|
|
2210
723
|
startSessionWatcher(id);
|
|
2211
724
|
}
|
|
@@ -2220,15 +733,16 @@ function stopAllWatchers() {
|
|
|
2220
733
|
function hasConnectedClients() {
|
|
2221
734
|
if (!wss) return false;
|
|
2222
735
|
for (const client of wss.clients) {
|
|
2223
|
-
if (client.readyState ===
|
|
736
|
+
if (client.readyState === WebSocket.OPEN) return true;
|
|
2224
737
|
}
|
|
2225
738
|
return false;
|
|
2226
739
|
}
|
|
2227
740
|
function broadcastSessionList() {
|
|
2228
|
-
const summaries = Array.from(
|
|
741
|
+
const summaries = Array.from(sessions.values()).map(toSummary);
|
|
2229
742
|
broadcastToAll({ type: "session:list", payload: summaries });
|
|
2230
743
|
}
|
|
2231
744
|
function recordReviewHistory(session, result) {
|
|
745
|
+
if (session.projectPath.startsWith("github:")) return;
|
|
2232
746
|
try {
|
|
2233
747
|
const { payload } = session;
|
|
2234
748
|
const entry = {
|
|
@@ -2266,7 +780,7 @@ async function handleApiRequest(req, res) {
|
|
|
2266
780
|
jsonResponse(res, 200, {
|
|
2267
781
|
running: true,
|
|
2268
782
|
pid: process.pid,
|
|
2269
|
-
sessions:
|
|
783
|
+
sessions: sessions.size,
|
|
2270
784
|
uptime: process.uptime()
|
|
2271
785
|
});
|
|
2272
786
|
return true;
|
|
@@ -2276,7 +790,7 @@ async function handleApiRequest(req, res) {
|
|
|
2276
790
|
const body = await readBody(req);
|
|
2277
791
|
const { payload, projectPath, diffRef } = JSON.parse(body);
|
|
2278
792
|
let existingSession;
|
|
2279
|
-
for (const session of
|
|
793
|
+
for (const session of sessions.values()) {
|
|
2280
794
|
if (session.projectPath === projectPath) {
|
|
2281
795
|
existingSession = session;
|
|
2282
796
|
break;
|
|
@@ -2298,7 +812,7 @@ async function handleApiRequest(req, res) {
|
|
|
2298
812
|
existingSession.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
2299
813
|
existingSession.hasNewChanges = false;
|
|
2300
814
|
existingSession.annotations = [];
|
|
2301
|
-
if (diffRef
|
|
815
|
+
if (diffRef) {
|
|
2302
816
|
startSessionWatcher(sessionId);
|
|
2303
817
|
}
|
|
2304
818
|
if (hasViewersForSession(sessionId)) {
|
|
@@ -2329,8 +843,8 @@ async function handleApiRequest(req, res) {
|
|
|
2329
843
|
hasNewChanges: false,
|
|
2330
844
|
annotations: []
|
|
2331
845
|
};
|
|
2332
|
-
|
|
2333
|
-
if (diffRef
|
|
846
|
+
sessions.set(sessionId, session);
|
|
847
|
+
if (diffRef) {
|
|
2334
848
|
startSessionWatcher(sessionId);
|
|
2335
849
|
}
|
|
2336
850
|
broadcastToAll({
|
|
@@ -2346,13 +860,13 @@ async function handleApiRequest(req, res) {
|
|
|
2346
860
|
return true;
|
|
2347
861
|
}
|
|
2348
862
|
if (method === "GET" && url === "/api/reviews") {
|
|
2349
|
-
const summaries = Array.from(
|
|
863
|
+
const summaries = Array.from(sessions.values()).map(toSummary);
|
|
2350
864
|
jsonResponse(res, 200, { sessions: summaries });
|
|
2351
865
|
return true;
|
|
2352
866
|
}
|
|
2353
867
|
const getReviewParams = matchRoute(method, url, "GET", "/api/reviews/:id");
|
|
2354
868
|
if (getReviewParams) {
|
|
2355
|
-
const session =
|
|
869
|
+
const session = sessions.get(getReviewParams.id);
|
|
2356
870
|
if (!session) {
|
|
2357
871
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2358
872
|
return true;
|
|
@@ -2362,7 +876,7 @@ async function handleApiRequest(req, res) {
|
|
|
2362
876
|
}
|
|
2363
877
|
const postResultParams = matchRoute(method, url, "POST", "/api/reviews/:id/result");
|
|
2364
878
|
if (postResultParams) {
|
|
2365
|
-
const session =
|
|
879
|
+
const session = sessions.get(postResultParams.id);
|
|
2366
880
|
if (!session) {
|
|
2367
881
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2368
882
|
return true;
|
|
@@ -2386,7 +900,7 @@ async function handleApiRequest(req, res) {
|
|
|
2386
900
|
}
|
|
2387
901
|
const getResultParams = matchRoute(method, url, "GET", "/api/reviews/:id/result");
|
|
2388
902
|
if (getResultParams) {
|
|
2389
|
-
const session =
|
|
903
|
+
const session = sessions.get(getResultParams.id);
|
|
2390
904
|
if (!session) {
|
|
2391
905
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2392
906
|
return true;
|
|
@@ -2400,7 +914,7 @@ async function handleApiRequest(req, res) {
|
|
|
2400
914
|
}
|
|
2401
915
|
const postContextParams = matchRoute(method, url, "POST", "/api/reviews/:id/context");
|
|
2402
916
|
if (postContextParams) {
|
|
2403
|
-
const session =
|
|
917
|
+
const session = sessions.get(postContextParams.id);
|
|
2404
918
|
if (!session) {
|
|
2405
919
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2406
920
|
return true;
|
|
@@ -2429,7 +943,7 @@ async function handleApiRequest(req, res) {
|
|
|
2429
943
|
}
|
|
2430
944
|
const postAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations");
|
|
2431
945
|
if (postAnnotationParams) {
|
|
2432
|
-
const session =
|
|
946
|
+
const session = sessions.get(postAnnotationParams.id);
|
|
2433
947
|
if (!session) {
|
|
2434
948
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2435
949
|
return true;
|
|
@@ -2462,7 +976,7 @@ async function handleApiRequest(req, res) {
|
|
|
2462
976
|
}
|
|
2463
977
|
const getAnnotationsParams = matchRoute(method, url, "GET", "/api/reviews/:id/annotations");
|
|
2464
978
|
if (getAnnotationsParams) {
|
|
2465
|
-
const session =
|
|
979
|
+
const session = sessions.get(getAnnotationsParams.id);
|
|
2466
980
|
if (!session) {
|
|
2467
981
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2468
982
|
return true;
|
|
@@ -2472,7 +986,7 @@ async function handleApiRequest(req, res) {
|
|
|
2472
986
|
}
|
|
2473
987
|
const dismissAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/dismiss");
|
|
2474
988
|
if (dismissAnnotationParams) {
|
|
2475
|
-
const session =
|
|
989
|
+
const session = sessions.get(dismissAnnotationParams.id);
|
|
2476
990
|
if (!session) {
|
|
2477
991
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2478
992
|
return true;
|
|
@@ -2493,7 +1007,7 @@ async function handleApiRequest(req, res) {
|
|
|
2493
1007
|
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
2494
1008
|
if (deleteParams) {
|
|
2495
1009
|
stopSessionWatcher(deleteParams.id);
|
|
2496
|
-
if (
|
|
1010
|
+
if (sessions.delete(deleteParams.id)) {
|
|
2497
1011
|
broadcastSessionRemoved(deleteParams.id);
|
|
2498
1012
|
jsonResponse(res, 200, { ok: true });
|
|
2499
1013
|
} else {
|
|
@@ -2503,11 +1017,15 @@ async function handleApiRequest(req, res) {
|
|
|
2503
1017
|
}
|
|
2504
1018
|
const getRefsParams = matchRoute(method, url, "GET", "/api/reviews/:id/refs");
|
|
2505
1019
|
if (getRefsParams) {
|
|
2506
|
-
const session =
|
|
1020
|
+
const session = sessions.get(getRefsParams.id);
|
|
2507
1021
|
if (!session) {
|
|
2508
1022
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2509
1023
|
return true;
|
|
2510
1024
|
}
|
|
1025
|
+
if (session.projectPath.startsWith("github:")) {
|
|
1026
|
+
jsonResponse(res, 400, { error: "Ref listing not available for GitHub PRs" });
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
2511
1029
|
try {
|
|
2512
1030
|
const branches = listBranches({ cwd: session.projectPath });
|
|
2513
1031
|
const commits = listCommits({ cwd: session.projectPath });
|
|
@@ -2520,11 +1038,15 @@ async function handleApiRequest(req, res) {
|
|
|
2520
1038
|
}
|
|
2521
1039
|
const postCompareParams = matchRoute(method, url, "POST", "/api/reviews/:id/compare");
|
|
2522
1040
|
if (postCompareParams) {
|
|
2523
|
-
const session =
|
|
1041
|
+
const session = sessions.get(postCompareParams.id);
|
|
2524
1042
|
if (!session) {
|
|
2525
1043
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2526
1044
|
return true;
|
|
2527
1045
|
}
|
|
1046
|
+
if (session.projectPath.startsWith("github:")) {
|
|
1047
|
+
jsonResponse(res, 400, { error: "Ref comparison not available for GitHub PRs" });
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
2528
1050
|
try {
|
|
2529
1051
|
const body = await readBody(req);
|
|
2530
1052
|
const { ref } = JSON.parse(body);
|
|
@@ -2568,7 +1090,7 @@ async function handleApiRequest(req, res) {
|
|
|
2568
1090
|
}
|
|
2569
1091
|
const getSessionHistoryParams = matchRoute(method, url, "GET", "/api/reviews/:id/history");
|
|
2570
1092
|
if (getSessionHistoryParams) {
|
|
2571
|
-
const session =
|
|
1093
|
+
const session = sessions.get(getSessionHistoryParams.id);
|
|
2572
1094
|
if (!session) {
|
|
2573
1095
|
jsonResponse(res, 404, { error: "Session not found" });
|
|
2574
1096
|
return true;
|
|
@@ -2607,40 +1129,41 @@ async function startGlobalServer(options = {}) {
|
|
|
2607
1129
|
wsPort: preferredWsPort = 24681,
|
|
2608
1130
|
silent = false,
|
|
2609
1131
|
dev = false,
|
|
2610
|
-
pollInterval = 2e3
|
|
1132
|
+
pollInterval = 2e3,
|
|
1133
|
+
openBrowser = true
|
|
2611
1134
|
} = options;
|
|
2612
1135
|
serverPollInterval = pollInterval;
|
|
2613
1136
|
const [httpPort, wsPort] = await Promise.all([
|
|
2614
|
-
|
|
2615
|
-
|
|
1137
|
+
getPort({ port: preferredHttpPort }),
|
|
1138
|
+
getPort({ port: preferredWsPort })
|
|
2616
1139
|
]);
|
|
2617
1140
|
let uiPort;
|
|
2618
1141
|
let uiHttpServer = null;
|
|
2619
1142
|
let viteServer = null;
|
|
2620
1143
|
if (dev) {
|
|
2621
|
-
uiPort = await
|
|
1144
|
+
uiPort = await getPort();
|
|
2622
1145
|
const uiRoot = resolveUiRoot();
|
|
2623
1146
|
viteServer = await startViteDevServer(uiRoot, uiPort, silent);
|
|
2624
1147
|
} else {
|
|
2625
|
-
uiPort = await
|
|
1148
|
+
uiPort = await getPort();
|
|
2626
1149
|
const uiDist = resolveUiDist();
|
|
2627
1150
|
uiHttpServer = await createStaticServer(uiDist, uiPort);
|
|
2628
1151
|
}
|
|
2629
|
-
const httpServer =
|
|
1152
|
+
const httpServer = http2.createServer(async (req, res) => {
|
|
2630
1153
|
const handled = await handleApiRequest(req, res);
|
|
2631
1154
|
if (!handled) {
|
|
2632
1155
|
res.writeHead(404);
|
|
2633
1156
|
res.end("Not found");
|
|
2634
1157
|
}
|
|
2635
1158
|
});
|
|
2636
|
-
wss = new
|
|
1159
|
+
wss = new WebSocketServer({ port: wsPort });
|
|
2637
1160
|
wss.on("connection", (ws, req) => {
|
|
2638
1161
|
startAllWatchers();
|
|
2639
1162
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
2640
1163
|
const sessionId = url.searchParams.get("sessionId");
|
|
2641
1164
|
if (sessionId) {
|
|
2642
1165
|
clientSessions.set(ws, sessionId);
|
|
2643
|
-
const session =
|
|
1166
|
+
const session = sessions.get(sessionId);
|
|
2644
1167
|
if (session) {
|
|
2645
1168
|
session.status = "in_review";
|
|
2646
1169
|
session.hasNewChanges = false;
|
|
@@ -2658,14 +1181,14 @@ async function startGlobalServer(options = {}) {
|
|
|
2658
1181
|
}
|
|
2659
1182
|
}
|
|
2660
1183
|
} else {
|
|
2661
|
-
const summaries = Array.from(
|
|
1184
|
+
const summaries = Array.from(sessions.values()).map(toSummary);
|
|
2662
1185
|
const msg = {
|
|
2663
1186
|
type: "session:list",
|
|
2664
1187
|
payload: summaries
|
|
2665
1188
|
};
|
|
2666
1189
|
ws.send(JSON.stringify(msg));
|
|
2667
1190
|
if (summaries.length === 1) {
|
|
2668
|
-
const session =
|
|
1191
|
+
const session = sessions.get(summaries[0].id);
|
|
2669
1192
|
if (session) {
|
|
2670
1193
|
clientSessions.set(ws, session.id);
|
|
2671
1194
|
session.status = "in_review";
|
|
@@ -2690,7 +1213,7 @@ async function startGlobalServer(options = {}) {
|
|
|
2690
1213
|
if (msg.type === "review:submit") {
|
|
2691
1214
|
const sid = clientSessions.get(ws);
|
|
2692
1215
|
if (sid) {
|
|
2693
|
-
const session =
|
|
1216
|
+
const session = sessions.get(sid);
|
|
2694
1217
|
if (session) {
|
|
2695
1218
|
session.result = msg.payload;
|
|
2696
1219
|
session.status = "submitted";
|
|
@@ -2703,7 +1226,7 @@ async function startGlobalServer(options = {}) {
|
|
|
2703
1226
|
}
|
|
2704
1227
|
}
|
|
2705
1228
|
} else if (msg.type === "session:select") {
|
|
2706
|
-
const session =
|
|
1229
|
+
const session = sessions.get(msg.payload.sessionId);
|
|
2707
1230
|
if (session) {
|
|
2708
1231
|
clientSessions.set(ws, session.id);
|
|
2709
1232
|
session.status = "in_review";
|
|
@@ -2724,7 +1247,7 @@ async function startGlobalServer(options = {}) {
|
|
|
2724
1247
|
} else if (msg.type === "session:close") {
|
|
2725
1248
|
const closedId = msg.payload.sessionId;
|
|
2726
1249
|
stopSessionWatcher(closedId);
|
|
2727
|
-
const closedSession =
|
|
1250
|
+
const closedSession = sessions.get(closedId);
|
|
2728
1251
|
if (closedSession && !closedSession.result) {
|
|
2729
1252
|
closedSession.result = { decision: "dismissed", comments: [] };
|
|
2730
1253
|
closedSession.status = "submitted";
|
|
@@ -2733,7 +1256,7 @@ async function startGlobalServer(options = {}) {
|
|
|
2733
1256
|
} else if (msg.type === "diff:change_ref") {
|
|
2734
1257
|
const sid = clientSessions.get(ws);
|
|
2735
1258
|
if (sid) {
|
|
2736
|
-
const session =
|
|
1259
|
+
const session = sessions.get(sid);
|
|
2737
1260
|
if (session) {
|
|
2738
1261
|
const newRef = msg.payload.diffRef;
|
|
2739
1262
|
try {
|
|
@@ -2779,9 +1302,6 @@ async function startGlobalServer(options = {}) {
|
|
|
2779
1302
|
});
|
|
2780
1303
|
ws.on("close", () => {
|
|
2781
1304
|
clientSessions.delete(ws);
|
|
2782
|
-
if (!hasConnectedClients()) {
|
|
2783
|
-
stopAllWatchers();
|
|
2784
|
-
}
|
|
2785
1305
|
});
|
|
2786
1306
|
});
|
|
2787
1307
|
await new Promise((resolve, reject) => {
|
|
@@ -2790,12 +1310,12 @@ async function startGlobalServer(options = {}) {
|
|
|
2790
1310
|
});
|
|
2791
1311
|
function cleanupExpiredSessions() {
|
|
2792
1312
|
const now = Date.now();
|
|
2793
|
-
for (const [id, session] of
|
|
1313
|
+
for (const [id, session] of sessions.entries()) {
|
|
2794
1314
|
const age = now - session.createdAt;
|
|
2795
1315
|
const expired = session.status === "submitted" && age > SUBMITTED_TTL_MS || session.status === "pending" && age > ABANDONED_TTL_MS;
|
|
2796
1316
|
if (expired) {
|
|
2797
1317
|
stopSessionWatcher(id);
|
|
2798
|
-
|
|
1318
|
+
sessions.delete(id);
|
|
2799
1319
|
broadcastSessionRemoved(id);
|
|
2800
1320
|
}
|
|
2801
1321
|
}
|
|
@@ -2820,10 +1340,12 @@ Waiting for reviews...
|
|
|
2820
1340
|
`);
|
|
2821
1341
|
}
|
|
2822
1342
|
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&httpPort=${httpPort}&serverMode=true`;
|
|
2823
|
-
|
|
1343
|
+
if (openBrowser) {
|
|
1344
|
+
await open(uiUrl);
|
|
1345
|
+
}
|
|
2824
1346
|
reopenBrowserIfNeeded = () => {
|
|
2825
1347
|
if (!hasConnectedClients()) {
|
|
2826
|
-
|
|
1348
|
+
open(uiUrl);
|
|
2827
1349
|
}
|
|
2828
1350
|
};
|
|
2829
1351
|
async function stop() {
|
|
@@ -2837,7 +1359,7 @@ Waiting for reviews...
|
|
|
2837
1359
|
wss = null;
|
|
2838
1360
|
}
|
|
2839
1361
|
clientSessions.clear();
|
|
2840
|
-
|
|
1362
|
+
sessions.clear();
|
|
2841
1363
|
reopenBrowserIfNeeded = null;
|
|
2842
1364
|
await new Promise((resolve) => {
|
|
2843
1365
|
httpServer.close(() => resolve());
|
|
@@ -2854,17 +1376,17 @@ Waiting for reviews...
|
|
|
2854
1376
|
}
|
|
2855
1377
|
|
|
2856
1378
|
// packages/github/src/auth.ts
|
|
2857
|
-
import { execSync
|
|
1379
|
+
import { execSync } from "child_process";
|
|
2858
1380
|
import fs5 from "fs";
|
|
2859
|
-
import
|
|
2860
|
-
import
|
|
1381
|
+
import path5 from "path";
|
|
1382
|
+
import os3 from "os";
|
|
2861
1383
|
function resolveGitHubToken() {
|
|
2862
1384
|
const envToken = process.env.GITHUB_TOKEN;
|
|
2863
1385
|
if (envToken) {
|
|
2864
1386
|
return envToken;
|
|
2865
1387
|
}
|
|
2866
1388
|
try {
|
|
2867
|
-
const token =
|
|
1389
|
+
const token = execSync("gh auth token", {
|
|
2868
1390
|
encoding: "utf-8",
|
|
2869
1391
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2870
1392
|
}).trim();
|
|
@@ -2873,7 +1395,7 @@ function resolveGitHubToken() {
|
|
|
2873
1395
|
}
|
|
2874
1396
|
} catch {
|
|
2875
1397
|
}
|
|
2876
|
-
const configPath =
|
|
1398
|
+
const configPath = path5.join(os3.homedir(), ".diffprism", "config.json");
|
|
2877
1399
|
try {
|
|
2878
1400
|
if (fs5.existsSync(configPath)) {
|
|
2879
1401
|
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
@@ -3837,17 +2359,17 @@ function requestLog(octokit) {
|
|
|
3837
2359
|
octokit.log.debug("request", options);
|
|
3838
2360
|
const start = Date.now();
|
|
3839
2361
|
const requestOptions = octokit.request.endpoint.parse(options);
|
|
3840
|
-
const
|
|
2362
|
+
const path6 = requestOptions.url.replace(options.baseUrl, "");
|
|
3841
2363
|
return request2(options).then((response) => {
|
|
3842
2364
|
const requestId = response.headers["x-github-request-id"];
|
|
3843
2365
|
octokit.log.info(
|
|
3844
|
-
`${requestOptions.method} ${
|
|
2366
|
+
`${requestOptions.method} ${path6} - ${response.status} with id ${requestId} in ${Date.now() - start}ms`
|
|
3845
2367
|
);
|
|
3846
2368
|
return response;
|
|
3847
2369
|
}).catch((error) => {
|
|
3848
2370
|
const requestId = error.response?.headers["x-github-request-id"] || "UNKNOWN";
|
|
3849
2371
|
octokit.log.error(
|
|
3850
|
-
`${requestOptions.method} ${
|
|
2372
|
+
`${requestOptions.method} ${path6} - ${error.status} with id ${requestId} in ${Date.now() - start}ms`
|
|
3851
2373
|
);
|
|
3852
2374
|
throw error;
|
|
3853
2375
|
});
|
|
@@ -6586,18 +5108,11 @@ function buildReviewBody(result) {
|
|
|
6586
5108
|
}
|
|
6587
5109
|
|
|
6588
5110
|
export {
|
|
6589
|
-
getCurrentBranch,
|
|
6590
|
-
detectWorktree,
|
|
6591
|
-
getDiff,
|
|
6592
|
-
analyze,
|
|
6593
|
-
readWatchFile,
|
|
6594
|
-
readReviewResult,
|
|
6595
|
-
consumeReviewResult,
|
|
6596
|
-
startReview,
|
|
6597
|
-
startWatch,
|
|
6598
5111
|
readServerFile,
|
|
6599
5112
|
isServerAlive,
|
|
6600
5113
|
startGlobalServer,
|
|
5114
|
+
ensureServer,
|
|
5115
|
+
submitReviewToServer,
|
|
6601
5116
|
resolveGitHubToken,
|
|
6602
5117
|
createGitHubClient,
|
|
6603
5118
|
fetchPullRequest,
|