getgloss 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli/index.js +219 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +91 -43
- package/dist/server/daemon.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,8 +100,11 @@ compares only against the requested ref and does not switch to a branch diff.
|
|
|
100
100
|
Use `gloss open --review <reviewId> --json` after applying feedback to capture
|
|
101
101
|
the next diff as another turn in the same browser review.
|
|
102
102
|
The background server exits automatically after a short idle window with no
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
live review clients. Pending review artifacts stay on disk and can be resumed,
|
|
104
|
+
but they do not keep the daemon alive by themselves. `gloss doctor` reports
|
|
105
|
+
unmanaged daemon processes, and normal startup/status commands also clean up
|
|
106
|
+
clearly stale daemons. `gloss stop --all` cleans up every Gloss daemon for the
|
|
107
|
+
current user.
|
|
105
108
|
|
|
106
109
|
You do not need to unlock `~/.gloss/server.json` after finishing a review.
|
|
107
110
|
That file is only the background daemon pointer, not a review lock. If a
|
package/dist/cli/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5
5
|
import { constants } from "fs";
|
|
6
|
-
import { access, rm as
|
|
7
|
-
import
|
|
6
|
+
import { access, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
7
|
+
import path7 from "path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import openBrowser from "open";
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ import path from "path";
|
|
|
30
30
|
// package.json
|
|
31
31
|
var package_default = {
|
|
32
32
|
name: "getgloss",
|
|
33
|
-
version: "0.8.
|
|
33
|
+
version: "0.8.5",
|
|
34
34
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
35
35
|
type: "module",
|
|
36
36
|
packageManager: "pnpm@10.33.2",
|
|
@@ -126,6 +126,9 @@ function globalStateDir() {
|
|
|
126
126
|
function globalServerFile() {
|
|
127
127
|
return path.join(globalStateDir(), "server.json");
|
|
128
128
|
}
|
|
129
|
+
function globalServerLockDir() {
|
|
130
|
+
return path.join(globalStateDir(), "server.lock");
|
|
131
|
+
}
|
|
129
132
|
function globalLogDir() {
|
|
130
133
|
return path.join(globalStateDir(), "logs");
|
|
131
134
|
}
|
|
@@ -205,10 +208,10 @@ function parseJsonValue(value, guard, label) {
|
|
|
205
208
|
return value;
|
|
206
209
|
}
|
|
207
210
|
function isServerInfo(value) {
|
|
208
|
-
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
|
|
211
|
+
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
209
212
|
}
|
|
210
213
|
function isHealthResponse(value) {
|
|
211
|
-
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews);
|
|
214
|
+
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews) && isOptionalNumber(value.connections) && isOptionalString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
212
215
|
}
|
|
213
216
|
function isClearReviewsResult(value) {
|
|
214
217
|
return isRecord(value) && isString(value.reviewsDir) && isString(value.cutoff) && isNumber(value.olderThanDays) && isBoolean(value.dryRun) && isArrayOf(value.candidates, isClearReviewEntry) && isArrayOf(value.deleted, isClearReviewEntry) && isArrayOf(value.skipped, isClearReviewSkipped) && isRecord(value.counts) && isNumber(value.counts.candidates) && isNumber(value.counts.deleted) && isNumber(value.counts.skipped);
|
|
@@ -1107,12 +1110,15 @@ async function assertGitAvailable() {
|
|
|
1107
1110
|
// src/cli/lifecycle.ts
|
|
1108
1111
|
import { execFile, spawn } from "child_process";
|
|
1109
1112
|
import { closeSync, existsSync, openSync } from "fs";
|
|
1113
|
+
import { mkdir as mkdir2, readFile as readFile3, rm as rm4, stat, writeFile as writeFile3 } from "fs/promises";
|
|
1110
1114
|
import { userInfo } from "os";
|
|
1115
|
+
import path5 from "path";
|
|
1111
1116
|
import { fileURLToPath } from "url";
|
|
1112
1117
|
import { promisify } from "util";
|
|
1113
1118
|
import getPort from "get-port";
|
|
1114
1119
|
|
|
1115
1120
|
// src/cli/server-client.ts
|
|
1121
|
+
var timedWatchAttemptMs = 1e3;
|
|
1116
1122
|
var ServerClient = class {
|
|
1117
1123
|
constructor(baseUrl) {
|
|
1118
1124
|
this.baseUrl = baseUrl;
|
|
@@ -1191,11 +1197,15 @@ var ServerClient = class {
|
|
|
1191
1197
|
throw new Error(`watch timed out after ${timeoutSeconds} seconds`);
|
|
1192
1198
|
}
|
|
1193
1199
|
const controller = new AbortController();
|
|
1194
|
-
const
|
|
1200
|
+
const timeoutMs = remainingMs === null ? null : Math.min(remainingMs, timedWatchAttemptMs);
|
|
1201
|
+
const timeout = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1195
1202
|
try {
|
|
1196
1203
|
return await this.readReviewEvents(reviewId, controller.signal);
|
|
1197
1204
|
} catch (error) {
|
|
1198
1205
|
if (isAbortError(error)) {
|
|
1206
|
+
if (deadline && Date.now() < deadline) {
|
|
1207
|
+
throw new Error("watch stream ended before completion");
|
|
1208
|
+
}
|
|
1199
1209
|
throw new Error(`watch timed out after ${timeoutSeconds} seconds`);
|
|
1200
1210
|
}
|
|
1201
1211
|
if (!isPrematureWatchEnd(error)) {
|
|
@@ -1240,20 +1250,20 @@ var ServerClient = class {
|
|
|
1240
1250
|
}
|
|
1241
1251
|
}
|
|
1242
1252
|
}
|
|
1243
|
-
async get(
|
|
1244
|
-
const response = await fetch(`${this.baseUrl}${
|
|
1253
|
+
async get(path8, guard, label) {
|
|
1254
|
+
const response = await fetch(`${this.baseUrl}${path8}`);
|
|
1245
1255
|
return parseResponse(response, guard, label);
|
|
1246
1256
|
}
|
|
1247
|
-
async post(
|
|
1248
|
-
const response = await fetch(`${this.baseUrl}${
|
|
1257
|
+
async post(path8, body, guard, label) {
|
|
1258
|
+
const response = await fetch(`${this.baseUrl}${path8}`, {
|
|
1249
1259
|
method: "POST",
|
|
1250
1260
|
headers: { "content-type": "application/json" },
|
|
1251
1261
|
body: JSON.stringify(body)
|
|
1252
1262
|
});
|
|
1253
1263
|
return parseResponse(response, guard, label);
|
|
1254
1264
|
}
|
|
1255
|
-
async delete(
|
|
1256
|
-
const response = await fetch(`${this.baseUrl}${
|
|
1265
|
+
async delete(path8, guard, label) {
|
|
1266
|
+
const response = await fetch(`${this.baseUrl}${path8}`, { method: "DELETE" });
|
|
1257
1267
|
return parseResponse(response, guard, label);
|
|
1258
1268
|
}
|
|
1259
1269
|
};
|
|
@@ -1278,6 +1288,8 @@ async function sleep(milliseconds) {
|
|
|
1278
1288
|
var execFileAsync = promisify(execFile);
|
|
1279
1289
|
var gracefulShutdownTimeoutMs = 2e3;
|
|
1280
1290
|
var forceShutdownTimeoutMs = 1e3;
|
|
1291
|
+
var serverLockTimeoutMs = 8e3;
|
|
1292
|
+
var staleServerLockMs = 3e4;
|
|
1281
1293
|
function serverUrl(info) {
|
|
1282
1294
|
return `http://localhost:${info.port}`;
|
|
1283
1295
|
}
|
|
@@ -1293,17 +1305,26 @@ async function isServerResponsive(info) {
|
|
|
1293
1305
|
}
|
|
1294
1306
|
}
|
|
1295
1307
|
async function ensureServer(options = {}) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1308
|
+
return withServerLock(async () => {
|
|
1309
|
+
const existing = await readServerInfo();
|
|
1310
|
+
await reapStaleDaemons(existing);
|
|
1311
|
+
if (existing && await isServerResponsive(existing)) {
|
|
1312
|
+
return existing;
|
|
1313
|
+
}
|
|
1314
|
+
return startServerUnlocked(existing, options);
|
|
1315
|
+
});
|
|
1301
1316
|
}
|
|
1302
1317
|
async function startServer(options = {}) {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1318
|
+
return withServerLock(async () => {
|
|
1319
|
+
const existing = await readServerInfo();
|
|
1320
|
+
await reapStaleDaemons(existing);
|
|
1321
|
+
if (existing && await isServerResponsive(existing)) {
|
|
1322
|
+
return existing;
|
|
1323
|
+
}
|
|
1324
|
+
return startServerUnlocked(existing, options);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
async function startServerUnlocked(existing, options = {}) {
|
|
1307
1328
|
if (existing) {
|
|
1308
1329
|
await retireServer(existing);
|
|
1309
1330
|
}
|
|
@@ -1342,7 +1363,9 @@ async function launchServer(port) {
|
|
|
1342
1363
|
port,
|
|
1343
1364
|
version: packageVersion,
|
|
1344
1365
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1345
|
-
stateDir: globalStateDir()
|
|
1366
|
+
stateDir: globalStateDir(),
|
|
1367
|
+
cwd: process.cwd(),
|
|
1368
|
+
daemonPath
|
|
1346
1369
|
};
|
|
1347
1370
|
try {
|
|
1348
1371
|
await writeServerInfo(info);
|
|
@@ -1361,10 +1384,86 @@ async function launchServer(port) {
|
|
|
1361
1384
|
await removeServerInfoForPid(info.pid);
|
|
1362
1385
|
throw new Error(`Server did not become responsive. See ${globalServerLogFile()}`);
|
|
1363
1386
|
}
|
|
1387
|
+
async function withServerLock(fn) {
|
|
1388
|
+
const release = await acquireServerLock();
|
|
1389
|
+
try {
|
|
1390
|
+
return await fn();
|
|
1391
|
+
} finally {
|
|
1392
|
+
await release();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async function acquireServerLock() {
|
|
1396
|
+
await ensureDir(globalStateDir());
|
|
1397
|
+
const lockDir = globalServerLockDir();
|
|
1398
|
+
const ownerFile = serverLockOwnerFile();
|
|
1399
|
+
const deadline = Date.now() + serverLockTimeoutMs;
|
|
1400
|
+
while (true) {
|
|
1401
|
+
try {
|
|
1402
|
+
await mkdir2(lockDir);
|
|
1403
|
+
try {
|
|
1404
|
+
await writeFile3(
|
|
1405
|
+
ownerFile,
|
|
1406
|
+
`${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
1407
|
+
`
|
|
1408
|
+
);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
await rm4(lockDir, { recursive: true, force: true }).catch(() => void 0);
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
return () => rm4(lockDir, { recursive: true, force: true });
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
if (!isAlreadyExistsError(error)) {
|
|
1416
|
+
throw error;
|
|
1417
|
+
}
|
|
1418
|
+
if (await removeStaleServerLock(lockDir, ownerFile)) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
if (Date.now() >= deadline) {
|
|
1422
|
+
throw new Error(`Timed out waiting for Gloss server lock at ${lockDir}`);
|
|
1423
|
+
}
|
|
1424
|
+
await sleep2(50);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function removeStaleServerLock(lockDir, ownerFile) {
|
|
1429
|
+
const owner = await readServerLockOwner(ownerFile);
|
|
1430
|
+
if (owner?.pid && !isPidAlive(owner.pid)) {
|
|
1431
|
+
await rm4(lockDir, { recursive: true, force: true });
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
if (!owner && await isOldLockDir(lockDir)) {
|
|
1435
|
+
await rm4(lockDir, { recursive: true, force: true });
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
async function readServerLockOwner(ownerFile) {
|
|
1441
|
+
try {
|
|
1442
|
+
const raw = await readFile3(ownerFile, "utf8");
|
|
1443
|
+
const parsed = JSON.parse(raw);
|
|
1444
|
+
return typeof parsed.pid === "number" && Number.isFinite(parsed.pid) ? { pid: parsed.pid } : null;
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async function isOldLockDir(lockDir) {
|
|
1450
|
+
try {
|
|
1451
|
+
const info = await stat(lockDir);
|
|
1452
|
+
return Date.now() - info.mtimeMs > staleServerLockMs;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function serverLockOwnerFile() {
|
|
1458
|
+
return path5.join(globalServerLockDir(), "owner.json");
|
|
1459
|
+
}
|
|
1460
|
+
function isAlreadyExistsError(error) {
|
|
1461
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
1462
|
+
}
|
|
1364
1463
|
async function stopServer(options = {}) {
|
|
1365
1464
|
if (options.all) {
|
|
1366
1465
|
const { info: info2, warning: readWarning2 } = await readServerInfoForStop();
|
|
1367
|
-
const daemonPids = await
|
|
1466
|
+
const daemonPids = (await listGlossDaemonProcesses()).map((processInfo) => processInfo.pid);
|
|
1368
1467
|
const stoppedPids = [];
|
|
1369
1468
|
for (const pid of daemonPids) {
|
|
1370
1469
|
if (await terminatePid(pid)) {
|
|
@@ -1393,6 +1492,28 @@ async function stopServer(options = {}) {
|
|
|
1393
1492
|
}
|
|
1394
1493
|
return withWarning({ stopped, info }, warning);
|
|
1395
1494
|
}
|
|
1495
|
+
async function reapStaleDaemons(managed) {
|
|
1496
|
+
if (process.env.GLOSS_SKIP_STALE_DAEMON_REAP === "1") {
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
const processes = await listGlossDaemonProcesses();
|
|
1500
|
+
const managedProcess = managed ? processes.find((processInfo) => processInfo.pid === managed.pid) : null;
|
|
1501
|
+
const managedSource = managedProcess ? daemonSourceKey(managedProcess) : managed?.daemonPath ? daemonPathSourceKey(managed.daemonPath) : null;
|
|
1502
|
+
const reapedPids = [];
|
|
1503
|
+
for (const processInfo of processes) {
|
|
1504
|
+
const shouldReap = isMissingDaemonSource(processInfo) || isStaleHomebrewDaemon(processInfo) || managedSource !== null && processInfo.pid !== managed?.pid && daemonSourceKey(processInfo) === managedSource;
|
|
1505
|
+
if (!shouldReap) {
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
if (await terminatePid(processInfo.pid)) {
|
|
1509
|
+
reapedPids.push(processInfo.pid);
|
|
1510
|
+
if (processInfo.pid === managed?.pid) {
|
|
1511
|
+
await removeServerInfoForPid(processInfo.pid);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return reapedPids;
|
|
1516
|
+
}
|
|
1396
1517
|
function isPidAlive(pid) {
|
|
1397
1518
|
if (pid <= 0) {
|
|
1398
1519
|
return false;
|
|
@@ -1435,7 +1556,7 @@ async function waitForPidExit(pid, timeoutMs) {
|
|
|
1435
1556
|
if (!isPidAlive(pid)) {
|
|
1436
1557
|
return true;
|
|
1437
1558
|
}
|
|
1438
|
-
await
|
|
1559
|
+
await sleep2(50);
|
|
1439
1560
|
}
|
|
1440
1561
|
return !isPidAlive(pid);
|
|
1441
1562
|
}
|
|
@@ -1462,7 +1583,7 @@ function combineWarnings(...warnings) {
|
|
|
1462
1583
|
}
|
|
1463
1584
|
async function isGlossDaemonPid(pid) {
|
|
1464
1585
|
const command = await readProcessCommand(pid);
|
|
1465
|
-
return command ?
|
|
1586
|
+
return command ? parseGlossDaemonCommand(command) !== null : false;
|
|
1466
1587
|
}
|
|
1467
1588
|
async function readProcessCommand(pid) {
|
|
1468
1589
|
try {
|
|
@@ -1473,6 +1594,9 @@ async function readProcessCommand(pid) {
|
|
|
1473
1594
|
}
|
|
1474
1595
|
}
|
|
1475
1596
|
async function listGlossDaemonPids() {
|
|
1597
|
+
return (await listGlossDaemonProcesses()).map((processInfo) => processInfo.pid);
|
|
1598
|
+
}
|
|
1599
|
+
async function listGlossDaemonProcesses() {
|
|
1476
1600
|
let stdout;
|
|
1477
1601
|
try {
|
|
1478
1602
|
({ stdout } = await execFileAsync("ps", ["-axo", "pid=,user=,command=", "-ww"]));
|
|
@@ -1480,25 +1604,60 @@ async function listGlossDaemonPids() {
|
|
|
1480
1604
|
return [];
|
|
1481
1605
|
}
|
|
1482
1606
|
const currentUser = userInfo().username;
|
|
1483
|
-
return
|
|
1607
|
+
return parseGlossDaemonProcesses(stdout, currentUser, process.pid);
|
|
1484
1608
|
}
|
|
1485
|
-
function
|
|
1609
|
+
function parseGlossDaemonProcesses(stdout, currentUser, currentPid = process.pid) {
|
|
1486
1610
|
return stdout.split("\n").map((line) => /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line)).filter((match) => Boolean(match)).map((match) => ({
|
|
1487
1611
|
pid: Number(match[1]),
|
|
1488
1612
|
user: match[2],
|
|
1489
1613
|
command: match[3]
|
|
1614
|
+
})).map((processInfo) => ({
|
|
1615
|
+
...processInfo,
|
|
1616
|
+
parsed: parseGlossDaemonCommand(processInfo.command)
|
|
1490
1617
|
})).filter(
|
|
1491
|
-
(
|
|
1492
|
-
).map(({ pid }) =>
|
|
1618
|
+
(processInfo) => processInfo.pid !== currentPid && processInfo.user === currentUser && processInfo.parsed !== null
|
|
1619
|
+
).map(({ pid, user, command, parsed }) => ({
|
|
1620
|
+
pid,
|
|
1621
|
+
user,
|
|
1622
|
+
command,
|
|
1623
|
+
daemonPath: parsed.daemonPath,
|
|
1624
|
+
...parsed.homebrewVersion ? { homebrewVersion: parsed.homebrewVersion } : {}
|
|
1625
|
+
}));
|
|
1626
|
+
}
|
|
1627
|
+
function parseGlossDaemonCommand(command) {
|
|
1628
|
+
const match = /(?:^|\s)(?:\S*\/)?node\s+(\S*dist\/server\/daemon\.js)(?:\s|$)/.exec(command);
|
|
1629
|
+
if (!match) {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
const daemonPath = match[1];
|
|
1633
|
+
const homebrewVersion = /\/Cellar\/gloss\/([^/]+)\/libexec\/lib\/node_modules\/getgloss\/dist\/server\/daemon\.js$/.exec(
|
|
1634
|
+
daemonPath
|
|
1635
|
+
)?.[1];
|
|
1636
|
+
return {
|
|
1637
|
+
daemonPath,
|
|
1638
|
+
...homebrewVersion ? { homebrewVersion } : {}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function isMissingDaemonSource(processInfo) {
|
|
1642
|
+
return !existsSync(processInfo.daemonPath);
|
|
1643
|
+
}
|
|
1644
|
+
function isStaleHomebrewDaemon(processInfo) {
|
|
1645
|
+
return Boolean(processInfo.homebrewVersion && processInfo.homebrewVersion !== packageVersion);
|
|
1493
1646
|
}
|
|
1494
|
-
function
|
|
1495
|
-
return
|
|
1647
|
+
function daemonSourceKey(processInfo) {
|
|
1648
|
+
return daemonPathSourceKey(processInfo.daemonPath);
|
|
1649
|
+
}
|
|
1650
|
+
function daemonPathSourceKey(daemonPath) {
|
|
1651
|
+
return path5.normalize(daemonPath);
|
|
1652
|
+
}
|
|
1653
|
+
async function sleep2(milliseconds) {
|
|
1654
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1496
1655
|
}
|
|
1497
1656
|
|
|
1498
1657
|
// src/server/store.ts
|
|
1499
1658
|
import { createHash } from "crypto";
|
|
1500
|
-
import { readdir as readdir2, readFile as
|
|
1501
|
-
import
|
|
1659
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
1660
|
+
import path6 from "path";
|
|
1502
1661
|
import { ulid } from "ulid";
|
|
1503
1662
|
|
|
1504
1663
|
// src/shared/comments.ts
|
|
@@ -1969,16 +2128,25 @@ var ReviewStore = class {
|
|
|
1969
2128
|
const reviewLoads = [];
|
|
1970
2129
|
for (const entry of entries) {
|
|
1971
2130
|
if (entry.isDirectory()) {
|
|
1972
|
-
reviewLoads.push(this.
|
|
2131
|
+
reviewLoads.push(this.loadReviewForList(entry.name));
|
|
1973
2132
|
}
|
|
1974
2133
|
}
|
|
1975
2134
|
await Promise.all(reviewLoads);
|
|
1976
2135
|
}
|
|
2136
|
+
async loadReviewForList(id) {
|
|
2137
|
+
try {
|
|
2138
|
+
return await this.loadReview(id);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
process.stderr.write(`Warning: Skipping corrupt review ${id}: ${formatError(error)}
|
|
2141
|
+
`);
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1977
2145
|
async loadReview(id) {
|
|
1978
2146
|
const metaPath = globalReviewMetaFile(id);
|
|
1979
2147
|
let metaRaw;
|
|
1980
2148
|
try {
|
|
1981
|
-
metaRaw = await
|
|
2149
|
+
metaRaw = await readFile4(metaPath, "utf8");
|
|
1982
2150
|
} catch (error) {
|
|
1983
2151
|
if (isFileNotFound(error)) {
|
|
1984
2152
|
return this.loadReviewFromTurnsOnly(id);
|
|
@@ -2060,8 +2228,8 @@ var ReviewStore = class {
|
|
|
2060
2228
|
let diffRaw;
|
|
2061
2229
|
try {
|
|
2062
2230
|
[metaRaw, diffRaw] = await Promise.all([
|
|
2063
|
-
|
|
2064
|
-
|
|
2231
|
+
readFile4(metaPath, "utf8"),
|
|
2232
|
+
readFile4(diffPath, "utf8")
|
|
2065
2233
|
]);
|
|
2066
2234
|
} catch (error) {
|
|
2067
2235
|
if (isFileNotFound(error)) {
|
|
@@ -2091,7 +2259,7 @@ var ReviewStore = class {
|
|
|
2091
2259
|
const diffPath = globalReviewDiffFile(id);
|
|
2092
2260
|
let diffRaw;
|
|
2093
2261
|
try {
|
|
2094
|
-
diffRaw = await
|
|
2262
|
+
diffRaw = await readFile4(diffPath, "utf8");
|
|
2095
2263
|
} catch (error) {
|
|
2096
2264
|
if (isFileNotFound(error)) {
|
|
2097
2265
|
return null;
|
|
@@ -2271,9 +2439,9 @@ function reconcileTurn(meta, diff, feedback, resolution) {
|
|
|
2271
2439
|
status,
|
|
2272
2440
|
submittedAt: feedback?.timestamp ?? meta.submittedAt,
|
|
2273
2441
|
resolvedAt: status === "resolved" ? resolution?.resolvedAt ?? meta.resolvedAt : void 0,
|
|
2274
|
-
feedbackPath: feedback ? meta.feedbackPath ??
|
|
2275
|
-
markdownPath: feedback ? meta.markdownPath ??
|
|
2276
|
-
resolvedPath: resolution ? meta.resolvedPath ??
|
|
2442
|
+
feedbackPath: feedback ? meta.feedbackPath ?? path6.join(meta.artifactDir, "feedback.json") : void 0,
|
|
2443
|
+
markdownPath: feedback ? meta.markdownPath ?? path6.join(meta.artifactDir, "feedback.md") : void 0,
|
|
2444
|
+
resolvedPath: resolution ? meta.resolvedPath ?? path6.join(meta.artifactDir, "resolved.json") : void 0,
|
|
2277
2445
|
diff,
|
|
2278
2446
|
...feedback ? { feedback } : {},
|
|
2279
2447
|
...resolution ? { resolution } : {}
|
|
@@ -2306,7 +2474,7 @@ function requiredPath(value, label) {
|
|
|
2306
2474
|
async function readOptionalJsonFile(filePath, guard, label) {
|
|
2307
2475
|
let raw;
|
|
2308
2476
|
try {
|
|
2309
|
-
raw = await
|
|
2477
|
+
raw = await readFile4(filePath, "utf8");
|
|
2310
2478
|
} catch (error) {
|
|
2311
2479
|
if (isFileNotFound(error)) {
|
|
2312
2480
|
return void 0;
|
|
@@ -2456,7 +2624,9 @@ program.command("start").description("Start or reuse the background server").opt
|
|
|
2456
2624
|
});
|
|
2457
2625
|
program.command("status").description("Show server and active reviews").action(async () => {
|
|
2458
2626
|
const globals = program.opts();
|
|
2459
|
-
|
|
2627
|
+
let info = await readServerInfo();
|
|
2628
|
+
await reapStaleDaemons(info);
|
|
2629
|
+
info = await readServerInfo();
|
|
2460
2630
|
const responsive = info ? await isServerResponsive(info) : false;
|
|
2461
2631
|
const reviews = await listReviewsForStatus({ responsive, server: info });
|
|
2462
2632
|
const status = { running: responsive, server: info, reviews };
|
|
@@ -2582,7 +2752,7 @@ async function watchReviewWithReconnect(reviewId, initialInfo, timeoutSeconds, o
|
|
|
2582
2752
|
if (!isReconnectableWatchError(error)) {
|
|
2583
2753
|
throw error;
|
|
2584
2754
|
}
|
|
2585
|
-
await
|
|
2755
|
+
await sleep3(500);
|
|
2586
2756
|
const nextInfo = await ensureServer();
|
|
2587
2757
|
if (nextInfo.port !== info.port) {
|
|
2588
2758
|
await onServerChanged(nextInfo);
|
|
@@ -2597,15 +2767,15 @@ function formatStopResult(result, all) {
|
|
|
2597
2767
|
Warning: ${result.warning}` : status;
|
|
2598
2768
|
}
|
|
2599
2769
|
async function checkStateDirAccess() {
|
|
2600
|
-
const probePath =
|
|
2770
|
+
const probePath = path7.join(globalStateDir(), `.doctor-${process.pid}-${randomUUID3()}.tmp`);
|
|
2601
2771
|
try {
|
|
2602
2772
|
await ensureDir(globalStateDir());
|
|
2603
2773
|
await access(globalStateDir(), constants.R_OK | constants.W_OK | constants.X_OK);
|
|
2604
|
-
await
|
|
2605
|
-
await
|
|
2774
|
+
await writeFile4(probePath, "");
|
|
2775
|
+
await rm5(probePath, { force: true });
|
|
2606
2776
|
return { name: "state-dir", ok: true, detail: stateDirDetail() };
|
|
2607
2777
|
} catch (error) {
|
|
2608
|
-
await
|
|
2778
|
+
await rm5(probePath, { force: true }).catch(() => void 0);
|
|
2609
2779
|
return {
|
|
2610
2780
|
name: "state-dir",
|
|
2611
2781
|
ok: false,
|
|
@@ -2661,7 +2831,7 @@ function isWatchTimeout(error) {
|
|
|
2661
2831
|
function isReconnectableWatchError(error) {
|
|
2662
2832
|
return error instanceof Error && !/^watch failed: [45]\d\d /.test(error.message);
|
|
2663
2833
|
}
|
|
2664
|
-
async function
|
|
2834
|
+
async function sleep3(milliseconds) {
|
|
2665
2835
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2666
2836
|
}
|
|
2667
2837
|
program.parseAsync(process.argv).catch((error) => {
|