diffprism 0.27.0 → 0.30.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.
|
@@ -116,6 +116,46 @@ function listCommits(options) {
|
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
function detectWorktree(options) {
|
|
120
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
121
|
+
try {
|
|
122
|
+
const gitDir = execSync("git rev-parse --git-dir", {
|
|
123
|
+
cwd,
|
|
124
|
+
encoding: "utf-8",
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
126
|
+
}).trim();
|
|
127
|
+
const gitCommonDir = execSync("git rev-parse --git-common-dir", {
|
|
128
|
+
cwd,
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
131
|
+
}).trim();
|
|
132
|
+
const resolvedGitDir = path.resolve(cwd, gitDir);
|
|
133
|
+
const resolvedCommonDir = path.resolve(cwd, gitCommonDir);
|
|
134
|
+
const isWorktree = resolvedGitDir !== resolvedCommonDir;
|
|
135
|
+
if (!isWorktree) {
|
|
136
|
+
return { isWorktree: false };
|
|
137
|
+
}
|
|
138
|
+
const worktreePath = execSync("git rev-parse --show-toplevel", {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
142
|
+
}).trim();
|
|
143
|
+
const mainWorktreePath = path.dirname(resolvedCommonDir);
|
|
144
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
145
|
+
cwd,
|
|
146
|
+
encoding: "utf-8",
|
|
147
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
148
|
+
}).trim();
|
|
149
|
+
return {
|
|
150
|
+
isWorktree: true,
|
|
151
|
+
worktreePath,
|
|
152
|
+
mainWorktreePath,
|
|
153
|
+
branch: branch === "HEAD" ? void 0 : branch
|
|
154
|
+
};
|
|
155
|
+
} catch {
|
|
156
|
+
return { isWorktree: false };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
119
159
|
function getUntrackedDiffs(cwd) {
|
|
120
160
|
let untrackedList;
|
|
121
161
|
try {
|
|
@@ -398,17 +438,138 @@ function getWorkingCopyDiff(options) {
|
|
|
398
438
|
}
|
|
399
439
|
|
|
400
440
|
// packages/analysis/src/deterministic.ts
|
|
441
|
+
var MECHANICAL_CONFIG_PATTERNS = [
|
|
442
|
+
/\.config\./,
|
|
443
|
+
/\.eslintrc/,
|
|
444
|
+
/\.prettierrc/,
|
|
445
|
+
/tsconfig.*\.json$/,
|
|
446
|
+
/\.gitignore$/,
|
|
447
|
+
/\.lock$/
|
|
448
|
+
];
|
|
449
|
+
var API_SURFACE_PATTERNS = [
|
|
450
|
+
/\/api\//,
|
|
451
|
+
/\/routes\//
|
|
452
|
+
];
|
|
453
|
+
function isFormattingOnly(file) {
|
|
454
|
+
if (file.hunks.length === 0) return false;
|
|
455
|
+
for (const hunk of file.hunks) {
|
|
456
|
+
const adds = hunk.changes.filter((c) => c.type === "add").map((c) => c.content.replace(/\s/g, ""));
|
|
457
|
+
const deletes = hunk.changes.filter((c) => c.type === "delete").map((c) => c.content.replace(/\s/g, ""));
|
|
458
|
+
if (adds.length === 0 || deletes.length === 0) return false;
|
|
459
|
+
const deleteBag = [...deletes];
|
|
460
|
+
for (const add of adds) {
|
|
461
|
+
const idx = deleteBag.indexOf(add);
|
|
462
|
+
if (idx === -1) return false;
|
|
463
|
+
deleteBag.splice(idx, 1);
|
|
464
|
+
}
|
|
465
|
+
if (deleteBag.length > 0) return false;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
function isImportOnly(file) {
|
|
470
|
+
if (file.hunks.length === 0) return false;
|
|
471
|
+
const importPattern = /^\s*(import\s|export\s.*from\s|const\s+\w+\s*=\s*require\(|require\()/;
|
|
472
|
+
for (const hunk of file.hunks) {
|
|
473
|
+
for (const change of hunk.changes) {
|
|
474
|
+
if (change.type === "context") continue;
|
|
475
|
+
const trimmed = change.content.trim();
|
|
476
|
+
if (trimmed === "") continue;
|
|
477
|
+
if (!importPattern.test(trimmed)) return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
function isMechanicalConfigFile(path7) {
|
|
483
|
+
return MECHANICAL_CONFIG_PATTERNS.some((re) => re.test(path7));
|
|
484
|
+
}
|
|
485
|
+
function isApiSurface(file) {
|
|
486
|
+
if (API_SURFACE_PATTERNS.some((re) => re.test(file.path))) return true;
|
|
487
|
+
const basename = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
488
|
+
if ((basename === "index.ts" || basename === "index.js") && file.additions >= 10) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
401
493
|
function categorizeFiles(files) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
494
|
+
const critical = [];
|
|
495
|
+
const notable = [];
|
|
496
|
+
const mechanical = [];
|
|
497
|
+
const securityFlags = detectSecurityPatterns(files);
|
|
498
|
+
const complexityScores = computeComplexityScores(files);
|
|
499
|
+
const securityByFile = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const flag of securityFlags) {
|
|
501
|
+
const existing = securityByFile.get(flag.file) || [];
|
|
502
|
+
existing.push(flag);
|
|
503
|
+
securityByFile.set(flag.file, existing);
|
|
504
|
+
}
|
|
505
|
+
const complexityByFile = /* @__PURE__ */ new Map();
|
|
506
|
+
for (const score of complexityScores) {
|
|
507
|
+
complexityByFile.set(score.path, score);
|
|
508
|
+
}
|
|
509
|
+
for (const file of files) {
|
|
510
|
+
const description = `${file.status} (${file.language || "unknown"}) +${file.additions} -${file.deletions}`;
|
|
511
|
+
const fileSecurityFlags = securityByFile.get(file.path);
|
|
512
|
+
const fileComplexity = complexityByFile.get(file.path);
|
|
513
|
+
const criticalReasons = [];
|
|
514
|
+
if (fileSecurityFlags && fileSecurityFlags.length > 0) {
|
|
515
|
+
const patterns = fileSecurityFlags.map((f) => f.pattern);
|
|
516
|
+
const unique = [...new Set(patterns)];
|
|
517
|
+
criticalReasons.push(`security patterns detected: ${unique.join(", ")}`);
|
|
518
|
+
}
|
|
519
|
+
if (fileComplexity && fileComplexity.score >= 8) {
|
|
520
|
+
criticalReasons.push(`high complexity score (${fileComplexity.score}/10)`);
|
|
521
|
+
}
|
|
522
|
+
if (isApiSurface(file)) {
|
|
523
|
+
criticalReasons.push("modifies public API surface");
|
|
524
|
+
}
|
|
525
|
+
if (criticalReasons.length > 0) {
|
|
526
|
+
critical.push({
|
|
527
|
+
file: file.path,
|
|
528
|
+
description,
|
|
529
|
+
reason: `Critical: ${criticalReasons.join("; ")}`
|
|
530
|
+
});
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const isPureRename = file.status === "renamed" && file.additions === 0 && file.deletions === 0;
|
|
534
|
+
if (isPureRename) {
|
|
535
|
+
mechanical.push({
|
|
536
|
+
file: file.path,
|
|
537
|
+
description,
|
|
538
|
+
reason: "Mechanical: pure rename with no content changes"
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (isFormattingOnly(file)) {
|
|
543
|
+
mechanical.push({
|
|
544
|
+
file: file.path,
|
|
545
|
+
description,
|
|
546
|
+
reason: "Mechanical: formatting/whitespace-only changes"
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (isMechanicalConfigFile(file.path)) {
|
|
551
|
+
mechanical.push({
|
|
552
|
+
file: file.path,
|
|
553
|
+
description,
|
|
554
|
+
reason: "Mechanical: config file change"
|
|
555
|
+
});
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (file.hunks.length > 0 && isImportOnly(file)) {
|
|
559
|
+
mechanical.push({
|
|
560
|
+
file: file.path,
|
|
561
|
+
description,
|
|
562
|
+
reason: "Mechanical: import/require-only changes"
|
|
563
|
+
});
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
notable.push({
|
|
567
|
+
file: file.path,
|
|
568
|
+
description,
|
|
569
|
+
reason: "Notable: requires review"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return { critical, notable, mechanical };
|
|
412
573
|
}
|
|
413
574
|
function computeFileStats(files) {
|
|
414
575
|
return files.map((f) => ({
|
|
@@ -584,15 +745,15 @@ var CONFIG_PATTERNS = [
|
|
|
584
745
|
/vite\.config/,
|
|
585
746
|
/vitest\.config/
|
|
586
747
|
];
|
|
587
|
-
function isTestFile(
|
|
588
|
-
return TEST_PATTERNS.some((re) => re.test(
|
|
748
|
+
function isTestFile(path7) {
|
|
749
|
+
return TEST_PATTERNS.some((re) => re.test(path7));
|
|
589
750
|
}
|
|
590
|
-
function isNonCodeFile(
|
|
591
|
-
const ext =
|
|
751
|
+
function isNonCodeFile(path7) {
|
|
752
|
+
const ext = path7.slice(path7.lastIndexOf("."));
|
|
592
753
|
return NON_CODE_EXTENSIONS.has(ext);
|
|
593
754
|
}
|
|
594
|
-
function isConfigFile(
|
|
595
|
-
return CONFIG_PATTERNS.some((re) => re.test(
|
|
755
|
+
function isConfigFile(path7) {
|
|
756
|
+
return CONFIG_PATTERNS.some((re) => re.test(path7));
|
|
596
757
|
}
|
|
597
758
|
function detectTestCoverageGaps(files) {
|
|
598
759
|
const filePaths = new Set(files.map((f) => f.path));
|
|
@@ -764,20 +925,222 @@ function analyze(diffSet) {
|
|
|
764
925
|
};
|
|
765
926
|
}
|
|
766
927
|
|
|
928
|
+
// packages/core/src/watch-file.ts
|
|
929
|
+
import fs from "fs";
|
|
930
|
+
import path3 from "path";
|
|
931
|
+
import { execSync as execSync2 } from "child_process";
|
|
932
|
+
function findGitRoot(cwd) {
|
|
933
|
+
const root = execSync2("git rev-parse --show-toplevel", {
|
|
934
|
+
cwd: cwd ?? process.cwd(),
|
|
935
|
+
encoding: "utf-8"
|
|
936
|
+
}).trim();
|
|
937
|
+
return root;
|
|
938
|
+
}
|
|
939
|
+
function watchFilePath(cwd) {
|
|
940
|
+
const gitRoot = findGitRoot(cwd);
|
|
941
|
+
return path3.join(gitRoot, ".diffprism", "watch.json");
|
|
942
|
+
}
|
|
943
|
+
function isPidAlive(pid) {
|
|
944
|
+
try {
|
|
945
|
+
process.kill(pid, 0);
|
|
946
|
+
return true;
|
|
947
|
+
} catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function writeWatchFile(cwd, info) {
|
|
952
|
+
const filePath = watchFilePath(cwd);
|
|
953
|
+
const dir = path3.dirname(filePath);
|
|
954
|
+
if (!fs.existsSync(dir)) {
|
|
955
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
956
|
+
}
|
|
957
|
+
fs.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
|
|
958
|
+
}
|
|
959
|
+
function readWatchFile(cwd) {
|
|
960
|
+
const filePath = watchFilePath(cwd);
|
|
961
|
+
if (!fs.existsSync(filePath)) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
966
|
+
const info = JSON.parse(raw);
|
|
967
|
+
if (!isPidAlive(info.pid)) {
|
|
968
|
+
fs.unlinkSync(filePath);
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
return info;
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function removeWatchFile(cwd) {
|
|
977
|
+
try {
|
|
978
|
+
const filePath = watchFilePath(cwd);
|
|
979
|
+
if (fs.existsSync(filePath)) {
|
|
980
|
+
fs.unlinkSync(filePath);
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function reviewResultPath(cwd) {
|
|
986
|
+
const gitRoot = findGitRoot(cwd);
|
|
987
|
+
return path3.join(gitRoot, ".diffprism", "last-review.json");
|
|
988
|
+
}
|
|
989
|
+
function writeReviewResult(cwd, result) {
|
|
990
|
+
const filePath = reviewResultPath(cwd);
|
|
991
|
+
const dir = path3.dirname(filePath);
|
|
992
|
+
if (!fs.existsSync(dir)) {
|
|
993
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
994
|
+
}
|
|
995
|
+
const data = {
|
|
996
|
+
result,
|
|
997
|
+
timestamp: Date.now(),
|
|
998
|
+
consumed: false
|
|
999
|
+
};
|
|
1000
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1001
|
+
}
|
|
1002
|
+
function readReviewResult(cwd) {
|
|
1003
|
+
try {
|
|
1004
|
+
const filePath = reviewResultPath(cwd);
|
|
1005
|
+
if (!fs.existsSync(filePath)) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1009
|
+
const data = JSON.parse(raw);
|
|
1010
|
+
if (data.consumed) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
return data;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function consumeReviewResult(cwd) {
|
|
1019
|
+
try {
|
|
1020
|
+
const filePath = reviewResultPath(cwd);
|
|
1021
|
+
if (!fs.existsSync(filePath)) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1025
|
+
const data = JSON.parse(raw);
|
|
1026
|
+
data.consumed = true;
|
|
1027
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
767
1032
|
// packages/core/src/pipeline.ts
|
|
768
1033
|
import getPort from "get-port";
|
|
769
1034
|
import open from "open";
|
|
770
1035
|
|
|
771
|
-
// packages/core/src/
|
|
1036
|
+
// packages/core/src/watch-bridge.ts
|
|
1037
|
+
import http from "http";
|
|
772
1038
|
import { WebSocketServer, WebSocket } from "ws";
|
|
773
|
-
function
|
|
774
|
-
const wss2 = new WebSocketServer({ port });
|
|
1039
|
+
function createWatchBridge(port, callbacks) {
|
|
775
1040
|
let client = null;
|
|
776
|
-
let resultResolve = null;
|
|
777
|
-
let resultReject = null;
|
|
778
|
-
let pendingInit = null;
|
|
779
1041
|
let initPayload = null;
|
|
1042
|
+
let pendingInit = null;
|
|
780
1043
|
let closeTimer = null;
|
|
1044
|
+
let submitCallback = null;
|
|
1045
|
+
let resultReject = null;
|
|
1046
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
1047
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1048
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1049
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1050
|
+
if (req.method === "OPTIONS") {
|
|
1051
|
+
res.writeHead(204);
|
|
1052
|
+
res.end();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (req.method === "GET" && req.url === "/api/status") {
|
|
1056
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1057
|
+
res.end(JSON.stringify({ running: true, pid: process.pid }));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (req.method === "POST" && req.url === "/api/context") {
|
|
1061
|
+
let body = "";
|
|
1062
|
+
req.on("data", (chunk) => {
|
|
1063
|
+
body += chunk.toString();
|
|
1064
|
+
});
|
|
1065
|
+
req.on("end", () => {
|
|
1066
|
+
try {
|
|
1067
|
+
const payload = JSON.parse(body);
|
|
1068
|
+
callbacks.onContextUpdate(payload);
|
|
1069
|
+
sendToClient({ type: "context:update", payload });
|
|
1070
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1071
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1072
|
+
} catch {
|
|
1073
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1074
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (req.method === "POST" && req.url === "/api/refresh") {
|
|
1080
|
+
callbacks.onRefreshRequest();
|
|
1081
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1082
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const pathname = (req.url ?? "").split("?")[0];
|
|
1086
|
+
if (req.method === "GET" && (pathname === "/api/refs" || /^\/api\/reviews\/[^/]+\/refs$/.test(pathname))) {
|
|
1087
|
+
if (callbacks.onRefsRequest) {
|
|
1088
|
+
const refsPayload = await callbacks.onRefsRequest();
|
|
1089
|
+
if (refsPayload) {
|
|
1090
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1091
|
+
res.end(JSON.stringify(refsPayload));
|
|
1092
|
+
} else {
|
|
1093
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1094
|
+
res.end(JSON.stringify({ error: "Failed to list git refs" }));
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
res.writeHead(404);
|
|
1098
|
+
res.end("Not found");
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (req.method === "POST" && (pathname === "/api/compare" || /^\/api\/reviews\/[^/]+\/compare$/.test(pathname))) {
|
|
1103
|
+
if (callbacks.onCompareRequest) {
|
|
1104
|
+
let body = "";
|
|
1105
|
+
req.on("data", (chunk) => {
|
|
1106
|
+
body += chunk.toString();
|
|
1107
|
+
});
|
|
1108
|
+
req.on("end", async () => {
|
|
1109
|
+
try {
|
|
1110
|
+
const { ref } = JSON.parse(body);
|
|
1111
|
+
if (!ref) {
|
|
1112
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1113
|
+
res.end(JSON.stringify({ error: "Missing ref" }));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const success = await callbacks.onCompareRequest(ref);
|
|
1117
|
+
if (success) {
|
|
1118
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1119
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1120
|
+
} else {
|
|
1121
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1122
|
+
res.end(JSON.stringify({ error: "Failed to compute diff" }));
|
|
1123
|
+
}
|
|
1124
|
+
} catch {
|
|
1125
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1126
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
} else {
|
|
1130
|
+
res.writeHead(404);
|
|
1131
|
+
res.end("Not found");
|
|
1132
|
+
}
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
res.writeHead(404);
|
|
1136
|
+
res.end("Not found");
|
|
1137
|
+
});
|
|
1138
|
+
const wss2 = new WebSocketServer({ server: httpServer });
|
|
1139
|
+
function sendToClient(msg) {
|
|
1140
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
1141
|
+
client.send(JSON.stringify(msg));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
781
1144
|
wss2.on("connection", (ws) => {
|
|
782
1145
|
if (closeTimer) {
|
|
783
1146
|
clearTimeout(closeTimer);
|
|
@@ -786,20 +1149,16 @@ function createWsBridge(port) {
|
|
|
786
1149
|
client = ws;
|
|
787
1150
|
const payload = pendingInit ?? initPayload;
|
|
788
1151
|
if (payload) {
|
|
789
|
-
|
|
790
|
-
type: "review:init",
|
|
791
|
-
payload
|
|
792
|
-
};
|
|
793
|
-
ws.send(JSON.stringify(msg));
|
|
1152
|
+
sendToClient({ type: "review:init", payload });
|
|
794
1153
|
pendingInit = null;
|
|
795
1154
|
}
|
|
796
1155
|
ws.on("message", (data) => {
|
|
797
1156
|
try {
|
|
798
1157
|
const msg = JSON.parse(data.toString());
|
|
799
|
-
if (msg.type === "review:submit" &&
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1158
|
+
if (msg.type === "review:submit" && submitCallback) {
|
|
1159
|
+
submitCallback(msg.payload);
|
|
1160
|
+
} else if (msg.type === "diff:change_ref" && callbacks.onDiffRefChange) {
|
|
1161
|
+
callbacks.onDiffRefChange(msg.payload.diffRef);
|
|
803
1162
|
}
|
|
804
1163
|
} catch {
|
|
805
1164
|
}
|
|
@@ -808,56 +1167,184 @@ function createWsBridge(port) {
|
|
|
808
1167
|
client = null;
|
|
809
1168
|
if (resultReject) {
|
|
810
1169
|
closeTimer = setTimeout(() => {
|
|
1170
|
+
closeTimer = null;
|
|
811
1171
|
if (resultReject) {
|
|
812
1172
|
resultReject(new Error("Browser closed before review was submitted"));
|
|
813
|
-
resultResolve = null;
|
|
814
1173
|
resultReject = null;
|
|
1174
|
+
submitCallback = null;
|
|
815
1175
|
}
|
|
816
1176
|
}, 2e3);
|
|
1177
|
+
} else {
|
|
1178
|
+
closeTimer = setTimeout(() => {
|
|
1179
|
+
closeTimer = null;
|
|
1180
|
+
}, 2e3);
|
|
817
1181
|
}
|
|
818
1182
|
});
|
|
819
1183
|
});
|
|
820
|
-
return {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1184
|
+
return new Promise((resolve, reject) => {
|
|
1185
|
+
httpServer.on("error", reject);
|
|
1186
|
+
httpServer.listen(port, () => {
|
|
1187
|
+
resolve({
|
|
1188
|
+
port,
|
|
1189
|
+
sendInit(payload) {
|
|
1190
|
+
initPayload = payload;
|
|
1191
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
1192
|
+
sendToClient({ type: "review:init", payload });
|
|
1193
|
+
} else {
|
|
1194
|
+
pendingInit = payload;
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
storeInitPayload(payload) {
|
|
1198
|
+
initPayload = payload;
|
|
1199
|
+
},
|
|
1200
|
+
sendDiffUpdate(payload) {
|
|
1201
|
+
sendToClient({ type: "diff:update", payload });
|
|
1202
|
+
},
|
|
1203
|
+
sendContextUpdate(payload) {
|
|
1204
|
+
sendToClient({ type: "context:update", payload });
|
|
1205
|
+
},
|
|
1206
|
+
sendDiffError(payload) {
|
|
1207
|
+
sendToClient({ type: "diff:error", payload });
|
|
1208
|
+
},
|
|
1209
|
+
onSubmit(callback) {
|
|
1210
|
+
submitCallback = callback;
|
|
1211
|
+
},
|
|
1212
|
+
waitForResult() {
|
|
1213
|
+
return new Promise((resolve2, reject2) => {
|
|
1214
|
+
submitCallback = resolve2;
|
|
1215
|
+
resultReject = reject2;
|
|
1216
|
+
});
|
|
1217
|
+
},
|
|
1218
|
+
triggerRefresh() {
|
|
1219
|
+
callbacks.onRefreshRequest();
|
|
1220
|
+
},
|
|
1221
|
+
async close() {
|
|
1222
|
+
if (closeTimer) {
|
|
1223
|
+
clearTimeout(closeTimer);
|
|
1224
|
+
}
|
|
1225
|
+
for (const ws of wss2.clients) {
|
|
1226
|
+
ws.close();
|
|
1227
|
+
}
|
|
1228
|
+
wss2.close();
|
|
1229
|
+
await new Promise((resolve2) => {
|
|
1230
|
+
httpServer.close(() => resolve2());
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// packages/core/src/diff-utils.ts
|
|
1239
|
+
import { createHash } from "crypto";
|
|
1240
|
+
function hashDiff(rawDiff) {
|
|
1241
|
+
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1242
|
+
}
|
|
1243
|
+
function fileKey(file) {
|
|
1244
|
+
return file.stage ? `${file.stage}:${file.path}` : file.path;
|
|
1245
|
+
}
|
|
1246
|
+
function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
1247
|
+
if (!oldDiffSet) {
|
|
1248
|
+
return newDiffSet.files.map(fileKey);
|
|
1249
|
+
}
|
|
1250
|
+
const oldFiles = new Map(
|
|
1251
|
+
oldDiffSet.files.map((f) => [fileKey(f), f])
|
|
1252
|
+
);
|
|
1253
|
+
const changed = [];
|
|
1254
|
+
for (const newFile of newDiffSet.files) {
|
|
1255
|
+
const key = fileKey(newFile);
|
|
1256
|
+
const oldFile = oldFiles.get(key);
|
|
1257
|
+
if (!oldFile) {
|
|
1258
|
+
changed.push(key);
|
|
1259
|
+
} else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
|
|
1260
|
+
changed.push(key);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const oldFile of oldDiffSet.files) {
|
|
1264
|
+
if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
|
|
1265
|
+
changed.push(fileKey(oldFile));
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return changed;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// packages/core/src/diff-poller.ts
|
|
1272
|
+
function createDiffPoller(options) {
|
|
1273
|
+
let { diffRef } = options;
|
|
1274
|
+
const { cwd, pollInterval, onDiffChanged, onError, silent } = options;
|
|
1275
|
+
let lastDiffHash = null;
|
|
1276
|
+
let lastDiffSet = null;
|
|
1277
|
+
let refreshRequested = false;
|
|
1278
|
+
let interval = null;
|
|
1279
|
+
let running = false;
|
|
1280
|
+
function poll() {
|
|
1281
|
+
if (!running) return;
|
|
1282
|
+
try {
|
|
1283
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
|
|
1284
|
+
const newHash = hashDiff(newRawDiff);
|
|
1285
|
+
if (newHash !== lastDiffHash || refreshRequested) {
|
|
1286
|
+
refreshRequested = false;
|
|
1287
|
+
const newBriefing = analyze(newDiffSet);
|
|
1288
|
+
const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
|
|
1289
|
+
lastDiffHash = newHash;
|
|
1290
|
+
lastDiffSet = newDiffSet;
|
|
1291
|
+
const updatePayload = {
|
|
1292
|
+
diffSet: newDiffSet,
|
|
1293
|
+
rawDiff: newRawDiff,
|
|
1294
|
+
briefing: newBriefing,
|
|
1295
|
+
changedFiles,
|
|
1296
|
+
timestamp: Date.now()
|
|
1297
|
+
};
|
|
1298
|
+
onDiffChanged(updatePayload);
|
|
1299
|
+
}
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
if (onError && err instanceof Error) {
|
|
1302
|
+
onError(err);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
start() {
|
|
1308
|
+
if (running) return;
|
|
1309
|
+
running = true;
|
|
1310
|
+
try {
|
|
1311
|
+
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1312
|
+
lastDiffHash = hashDiff(initialRawDiff);
|
|
1313
|
+
lastDiffSet = initialDiffSet;
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
interval = setInterval(poll, pollInterval);
|
|
1317
|
+
},
|
|
1318
|
+
stop() {
|
|
1319
|
+
running = false;
|
|
1320
|
+
if (interval) {
|
|
1321
|
+
clearInterval(interval);
|
|
1322
|
+
interval = null;
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
setDiffRef(newRef) {
|
|
1326
|
+
diffRef = newRef;
|
|
1327
|
+
lastDiffHash = null;
|
|
1328
|
+
lastDiffSet = null;
|
|
1329
|
+
},
|
|
1330
|
+
refresh() {
|
|
1331
|
+
refreshRequested = true;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// packages/core/src/review-manager.ts
|
|
1337
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1338
|
+
var idCounter = 0;
|
|
1339
|
+
function createSession(options) {
|
|
1340
|
+
const id = `review-${Date.now()}-${++idCounter}`;
|
|
1341
|
+
const session = {
|
|
1342
|
+
id,
|
|
1343
|
+
options,
|
|
1344
|
+
status: "pending",
|
|
1345
|
+
createdAt: Date.now()
|
|
1346
|
+
};
|
|
1347
|
+
sessions.set(id, session);
|
|
861
1348
|
return session;
|
|
862
1349
|
}
|
|
863
1350
|
function updateSession(id, update) {
|
|
@@ -868,9 +1355,9 @@ function updateSession(id, update) {
|
|
|
868
1355
|
}
|
|
869
1356
|
|
|
870
1357
|
// packages/core/src/ui-server.ts
|
|
871
|
-
import
|
|
872
|
-
import
|
|
873
|
-
import
|
|
1358
|
+
import http2 from "http";
|
|
1359
|
+
import fs2 from "fs";
|
|
1360
|
+
import path4 from "path";
|
|
874
1361
|
import { fileURLToPath } from "url";
|
|
875
1362
|
var MIME_TYPES = {
|
|
876
1363
|
".html": "text/html",
|
|
@@ -885,14 +1372,14 @@ var MIME_TYPES = {
|
|
|
885
1372
|
};
|
|
886
1373
|
function resolveUiDist() {
|
|
887
1374
|
const thisFile = fileURLToPath(import.meta.url);
|
|
888
|
-
const thisDir =
|
|
889
|
-
const publishedUiDist =
|
|
890
|
-
if (
|
|
1375
|
+
const thisDir = path4.dirname(thisFile);
|
|
1376
|
+
const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
|
|
1377
|
+
if (fs2.existsSync(path4.join(publishedUiDist, "index.html"))) {
|
|
891
1378
|
return publishedUiDist;
|
|
892
1379
|
}
|
|
893
|
-
const workspaceRoot =
|
|
894
|
-
const devUiDist =
|
|
895
|
-
if (
|
|
1380
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
1381
|
+
const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
|
|
1382
|
+
if (fs2.existsSync(path4.join(devUiDist, "index.html"))) {
|
|
896
1383
|
return devUiDist;
|
|
897
1384
|
}
|
|
898
1385
|
throw new Error(
|
|
@@ -901,10 +1388,10 @@ function resolveUiDist() {
|
|
|
901
1388
|
}
|
|
902
1389
|
function resolveUiRoot() {
|
|
903
1390
|
const thisFile = fileURLToPath(import.meta.url);
|
|
904
|
-
const thisDir =
|
|
905
|
-
const workspaceRoot =
|
|
906
|
-
const uiRoot =
|
|
907
|
-
if (
|
|
1391
|
+
const thisDir = path4.dirname(thisFile);
|
|
1392
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
1393
|
+
const uiRoot = path4.join(workspaceRoot, "packages", "ui");
|
|
1394
|
+
if (fs2.existsSync(path4.join(uiRoot, "index.html"))) {
|
|
908
1395
|
return uiRoot;
|
|
909
1396
|
}
|
|
910
1397
|
throw new Error(
|
|
@@ -922,16 +1409,16 @@ async function startViteDevServer(uiRoot, port, silent) {
|
|
|
922
1409
|
return vite;
|
|
923
1410
|
}
|
|
924
1411
|
function createStaticServer(distPath, port) {
|
|
925
|
-
const server =
|
|
1412
|
+
const server = http2.createServer((req, res) => {
|
|
926
1413
|
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
927
|
-
let filePath =
|
|
928
|
-
if (!
|
|
929
|
-
filePath =
|
|
1414
|
+
let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
1415
|
+
if (!fs2.existsSync(filePath)) {
|
|
1416
|
+
filePath = path4.join(distPath, "index.html");
|
|
930
1417
|
}
|
|
931
|
-
const ext =
|
|
1418
|
+
const ext = path4.extname(filePath);
|
|
932
1419
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
933
1420
|
try {
|
|
934
|
-
const content =
|
|
1421
|
+
const content = fs2.readFileSync(filePath);
|
|
935
1422
|
res.writeHead(200, { "Content-Type": contentType });
|
|
936
1423
|
res.end(content);
|
|
937
1424
|
} catch {
|
|
@@ -963,392 +1450,228 @@ async function startReview(options) {
|
|
|
963
1450
|
const briefing = analyze(diffSet);
|
|
964
1451
|
const session = createSession(options);
|
|
965
1452
|
updateSession(session.id, { status: "in_progress" });
|
|
966
|
-
const
|
|
1453
|
+
const worktreeInfo = detectWorktree({ cwd });
|
|
1454
|
+
const metadata = {
|
|
1455
|
+
title,
|
|
1456
|
+
description,
|
|
1457
|
+
reasoning,
|
|
1458
|
+
currentBranch,
|
|
1459
|
+
worktree: worktreeInfo.isWorktree ? {
|
|
1460
|
+
isWorktree: true,
|
|
1461
|
+
worktreePath: worktreeInfo.worktreePath,
|
|
1462
|
+
mainWorktreePath: worktreeInfo.mainWorktreePath
|
|
1463
|
+
} : void 0
|
|
1464
|
+
};
|
|
1465
|
+
let poller = null;
|
|
1466
|
+
const [bridgePort, httpPort] = await Promise.all([
|
|
967
1467
|
getPort(),
|
|
968
1468
|
getPort()
|
|
969
1469
|
]);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const url = `http://localhost:${httpPort}?wsPort=${wsPort}&reviewId=${session.id}`;
|
|
982
|
-
if (!silent) {
|
|
983
|
-
console.log(`
|
|
984
|
-
DiffPrism Review: ${title ?? briefing.summary}`);
|
|
985
|
-
console.log(`Opening browser at ${url}
|
|
986
|
-
`);
|
|
987
|
-
}
|
|
988
|
-
await open(url);
|
|
989
|
-
const initPayload = {
|
|
1470
|
+
function handleDiffRefChange(newRef) {
|
|
1471
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1472
|
+
const newBriefing = analyze(newDiffSet);
|
|
1473
|
+
bridge.sendDiffUpdate({
|
|
1474
|
+
diffSet: newDiffSet,
|
|
1475
|
+
rawDiff: newRawDiff,
|
|
1476
|
+
briefing: newBriefing,
|
|
1477
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1478
|
+
timestamp: Date.now()
|
|
1479
|
+
});
|
|
1480
|
+
bridge.storeInitPayload({
|
|
990
1481
|
reviewId: session.id,
|
|
991
|
-
diffSet,
|
|
992
|
-
rawDiff,
|
|
993
|
-
briefing,
|
|
994
|
-
metadata
|
|
995
|
-
|
|
996
|
-
bridge.sendInit(initPayload);
|
|
997
|
-
const result = await bridge.waitForResult();
|
|
998
|
-
updateSession(session.id, { status: "completed", result });
|
|
999
|
-
return result;
|
|
1000
|
-
} finally {
|
|
1001
|
-
bridge.close();
|
|
1002
|
-
if (viteServer) {
|
|
1003
|
-
await viteServer.close();
|
|
1004
|
-
}
|
|
1005
|
-
if (httpServer) {
|
|
1006
|
-
httpServer.close();
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
// packages/core/src/watch-file.ts
|
|
1012
|
-
import fs2 from "fs";
|
|
1013
|
-
import path4 from "path";
|
|
1014
|
-
import { execSync as execSync2 } from "child_process";
|
|
1015
|
-
function findGitRoot(cwd) {
|
|
1016
|
-
const root = execSync2("git rev-parse --show-toplevel", {
|
|
1017
|
-
cwd: cwd ?? process.cwd(),
|
|
1018
|
-
encoding: "utf-8"
|
|
1019
|
-
}).trim();
|
|
1020
|
-
return root;
|
|
1021
|
-
}
|
|
1022
|
-
function watchFilePath(cwd) {
|
|
1023
|
-
const gitRoot = findGitRoot(cwd);
|
|
1024
|
-
return path4.join(gitRoot, ".diffprism", "watch.json");
|
|
1025
|
-
}
|
|
1026
|
-
function isPidAlive(pid) {
|
|
1027
|
-
try {
|
|
1028
|
-
process.kill(pid, 0);
|
|
1029
|
-
return true;
|
|
1030
|
-
} catch {
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
function writeWatchFile(cwd, info) {
|
|
1035
|
-
const filePath = watchFilePath(cwd);
|
|
1036
|
-
const dir = path4.dirname(filePath);
|
|
1037
|
-
if (!fs2.existsSync(dir)) {
|
|
1038
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
1039
|
-
}
|
|
1040
|
-
fs2.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
|
|
1041
|
-
}
|
|
1042
|
-
function readWatchFile(cwd) {
|
|
1043
|
-
const filePath = watchFilePath(cwd);
|
|
1044
|
-
if (!fs2.existsSync(filePath)) {
|
|
1045
|
-
return null;
|
|
1046
|
-
}
|
|
1047
|
-
try {
|
|
1048
|
-
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1049
|
-
const info = JSON.parse(raw);
|
|
1050
|
-
if (!isPidAlive(info.pid)) {
|
|
1051
|
-
fs2.unlinkSync(filePath);
|
|
1052
|
-
return null;
|
|
1053
|
-
}
|
|
1054
|
-
return info;
|
|
1055
|
-
} catch {
|
|
1056
|
-
return null;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
function removeWatchFile(cwd) {
|
|
1060
|
-
try {
|
|
1061
|
-
const filePath = watchFilePath(cwd);
|
|
1062
|
-
if (fs2.existsSync(filePath)) {
|
|
1063
|
-
fs2.unlinkSync(filePath);
|
|
1064
|
-
}
|
|
1065
|
-
} catch {
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
function reviewResultPath(cwd) {
|
|
1069
|
-
const gitRoot = findGitRoot(cwd);
|
|
1070
|
-
return path4.join(gitRoot, ".diffprism", "last-review.json");
|
|
1071
|
-
}
|
|
1072
|
-
function writeReviewResult(cwd, result) {
|
|
1073
|
-
const filePath = reviewResultPath(cwd);
|
|
1074
|
-
const dir = path4.dirname(filePath);
|
|
1075
|
-
if (!fs2.existsSync(dir)) {
|
|
1076
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
1077
|
-
}
|
|
1078
|
-
const data = {
|
|
1079
|
-
result,
|
|
1080
|
-
timestamp: Date.now(),
|
|
1081
|
-
consumed: false
|
|
1082
|
-
};
|
|
1083
|
-
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1084
|
-
}
|
|
1085
|
-
function readReviewResult(cwd) {
|
|
1086
|
-
try {
|
|
1087
|
-
const filePath = reviewResultPath(cwd);
|
|
1088
|
-
if (!fs2.existsSync(filePath)) {
|
|
1089
|
-
return null;
|
|
1090
|
-
}
|
|
1091
|
-
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1092
|
-
const data = JSON.parse(raw);
|
|
1093
|
-
if (data.consumed) {
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
return data;
|
|
1097
|
-
} catch {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
function consumeReviewResult(cwd) {
|
|
1102
|
-
try {
|
|
1103
|
-
const filePath = reviewResultPath(cwd);
|
|
1104
|
-
if (!fs2.existsSync(filePath)) {
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
1108
|
-
const data = JSON.parse(raw);
|
|
1109
|
-
data.consumed = true;
|
|
1110
|
-
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1111
|
-
} catch {
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// packages/core/src/server-file.ts
|
|
1116
|
-
import fs3 from "fs";
|
|
1117
|
-
import path5 from "path";
|
|
1118
|
-
import os from "os";
|
|
1119
|
-
function serverDir() {
|
|
1120
|
-
return path5.join(os.homedir(), ".diffprism");
|
|
1121
|
-
}
|
|
1122
|
-
function serverFilePath() {
|
|
1123
|
-
return path5.join(serverDir(), "server.json");
|
|
1124
|
-
}
|
|
1125
|
-
function isPidAlive2(pid) {
|
|
1126
|
-
try {
|
|
1127
|
-
process.kill(pid, 0);
|
|
1128
|
-
return true;
|
|
1129
|
-
} catch {
|
|
1130
|
-
return false;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
function writeServerFile(info) {
|
|
1134
|
-
const dir = serverDir();
|
|
1135
|
-
if (!fs3.existsSync(dir)) {
|
|
1136
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
1137
|
-
}
|
|
1138
|
-
fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1139
|
-
}
|
|
1140
|
-
function readServerFile() {
|
|
1141
|
-
const filePath = serverFilePath();
|
|
1142
|
-
if (!fs3.existsSync(filePath)) {
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
try {
|
|
1146
|
-
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1147
|
-
const info = JSON.parse(raw);
|
|
1148
|
-
if (!isPidAlive2(info.pid)) {
|
|
1149
|
-
fs3.unlinkSync(filePath);
|
|
1150
|
-
return null;
|
|
1151
|
-
}
|
|
1152
|
-
return info;
|
|
1153
|
-
} catch {
|
|
1154
|
-
return null;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
function removeServerFile() {
|
|
1158
|
-
try {
|
|
1159
|
-
const filePath = serverFilePath();
|
|
1160
|
-
if (fs3.existsSync(filePath)) {
|
|
1161
|
-
fs3.unlinkSync(filePath);
|
|
1162
|
-
}
|
|
1163
|
-
} catch {
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
async function isServerAlive() {
|
|
1167
|
-
const info = readServerFile();
|
|
1168
|
-
if (!info) {
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
try {
|
|
1172
|
-
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
1173
|
-
signal: AbortSignal.timeout(2e3)
|
|
1482
|
+
diffSet: newDiffSet,
|
|
1483
|
+
rawDiff: newRawDiff,
|
|
1484
|
+
briefing: newBriefing,
|
|
1485
|
+
metadata,
|
|
1486
|
+
watchMode: true
|
|
1174
1487
|
});
|
|
1175
|
-
|
|
1176
|
-
return info;
|
|
1177
|
-
}
|
|
1178
|
-
return null;
|
|
1179
|
-
} catch {
|
|
1180
|
-
removeServerFile();
|
|
1181
|
-
return null;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// packages/core/src/watch.ts
|
|
1186
|
-
import getPort2 from "get-port";
|
|
1187
|
-
import open2 from "open";
|
|
1188
|
-
|
|
1189
|
-
// packages/core/src/watch-bridge.ts
|
|
1190
|
-
import http2 from "http";
|
|
1191
|
-
import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
|
|
1192
|
-
function createWatchBridge(port, callbacks) {
|
|
1193
|
-
let client = null;
|
|
1194
|
-
let initPayload = null;
|
|
1195
|
-
let pendingInit = null;
|
|
1196
|
-
let closeTimer = null;
|
|
1197
|
-
let submitCallback = null;
|
|
1198
|
-
const httpServer = http2.createServer((req, res) => {
|
|
1199
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1200
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1201
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1202
|
-
if (req.method === "OPTIONS") {
|
|
1203
|
-
res.writeHead(204);
|
|
1204
|
-
res.end();
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (req.method === "GET" && req.url === "/api/status") {
|
|
1208
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1209
|
-
res.end(JSON.stringify({ running: true, pid: process.pid }));
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
if (req.method === "POST" && req.url === "/api/context") {
|
|
1213
|
-
let body = "";
|
|
1214
|
-
req.on("data", (chunk) => {
|
|
1215
|
-
body += chunk.toString();
|
|
1216
|
-
});
|
|
1217
|
-
req.on("end", () => {
|
|
1218
|
-
try {
|
|
1219
|
-
const payload = JSON.parse(body);
|
|
1220
|
-
callbacks.onContextUpdate(payload);
|
|
1221
|
-
sendToClient({ type: "context:update", payload });
|
|
1222
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1223
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1224
|
-
} catch {
|
|
1225
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1226
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
if (req.method === "POST" && req.url === "/api/refresh") {
|
|
1232
|
-
callbacks.onRefreshRequest();
|
|
1233
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1234
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
res.writeHead(404);
|
|
1238
|
-
res.end("Not found");
|
|
1239
|
-
});
|
|
1240
|
-
const wss2 = new WebSocketServer2({ server: httpServer });
|
|
1241
|
-
function sendToClient(msg) {
|
|
1242
|
-
if (client && client.readyState === WebSocket2.OPEN) {
|
|
1243
|
-
client.send(JSON.stringify(msg));
|
|
1244
|
-
}
|
|
1488
|
+
poller?.setDiffRef(newRef);
|
|
1245
1489
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}
|
|
1257
|
-
ws.on("message", (data) => {
|
|
1490
|
+
const bridge = await createWatchBridge(bridgePort, {
|
|
1491
|
+
onRefreshRequest: () => {
|
|
1492
|
+
poller?.refresh();
|
|
1493
|
+
},
|
|
1494
|
+
onContextUpdate: (payload) => {
|
|
1495
|
+
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1496
|
+
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1497
|
+
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1498
|
+
},
|
|
1499
|
+
onDiffRefChange: (newRef) => {
|
|
1258
1500
|
try {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1501
|
+
handleDiffRefChange(newRef);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
bridge.sendDiffError({
|
|
1504
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1505
|
+
});
|
|
1264
1506
|
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1507
|
+
},
|
|
1508
|
+
onRefsRequest: async () => {
|
|
1509
|
+
try {
|
|
1510
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1511
|
+
const branches = listBranches({ cwd: resolvedCwd });
|
|
1512
|
+
const commits = listCommits({ cwd: resolvedCwd });
|
|
1513
|
+
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1514
|
+
return { branches, commits, currentBranch: branch };
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
onCompareRequest: async (ref) => {
|
|
1520
|
+
try {
|
|
1521
|
+
handleDiffRefChange(ref);
|
|
1522
|
+
return true;
|
|
1523
|
+
} catch {
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
let httpServer = null;
|
|
1529
|
+
let viteServer = null;
|
|
1530
|
+
try {
|
|
1531
|
+
if (dev) {
|
|
1532
|
+
const uiRoot = resolveUiRoot();
|
|
1533
|
+
viteServer = await startViteDevServer(uiRoot, httpPort, !!silent);
|
|
1534
|
+
} else {
|
|
1535
|
+
const uiDist = resolveUiDist();
|
|
1536
|
+
httpServer = await createStaticServer(uiDist, httpPort);
|
|
1537
|
+
}
|
|
1538
|
+
writeWatchFile(cwd, {
|
|
1539
|
+
wsPort: bridgePort,
|
|
1540
|
+
uiPort: httpPort,
|
|
1541
|
+
pid: process.pid,
|
|
1542
|
+
cwd: cwd ?? process.cwd(),
|
|
1543
|
+
diffRef,
|
|
1544
|
+
startedAt: Date.now()
|
|
1545
|
+
});
|
|
1546
|
+
const url = `http://localhost:${httpPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${session.id}`;
|
|
1547
|
+
if (!silent) {
|
|
1548
|
+
console.log(`
|
|
1549
|
+
DiffPrism Review: ${title ?? briefing.summary}`);
|
|
1550
|
+
console.log(`Opening browser at ${url}
|
|
1551
|
+
`);
|
|
1552
|
+
}
|
|
1553
|
+
await open(url);
|
|
1554
|
+
const initPayload = {
|
|
1555
|
+
reviewId: session.id,
|
|
1556
|
+
diffSet,
|
|
1557
|
+
rawDiff,
|
|
1558
|
+
briefing,
|
|
1559
|
+
metadata,
|
|
1560
|
+
watchMode: true
|
|
1561
|
+
};
|
|
1562
|
+
bridge.sendInit(initPayload);
|
|
1563
|
+
poller = createDiffPoller({
|
|
1564
|
+
diffRef,
|
|
1565
|
+
cwd: cwd ?? process.cwd(),
|
|
1566
|
+
pollInterval: 1e3,
|
|
1567
|
+
onDiffChanged: (updatePayload) => {
|
|
1568
|
+
bridge.storeInitPayload({
|
|
1569
|
+
reviewId: session.id,
|
|
1570
|
+
diffSet: updatePayload.diffSet,
|
|
1571
|
+
rawDiff: updatePayload.rawDiff,
|
|
1572
|
+
briefing: updatePayload.briefing,
|
|
1573
|
+
metadata,
|
|
1574
|
+
watchMode: true
|
|
1575
|
+
});
|
|
1576
|
+
bridge.sendDiffUpdate(updatePayload);
|
|
1577
|
+
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
1578
|
+
console.log(
|
|
1579
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
1580
|
+
);
|
|
1312
1581
|
}
|
|
1313
|
-
}
|
|
1582
|
+
}
|
|
1314
1583
|
});
|
|
1315
|
-
|
|
1584
|
+
poller.start();
|
|
1585
|
+
const result = await bridge.waitForResult();
|
|
1586
|
+
poller.stop();
|
|
1587
|
+
updateSession(session.id, { status: "completed", result });
|
|
1588
|
+
return result;
|
|
1589
|
+
} finally {
|
|
1590
|
+
poller?.stop();
|
|
1591
|
+
await bridge.close();
|
|
1592
|
+
removeWatchFile(cwd);
|
|
1593
|
+
if (viteServer) {
|
|
1594
|
+
await viteServer.close();
|
|
1595
|
+
}
|
|
1596
|
+
if (httpServer) {
|
|
1597
|
+
httpServer.close();
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1316
1600
|
}
|
|
1317
1601
|
|
|
1318
|
-
// packages/core/src/
|
|
1319
|
-
import
|
|
1320
|
-
|
|
1321
|
-
|
|
1602
|
+
// packages/core/src/server-file.ts
|
|
1603
|
+
import fs3 from "fs";
|
|
1604
|
+
import path5 from "path";
|
|
1605
|
+
import os from "os";
|
|
1606
|
+
function serverDir() {
|
|
1607
|
+
return path5.join(os.homedir(), ".diffprism");
|
|
1322
1608
|
}
|
|
1323
|
-
function
|
|
1324
|
-
return
|
|
1609
|
+
function serverFilePath() {
|
|
1610
|
+
return path5.join(serverDir(), "server.json");
|
|
1325
1611
|
}
|
|
1326
|
-
function
|
|
1327
|
-
|
|
1328
|
-
|
|
1612
|
+
function isPidAlive2(pid) {
|
|
1613
|
+
try {
|
|
1614
|
+
process.kill(pid, 0);
|
|
1615
|
+
return true;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return false;
|
|
1329
1618
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
);
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1619
|
+
}
|
|
1620
|
+
function writeServerFile(info) {
|
|
1621
|
+
const dir = serverDir();
|
|
1622
|
+
if (!fs3.existsSync(dir)) {
|
|
1623
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1624
|
+
}
|
|
1625
|
+
fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1626
|
+
}
|
|
1627
|
+
function readServerFile() {
|
|
1628
|
+
const filePath = serverFilePath();
|
|
1629
|
+
if (!fs3.existsSync(filePath)) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
try {
|
|
1633
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1634
|
+
const info = JSON.parse(raw);
|
|
1635
|
+
if (!isPidAlive2(info.pid)) {
|
|
1636
|
+
fs3.unlinkSync(filePath);
|
|
1637
|
+
return null;
|
|
1341
1638
|
}
|
|
1639
|
+
return info;
|
|
1640
|
+
} catch {
|
|
1641
|
+
return null;
|
|
1342
1642
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1643
|
+
}
|
|
1644
|
+
function removeServerFile() {
|
|
1645
|
+
try {
|
|
1646
|
+
const filePath = serverFilePath();
|
|
1647
|
+
if (fs3.existsSync(filePath)) {
|
|
1648
|
+
fs3.unlinkSync(filePath);
|
|
1346
1649
|
}
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
async function isServerAlive() {
|
|
1654
|
+
const info = readServerFile();
|
|
1655
|
+
if (!info) {
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
try {
|
|
1659
|
+
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
1660
|
+
signal: AbortSignal.timeout(2e3)
|
|
1661
|
+
});
|
|
1662
|
+
if (response.ok) {
|
|
1663
|
+
return info;
|
|
1664
|
+
}
|
|
1665
|
+
return null;
|
|
1666
|
+
} catch {
|
|
1667
|
+
removeServerFile();
|
|
1668
|
+
return null;
|
|
1347
1669
|
}
|
|
1348
|
-
return changed;
|
|
1349
1670
|
}
|
|
1350
1671
|
|
|
1351
1672
|
// packages/core/src/watch.ts
|
|
1673
|
+
import getPort2 from "get-port";
|
|
1674
|
+
import open2 from "open";
|
|
1352
1675
|
async function startWatch(options) {
|
|
1353
1676
|
const {
|
|
1354
1677
|
diffRef,
|
|
@@ -1363,8 +1686,6 @@ async function startWatch(options) {
|
|
|
1363
1686
|
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1364
1687
|
const currentBranch = getCurrentBranch({ cwd });
|
|
1365
1688
|
const initialBriefing = analyze(initialDiffSet);
|
|
1366
|
-
let lastDiffHash = hashDiff(initialRawDiff);
|
|
1367
|
-
let lastDiffSet = initialDiffSet;
|
|
1368
1689
|
const metadata = {
|
|
1369
1690
|
title,
|
|
1370
1691
|
description,
|
|
@@ -1375,15 +1696,63 @@ async function startWatch(options) {
|
|
|
1375
1696
|
getPort2(),
|
|
1376
1697
|
getPort2()
|
|
1377
1698
|
]);
|
|
1378
|
-
|
|
1699
|
+
const reviewId = "watch-session";
|
|
1700
|
+
function handleDiffRefChange(newRef) {
|
|
1701
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1702
|
+
const newBriefing = analyze(newDiffSet);
|
|
1703
|
+
bridge.sendDiffUpdate({
|
|
1704
|
+
diffSet: newDiffSet,
|
|
1705
|
+
rawDiff: newRawDiff,
|
|
1706
|
+
briefing: newBriefing,
|
|
1707
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1708
|
+
timestamp: Date.now()
|
|
1709
|
+
});
|
|
1710
|
+
bridge.storeInitPayload({
|
|
1711
|
+
reviewId,
|
|
1712
|
+
diffSet: newDiffSet,
|
|
1713
|
+
rawDiff: newRawDiff,
|
|
1714
|
+
briefing: newBriefing,
|
|
1715
|
+
metadata,
|
|
1716
|
+
watchMode: true
|
|
1717
|
+
});
|
|
1718
|
+
poller.setDiffRef(newRef);
|
|
1719
|
+
}
|
|
1379
1720
|
const bridge = await createWatchBridge(bridgePort, {
|
|
1380
1721
|
onRefreshRequest: () => {
|
|
1381
|
-
|
|
1722
|
+
poller.refresh();
|
|
1382
1723
|
},
|
|
1383
1724
|
onContextUpdate: (payload) => {
|
|
1384
1725
|
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1385
1726
|
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1386
1727
|
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1728
|
+
},
|
|
1729
|
+
onDiffRefChange: (newRef) => {
|
|
1730
|
+
try {
|
|
1731
|
+
handleDiffRefChange(newRef);
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
bridge.sendDiffError({
|
|
1734
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
onRefsRequest: async () => {
|
|
1739
|
+
try {
|
|
1740
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1741
|
+
const branches = listBranches({ cwd: resolvedCwd });
|
|
1742
|
+
const commits = listCommits({ cwd: resolvedCwd });
|
|
1743
|
+
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1744
|
+
return { branches, commits, currentBranch: branch };
|
|
1745
|
+
} catch {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
onCompareRequest: async (ref) => {
|
|
1750
|
+
try {
|
|
1751
|
+
handleDiffRefChange(ref);
|
|
1752
|
+
return true;
|
|
1753
|
+
} catch {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1387
1756
|
}
|
|
1388
1757
|
});
|
|
1389
1758
|
let httpServer = null;
|
|
@@ -1403,8 +1772,7 @@ async function startWatch(options) {
|
|
|
1403
1772
|
diffRef,
|
|
1404
1773
|
startedAt: Date.now()
|
|
1405
1774
|
});
|
|
1406
|
-
const
|
|
1407
|
-
const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&reviewId=${reviewId}`;
|
|
1775
|
+
const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${reviewId}`;
|
|
1408
1776
|
if (!silent) {
|
|
1409
1777
|
console.log(`
|
|
1410
1778
|
DiffPrism Watch: ${title ?? `watching ${diffRef}`}`);
|
|
@@ -1434,46 +1802,30 @@ Review submitted: ${result.decision}`);
|
|
|
1434
1802
|
}
|
|
1435
1803
|
writeReviewResult(cwd, result);
|
|
1436
1804
|
});
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
watchMode: true
|
|
1456
|
-
});
|
|
1457
|
-
const updatePayload = {
|
|
1458
|
-
diffSet: newDiffSet,
|
|
1459
|
-
rawDiff: newRawDiff,
|
|
1460
|
-
briefing: newBriefing,
|
|
1461
|
-
changedFiles,
|
|
1462
|
-
timestamp: Date.now()
|
|
1463
|
-
};
|
|
1464
|
-
bridge.sendDiffUpdate(updatePayload);
|
|
1465
|
-
if (!silent && changedFiles.length > 0) {
|
|
1466
|
-
console.log(
|
|
1467
|
-
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${changedFiles.length} file(s) changed`
|
|
1468
|
-
);
|
|
1469
|
-
}
|
|
1805
|
+
const poller = createDiffPoller({
|
|
1806
|
+
diffRef,
|
|
1807
|
+
cwd: cwd ?? process.cwd(),
|
|
1808
|
+
pollInterval,
|
|
1809
|
+
onDiffChanged: (updatePayload) => {
|
|
1810
|
+
bridge.storeInitPayload({
|
|
1811
|
+
reviewId,
|
|
1812
|
+
diffSet: updatePayload.diffSet,
|
|
1813
|
+
rawDiff: updatePayload.rawDiff,
|
|
1814
|
+
briefing: updatePayload.briefing,
|
|
1815
|
+
metadata,
|
|
1816
|
+
watchMode: true
|
|
1817
|
+
});
|
|
1818
|
+
bridge.sendDiffUpdate(updatePayload);
|
|
1819
|
+
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
1820
|
+
console.log(
|
|
1821
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
1822
|
+
);
|
|
1470
1823
|
}
|
|
1471
|
-
} catch {
|
|
1472
1824
|
}
|
|
1473
|
-
}
|
|
1825
|
+
});
|
|
1826
|
+
poller.start();
|
|
1474
1827
|
async function stop() {
|
|
1475
|
-
|
|
1476
|
-
clearInterval(pollLoop);
|
|
1828
|
+
poller.stop();
|
|
1477
1829
|
await bridge.close();
|
|
1478
1830
|
if (viteServer) {
|
|
1479
1831
|
await viteServer.close();
|
|
@@ -1497,7 +1849,7 @@ import http3 from "http";
|
|
|
1497
1849
|
import { randomUUID } from "crypto";
|
|
1498
1850
|
import getPort3 from "get-port";
|
|
1499
1851
|
import open3 from "open";
|
|
1500
|
-
import { WebSocketServer as
|
|
1852
|
+
import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
|
|
1501
1853
|
var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
|
|
1502
1854
|
var ABANDONED_TTL_MS = 60 * 60 * 1e3;
|
|
1503
1855
|
var CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
@@ -1563,7 +1915,7 @@ function broadcastToAll(msg) {
|
|
|
1563
1915
|
if (!wss) return;
|
|
1564
1916
|
const data = JSON.stringify(msg);
|
|
1565
1917
|
for (const client of wss.clients) {
|
|
1566
|
-
if (client.readyState ===
|
|
1918
|
+
if (client.readyState === WebSocket2.OPEN) {
|
|
1567
1919
|
client.send(data);
|
|
1568
1920
|
}
|
|
1569
1921
|
}
|
|
@@ -1572,7 +1924,7 @@ function sendToSessionClients(sessionId, msg) {
|
|
|
1572
1924
|
if (!wss) return;
|
|
1573
1925
|
const data = JSON.stringify(msg);
|
|
1574
1926
|
for (const [client, sid] of clientSessions.entries()) {
|
|
1575
|
-
if (sid === sessionId && client.readyState ===
|
|
1927
|
+
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
1576
1928
|
client.send(data);
|
|
1577
1929
|
}
|
|
1578
1930
|
}
|
|
@@ -1596,7 +1948,7 @@ function broadcastSessionRemoved(sessionId) {
|
|
|
1596
1948
|
}
|
|
1597
1949
|
function hasViewersForSession(sessionId) {
|
|
1598
1950
|
for (const [client, sid] of clientSessions.entries()) {
|
|
1599
|
-
if (sid === sessionId && client.readyState ===
|
|
1951
|
+
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
1600
1952
|
return true;
|
|
1601
1953
|
}
|
|
1602
1954
|
}
|
|
@@ -1606,54 +1958,40 @@ function startSessionWatcher(sessionId) {
|
|
|
1606
1958
|
if (sessionWatchers.has(sessionId)) return;
|
|
1607
1959
|
const session = sessions2.get(sessionId);
|
|
1608
1960
|
if (!session?.diffRef) return;
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
};
|
|
1629
|
-
s.
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
type: "diff:update",
|
|
1634
|
-
payload: {
|
|
1635
|
-
diffSet: newDiffSet,
|
|
1636
|
-
rawDiff: newRawDiff,
|
|
1637
|
-
briefing: newBriefing,
|
|
1638
|
-
changedFiles,
|
|
1639
|
-
timestamp: Date.now()
|
|
1640
|
-
}
|
|
1641
|
-
});
|
|
1642
|
-
s.hasNewChanges = false;
|
|
1643
|
-
} else {
|
|
1644
|
-
s.hasNewChanges = true;
|
|
1645
|
-
broadcastSessionList();
|
|
1646
|
-
}
|
|
1961
|
+
const poller = createDiffPoller({
|
|
1962
|
+
diffRef: session.diffRef,
|
|
1963
|
+
cwd: session.projectPath,
|
|
1964
|
+
pollInterval: serverPollInterval,
|
|
1965
|
+
onDiffChanged: (updatePayload) => {
|
|
1966
|
+
const s = sessions2.get(sessionId);
|
|
1967
|
+
if (!s) return;
|
|
1968
|
+
s.payload = {
|
|
1969
|
+
...s.payload,
|
|
1970
|
+
diffSet: updatePayload.diffSet,
|
|
1971
|
+
rawDiff: updatePayload.rawDiff,
|
|
1972
|
+
briefing: updatePayload.briefing
|
|
1973
|
+
};
|
|
1974
|
+
s.lastDiffHash = hashDiff(updatePayload.rawDiff);
|
|
1975
|
+
s.lastDiffSet = updatePayload.diffSet;
|
|
1976
|
+
if (hasViewersForSession(sessionId)) {
|
|
1977
|
+
sendToSessionClients(sessionId, {
|
|
1978
|
+
type: "diff:update",
|
|
1979
|
+
payload: updatePayload
|
|
1980
|
+
});
|
|
1981
|
+
s.hasNewChanges = false;
|
|
1982
|
+
} else {
|
|
1983
|
+
s.hasNewChanges = true;
|
|
1984
|
+
broadcastSessionList();
|
|
1647
1985
|
}
|
|
1648
|
-
} catch {
|
|
1649
1986
|
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1987
|
+
});
|
|
1988
|
+
poller.start();
|
|
1989
|
+
sessionWatchers.set(sessionId, poller);
|
|
1652
1990
|
}
|
|
1653
1991
|
function stopSessionWatcher(sessionId) {
|
|
1654
|
-
const
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1992
|
+
const poller = sessionWatchers.get(sessionId);
|
|
1993
|
+
if (poller) {
|
|
1994
|
+
poller.stop();
|
|
1657
1995
|
sessionWatchers.delete(sessionId);
|
|
1658
1996
|
}
|
|
1659
1997
|
}
|
|
@@ -1665,15 +2003,15 @@ function startAllWatchers() {
|
|
|
1665
2003
|
}
|
|
1666
2004
|
}
|
|
1667
2005
|
function stopAllWatchers() {
|
|
1668
|
-
for (const [
|
|
1669
|
-
|
|
2006
|
+
for (const [, poller] of sessionWatchers.entries()) {
|
|
2007
|
+
poller.stop();
|
|
1670
2008
|
}
|
|
1671
2009
|
sessionWatchers.clear();
|
|
1672
2010
|
}
|
|
1673
2011
|
function hasConnectedClients() {
|
|
1674
2012
|
if (!wss) return false;
|
|
1675
2013
|
for (const client of wss.clients) {
|
|
1676
|
-
if (client.readyState ===
|
|
2014
|
+
if (client.readyState === WebSocket2.OPEN) return true;
|
|
1677
2015
|
}
|
|
1678
2016
|
return false;
|
|
1679
2017
|
}
|
|
@@ -1730,6 +2068,7 @@ async function handleApiRequest(req, res) {
|
|
|
1730
2068
|
existingSession.lastDiffHash = diffRef ? hashDiff(payload.rawDiff) : void 0;
|
|
1731
2069
|
existingSession.lastDiffSet = diffRef ? payload.diffSet : void 0;
|
|
1732
2070
|
existingSession.hasNewChanges = false;
|
|
2071
|
+
existingSession.annotations = [];
|
|
1733
2072
|
if (diffRef && hasConnectedClients()) {
|
|
1734
2073
|
startSessionWatcher(sessionId);
|
|
1735
2074
|
}
|
|
@@ -1758,7 +2097,8 @@ async function handleApiRequest(req, res) {
|
|
|
1758
2097
|
diffRef,
|
|
1759
2098
|
lastDiffHash: diffRef ? hashDiff(payload.rawDiff) : void 0,
|
|
1760
2099
|
lastDiffSet: diffRef ? payload.diffSet : void 0,
|
|
1761
|
-
hasNewChanges: false
|
|
2100
|
+
hasNewChanges: false,
|
|
2101
|
+
annotations: []
|
|
1762
2102
|
};
|
|
1763
2103
|
sessions2.set(sessionId, session);
|
|
1764
2104
|
if (diffRef && hasConnectedClients()) {
|
|
@@ -1857,6 +2197,65 @@ async function handleApiRequest(req, res) {
|
|
|
1857
2197
|
}
|
|
1858
2198
|
return true;
|
|
1859
2199
|
}
|
|
2200
|
+
const postAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations");
|
|
2201
|
+
if (postAnnotationParams) {
|
|
2202
|
+
const session = sessions2.get(postAnnotationParams.id);
|
|
2203
|
+
if (!session) {
|
|
2204
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2205
|
+
return true;
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const body = await readBody(req);
|
|
2209
|
+
const { file, line, body: annotationBody, type, confidence, category, source } = JSON.parse(body);
|
|
2210
|
+
const annotation = {
|
|
2211
|
+
id: randomUUID(),
|
|
2212
|
+
sessionId: session.id,
|
|
2213
|
+
file,
|
|
2214
|
+
line,
|
|
2215
|
+
body: annotationBody,
|
|
2216
|
+
type,
|
|
2217
|
+
confidence: confidence ?? 1,
|
|
2218
|
+
category: category ?? "other",
|
|
2219
|
+
source,
|
|
2220
|
+
createdAt: Date.now()
|
|
2221
|
+
};
|
|
2222
|
+
session.annotations.push(annotation);
|
|
2223
|
+
sendToSessionClients(session.id, {
|
|
2224
|
+
type: "annotation:added",
|
|
2225
|
+
payload: annotation
|
|
2226
|
+
});
|
|
2227
|
+
jsonResponse(res, 200, { annotationId: annotation.id });
|
|
2228
|
+
} catch {
|
|
2229
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
2230
|
+
}
|
|
2231
|
+
return true;
|
|
2232
|
+
}
|
|
2233
|
+
const getAnnotationsParams = matchRoute(method, url, "GET", "/api/reviews/:id/annotations");
|
|
2234
|
+
if (getAnnotationsParams) {
|
|
2235
|
+
const session = sessions2.get(getAnnotationsParams.id);
|
|
2236
|
+
if (!session) {
|
|
2237
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
jsonResponse(res, 200, { annotations: session.annotations });
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
const dismissAnnotationParams = matchRoute(method, url, "POST", "/api/reviews/:id/annotations/:annotationId/dismiss");
|
|
2244
|
+
if (dismissAnnotationParams) {
|
|
2245
|
+
const session = sessions2.get(dismissAnnotationParams.id);
|
|
2246
|
+
if (!session) {
|
|
2247
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
2248
|
+
return true;
|
|
2249
|
+
}
|
|
2250
|
+
const annotation = session.annotations.find((a) => a.id === dismissAnnotationParams.annotationId);
|
|
2251
|
+
if (!annotation) {
|
|
2252
|
+
jsonResponse(res, 404, { error: "Annotation not found" });
|
|
2253
|
+
return true;
|
|
2254
|
+
}
|
|
2255
|
+
annotation.dismissed = true;
|
|
2256
|
+
jsonResponse(res, 200, { ok: true });
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
1860
2259
|
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
1861
2260
|
if (deleteParams) {
|
|
1862
2261
|
stopSessionWatcher(deleteParams.id);
|
|
@@ -1968,7 +2367,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1968
2367
|
res.end("Not found");
|
|
1969
2368
|
}
|
|
1970
2369
|
});
|
|
1971
|
-
wss = new
|
|
2370
|
+
wss = new WebSocketServer2({ port: wsPort });
|
|
1972
2371
|
wss.on("connection", (ws, req) => {
|
|
1973
2372
|
startAllWatchers();
|
|
1974
2373
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
@@ -2046,6 +2445,49 @@ async function startGlobalServer(options = {}) {
|
|
|
2046
2445
|
closedSession.status = "submitted";
|
|
2047
2446
|
}
|
|
2048
2447
|
broadcastSessionRemoved(closedId);
|
|
2448
|
+
} else if (msg.type === "diff:change_ref") {
|
|
2449
|
+
const sid = clientSessions.get(ws);
|
|
2450
|
+
if (sid) {
|
|
2451
|
+
const session = sessions2.get(sid);
|
|
2452
|
+
if (session) {
|
|
2453
|
+
const newRef = msg.payload.diffRef;
|
|
2454
|
+
try {
|
|
2455
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, {
|
|
2456
|
+
cwd: session.projectPath
|
|
2457
|
+
});
|
|
2458
|
+
const newBriefing = analyze(newDiffSet);
|
|
2459
|
+
session.payload = {
|
|
2460
|
+
...session.payload,
|
|
2461
|
+
diffSet: newDiffSet,
|
|
2462
|
+
rawDiff: newRawDiff,
|
|
2463
|
+
briefing: newBriefing
|
|
2464
|
+
};
|
|
2465
|
+
session.diffRef = newRef;
|
|
2466
|
+
session.lastDiffHash = hashDiff(newRawDiff);
|
|
2467
|
+
session.lastDiffSet = newDiffSet;
|
|
2468
|
+
stopSessionWatcher(sid);
|
|
2469
|
+
startSessionWatcher(sid);
|
|
2470
|
+
sendToSessionClients(sid, {
|
|
2471
|
+
type: "diff:update",
|
|
2472
|
+
payload: {
|
|
2473
|
+
diffSet: newDiffSet,
|
|
2474
|
+
rawDiff: newRawDiff,
|
|
2475
|
+
briefing: newBriefing,
|
|
2476
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
2477
|
+
timestamp: Date.now()
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
const errorMsg = {
|
|
2482
|
+
type: "diff:error",
|
|
2483
|
+
payload: {
|
|
2484
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
ws.send(JSON.stringify(errorMsg));
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2049
2491
|
}
|
|
2050
2492
|
} catch {
|
|
2051
2493
|
}
|
|
@@ -2126,14 +2568,20 @@ Waiting for reviews...
|
|
|
2126
2568
|
return { httpPort, wsPort, stop };
|
|
2127
2569
|
}
|
|
2128
2570
|
|
|
2571
|
+
// packages/core/src/review-history.ts
|
|
2572
|
+
import fs4 from "fs";
|
|
2573
|
+
import path6 from "path";
|
|
2574
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2575
|
+
|
|
2129
2576
|
export {
|
|
2130
2577
|
getCurrentBranch,
|
|
2578
|
+
detectWorktree,
|
|
2131
2579
|
getDiff,
|
|
2132
2580
|
analyze,
|
|
2133
|
-
startReview,
|
|
2134
2581
|
readWatchFile,
|
|
2135
2582
|
readReviewResult,
|
|
2136
2583
|
consumeReviewResult,
|
|
2584
|
+
startReview,
|
|
2137
2585
|
startWatch,
|
|
2138
2586
|
readServerFile,
|
|
2139
2587
|
isServerAlive,
|