diffprism 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +91 -2
- package/dist/{chunk-TVXIMP3G.js → chunk-4WN4FIY4.js} +450 -19
- package/dist/mcp-server.js +173 -13
- package/package.json +1 -1
- package/ui-dist/assets/{index-CKJwY3F0.js → index-CNL0CLAo.js} +67 -62
- package/ui-dist/assets/index-ymFelmyA.css +1 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-D39rVNSs.css +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
isServerAlive,
|
|
4
|
+
readServerFile,
|
|
3
5
|
readWatchFile,
|
|
6
|
+
startGlobalServer,
|
|
4
7
|
startReview,
|
|
5
8
|
startWatch
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-4WN4FIY4.js";
|
|
7
10
|
|
|
8
11
|
// cli/src/index.ts
|
|
9
12
|
import { Command } from "commander";
|
|
@@ -490,9 +493,92 @@ async function notifyStop() {
|
|
|
490
493
|
process.exit(0);
|
|
491
494
|
}
|
|
492
495
|
|
|
496
|
+
// cli/src/commands/server.ts
|
|
497
|
+
async function server(flags) {
|
|
498
|
+
const existing = await isServerAlive();
|
|
499
|
+
if (existing) {
|
|
500
|
+
console.log(`DiffPrism server is already running on port ${existing.httpPort} (PID ${existing.pid})`);
|
|
501
|
+
console.log(`Use 'diffprism server stop' to stop it first.`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const httpPort = flags.port ? parseInt(flags.port, 10) : void 0;
|
|
506
|
+
const wsPort = flags.wsPort ? parseInt(flags.wsPort, 10) : void 0;
|
|
507
|
+
try {
|
|
508
|
+
const handle = await startGlobalServer({
|
|
509
|
+
httpPort,
|
|
510
|
+
wsPort,
|
|
511
|
+
dev: flags.dev
|
|
512
|
+
});
|
|
513
|
+
const shutdown = async () => {
|
|
514
|
+
console.log("\nStopping server...");
|
|
515
|
+
await handle.stop();
|
|
516
|
+
process.exit(0);
|
|
517
|
+
};
|
|
518
|
+
process.on("SIGINT", shutdown);
|
|
519
|
+
process.on("SIGTERM", shutdown);
|
|
520
|
+
await new Promise(() => {
|
|
521
|
+
});
|
|
522
|
+
} catch (err) {
|
|
523
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
524
|
+
console.error(`Error starting server: ${message}`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function serverStatus() {
|
|
529
|
+
const info = await isServerAlive();
|
|
530
|
+
if (!info) {
|
|
531
|
+
console.log("No DiffPrism server is running.");
|
|
532
|
+
process.exit(1);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
537
|
+
signal: AbortSignal.timeout(2e3)
|
|
538
|
+
});
|
|
539
|
+
const status = await response.json();
|
|
540
|
+
console.log(`DiffPrism Server`);
|
|
541
|
+
console.log(` API: http://localhost:${info.httpPort}`);
|
|
542
|
+
console.log(` WS: ws://localhost:${info.wsPort}`);
|
|
543
|
+
console.log(` PID: ${status.pid}`);
|
|
544
|
+
console.log(` Sessions: ${status.sessions}`);
|
|
545
|
+
console.log(` Uptime: ${Math.floor(status.uptime)}s`);
|
|
546
|
+
if (status.sessions > 0) {
|
|
547
|
+
const sessionsResponse = await fetch(
|
|
548
|
+
`http://localhost:${info.httpPort}/api/reviews`,
|
|
549
|
+
{ signal: AbortSignal.timeout(2e3) }
|
|
550
|
+
);
|
|
551
|
+
const { sessions } = await sessionsResponse.json();
|
|
552
|
+
console.log(`
|
|
553
|
+
Active Sessions:`);
|
|
554
|
+
for (const s of sessions) {
|
|
555
|
+
const label = s.title ?? s.branch ?? s.projectPath;
|
|
556
|
+
console.log(` ${s.id} \u2014 ${label} (${s.status}, ${s.fileCount} files, +${s.additions}/-${s.deletions})`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
561
|
+
console.error(`Error checking server status: ${message}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function serverStop() {
|
|
566
|
+
const info = readServerFile();
|
|
567
|
+
if (!info) {
|
|
568
|
+
console.log("No DiffPrism server is running.");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
process.kill(info.pid, "SIGTERM");
|
|
573
|
+
console.log(`Sent stop signal to DiffPrism server (PID ${info.pid}).`);
|
|
574
|
+
} catch {
|
|
575
|
+
console.log(`Server process (PID ${info.pid}) is no longer running. Cleaning up.`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
493
579
|
// cli/src/index.ts
|
|
494
580
|
var program = new Command();
|
|
495
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.
|
|
581
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.16.0" : "0.0.0-dev");
|
|
496
582
|
program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
|
|
497
583
|
program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
|
|
498
584
|
program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
|
|
@@ -501,4 +587,7 @@ program.command("serve").description("Start the MCP server for Claude Code integ
|
|
|
501
587
|
program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action((flags) => {
|
|
502
588
|
setup(flags);
|
|
503
589
|
});
|
|
590
|
+
var serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").action(server);
|
|
591
|
+
serverCmd.command("status").description("Check if the global server is running and list active sessions").action(serverStatus);
|
|
592
|
+
serverCmd.command("stop").description("Stop the running global server").action(serverStop);
|
|
504
593
|
program.parse();
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// packages/core/src/pipeline.ts
|
|
2
|
-
import getPort from "get-port";
|
|
3
|
-
import open from "open";
|
|
4
|
-
|
|
5
1
|
// packages/git/src/local.ts
|
|
6
2
|
import { execSync } from "child_process";
|
|
7
3
|
import { readFileSync } from "fs";
|
|
@@ -537,15 +533,15 @@ var CONFIG_PATTERNS = [
|
|
|
537
533
|
/vite\.config/,
|
|
538
534
|
/vitest\.config/
|
|
539
535
|
];
|
|
540
|
-
function isTestFile(
|
|
541
|
-
return TEST_PATTERNS.some((re) => re.test(
|
|
536
|
+
function isTestFile(path6) {
|
|
537
|
+
return TEST_PATTERNS.some((re) => re.test(path6));
|
|
542
538
|
}
|
|
543
|
-
function isNonCodeFile(
|
|
544
|
-
const ext =
|
|
539
|
+
function isNonCodeFile(path6) {
|
|
540
|
+
const ext = path6.slice(path6.lastIndexOf("."));
|
|
545
541
|
return NON_CODE_EXTENSIONS.has(ext);
|
|
546
542
|
}
|
|
547
|
-
function isConfigFile(
|
|
548
|
-
return CONFIG_PATTERNS.some((re) => re.test(
|
|
543
|
+
function isConfigFile(path6) {
|
|
544
|
+
return CONFIG_PATTERNS.some((re) => re.test(path6));
|
|
549
545
|
}
|
|
550
546
|
function detectTestCoverageGaps(files) {
|
|
551
547
|
const filePaths = new Set(files.map((f) => f.path));
|
|
@@ -717,17 +713,21 @@ function analyze(diffSet) {
|
|
|
717
713
|
};
|
|
718
714
|
}
|
|
719
715
|
|
|
716
|
+
// packages/core/src/pipeline.ts
|
|
717
|
+
import getPort from "get-port";
|
|
718
|
+
import open from "open";
|
|
719
|
+
|
|
720
720
|
// packages/core/src/ws-bridge.ts
|
|
721
721
|
import { WebSocketServer, WebSocket } from "ws";
|
|
722
722
|
function createWsBridge(port) {
|
|
723
|
-
const
|
|
723
|
+
const wss2 = new WebSocketServer({ port });
|
|
724
724
|
let client = null;
|
|
725
725
|
let resultResolve = null;
|
|
726
726
|
let resultReject = null;
|
|
727
727
|
let pendingInit = null;
|
|
728
728
|
let initPayload = null;
|
|
729
729
|
let closeTimer = null;
|
|
730
|
-
|
|
730
|
+
wss2.on("connection", (ws) => {
|
|
731
731
|
if (closeTimer) {
|
|
732
732
|
clearTimeout(closeTimer);
|
|
733
733
|
closeTimer = null;
|
|
@@ -787,10 +787,10 @@ function createWsBridge(port) {
|
|
|
787
787
|
});
|
|
788
788
|
},
|
|
789
789
|
close() {
|
|
790
|
-
for (const ws of
|
|
790
|
+
for (const ws of wss2.clients) {
|
|
791
791
|
ws.close();
|
|
792
792
|
}
|
|
793
|
-
|
|
793
|
+
wss2.close();
|
|
794
794
|
}
|
|
795
795
|
};
|
|
796
796
|
}
|
|
@@ -1061,6 +1061,76 @@ function consumeReviewResult(cwd) {
|
|
|
1061
1061
|
}
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
+
// packages/core/src/server-file.ts
|
|
1065
|
+
import fs3 from "fs";
|
|
1066
|
+
import path5 from "path";
|
|
1067
|
+
import os from "os";
|
|
1068
|
+
function serverDir() {
|
|
1069
|
+
return path5.join(os.homedir(), ".diffprism");
|
|
1070
|
+
}
|
|
1071
|
+
function serverFilePath() {
|
|
1072
|
+
return path5.join(serverDir(), "server.json");
|
|
1073
|
+
}
|
|
1074
|
+
function isPidAlive2(pid) {
|
|
1075
|
+
try {
|
|
1076
|
+
process.kill(pid, 0);
|
|
1077
|
+
return true;
|
|
1078
|
+
} catch {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function writeServerFile(info) {
|
|
1083
|
+
const dir = serverDir();
|
|
1084
|
+
if (!fs3.existsSync(dir)) {
|
|
1085
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1086
|
+
}
|
|
1087
|
+
fs3.writeFileSync(serverFilePath(), JSON.stringify(info, null, 2) + "\n");
|
|
1088
|
+
}
|
|
1089
|
+
function readServerFile() {
|
|
1090
|
+
const filePath = serverFilePath();
|
|
1091
|
+
if (!fs3.existsSync(filePath)) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1096
|
+
const info = JSON.parse(raw);
|
|
1097
|
+
if (!isPidAlive2(info.pid)) {
|
|
1098
|
+
fs3.unlinkSync(filePath);
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
return info;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function removeServerFile() {
|
|
1107
|
+
try {
|
|
1108
|
+
const filePath = serverFilePath();
|
|
1109
|
+
if (fs3.existsSync(filePath)) {
|
|
1110
|
+
fs3.unlinkSync(filePath);
|
|
1111
|
+
}
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function isServerAlive() {
|
|
1116
|
+
const info = readServerFile();
|
|
1117
|
+
if (!info) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const response = await fetch(`http://localhost:${info.httpPort}/api/status`, {
|
|
1122
|
+
signal: AbortSignal.timeout(2e3)
|
|
1123
|
+
});
|
|
1124
|
+
if (response.ok) {
|
|
1125
|
+
return info;
|
|
1126
|
+
}
|
|
1127
|
+
return null;
|
|
1128
|
+
} catch {
|
|
1129
|
+
removeServerFile();
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1064
1134
|
// packages/core/src/watch.ts
|
|
1065
1135
|
import { createHash } from "crypto";
|
|
1066
1136
|
import getPort2 from "get-port";
|
|
@@ -1117,13 +1187,13 @@ function createWatchBridge(port, callbacks) {
|
|
|
1117
1187
|
res.writeHead(404);
|
|
1118
1188
|
res.end("Not found");
|
|
1119
1189
|
});
|
|
1120
|
-
const
|
|
1190
|
+
const wss2 = new WebSocketServer2({ server: httpServer });
|
|
1121
1191
|
function sendToClient(msg) {
|
|
1122
1192
|
if (client && client.readyState === WebSocket2.OPEN) {
|
|
1123
1193
|
client.send(JSON.stringify(msg));
|
|
1124
1194
|
}
|
|
1125
1195
|
}
|
|
1126
|
-
|
|
1196
|
+
wss2.on("connection", (ws) => {
|
|
1127
1197
|
if (closeTimer) {
|
|
1128
1198
|
clearTimeout(closeTimer);
|
|
1129
1199
|
closeTimer = null;
|
|
@@ -1182,10 +1252,10 @@ function createWatchBridge(port, callbacks) {
|
|
|
1182
1252
|
if (closeTimer) {
|
|
1183
1253
|
clearTimeout(closeTimer);
|
|
1184
1254
|
}
|
|
1185
|
-
for (const ws of
|
|
1255
|
+
for (const ws of wss2.clients) {
|
|
1186
1256
|
ws.close();
|
|
1187
1257
|
}
|
|
1188
|
-
|
|
1258
|
+
wss2.close();
|
|
1189
1259
|
await new Promise((resolve2) => {
|
|
1190
1260
|
httpServer.close(() => resolve2());
|
|
1191
1261
|
});
|
|
@@ -1369,10 +1439,371 @@ Review submitted: ${result.decision}`);
|
|
|
1369
1439
|
return { stop, updateContext };
|
|
1370
1440
|
}
|
|
1371
1441
|
|
|
1442
|
+
// packages/core/src/global-server.ts
|
|
1443
|
+
import http3 from "http";
|
|
1444
|
+
import { randomUUID } from "crypto";
|
|
1445
|
+
import getPort3 from "get-port";
|
|
1446
|
+
import open3 from "open";
|
|
1447
|
+
import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
|
|
1448
|
+
var sessions2 = /* @__PURE__ */ new Map();
|
|
1449
|
+
var clientSessions = /* @__PURE__ */ new Map();
|
|
1450
|
+
function toSummary(session) {
|
|
1451
|
+
const { payload } = session;
|
|
1452
|
+
const fileCount = payload.diffSet.files.length;
|
|
1453
|
+
let additions = 0;
|
|
1454
|
+
let deletions = 0;
|
|
1455
|
+
for (const file of payload.diffSet.files) {
|
|
1456
|
+
additions += file.additions;
|
|
1457
|
+
deletions += file.deletions;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
id: session.id,
|
|
1461
|
+
projectPath: session.projectPath,
|
|
1462
|
+
branch: payload.metadata.currentBranch,
|
|
1463
|
+
title: payload.metadata.title,
|
|
1464
|
+
fileCount,
|
|
1465
|
+
additions,
|
|
1466
|
+
deletions,
|
|
1467
|
+
status: session.status,
|
|
1468
|
+
createdAt: session.createdAt
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function readBody(req) {
|
|
1472
|
+
return new Promise((resolve, reject) => {
|
|
1473
|
+
let body = "";
|
|
1474
|
+
req.on("data", (chunk) => {
|
|
1475
|
+
body += chunk.toString();
|
|
1476
|
+
});
|
|
1477
|
+
req.on("end", () => resolve(body));
|
|
1478
|
+
req.on("error", reject);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
function jsonResponse(res, status, data) {
|
|
1482
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1483
|
+
res.end(JSON.stringify(data));
|
|
1484
|
+
}
|
|
1485
|
+
function matchRoute(method, url, expectedMethod, pattern) {
|
|
1486
|
+
if (method !== expectedMethod) return null;
|
|
1487
|
+
const patternParts = pattern.split("/");
|
|
1488
|
+
const urlParts = url.split("/");
|
|
1489
|
+
if (patternParts.length !== urlParts.length) return null;
|
|
1490
|
+
const params = {};
|
|
1491
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1492
|
+
if (patternParts[i].startsWith(":")) {
|
|
1493
|
+
params[patternParts[i].slice(1)] = urlParts[i];
|
|
1494
|
+
} else if (patternParts[i] !== urlParts[i]) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return params;
|
|
1499
|
+
}
|
|
1500
|
+
var wss = null;
|
|
1501
|
+
function broadcastToAll(msg) {
|
|
1502
|
+
if (!wss) return;
|
|
1503
|
+
const data = JSON.stringify(msg);
|
|
1504
|
+
for (const client of wss.clients) {
|
|
1505
|
+
if (client.readyState === WebSocket3.OPEN) {
|
|
1506
|
+
client.send(data);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function sendToSessionClients(sessionId, msg) {
|
|
1511
|
+
if (!wss) return;
|
|
1512
|
+
const data = JSON.stringify(msg);
|
|
1513
|
+
for (const [client, sid] of clientSessions.entries()) {
|
|
1514
|
+
if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
|
|
1515
|
+
client.send(data);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
async function handleApiRequest(req, res) {
|
|
1520
|
+
const method = req.method ?? "GET";
|
|
1521
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
1522
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1523
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1524
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1525
|
+
if (method === "OPTIONS") {
|
|
1526
|
+
res.writeHead(204);
|
|
1527
|
+
res.end();
|
|
1528
|
+
return true;
|
|
1529
|
+
}
|
|
1530
|
+
if (!url.startsWith("/api/")) {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
if (method === "GET" && url === "/api/status") {
|
|
1534
|
+
jsonResponse(res, 200, {
|
|
1535
|
+
running: true,
|
|
1536
|
+
pid: process.pid,
|
|
1537
|
+
sessions: sessions2.size,
|
|
1538
|
+
uptime: process.uptime()
|
|
1539
|
+
});
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
if (method === "POST" && url === "/api/reviews") {
|
|
1543
|
+
try {
|
|
1544
|
+
const body = await readBody(req);
|
|
1545
|
+
const { payload, projectPath } = JSON.parse(body);
|
|
1546
|
+
const sessionId = `session-${randomUUID().slice(0, 8)}`;
|
|
1547
|
+
payload.reviewId = sessionId;
|
|
1548
|
+
const session = {
|
|
1549
|
+
id: sessionId,
|
|
1550
|
+
payload,
|
|
1551
|
+
projectPath,
|
|
1552
|
+
status: "pending",
|
|
1553
|
+
createdAt: Date.now(),
|
|
1554
|
+
result: null
|
|
1555
|
+
};
|
|
1556
|
+
sessions2.set(sessionId, session);
|
|
1557
|
+
broadcastToAll({
|
|
1558
|
+
type: "session:added",
|
|
1559
|
+
payload: toSummary(session)
|
|
1560
|
+
});
|
|
1561
|
+
jsonResponse(res, 201, { sessionId });
|
|
1562
|
+
} catch {
|
|
1563
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1564
|
+
}
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
if (method === "GET" && url === "/api/reviews") {
|
|
1568
|
+
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
1569
|
+
jsonResponse(res, 200, { sessions: summaries });
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
const getReviewParams = matchRoute(method, url, "GET", "/api/reviews/:id");
|
|
1573
|
+
if (getReviewParams) {
|
|
1574
|
+
const session = sessions2.get(getReviewParams.id);
|
|
1575
|
+
if (!session) {
|
|
1576
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
jsonResponse(res, 200, toSummary(session));
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
const postResultParams = matchRoute(method, url, "POST", "/api/reviews/:id/result");
|
|
1583
|
+
if (postResultParams) {
|
|
1584
|
+
const session = sessions2.get(postResultParams.id);
|
|
1585
|
+
if (!session) {
|
|
1586
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const body = await readBody(req);
|
|
1591
|
+
const result = JSON.parse(body);
|
|
1592
|
+
session.result = result;
|
|
1593
|
+
session.status = "submitted";
|
|
1594
|
+
jsonResponse(res, 200, { ok: true });
|
|
1595
|
+
} catch {
|
|
1596
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1597
|
+
}
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
const getResultParams = matchRoute(method, url, "GET", "/api/reviews/:id/result");
|
|
1601
|
+
if (getResultParams) {
|
|
1602
|
+
const session = sessions2.get(getResultParams.id);
|
|
1603
|
+
if (!session) {
|
|
1604
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
if (session.result) {
|
|
1608
|
+
jsonResponse(res, 200, { result: session.result, status: "submitted" });
|
|
1609
|
+
} else {
|
|
1610
|
+
jsonResponse(res, 200, { result: null, status: session.status });
|
|
1611
|
+
}
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
const postContextParams = matchRoute(method, url, "POST", "/api/reviews/:id/context");
|
|
1615
|
+
if (postContextParams) {
|
|
1616
|
+
const session = sessions2.get(postContextParams.id);
|
|
1617
|
+
if (!session) {
|
|
1618
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
const body = await readBody(req);
|
|
1623
|
+
const contextPayload = JSON.parse(body);
|
|
1624
|
+
if (contextPayload.reasoning !== void 0) {
|
|
1625
|
+
session.payload.metadata.reasoning = contextPayload.reasoning;
|
|
1626
|
+
}
|
|
1627
|
+
if (contextPayload.title !== void 0) {
|
|
1628
|
+
session.payload.metadata.title = contextPayload.title;
|
|
1629
|
+
}
|
|
1630
|
+
if (contextPayload.description !== void 0) {
|
|
1631
|
+
session.payload.metadata.description = contextPayload.description;
|
|
1632
|
+
}
|
|
1633
|
+
sendToSessionClients(session.id, {
|
|
1634
|
+
type: "context:update",
|
|
1635
|
+
payload: contextPayload
|
|
1636
|
+
});
|
|
1637
|
+
jsonResponse(res, 200, { ok: true });
|
|
1638
|
+
} catch {
|
|
1639
|
+
jsonResponse(res, 400, { error: "Invalid request body" });
|
|
1640
|
+
}
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
const deleteParams = matchRoute(method, url, "DELETE", "/api/reviews/:id");
|
|
1644
|
+
if (deleteParams) {
|
|
1645
|
+
if (sessions2.delete(deleteParams.id)) {
|
|
1646
|
+
jsonResponse(res, 200, { ok: true });
|
|
1647
|
+
} else {
|
|
1648
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
1649
|
+
}
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
jsonResponse(res, 404, { error: "Not found" });
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
async function startGlobalServer(options = {}) {
|
|
1656
|
+
const {
|
|
1657
|
+
httpPort: preferredHttpPort = 24680,
|
|
1658
|
+
wsPort: preferredWsPort = 24681,
|
|
1659
|
+
silent = false,
|
|
1660
|
+
dev = false
|
|
1661
|
+
} = options;
|
|
1662
|
+
const [httpPort, wsPort] = await Promise.all([
|
|
1663
|
+
getPort3({ port: preferredHttpPort }),
|
|
1664
|
+
getPort3({ port: preferredWsPort })
|
|
1665
|
+
]);
|
|
1666
|
+
let uiPort;
|
|
1667
|
+
let uiHttpServer = null;
|
|
1668
|
+
let viteServer = null;
|
|
1669
|
+
if (dev) {
|
|
1670
|
+
uiPort = await getPort3();
|
|
1671
|
+
const uiRoot = resolveUiRoot();
|
|
1672
|
+
viteServer = await startViteDevServer(uiRoot, uiPort, silent);
|
|
1673
|
+
} else {
|
|
1674
|
+
uiPort = await getPort3();
|
|
1675
|
+
const uiDist = resolveUiDist();
|
|
1676
|
+
uiHttpServer = await createStaticServer(uiDist, uiPort);
|
|
1677
|
+
}
|
|
1678
|
+
const httpServer = http3.createServer(async (req, res) => {
|
|
1679
|
+
const handled = await handleApiRequest(req, res);
|
|
1680
|
+
if (!handled) {
|
|
1681
|
+
res.writeHead(404);
|
|
1682
|
+
res.end("Not found");
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
wss = new WebSocketServer3({ port: wsPort });
|
|
1686
|
+
wss.on("connection", (ws, req) => {
|
|
1687
|
+
const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
|
|
1688
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
1689
|
+
if (sessionId) {
|
|
1690
|
+
clientSessions.set(ws, sessionId);
|
|
1691
|
+
const session = sessions2.get(sessionId);
|
|
1692
|
+
if (session) {
|
|
1693
|
+
session.status = "in_review";
|
|
1694
|
+
const msg = {
|
|
1695
|
+
type: "review:init",
|
|
1696
|
+
payload: session.payload
|
|
1697
|
+
};
|
|
1698
|
+
ws.send(JSON.stringify(msg));
|
|
1699
|
+
}
|
|
1700
|
+
} else {
|
|
1701
|
+
const summaries = Array.from(sessions2.values()).map(toSummary);
|
|
1702
|
+
const msg = {
|
|
1703
|
+
type: "session:list",
|
|
1704
|
+
payload: summaries
|
|
1705
|
+
};
|
|
1706
|
+
ws.send(JSON.stringify(msg));
|
|
1707
|
+
if (summaries.length === 1) {
|
|
1708
|
+
const session = sessions2.get(summaries[0].id);
|
|
1709
|
+
if (session) {
|
|
1710
|
+
clientSessions.set(ws, session.id);
|
|
1711
|
+
session.status = "in_review";
|
|
1712
|
+
ws.send(JSON.stringify({
|
|
1713
|
+
type: "review:init",
|
|
1714
|
+
payload: session.payload
|
|
1715
|
+
}));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
ws.on("message", (data) => {
|
|
1720
|
+
try {
|
|
1721
|
+
const msg = JSON.parse(data.toString());
|
|
1722
|
+
if (msg.type === "review:submit") {
|
|
1723
|
+
const sid = clientSessions.get(ws);
|
|
1724
|
+
if (sid) {
|
|
1725
|
+
const session = sessions2.get(sid);
|
|
1726
|
+
if (session) {
|
|
1727
|
+
session.result = msg.payload;
|
|
1728
|
+
session.status = "submitted";
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
} else if (msg.type === "session:select") {
|
|
1732
|
+
const session = sessions2.get(msg.payload.sessionId);
|
|
1733
|
+
if (session) {
|
|
1734
|
+
clientSessions.set(ws, session.id);
|
|
1735
|
+
session.status = "in_review";
|
|
1736
|
+
ws.send(JSON.stringify({
|
|
1737
|
+
type: "review:init",
|
|
1738
|
+
payload: session.payload
|
|
1739
|
+
}));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
ws.on("close", () => {
|
|
1746
|
+
clientSessions.delete(ws);
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
await new Promise((resolve, reject) => {
|
|
1750
|
+
httpServer.on("error", reject);
|
|
1751
|
+
httpServer.listen(httpPort, () => resolve());
|
|
1752
|
+
});
|
|
1753
|
+
const serverInfo = {
|
|
1754
|
+
httpPort,
|
|
1755
|
+
wsPort,
|
|
1756
|
+
pid: process.pid,
|
|
1757
|
+
startedAt: Date.now()
|
|
1758
|
+
};
|
|
1759
|
+
writeServerFile(serverInfo);
|
|
1760
|
+
if (!silent) {
|
|
1761
|
+
console.log(`
|
|
1762
|
+
DiffPrism Global Server`);
|
|
1763
|
+
console.log(` API: http://localhost:${httpPort}`);
|
|
1764
|
+
console.log(` WS: ws://localhost:${wsPort}`);
|
|
1765
|
+
console.log(` UI: http://localhost:${uiPort}`);
|
|
1766
|
+
console.log(` PID: ${process.pid}`);
|
|
1767
|
+
console.log(`
|
|
1768
|
+
Waiting for reviews...
|
|
1769
|
+
`);
|
|
1770
|
+
}
|
|
1771
|
+
const uiUrl = `http://localhost:${uiPort}?wsPort=${wsPort}&serverMode=true`;
|
|
1772
|
+
await open3(uiUrl);
|
|
1773
|
+
async function stop() {
|
|
1774
|
+
if (wss) {
|
|
1775
|
+
for (const client of wss.clients) {
|
|
1776
|
+
client.close();
|
|
1777
|
+
}
|
|
1778
|
+
wss.close();
|
|
1779
|
+
wss = null;
|
|
1780
|
+
}
|
|
1781
|
+
clientSessions.clear();
|
|
1782
|
+
sessions2.clear();
|
|
1783
|
+
await new Promise((resolve) => {
|
|
1784
|
+
httpServer.close(() => resolve());
|
|
1785
|
+
});
|
|
1786
|
+
if (viteServer) {
|
|
1787
|
+
await viteServer.close();
|
|
1788
|
+
}
|
|
1789
|
+
if (uiHttpServer) {
|
|
1790
|
+
uiHttpServer.close();
|
|
1791
|
+
}
|
|
1792
|
+
removeServerFile();
|
|
1793
|
+
}
|
|
1794
|
+
return { httpPort, wsPort, stop };
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1372
1797
|
export {
|
|
1798
|
+
getCurrentBranch,
|
|
1799
|
+
getDiff,
|
|
1800
|
+
analyze,
|
|
1373
1801
|
startReview,
|
|
1374
1802
|
readWatchFile,
|
|
1375
1803
|
readReviewResult,
|
|
1376
1804
|
consumeReviewResult,
|
|
1377
|
-
startWatch
|
|
1805
|
+
startWatch,
|
|
1806
|
+
readServerFile,
|
|
1807
|
+
isServerAlive,
|
|
1808
|
+
startGlobalServer
|
|
1378
1809
|
};
|