diffprism 0.27.0 → 0.28.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.
|
@@ -764,20 +764,222 @@ function analyze(diffSet) {
|
|
|
764
764
|
};
|
|
765
765
|
}
|
|
766
766
|
|
|
767
|
+
// packages/core/src/watch-file.ts
|
|
768
|
+
import fs from "fs";
|
|
769
|
+
import path3 from "path";
|
|
770
|
+
import { execSync as execSync2 } from "child_process";
|
|
771
|
+
function findGitRoot(cwd) {
|
|
772
|
+
const root = execSync2("git rev-parse --show-toplevel", {
|
|
773
|
+
cwd: cwd ?? process.cwd(),
|
|
774
|
+
encoding: "utf-8"
|
|
775
|
+
}).trim();
|
|
776
|
+
return root;
|
|
777
|
+
}
|
|
778
|
+
function watchFilePath(cwd) {
|
|
779
|
+
const gitRoot = findGitRoot(cwd);
|
|
780
|
+
return path3.join(gitRoot, ".diffprism", "watch.json");
|
|
781
|
+
}
|
|
782
|
+
function isPidAlive(pid) {
|
|
783
|
+
try {
|
|
784
|
+
process.kill(pid, 0);
|
|
785
|
+
return true;
|
|
786
|
+
} catch {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function writeWatchFile(cwd, info) {
|
|
791
|
+
const filePath = watchFilePath(cwd);
|
|
792
|
+
const dir = path3.dirname(filePath);
|
|
793
|
+
if (!fs.existsSync(dir)) {
|
|
794
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
795
|
+
}
|
|
796
|
+
fs.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
|
|
797
|
+
}
|
|
798
|
+
function readWatchFile(cwd) {
|
|
799
|
+
const filePath = watchFilePath(cwd);
|
|
800
|
+
if (!fs.existsSync(filePath)) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
805
|
+
const info = JSON.parse(raw);
|
|
806
|
+
if (!isPidAlive(info.pid)) {
|
|
807
|
+
fs.unlinkSync(filePath);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
return info;
|
|
811
|
+
} catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function removeWatchFile(cwd) {
|
|
816
|
+
try {
|
|
817
|
+
const filePath = watchFilePath(cwd);
|
|
818
|
+
if (fs.existsSync(filePath)) {
|
|
819
|
+
fs.unlinkSync(filePath);
|
|
820
|
+
}
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function reviewResultPath(cwd) {
|
|
825
|
+
const gitRoot = findGitRoot(cwd);
|
|
826
|
+
return path3.join(gitRoot, ".diffprism", "last-review.json");
|
|
827
|
+
}
|
|
828
|
+
function writeReviewResult(cwd, result) {
|
|
829
|
+
const filePath = reviewResultPath(cwd);
|
|
830
|
+
const dir = path3.dirname(filePath);
|
|
831
|
+
if (!fs.existsSync(dir)) {
|
|
832
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
833
|
+
}
|
|
834
|
+
const data = {
|
|
835
|
+
result,
|
|
836
|
+
timestamp: Date.now(),
|
|
837
|
+
consumed: false
|
|
838
|
+
};
|
|
839
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
840
|
+
}
|
|
841
|
+
function readReviewResult(cwd) {
|
|
842
|
+
try {
|
|
843
|
+
const filePath = reviewResultPath(cwd);
|
|
844
|
+
if (!fs.existsSync(filePath)) {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
848
|
+
const data = JSON.parse(raw);
|
|
849
|
+
if (data.consumed) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return data;
|
|
853
|
+
} catch {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function consumeReviewResult(cwd) {
|
|
858
|
+
try {
|
|
859
|
+
const filePath = reviewResultPath(cwd);
|
|
860
|
+
if (!fs.existsSync(filePath)) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
864
|
+
const data = JSON.parse(raw);
|
|
865
|
+
data.consumed = true;
|
|
866
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
767
871
|
// packages/core/src/pipeline.ts
|
|
768
872
|
import getPort from "get-port";
|
|
769
873
|
import open from "open";
|
|
770
874
|
|
|
771
|
-
// packages/core/src/
|
|
875
|
+
// packages/core/src/watch-bridge.ts
|
|
876
|
+
import http from "http";
|
|
772
877
|
import { WebSocketServer, WebSocket } from "ws";
|
|
773
|
-
function
|
|
774
|
-
const wss2 = new WebSocketServer({ port });
|
|
878
|
+
function createWatchBridge(port, callbacks) {
|
|
775
879
|
let client = null;
|
|
776
|
-
let resultResolve = null;
|
|
777
|
-
let resultReject = null;
|
|
778
|
-
let pendingInit = null;
|
|
779
880
|
let initPayload = null;
|
|
881
|
+
let pendingInit = null;
|
|
780
882
|
let closeTimer = null;
|
|
883
|
+
let submitCallback = null;
|
|
884
|
+
let resultReject = null;
|
|
885
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
886
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
887
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
888
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
889
|
+
if (req.method === "OPTIONS") {
|
|
890
|
+
res.writeHead(204);
|
|
891
|
+
res.end();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (req.method === "GET" && req.url === "/api/status") {
|
|
895
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
896
|
+
res.end(JSON.stringify({ running: true, pid: process.pid }));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (req.method === "POST" && req.url === "/api/context") {
|
|
900
|
+
let body = "";
|
|
901
|
+
req.on("data", (chunk) => {
|
|
902
|
+
body += chunk.toString();
|
|
903
|
+
});
|
|
904
|
+
req.on("end", () => {
|
|
905
|
+
try {
|
|
906
|
+
const payload = JSON.parse(body);
|
|
907
|
+
callbacks.onContextUpdate(payload);
|
|
908
|
+
sendToClient({ type: "context:update", payload });
|
|
909
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
910
|
+
res.end(JSON.stringify({ ok: true }));
|
|
911
|
+
} catch {
|
|
912
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
913
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (req.method === "POST" && req.url === "/api/refresh") {
|
|
919
|
+
callbacks.onRefreshRequest();
|
|
920
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
921
|
+
res.end(JSON.stringify({ ok: true }));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const pathname = (req.url ?? "").split("?")[0];
|
|
925
|
+
if (req.method === "GET" && (pathname === "/api/refs" || /^\/api\/reviews\/[^/]+\/refs$/.test(pathname))) {
|
|
926
|
+
if (callbacks.onRefsRequest) {
|
|
927
|
+
const refsPayload = await callbacks.onRefsRequest();
|
|
928
|
+
if (refsPayload) {
|
|
929
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
930
|
+
res.end(JSON.stringify(refsPayload));
|
|
931
|
+
} else {
|
|
932
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
933
|
+
res.end(JSON.stringify({ error: "Failed to list git refs" }));
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
res.writeHead(404);
|
|
937
|
+
res.end("Not found");
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (req.method === "POST" && (pathname === "/api/compare" || /^\/api\/reviews\/[^/]+\/compare$/.test(pathname))) {
|
|
942
|
+
if (callbacks.onCompareRequest) {
|
|
943
|
+
let body = "";
|
|
944
|
+
req.on("data", (chunk) => {
|
|
945
|
+
body += chunk.toString();
|
|
946
|
+
});
|
|
947
|
+
req.on("end", async () => {
|
|
948
|
+
try {
|
|
949
|
+
const { ref } = JSON.parse(body);
|
|
950
|
+
if (!ref) {
|
|
951
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
952
|
+
res.end(JSON.stringify({ error: "Missing ref" }));
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const success = await callbacks.onCompareRequest(ref);
|
|
956
|
+
if (success) {
|
|
957
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
958
|
+
res.end(JSON.stringify({ ok: true }));
|
|
959
|
+
} else {
|
|
960
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
961
|
+
res.end(JSON.stringify({ error: "Failed to compute diff" }));
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
965
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
} else {
|
|
969
|
+
res.writeHead(404);
|
|
970
|
+
res.end("Not found");
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
res.writeHead(404);
|
|
975
|
+
res.end("Not found");
|
|
976
|
+
});
|
|
977
|
+
const wss2 = new WebSocketServer({ server: httpServer });
|
|
978
|
+
function sendToClient(msg) {
|
|
979
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
980
|
+
client.send(JSON.stringify(msg));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
781
983
|
wss2.on("connection", (ws) => {
|
|
782
984
|
if (closeTimer) {
|
|
783
985
|
clearTimeout(closeTimer);
|
|
@@ -786,20 +988,16 @@ function createWsBridge(port) {
|
|
|
786
988
|
client = ws;
|
|
787
989
|
const payload = pendingInit ?? initPayload;
|
|
788
990
|
if (payload) {
|
|
789
|
-
|
|
790
|
-
type: "review:init",
|
|
791
|
-
payload
|
|
792
|
-
};
|
|
793
|
-
ws.send(JSON.stringify(msg));
|
|
991
|
+
sendToClient({ type: "review:init", payload });
|
|
794
992
|
pendingInit = null;
|
|
795
993
|
}
|
|
796
994
|
ws.on("message", (data) => {
|
|
797
995
|
try {
|
|
798
996
|
const msg = JSON.parse(data.toString());
|
|
799
|
-
if (msg.type === "review:submit" &&
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
997
|
+
if (msg.type === "review:submit" && submitCallback) {
|
|
998
|
+
submitCallback(msg.payload);
|
|
999
|
+
} else if (msg.type === "diff:change_ref" && callbacks.onDiffRefChange) {
|
|
1000
|
+
callbacks.onDiffRefChange(msg.payload.diffRef);
|
|
803
1001
|
}
|
|
804
1002
|
} catch {
|
|
805
1003
|
}
|
|
@@ -808,40 +1006,168 @@ function createWsBridge(port) {
|
|
|
808
1006
|
client = null;
|
|
809
1007
|
if (resultReject) {
|
|
810
1008
|
closeTimer = setTimeout(() => {
|
|
1009
|
+
closeTimer = null;
|
|
811
1010
|
if (resultReject) {
|
|
812
1011
|
resultReject(new Error("Browser closed before review was submitted"));
|
|
813
|
-
resultResolve = null;
|
|
814
1012
|
resultReject = null;
|
|
1013
|
+
submitCallback = null;
|
|
815
1014
|
}
|
|
816
1015
|
}, 2e3);
|
|
1016
|
+
} else {
|
|
1017
|
+
closeTimer = setTimeout(() => {
|
|
1018
|
+
closeTimer = null;
|
|
1019
|
+
}, 2e3);
|
|
817
1020
|
}
|
|
818
1021
|
});
|
|
819
1022
|
});
|
|
820
|
-
return {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1023
|
+
return new Promise((resolve, reject) => {
|
|
1024
|
+
httpServer.on("error", reject);
|
|
1025
|
+
httpServer.listen(port, () => {
|
|
1026
|
+
resolve({
|
|
1027
|
+
port,
|
|
1028
|
+
sendInit(payload) {
|
|
1029
|
+
initPayload = payload;
|
|
1030
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
1031
|
+
sendToClient({ type: "review:init", payload });
|
|
1032
|
+
} else {
|
|
1033
|
+
pendingInit = payload;
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
storeInitPayload(payload) {
|
|
1037
|
+
initPayload = payload;
|
|
1038
|
+
},
|
|
1039
|
+
sendDiffUpdate(payload) {
|
|
1040
|
+
sendToClient({ type: "diff:update", payload });
|
|
1041
|
+
},
|
|
1042
|
+
sendContextUpdate(payload) {
|
|
1043
|
+
sendToClient({ type: "context:update", payload });
|
|
1044
|
+
},
|
|
1045
|
+
sendDiffError(payload) {
|
|
1046
|
+
sendToClient({ type: "diff:error", payload });
|
|
1047
|
+
},
|
|
1048
|
+
onSubmit(callback) {
|
|
1049
|
+
submitCallback = callback;
|
|
1050
|
+
},
|
|
1051
|
+
waitForResult() {
|
|
1052
|
+
return new Promise((resolve2, reject2) => {
|
|
1053
|
+
submitCallback = resolve2;
|
|
1054
|
+
resultReject = reject2;
|
|
1055
|
+
});
|
|
1056
|
+
},
|
|
1057
|
+
triggerRefresh() {
|
|
1058
|
+
callbacks.onRefreshRequest();
|
|
1059
|
+
},
|
|
1060
|
+
async close() {
|
|
1061
|
+
if (closeTimer) {
|
|
1062
|
+
clearTimeout(closeTimer);
|
|
1063
|
+
}
|
|
1064
|
+
for (const ws of wss2.clients) {
|
|
1065
|
+
ws.close();
|
|
1066
|
+
}
|
|
1067
|
+
wss2.close();
|
|
1068
|
+
await new Promise((resolve2) => {
|
|
1069
|
+
httpServer.close(() => resolve2());
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// packages/core/src/diff-utils.ts
|
|
1078
|
+
import { createHash } from "crypto";
|
|
1079
|
+
function hashDiff(rawDiff) {
|
|
1080
|
+
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1081
|
+
}
|
|
1082
|
+
function fileKey(file) {
|
|
1083
|
+
return file.stage ? `${file.stage}:${file.path}` : file.path;
|
|
1084
|
+
}
|
|
1085
|
+
function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
1086
|
+
if (!oldDiffSet) {
|
|
1087
|
+
return newDiffSet.files.map(fileKey);
|
|
1088
|
+
}
|
|
1089
|
+
const oldFiles = new Map(
|
|
1090
|
+
oldDiffSet.files.map((f) => [fileKey(f), f])
|
|
1091
|
+
);
|
|
1092
|
+
const changed = [];
|
|
1093
|
+
for (const newFile of newDiffSet.files) {
|
|
1094
|
+
const key = fileKey(newFile);
|
|
1095
|
+
const oldFile = oldFiles.get(key);
|
|
1096
|
+
if (!oldFile) {
|
|
1097
|
+
changed.push(key);
|
|
1098
|
+
} else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
|
|
1099
|
+
changed.push(key);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const oldFile of oldDiffSet.files) {
|
|
1103
|
+
if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
|
|
1104
|
+
changed.push(fileKey(oldFile));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return changed;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// packages/core/src/diff-poller.ts
|
|
1111
|
+
function createDiffPoller(options) {
|
|
1112
|
+
let { diffRef } = options;
|
|
1113
|
+
const { cwd, pollInterval, onDiffChanged, onError, silent } = options;
|
|
1114
|
+
let lastDiffHash = null;
|
|
1115
|
+
let lastDiffSet = null;
|
|
1116
|
+
let refreshRequested = false;
|
|
1117
|
+
let interval = null;
|
|
1118
|
+
let running = false;
|
|
1119
|
+
function poll() {
|
|
1120
|
+
if (!running) return;
|
|
1121
|
+
try {
|
|
1122
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
|
|
1123
|
+
const newHash = hashDiff(newRawDiff);
|
|
1124
|
+
if (newHash !== lastDiffHash || refreshRequested) {
|
|
1125
|
+
refreshRequested = false;
|
|
1126
|
+
const newBriefing = analyze(newDiffSet);
|
|
1127
|
+
const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
|
|
1128
|
+
lastDiffHash = newHash;
|
|
1129
|
+
lastDiffSet = newDiffSet;
|
|
1130
|
+
const updatePayload = {
|
|
1131
|
+
diffSet: newDiffSet,
|
|
1132
|
+
rawDiff: newRawDiff,
|
|
1133
|
+
briefing: newBriefing,
|
|
1134
|
+
changedFiles,
|
|
1135
|
+
timestamp: Date.now()
|
|
828
1136
|
};
|
|
829
|
-
|
|
830
|
-
} else {
|
|
831
|
-
pendingInit = payload;
|
|
1137
|
+
onDiffChanged(updatePayload);
|
|
832
1138
|
}
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
if (onError && err instanceof Error) {
|
|
1141
|
+
onError(err);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
start() {
|
|
1147
|
+
if (running) return;
|
|
1148
|
+
running = true;
|
|
1149
|
+
try {
|
|
1150
|
+
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1151
|
+
lastDiffHash = hashDiff(initialRawDiff);
|
|
1152
|
+
lastDiffSet = initialDiffSet;
|
|
1153
|
+
} catch {
|
|
1154
|
+
}
|
|
1155
|
+
interval = setInterval(poll, pollInterval);
|
|
833
1156
|
},
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
},
|
|
840
|
-
close() {
|
|
841
|
-
for (const ws of wss2.clients) {
|
|
842
|
-
ws.close();
|
|
1157
|
+
stop() {
|
|
1158
|
+
running = false;
|
|
1159
|
+
if (interval) {
|
|
1160
|
+
clearInterval(interval);
|
|
1161
|
+
interval = null;
|
|
843
1162
|
}
|
|
844
|
-
|
|
1163
|
+
},
|
|
1164
|
+
setDiffRef(newRef) {
|
|
1165
|
+
diffRef = newRef;
|
|
1166
|
+
lastDiffHash = null;
|
|
1167
|
+
lastDiffSet = null;
|
|
1168
|
+
},
|
|
1169
|
+
refresh() {
|
|
1170
|
+
refreshRequested = true;
|
|
845
1171
|
}
|
|
846
1172
|
};
|
|
847
1173
|
}
|
|
@@ -868,9 +1194,9 @@ function updateSession(id, update) {
|
|
|
868
1194
|
}
|
|
869
1195
|
|
|
870
1196
|
// packages/core/src/ui-server.ts
|
|
871
|
-
import
|
|
872
|
-
import
|
|
873
|
-
import
|
|
1197
|
+
import http2 from "http";
|
|
1198
|
+
import fs2 from "fs";
|
|
1199
|
+
import path4 from "path";
|
|
874
1200
|
import { fileURLToPath } from "url";
|
|
875
1201
|
var MIME_TYPES = {
|
|
876
1202
|
".html": "text/html",
|
|
@@ -885,14 +1211,14 @@ var MIME_TYPES = {
|
|
|
885
1211
|
};
|
|
886
1212
|
function resolveUiDist() {
|
|
887
1213
|
const thisFile = fileURLToPath(import.meta.url);
|
|
888
|
-
const thisDir =
|
|
889
|
-
const publishedUiDist =
|
|
890
|
-
if (
|
|
1214
|
+
const thisDir = path4.dirname(thisFile);
|
|
1215
|
+
const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
|
|
1216
|
+
if (fs2.existsSync(path4.join(publishedUiDist, "index.html"))) {
|
|
891
1217
|
return publishedUiDist;
|
|
892
1218
|
}
|
|
893
|
-
const workspaceRoot =
|
|
894
|
-
const devUiDist =
|
|
895
|
-
if (
|
|
1219
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
1220
|
+
const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
|
|
1221
|
+
if (fs2.existsSync(path4.join(devUiDist, "index.html"))) {
|
|
896
1222
|
return devUiDist;
|
|
897
1223
|
}
|
|
898
1224
|
throw new Error(
|
|
@@ -901,10 +1227,10 @@ function resolveUiDist() {
|
|
|
901
1227
|
}
|
|
902
1228
|
function resolveUiRoot() {
|
|
903
1229
|
const thisFile = fileURLToPath(import.meta.url);
|
|
904
|
-
const thisDir =
|
|
905
|
-
const workspaceRoot =
|
|
906
|
-
const uiRoot =
|
|
907
|
-
if (
|
|
1230
|
+
const thisDir = path4.dirname(thisFile);
|
|
1231
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
1232
|
+
const uiRoot = path4.join(workspaceRoot, "packages", "ui");
|
|
1233
|
+
if (fs2.existsSync(path4.join(uiRoot, "index.html"))) {
|
|
908
1234
|
return uiRoot;
|
|
909
1235
|
}
|
|
910
1236
|
throw new Error(
|
|
@@ -922,16 +1248,16 @@ async function startViteDevServer(uiRoot, port, silent) {
|
|
|
922
1248
|
return vite;
|
|
923
1249
|
}
|
|
924
1250
|
function createStaticServer(distPath, port) {
|
|
925
|
-
const server =
|
|
1251
|
+
const server = http2.createServer((req, res) => {
|
|
926
1252
|
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
927
|
-
let filePath =
|
|
928
|
-
if (!
|
|
929
|
-
filePath =
|
|
1253
|
+
let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
1254
|
+
if (!fs2.existsSync(filePath)) {
|
|
1255
|
+
filePath = path4.join(distPath, "index.html");
|
|
930
1256
|
}
|
|
931
|
-
const ext =
|
|
1257
|
+
const ext = path4.extname(filePath);
|
|
932
1258
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
933
1259
|
try {
|
|
934
|
-
const content =
|
|
1260
|
+
const content = fs2.readFileSync(filePath);
|
|
935
1261
|
res.writeHead(200, { "Content-Type": contentType });
|
|
936
1262
|
res.end(content);
|
|
937
1263
|
} catch {
|
|
@@ -963,11 +1289,75 @@ async function startReview(options) {
|
|
|
963
1289
|
const briefing = analyze(diffSet);
|
|
964
1290
|
const session = createSession(options);
|
|
965
1291
|
updateSession(session.id, { status: "in_progress" });
|
|
966
|
-
const
|
|
1292
|
+
const metadata = {
|
|
1293
|
+
title,
|
|
1294
|
+
description,
|
|
1295
|
+
reasoning,
|
|
1296
|
+
currentBranch
|
|
1297
|
+
};
|
|
1298
|
+
let poller = null;
|
|
1299
|
+
const [bridgePort, httpPort] = await Promise.all([
|
|
967
1300
|
getPort(),
|
|
968
1301
|
getPort()
|
|
969
1302
|
]);
|
|
970
|
-
|
|
1303
|
+
function handleDiffRefChange(newRef) {
|
|
1304
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1305
|
+
const newBriefing = analyze(newDiffSet);
|
|
1306
|
+
bridge.sendDiffUpdate({
|
|
1307
|
+
diffSet: newDiffSet,
|
|
1308
|
+
rawDiff: newRawDiff,
|
|
1309
|
+
briefing: newBriefing,
|
|
1310
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1311
|
+
timestamp: Date.now()
|
|
1312
|
+
});
|
|
1313
|
+
bridge.storeInitPayload({
|
|
1314
|
+
reviewId: session.id,
|
|
1315
|
+
diffSet: newDiffSet,
|
|
1316
|
+
rawDiff: newRawDiff,
|
|
1317
|
+
briefing: newBriefing,
|
|
1318
|
+
metadata,
|
|
1319
|
+
watchMode: true
|
|
1320
|
+
});
|
|
1321
|
+
poller?.setDiffRef(newRef);
|
|
1322
|
+
}
|
|
1323
|
+
const bridge = await createWatchBridge(bridgePort, {
|
|
1324
|
+
onRefreshRequest: () => {
|
|
1325
|
+
poller?.refresh();
|
|
1326
|
+
},
|
|
1327
|
+
onContextUpdate: (payload) => {
|
|
1328
|
+
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1329
|
+
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1330
|
+
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1331
|
+
},
|
|
1332
|
+
onDiffRefChange: (newRef) => {
|
|
1333
|
+
try {
|
|
1334
|
+
handleDiffRefChange(newRef);
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
bridge.sendDiffError({
|
|
1337
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
onRefsRequest: async () => {
|
|
1342
|
+
try {
|
|
1343
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1344
|
+
const branches = listBranches({ cwd: resolvedCwd });
|
|
1345
|
+
const commits = listCommits({ cwd: resolvedCwd });
|
|
1346
|
+
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1347
|
+
return { branches, commits, currentBranch: branch };
|
|
1348
|
+
} catch {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
onCompareRequest: async (ref) => {
|
|
1353
|
+
try {
|
|
1354
|
+
handleDiffRefChange(ref);
|
|
1355
|
+
return true;
|
|
1356
|
+
} catch {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
971
1361
|
let httpServer = null;
|
|
972
1362
|
let viteServer = null;
|
|
973
1363
|
try {
|
|
@@ -978,7 +1368,15 @@ async function startReview(options) {
|
|
|
978
1368
|
const uiDist = resolveUiDist();
|
|
979
1369
|
httpServer = await createStaticServer(uiDist, httpPort);
|
|
980
1370
|
}
|
|
981
|
-
|
|
1371
|
+
writeWatchFile(cwd, {
|
|
1372
|
+
wsPort: bridgePort,
|
|
1373
|
+
uiPort: httpPort,
|
|
1374
|
+
pid: process.pid,
|
|
1375
|
+
cwd: cwd ?? process.cwd(),
|
|
1376
|
+
diffRef,
|
|
1377
|
+
startedAt: Date.now()
|
|
1378
|
+
});
|
|
1379
|
+
const url = `http://localhost:${httpPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${session.id}`;
|
|
982
1380
|
if (!silent) {
|
|
983
1381
|
console.log(`
|
|
984
1382
|
DiffPrism Review: ${title ?? briefing.summary}`);
|
|
@@ -991,124 +1389,46 @@ DiffPrism Review: ${title ?? briefing.summary}`);
|
|
|
991
1389
|
diffSet,
|
|
992
1390
|
rawDiff,
|
|
993
1391
|
briefing,
|
|
994
|
-
metadata
|
|
1392
|
+
metadata,
|
|
1393
|
+
watchMode: true
|
|
995
1394
|
};
|
|
996
1395
|
bridge.sendInit(initPayload);
|
|
1396
|
+
poller = createDiffPoller({
|
|
1397
|
+
diffRef,
|
|
1398
|
+
cwd: cwd ?? process.cwd(),
|
|
1399
|
+
pollInterval: 1e3,
|
|
1400
|
+
onDiffChanged: (updatePayload) => {
|
|
1401
|
+
bridge.storeInitPayload({
|
|
1402
|
+
reviewId: session.id,
|
|
1403
|
+
diffSet: updatePayload.diffSet,
|
|
1404
|
+
rawDiff: updatePayload.rawDiff,
|
|
1405
|
+
briefing: updatePayload.briefing,
|
|
1406
|
+
metadata,
|
|
1407
|
+
watchMode: true
|
|
1408
|
+
});
|
|
1409
|
+
bridge.sendDiffUpdate(updatePayload);
|
|
1410
|
+
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
1411
|
+
console.log(
|
|
1412
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
poller.start();
|
|
997
1418
|
const result = await bridge.waitForResult();
|
|
1419
|
+
poller.stop();
|
|
998
1420
|
updateSession(session.id, { status: "completed", result });
|
|
999
1421
|
return result;
|
|
1000
1422
|
} finally {
|
|
1001
|
-
|
|
1423
|
+
poller?.stop();
|
|
1424
|
+
await bridge.close();
|
|
1425
|
+
removeWatchFile(cwd);
|
|
1002
1426
|
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;
|
|
1427
|
+
await viteServer.close();
|
|
1095
1428
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
function consumeReviewResult(cwd) {
|
|
1102
|
-
try {
|
|
1103
|
-
const filePath = reviewResultPath(cwd);
|
|
1104
|
-
if (!fs2.existsSync(filePath)) {
|
|
1105
|
-
return;
|
|
1429
|
+
if (httpServer) {
|
|
1430
|
+
httpServer.close();
|
|
1106
1431
|
}
|
|
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
1432
|
}
|
|
1113
1433
|
}
|
|
1114
1434
|
|
|
@@ -1185,170 +1505,6 @@ async function isServerAlive() {
|
|
|
1185
1505
|
// packages/core/src/watch.ts
|
|
1186
1506
|
import getPort2 from "get-port";
|
|
1187
1507
|
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
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
wss2.on("connection", (ws) => {
|
|
1247
|
-
if (closeTimer) {
|
|
1248
|
-
clearTimeout(closeTimer);
|
|
1249
|
-
closeTimer = null;
|
|
1250
|
-
}
|
|
1251
|
-
client = ws;
|
|
1252
|
-
const payload = pendingInit ?? initPayload;
|
|
1253
|
-
if (payload) {
|
|
1254
|
-
sendToClient({ type: "review:init", payload });
|
|
1255
|
-
pendingInit = null;
|
|
1256
|
-
}
|
|
1257
|
-
ws.on("message", (data) => {
|
|
1258
|
-
try {
|
|
1259
|
-
const msg = JSON.parse(data.toString());
|
|
1260
|
-
if (msg.type === "review:submit" && submitCallback) {
|
|
1261
|
-
submitCallback(msg.payload);
|
|
1262
|
-
}
|
|
1263
|
-
} catch {
|
|
1264
|
-
}
|
|
1265
|
-
});
|
|
1266
|
-
ws.on("close", () => {
|
|
1267
|
-
client = null;
|
|
1268
|
-
closeTimer = setTimeout(() => {
|
|
1269
|
-
closeTimer = null;
|
|
1270
|
-
}, 2e3);
|
|
1271
|
-
});
|
|
1272
|
-
});
|
|
1273
|
-
return new Promise((resolve, reject) => {
|
|
1274
|
-
httpServer.on("error", reject);
|
|
1275
|
-
httpServer.listen(port, () => {
|
|
1276
|
-
resolve({
|
|
1277
|
-
port,
|
|
1278
|
-
sendInit(payload) {
|
|
1279
|
-
initPayload = payload;
|
|
1280
|
-
if (client && client.readyState === WebSocket2.OPEN) {
|
|
1281
|
-
sendToClient({ type: "review:init", payload });
|
|
1282
|
-
} else {
|
|
1283
|
-
pendingInit = payload;
|
|
1284
|
-
}
|
|
1285
|
-
},
|
|
1286
|
-
storeInitPayload(payload) {
|
|
1287
|
-
initPayload = payload;
|
|
1288
|
-
},
|
|
1289
|
-
sendDiffUpdate(payload) {
|
|
1290
|
-
sendToClient({ type: "diff:update", payload });
|
|
1291
|
-
},
|
|
1292
|
-
sendContextUpdate(payload) {
|
|
1293
|
-
sendToClient({ type: "context:update", payload });
|
|
1294
|
-
},
|
|
1295
|
-
onSubmit(callback) {
|
|
1296
|
-
submitCallback = callback;
|
|
1297
|
-
},
|
|
1298
|
-
triggerRefresh() {
|
|
1299
|
-
callbacks.onRefreshRequest();
|
|
1300
|
-
},
|
|
1301
|
-
async close() {
|
|
1302
|
-
if (closeTimer) {
|
|
1303
|
-
clearTimeout(closeTimer);
|
|
1304
|
-
}
|
|
1305
|
-
for (const ws of wss2.clients) {
|
|
1306
|
-
ws.close();
|
|
1307
|
-
}
|
|
1308
|
-
wss2.close();
|
|
1309
|
-
await new Promise((resolve2) => {
|
|
1310
|
-
httpServer.close(() => resolve2());
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
});
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// packages/core/src/diff-utils.ts
|
|
1319
|
-
import { createHash } from "crypto";
|
|
1320
|
-
function hashDiff(rawDiff) {
|
|
1321
|
-
return createHash("sha256").update(rawDiff).digest("hex");
|
|
1322
|
-
}
|
|
1323
|
-
function fileKey(file) {
|
|
1324
|
-
return file.stage ? `${file.stage}:${file.path}` : file.path;
|
|
1325
|
-
}
|
|
1326
|
-
function detectChangedFiles(oldDiffSet, newDiffSet) {
|
|
1327
|
-
if (!oldDiffSet) {
|
|
1328
|
-
return newDiffSet.files.map(fileKey);
|
|
1329
|
-
}
|
|
1330
|
-
const oldFiles = new Map(
|
|
1331
|
-
oldDiffSet.files.map((f) => [fileKey(f), f])
|
|
1332
|
-
);
|
|
1333
|
-
const changed = [];
|
|
1334
|
-
for (const newFile of newDiffSet.files) {
|
|
1335
|
-
const key = fileKey(newFile);
|
|
1336
|
-
const oldFile = oldFiles.get(key);
|
|
1337
|
-
if (!oldFile) {
|
|
1338
|
-
changed.push(key);
|
|
1339
|
-
} else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
|
|
1340
|
-
changed.push(key);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
for (const oldFile of oldDiffSet.files) {
|
|
1344
|
-
if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
|
|
1345
|
-
changed.push(fileKey(oldFile));
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
return changed;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// packages/core/src/watch.ts
|
|
1352
1508
|
async function startWatch(options) {
|
|
1353
1509
|
const {
|
|
1354
1510
|
diffRef,
|
|
@@ -1363,8 +1519,6 @@ async function startWatch(options) {
|
|
|
1363
1519
|
const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
|
|
1364
1520
|
const currentBranch = getCurrentBranch({ cwd });
|
|
1365
1521
|
const initialBriefing = analyze(initialDiffSet);
|
|
1366
|
-
let lastDiffHash = hashDiff(initialRawDiff);
|
|
1367
|
-
let lastDiffSet = initialDiffSet;
|
|
1368
1522
|
const metadata = {
|
|
1369
1523
|
title,
|
|
1370
1524
|
description,
|
|
@@ -1375,15 +1529,63 @@ async function startWatch(options) {
|
|
|
1375
1529
|
getPort2(),
|
|
1376
1530
|
getPort2()
|
|
1377
1531
|
]);
|
|
1378
|
-
|
|
1532
|
+
const reviewId = "watch-session";
|
|
1533
|
+
function handleDiffRefChange(newRef) {
|
|
1534
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
|
|
1535
|
+
const newBriefing = analyze(newDiffSet);
|
|
1536
|
+
bridge.sendDiffUpdate({
|
|
1537
|
+
diffSet: newDiffSet,
|
|
1538
|
+
rawDiff: newRawDiff,
|
|
1539
|
+
briefing: newBriefing,
|
|
1540
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
1541
|
+
timestamp: Date.now()
|
|
1542
|
+
});
|
|
1543
|
+
bridge.storeInitPayload({
|
|
1544
|
+
reviewId,
|
|
1545
|
+
diffSet: newDiffSet,
|
|
1546
|
+
rawDiff: newRawDiff,
|
|
1547
|
+
briefing: newBriefing,
|
|
1548
|
+
metadata,
|
|
1549
|
+
watchMode: true
|
|
1550
|
+
});
|
|
1551
|
+
poller.setDiffRef(newRef);
|
|
1552
|
+
}
|
|
1379
1553
|
const bridge = await createWatchBridge(bridgePort, {
|
|
1380
1554
|
onRefreshRequest: () => {
|
|
1381
|
-
|
|
1555
|
+
poller.refresh();
|
|
1382
1556
|
},
|
|
1383
1557
|
onContextUpdate: (payload) => {
|
|
1384
1558
|
if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
|
|
1385
1559
|
if (payload.title !== void 0) metadata.title = payload.title;
|
|
1386
1560
|
if (payload.description !== void 0) metadata.description = payload.description;
|
|
1561
|
+
},
|
|
1562
|
+
onDiffRefChange: (newRef) => {
|
|
1563
|
+
try {
|
|
1564
|
+
handleDiffRefChange(newRef);
|
|
1565
|
+
} catch (err) {
|
|
1566
|
+
bridge.sendDiffError({
|
|
1567
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
onRefsRequest: async () => {
|
|
1572
|
+
try {
|
|
1573
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1574
|
+
const branches = listBranches({ cwd: resolvedCwd });
|
|
1575
|
+
const commits = listCommits({ cwd: resolvedCwd });
|
|
1576
|
+
const branch = getCurrentBranch({ cwd: resolvedCwd });
|
|
1577
|
+
return { branches, commits, currentBranch: branch };
|
|
1578
|
+
} catch {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
onCompareRequest: async (ref) => {
|
|
1583
|
+
try {
|
|
1584
|
+
handleDiffRefChange(ref);
|
|
1585
|
+
return true;
|
|
1586
|
+
} catch {
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1387
1589
|
}
|
|
1388
1590
|
});
|
|
1389
1591
|
let httpServer = null;
|
|
@@ -1403,8 +1605,7 @@ async function startWatch(options) {
|
|
|
1403
1605
|
diffRef,
|
|
1404
1606
|
startedAt: Date.now()
|
|
1405
1607
|
});
|
|
1406
|
-
const
|
|
1407
|
-
const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&reviewId=${reviewId}`;
|
|
1608
|
+
const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${reviewId}`;
|
|
1408
1609
|
if (!silent) {
|
|
1409
1610
|
console.log(`
|
|
1410
1611
|
DiffPrism Watch: ${title ?? `watching ${diffRef}`}`);
|
|
@@ -1434,46 +1635,30 @@ Review submitted: ${result.decision}`);
|
|
|
1434
1635
|
}
|
|
1435
1636
|
writeReviewResult(cwd, result);
|
|
1436
1637
|
});
|
|
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
|
-
}
|
|
1638
|
+
const poller = createDiffPoller({
|
|
1639
|
+
diffRef,
|
|
1640
|
+
cwd: cwd ?? process.cwd(),
|
|
1641
|
+
pollInterval,
|
|
1642
|
+
onDiffChanged: (updatePayload) => {
|
|
1643
|
+
bridge.storeInitPayload({
|
|
1644
|
+
reviewId,
|
|
1645
|
+
diffSet: updatePayload.diffSet,
|
|
1646
|
+
rawDiff: updatePayload.rawDiff,
|
|
1647
|
+
briefing: updatePayload.briefing,
|
|
1648
|
+
metadata,
|
|
1649
|
+
watchMode: true
|
|
1650
|
+
});
|
|
1651
|
+
bridge.sendDiffUpdate(updatePayload);
|
|
1652
|
+
if (!silent && updatePayload.changedFiles.length > 0) {
|
|
1653
|
+
console.log(
|
|
1654
|
+
`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
|
|
1655
|
+
);
|
|
1470
1656
|
}
|
|
1471
|
-
} catch {
|
|
1472
1657
|
}
|
|
1473
|
-
}
|
|
1658
|
+
});
|
|
1659
|
+
poller.start();
|
|
1474
1660
|
async function stop() {
|
|
1475
|
-
|
|
1476
|
-
clearInterval(pollLoop);
|
|
1661
|
+
poller.stop();
|
|
1477
1662
|
await bridge.close();
|
|
1478
1663
|
if (viteServer) {
|
|
1479
1664
|
await viteServer.close();
|
|
@@ -1497,7 +1682,7 @@ import http3 from "http";
|
|
|
1497
1682
|
import { randomUUID } from "crypto";
|
|
1498
1683
|
import getPort3 from "get-port";
|
|
1499
1684
|
import open3 from "open";
|
|
1500
|
-
import { WebSocketServer as
|
|
1685
|
+
import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
|
|
1501
1686
|
var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
|
|
1502
1687
|
var ABANDONED_TTL_MS = 60 * 60 * 1e3;
|
|
1503
1688
|
var CLEANUP_INTERVAL_MS = 60 * 1e3;
|
|
@@ -1563,7 +1748,7 @@ function broadcastToAll(msg) {
|
|
|
1563
1748
|
if (!wss) return;
|
|
1564
1749
|
const data = JSON.stringify(msg);
|
|
1565
1750
|
for (const client of wss.clients) {
|
|
1566
|
-
if (client.readyState ===
|
|
1751
|
+
if (client.readyState === WebSocket2.OPEN) {
|
|
1567
1752
|
client.send(data);
|
|
1568
1753
|
}
|
|
1569
1754
|
}
|
|
@@ -1572,7 +1757,7 @@ function sendToSessionClients(sessionId, msg) {
|
|
|
1572
1757
|
if (!wss) return;
|
|
1573
1758
|
const data = JSON.stringify(msg);
|
|
1574
1759
|
for (const [client, sid] of clientSessions.entries()) {
|
|
1575
|
-
if (sid === sessionId && client.readyState ===
|
|
1760
|
+
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
1576
1761
|
client.send(data);
|
|
1577
1762
|
}
|
|
1578
1763
|
}
|
|
@@ -1596,7 +1781,7 @@ function broadcastSessionRemoved(sessionId) {
|
|
|
1596
1781
|
}
|
|
1597
1782
|
function hasViewersForSession(sessionId) {
|
|
1598
1783
|
for (const [client, sid] of clientSessions.entries()) {
|
|
1599
|
-
if (sid === sessionId && client.readyState ===
|
|
1784
|
+
if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
|
|
1600
1785
|
return true;
|
|
1601
1786
|
}
|
|
1602
1787
|
}
|
|
@@ -1606,54 +1791,40 @@ function startSessionWatcher(sessionId) {
|
|
|
1606
1791
|
if (sessionWatchers.has(sessionId)) return;
|
|
1607
1792
|
const session = sessions2.get(sessionId);
|
|
1608
1793
|
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
|
-
}
|
|
1794
|
+
const poller = createDiffPoller({
|
|
1795
|
+
diffRef: session.diffRef,
|
|
1796
|
+
cwd: session.projectPath,
|
|
1797
|
+
pollInterval: serverPollInterval,
|
|
1798
|
+
onDiffChanged: (updatePayload) => {
|
|
1799
|
+
const s = sessions2.get(sessionId);
|
|
1800
|
+
if (!s) return;
|
|
1801
|
+
s.payload = {
|
|
1802
|
+
...s.payload,
|
|
1803
|
+
diffSet: updatePayload.diffSet,
|
|
1804
|
+
rawDiff: updatePayload.rawDiff,
|
|
1805
|
+
briefing: updatePayload.briefing
|
|
1806
|
+
};
|
|
1807
|
+
s.lastDiffHash = hashDiff(updatePayload.rawDiff);
|
|
1808
|
+
s.lastDiffSet = updatePayload.diffSet;
|
|
1809
|
+
if (hasViewersForSession(sessionId)) {
|
|
1810
|
+
sendToSessionClients(sessionId, {
|
|
1811
|
+
type: "diff:update",
|
|
1812
|
+
payload: updatePayload
|
|
1813
|
+
});
|
|
1814
|
+
s.hasNewChanges = false;
|
|
1815
|
+
} else {
|
|
1816
|
+
s.hasNewChanges = true;
|
|
1817
|
+
broadcastSessionList();
|
|
1647
1818
|
}
|
|
1648
|
-
} catch {
|
|
1649
1819
|
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1820
|
+
});
|
|
1821
|
+
poller.start();
|
|
1822
|
+
sessionWatchers.set(sessionId, poller);
|
|
1652
1823
|
}
|
|
1653
1824
|
function stopSessionWatcher(sessionId) {
|
|
1654
|
-
const
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1825
|
+
const poller = sessionWatchers.get(sessionId);
|
|
1826
|
+
if (poller) {
|
|
1827
|
+
poller.stop();
|
|
1657
1828
|
sessionWatchers.delete(sessionId);
|
|
1658
1829
|
}
|
|
1659
1830
|
}
|
|
@@ -1665,15 +1836,15 @@ function startAllWatchers() {
|
|
|
1665
1836
|
}
|
|
1666
1837
|
}
|
|
1667
1838
|
function stopAllWatchers() {
|
|
1668
|
-
for (const [
|
|
1669
|
-
|
|
1839
|
+
for (const [, poller] of sessionWatchers.entries()) {
|
|
1840
|
+
poller.stop();
|
|
1670
1841
|
}
|
|
1671
1842
|
sessionWatchers.clear();
|
|
1672
1843
|
}
|
|
1673
1844
|
function hasConnectedClients() {
|
|
1674
1845
|
if (!wss) return false;
|
|
1675
1846
|
for (const client of wss.clients) {
|
|
1676
|
-
if (client.readyState ===
|
|
1847
|
+
if (client.readyState === WebSocket2.OPEN) return true;
|
|
1677
1848
|
}
|
|
1678
1849
|
return false;
|
|
1679
1850
|
}
|
|
@@ -1968,7 +2139,7 @@ async function startGlobalServer(options = {}) {
|
|
|
1968
2139
|
res.end("Not found");
|
|
1969
2140
|
}
|
|
1970
2141
|
});
|
|
1971
|
-
wss = new
|
|
2142
|
+
wss = new WebSocketServer2({ port: wsPort });
|
|
1972
2143
|
wss.on("connection", (ws, req) => {
|
|
1973
2144
|
startAllWatchers();
|
|
1974
2145
|
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
@@ -2046,6 +2217,49 @@ async function startGlobalServer(options = {}) {
|
|
|
2046
2217
|
closedSession.status = "submitted";
|
|
2047
2218
|
}
|
|
2048
2219
|
broadcastSessionRemoved(closedId);
|
|
2220
|
+
} else if (msg.type === "diff:change_ref") {
|
|
2221
|
+
const sid = clientSessions.get(ws);
|
|
2222
|
+
if (sid) {
|
|
2223
|
+
const session = sessions2.get(sid);
|
|
2224
|
+
if (session) {
|
|
2225
|
+
const newRef = msg.payload.diffRef;
|
|
2226
|
+
try {
|
|
2227
|
+
const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, {
|
|
2228
|
+
cwd: session.projectPath
|
|
2229
|
+
});
|
|
2230
|
+
const newBriefing = analyze(newDiffSet);
|
|
2231
|
+
session.payload = {
|
|
2232
|
+
...session.payload,
|
|
2233
|
+
diffSet: newDiffSet,
|
|
2234
|
+
rawDiff: newRawDiff,
|
|
2235
|
+
briefing: newBriefing
|
|
2236
|
+
};
|
|
2237
|
+
session.diffRef = newRef;
|
|
2238
|
+
session.lastDiffHash = hashDiff(newRawDiff);
|
|
2239
|
+
session.lastDiffSet = newDiffSet;
|
|
2240
|
+
stopSessionWatcher(sid);
|
|
2241
|
+
startSessionWatcher(sid);
|
|
2242
|
+
sendToSessionClients(sid, {
|
|
2243
|
+
type: "diff:update",
|
|
2244
|
+
payload: {
|
|
2245
|
+
diffSet: newDiffSet,
|
|
2246
|
+
rawDiff: newRawDiff,
|
|
2247
|
+
briefing: newBriefing,
|
|
2248
|
+
changedFiles: newDiffSet.files.map((f) => f.path),
|
|
2249
|
+
timestamp: Date.now()
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
const errorMsg = {
|
|
2254
|
+
type: "diff:error",
|
|
2255
|
+
payload: {
|
|
2256
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
ws.send(JSON.stringify(errorMsg));
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2049
2263
|
}
|
|
2050
2264
|
} catch {
|
|
2051
2265
|
}
|
|
@@ -2130,10 +2344,10 @@ export {
|
|
|
2130
2344
|
getCurrentBranch,
|
|
2131
2345
|
getDiff,
|
|
2132
2346
|
analyze,
|
|
2133
|
-
startReview,
|
|
2134
2347
|
readWatchFile,
|
|
2135
2348
|
readReviewResult,
|
|
2136
2349
|
consumeReviewResult,
|
|
2350
|
+
startReview,
|
|
2137
2351
|
startWatch,
|
|
2138
2352
|
readServerFile,
|
|
2139
2353
|
isServerAlive,
|