getgloss 0.8.4 → 0.9.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/README.md +5 -2
- package/dist/cli/index.js +219 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +303 -69
- package/dist/server/daemon.js.map +1 -1
- package/dist/web/assets/index-DpUegwGY.css +1 -0
- package/dist/web/assets/index-TgxemtXN.js +254 -0
- package/dist/web/assets/{syntax-B7bGuVNL.js → syntax-COE-RRjn.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BYy-6xmS.js +0 -244
- package/dist/web/assets/index-CRYcChM8.css +0 -1
package/dist/server/daemon.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/server/daemon.ts
|
|
2
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2
3
|
import { serve } from "@hono/node-server";
|
|
3
4
|
|
|
4
5
|
// src/shared/paths.ts
|
|
@@ -9,7 +10,7 @@ import path from "path";
|
|
|
9
10
|
// package.json
|
|
10
11
|
var package_default = {
|
|
11
12
|
name: "getgloss",
|
|
12
|
-
version: "0.
|
|
13
|
+
version: "0.9.0",
|
|
13
14
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
14
15
|
type: "module",
|
|
15
16
|
packageManager: "pnpm@10.33.2",
|
|
@@ -198,6 +199,7 @@ var REVIEW_STATUSES = ["pending", "submitted", "cancelled", "resolved"];
|
|
|
198
199
|
var DIFF_LINE_TYPES = ["context", "add", "delete"];
|
|
199
200
|
var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
200
201
|
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
202
|
+
var DIFF_CONTEXT_MAX_LINES = 500;
|
|
201
203
|
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
202
204
|
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
203
205
|
|
|
@@ -213,7 +215,7 @@ function parseJsonValue(value, guard, label) {
|
|
|
213
215
|
return value;
|
|
214
216
|
}
|
|
215
217
|
function isServerInfo(value) {
|
|
216
|
-
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
|
|
218
|
+
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
217
219
|
}
|
|
218
220
|
function isClearReviewsRequest(value) {
|
|
219
221
|
return isRecord(value) && isOptionalNonNegativeInteger(value.olderThanDays) && isOptionalBoolean(value.dryRun);
|
|
@@ -224,6 +226,9 @@ function isOpenFileRequest(value) {
|
|
|
224
226
|
function isCommitRangeDiffRequest(value) {
|
|
225
227
|
return isRecord(value) && isString(value.fromSha) && isString(value.toSha) && isOptionalString(value.turnId);
|
|
226
228
|
}
|
|
229
|
+
function isDiffContextRequest(value) {
|
|
230
|
+
return isRecord(value) && isString(value.filePath) && isNullableString(value.oldPath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isPositiveInteger(value.oldStart) && isPositiveInteger(value.newStart) && isPositiveInteger(value.lineCount);
|
|
231
|
+
}
|
|
227
232
|
function isSubmitReviewRequest(value) {
|
|
228
233
|
return isRecord(value) && isArrayOf(value.comments, isComment) && isOptional(value.reviewScope, isReviewScope);
|
|
229
234
|
}
|
|
@@ -275,6 +280,21 @@ function isDiffCommit(value) {
|
|
|
275
280
|
function isCommitDiff(value) {
|
|
276
281
|
return isRecord(value) && isDiffCommit(value.commit) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile);
|
|
277
282
|
}
|
|
283
|
+
function isDiffContextSource(value) {
|
|
284
|
+
if (!isRecord(value) || !isString(value.mode)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
switch (value.mode) {
|
|
288
|
+
case "turn":
|
|
289
|
+
return true;
|
|
290
|
+
case "commit":
|
|
291
|
+
return isString(value.sha);
|
|
292
|
+
case "range":
|
|
293
|
+
return isString(value.fromSha) && isString(value.toSha);
|
|
294
|
+
default:
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
278
298
|
function isReviewScope(value) {
|
|
279
299
|
if (!isRecord(value) || !isOneOf(value.mode, REVIEW_SCOPE_MODES)) {
|
|
280
300
|
return false;
|
|
@@ -333,6 +353,9 @@ function isNullableString(value) {
|
|
|
333
353
|
function isNumber(value) {
|
|
334
354
|
return typeof value === "number" && Number.isFinite(value);
|
|
335
355
|
}
|
|
356
|
+
function isPositiveInteger(value) {
|
|
357
|
+
return isNumber(value) && Number.isInteger(value) && value > 0;
|
|
358
|
+
}
|
|
336
359
|
function isOptionalNumber(value) {
|
|
337
360
|
return value === void 0 || isNumber(value);
|
|
338
361
|
}
|
|
@@ -438,9 +461,61 @@ async function assertStateDirWritable() {
|
|
|
438
461
|
}
|
|
439
462
|
}
|
|
440
463
|
|
|
464
|
+
// src/server/idle.ts
|
|
465
|
+
var DEFAULT_DAEMON_IDLE_TIMEOUT_MS = 12e4;
|
|
466
|
+
function normalizeIdleTimeoutMs(rawValue = process.env.GLOSS_IDLE_TIMEOUT_MS, warn = (message) => process.stderr.write(message)) {
|
|
467
|
+
if (rawValue === void 0) {
|
|
468
|
+
return DEFAULT_DAEMON_IDLE_TIMEOUT_MS;
|
|
469
|
+
}
|
|
470
|
+
const parsed = Number(rawValue);
|
|
471
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
warn(
|
|
475
|
+
`Warning: GLOSS_IDLE_TIMEOUT_MS=${JSON.stringify(rawValue)} is not positive; using ${DEFAULT_DAEMON_IDLE_TIMEOUT_MS}.
|
|
476
|
+
`
|
|
477
|
+
);
|
|
478
|
+
return DEFAULT_DAEMON_IDLE_TIMEOUT_MS;
|
|
479
|
+
}
|
|
480
|
+
function createIdleScheduler(options) {
|
|
481
|
+
let idleTimer = null;
|
|
482
|
+
const cancel = () => {
|
|
483
|
+
if (idleTimer) {
|
|
484
|
+
clearTimeout(idleTimer);
|
|
485
|
+
idleTimer = null;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const schedule = () => {
|
|
489
|
+
if (options.isShuttingDown()) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (options.hasLiveClients()) {
|
|
493
|
+
cancel();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (!idleTimer) {
|
|
497
|
+
idleTimer = setTimeout(() => {
|
|
498
|
+
idleTimer = null;
|
|
499
|
+
void shutdownIfIdle();
|
|
500
|
+
}, options.timeoutMs);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const shutdownIfIdle = async () => {
|
|
504
|
+
if (options.isShuttingDown()) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (options.hasLiveClients()) {
|
|
508
|
+
schedule();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
await options.shutdown();
|
|
512
|
+
};
|
|
513
|
+
return { cancel, schedule };
|
|
514
|
+
}
|
|
515
|
+
|
|
441
516
|
// src/server/index.ts
|
|
442
|
-
import { readFile as
|
|
443
|
-
import
|
|
517
|
+
import { readFile as readFile5, realpath, stat } from "fs/promises";
|
|
518
|
+
import path7 from "path";
|
|
444
519
|
import { fileURLToPath } from "url";
|
|
445
520
|
import { Hono } from "hono";
|
|
446
521
|
import { streamSSE } from "hono/streaming";
|
|
@@ -473,6 +548,8 @@ function resolutionCounts(feedback, resolvedComments = []) {
|
|
|
473
548
|
}
|
|
474
549
|
|
|
475
550
|
// src/shared/git-diff.ts
|
|
551
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
552
|
+
import path5 from "path";
|
|
476
553
|
import { execa } from "execa";
|
|
477
554
|
|
|
478
555
|
// src/shared/language.ts
|
|
@@ -660,6 +737,59 @@ async function captureCommitRangeDiff(fromSha, toSha, repoRoot) {
|
|
|
660
737
|
files
|
|
661
738
|
};
|
|
662
739
|
}
|
|
740
|
+
async function captureDiffContext({
|
|
741
|
+
filePath,
|
|
742
|
+
lineCount,
|
|
743
|
+
newRef,
|
|
744
|
+
oldPath,
|
|
745
|
+
oldRef,
|
|
746
|
+
newStart,
|
|
747
|
+
oldStart,
|
|
748
|
+
repoRoot
|
|
749
|
+
}) {
|
|
750
|
+
const [oldLines, newLines] = await Promise.all([
|
|
751
|
+
oldRef && oldPath ? readRevisionLines(repoRoot, oldRef, oldPath).catch(() => null) : null,
|
|
752
|
+
newRef === null ? readWorkingTreeLines(repoRoot, filePath).catch(() => null) : readRevisionLines(repoRoot, newRef, filePath).catch(() => null)
|
|
753
|
+
]);
|
|
754
|
+
if (!oldLines && !newLines) {
|
|
755
|
+
throw new Error(`Could not read ${filePath} from either side of the diff`);
|
|
756
|
+
}
|
|
757
|
+
const lines = [];
|
|
758
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
759
|
+
const oldLine = oldStart + index;
|
|
760
|
+
const newLine = newStart + index;
|
|
761
|
+
const content = newLines?.[newLine - 1] ?? oldLines?.[oldLine - 1];
|
|
762
|
+
if (content === void 0) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
lines.push({
|
|
766
|
+
type: "context",
|
|
767
|
+
oldLine,
|
|
768
|
+
newLine,
|
|
769
|
+
content
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
filePath,
|
|
774
|
+
oldStart,
|
|
775
|
+
newStart,
|
|
776
|
+
lines
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async function readRevisionLines(repoRoot, ref, filePath) {
|
|
780
|
+
const result = await execa("git", ["show", `${ref}:${filePath}`], { cwd: repoRoot });
|
|
781
|
+
return splitFileLines(result.stdout);
|
|
782
|
+
}
|
|
783
|
+
async function readWorkingTreeLines(repoRoot, filePath) {
|
|
784
|
+
return splitFileLines(await readFile2(path5.resolve(repoRoot, filePath), "utf8"));
|
|
785
|
+
}
|
|
786
|
+
function splitFileLines(contents) {
|
|
787
|
+
const lines = contents.replace(/\r\n/g, "\n").split("\n");
|
|
788
|
+
if (lines.at(-1) === "") {
|
|
789
|
+
lines.pop();
|
|
790
|
+
}
|
|
791
|
+
return lines;
|
|
792
|
+
}
|
|
663
793
|
|
|
664
794
|
// src/shared/reviews.ts
|
|
665
795
|
function isResolvableReviewStatus(status) {
|
|
@@ -674,12 +804,12 @@ async function openLocalPath(filePath) {
|
|
|
674
804
|
|
|
675
805
|
// src/server/store.ts
|
|
676
806
|
import { createHash } from "crypto";
|
|
677
|
-
import { readdir as readdir2, readFile as
|
|
678
|
-
import
|
|
807
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
808
|
+
import path6 from "path";
|
|
679
809
|
import { ulid } from "ulid";
|
|
680
810
|
|
|
681
811
|
// src/shared/cleanup.ts
|
|
682
|
-
import { readdir, readFile as
|
|
812
|
+
import { readdir, readFile as readFile3, rm as rm3 } from "fs/promises";
|
|
683
813
|
var DEFAULT_REVIEW_RETENTION_DAYS = 30;
|
|
684
814
|
var clearableStatuses = /* @__PURE__ */ new Set(["submitted", "resolved", "cancelled"]);
|
|
685
815
|
var millisecondsPerDay = 24 * 60 * 60 * 1e3;
|
|
@@ -739,7 +869,7 @@ function normalizeRetentionDays(value) {
|
|
|
739
869
|
async function cleanupCandidate(reviewId, artifactDir, cutoff, skipped) {
|
|
740
870
|
let raw;
|
|
741
871
|
try {
|
|
742
|
-
raw = await
|
|
872
|
+
raw = await readFile3(globalReviewMetaFile(reviewId), "utf8");
|
|
743
873
|
} catch (error) {
|
|
744
874
|
if (isFileNotFound(error)) {
|
|
745
875
|
skipped.push({ reviewId, artifactDir, reason: "missing metadata" });
|
|
@@ -819,7 +949,7 @@ async function persistedTurnCleanupState(reviewId, artifactDir, skipped) {
|
|
|
819
949
|
async function readPersistedTurnMeta(reviewId, turnDirName, artifactDir, skipped) {
|
|
820
950
|
let raw;
|
|
821
951
|
try {
|
|
822
|
-
raw = await
|
|
952
|
+
raw = await readFile3(globalReviewTurnMetaFile(reviewId, turnDirName), "utf8");
|
|
823
953
|
} catch (error) {
|
|
824
954
|
skipped.push({
|
|
825
955
|
reviewId,
|
|
@@ -1328,16 +1458,25 @@ var ReviewStore = class {
|
|
|
1328
1458
|
const reviewLoads = [];
|
|
1329
1459
|
for (const entry of entries) {
|
|
1330
1460
|
if (entry.isDirectory()) {
|
|
1331
|
-
reviewLoads.push(this.
|
|
1461
|
+
reviewLoads.push(this.loadReviewForList(entry.name));
|
|
1332
1462
|
}
|
|
1333
1463
|
}
|
|
1334
1464
|
await Promise.all(reviewLoads);
|
|
1335
1465
|
}
|
|
1466
|
+
async loadReviewForList(id) {
|
|
1467
|
+
try {
|
|
1468
|
+
return await this.loadReview(id);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
process.stderr.write(`Warning: Skipping corrupt review ${id}: ${formatError(error)}
|
|
1471
|
+
`);
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1336
1475
|
async loadReview(id) {
|
|
1337
1476
|
const metaPath = globalReviewMetaFile(id);
|
|
1338
1477
|
let metaRaw;
|
|
1339
1478
|
try {
|
|
1340
|
-
metaRaw = await
|
|
1479
|
+
metaRaw = await readFile4(metaPath, "utf8");
|
|
1341
1480
|
} catch (error) {
|
|
1342
1481
|
if (isFileNotFound(error)) {
|
|
1343
1482
|
return this.loadReviewFromTurnsOnly(id);
|
|
@@ -1419,8 +1558,8 @@ var ReviewStore = class {
|
|
|
1419
1558
|
let diffRaw;
|
|
1420
1559
|
try {
|
|
1421
1560
|
[metaRaw, diffRaw] = await Promise.all([
|
|
1422
|
-
|
|
1423
|
-
|
|
1561
|
+
readFile4(metaPath, "utf8"),
|
|
1562
|
+
readFile4(diffPath, "utf8")
|
|
1424
1563
|
]);
|
|
1425
1564
|
} catch (error) {
|
|
1426
1565
|
if (isFileNotFound(error)) {
|
|
@@ -1450,7 +1589,7 @@ var ReviewStore = class {
|
|
|
1450
1589
|
const diffPath = globalReviewDiffFile(id);
|
|
1451
1590
|
let diffRaw;
|
|
1452
1591
|
try {
|
|
1453
|
-
diffRaw = await
|
|
1592
|
+
diffRaw = await readFile4(diffPath, "utf8");
|
|
1454
1593
|
} catch (error) {
|
|
1455
1594
|
if (isFileNotFound(error)) {
|
|
1456
1595
|
return null;
|
|
@@ -1630,9 +1769,9 @@ function reconcileTurn(meta, diff, feedback, resolution) {
|
|
|
1630
1769
|
status,
|
|
1631
1770
|
submittedAt: feedback?.timestamp ?? meta.submittedAt,
|
|
1632
1771
|
resolvedAt: status === "resolved" ? resolution?.resolvedAt ?? meta.resolvedAt : void 0,
|
|
1633
|
-
feedbackPath: feedback ? meta.feedbackPath ??
|
|
1634
|
-
markdownPath: feedback ? meta.markdownPath ??
|
|
1635
|
-
resolvedPath: resolution ? meta.resolvedPath ??
|
|
1772
|
+
feedbackPath: feedback ? meta.feedbackPath ?? path6.join(meta.artifactDir, "feedback.json") : void 0,
|
|
1773
|
+
markdownPath: feedback ? meta.markdownPath ?? path6.join(meta.artifactDir, "feedback.md") : void 0,
|
|
1774
|
+
resolvedPath: resolution ? meta.resolvedPath ?? path6.join(meta.artifactDir, "resolved.json") : void 0,
|
|
1636
1775
|
diff,
|
|
1637
1776
|
...feedback ? { feedback } : {},
|
|
1638
1777
|
...resolution ? { resolution } : {}
|
|
@@ -1665,7 +1804,7 @@ function requiredPath(value, label) {
|
|
|
1665
1804
|
async function readOptionalJsonFile(filePath, guard, label) {
|
|
1666
1805
|
let raw;
|
|
1667
1806
|
try {
|
|
1668
|
-
raw = await
|
|
1807
|
+
raw = await readFile4(filePath, "utf8");
|
|
1669
1808
|
} catch (error) {
|
|
1670
1809
|
if (isFileNotFound(error)) {
|
|
1671
1810
|
return void 0;
|
|
@@ -1705,7 +1844,8 @@ function createApp(origin2, options = {}) {
|
|
|
1705
1844
|
const response = {
|
|
1706
1845
|
ok: true,
|
|
1707
1846
|
version: packageVersion,
|
|
1708
|
-
activeReviews: reviews.filter((review) => review.status === "pending").length
|
|
1847
|
+
activeReviews: reviews.filter((review) => review.status === "pending").length,
|
|
1848
|
+
...options.health?.()
|
|
1709
1849
|
};
|
|
1710
1850
|
return c.json(response);
|
|
1711
1851
|
});
|
|
@@ -1916,6 +2056,60 @@ function createApp(origin2, options = {}) {
|
|
|
1916
2056
|
};
|
|
1917
2057
|
return c.json(response);
|
|
1918
2058
|
});
|
|
2059
|
+
app.post("/api/reviews/:id/files/context", async (c) => {
|
|
2060
|
+
const id = c.req.param("id");
|
|
2061
|
+
const existing = await reviewStore.get(id);
|
|
2062
|
+
if (!existing) {
|
|
2063
|
+
return c.json({ error: "review not found" }, 404);
|
|
2064
|
+
}
|
|
2065
|
+
const parsed = await readJsonBody(c, isDiffContextRequest, "diff context request");
|
|
2066
|
+
if (!parsed.ok) {
|
|
2067
|
+
return parsed.response;
|
|
2068
|
+
}
|
|
2069
|
+
const body = parsed.body;
|
|
2070
|
+
if (body.lineCount > DIFF_CONTEXT_MAX_LINES) {
|
|
2071
|
+
return c.json({ error: `lineCount must be ${DIFF_CONTEXT_MAX_LINES} or less` }, 400);
|
|
2072
|
+
}
|
|
2073
|
+
const turn = body.turnId ? await reviewStore.getTurn(id, body.turnId) : null;
|
|
2074
|
+
if (body.turnId && !turn) {
|
|
2075
|
+
return c.json({ error: "turn not found" }, 404);
|
|
2076
|
+
}
|
|
2077
|
+
const diffPayload = turn?.diff ?? existing.diff;
|
|
2078
|
+
const repoRoot = path7.resolve(diffPayload.cwd);
|
|
2079
|
+
const pathError = validateContextPath(repoRoot, body.filePath, "filePath") ?? (body.oldPath ? validateContextPath(repoRoot, body.oldPath, "oldPath") : null);
|
|
2080
|
+
if (pathError) {
|
|
2081
|
+
return c.json({ error: pathError }, 400);
|
|
2082
|
+
}
|
|
2083
|
+
const source = await resolveContextSource(diffPayload, body.source);
|
|
2084
|
+
if (!source.ok) {
|
|
2085
|
+
return c.json({ error: source.error }, source.status);
|
|
2086
|
+
}
|
|
2087
|
+
const reviewFile = source.files.find((file) => file.path === body.filePath);
|
|
2088
|
+
if (!reviewFile) {
|
|
2089
|
+
return c.json({ error: "file is not part of this review context" }, 404);
|
|
2090
|
+
}
|
|
2091
|
+
if ((reviewFile.oldPath ?? null) !== body.oldPath) {
|
|
2092
|
+
return c.json({ error: "oldPath does not match the reviewed file" }, 400);
|
|
2093
|
+
}
|
|
2094
|
+
if (reviewFile.isBinary) {
|
|
2095
|
+
return c.json({ error: "binary file context is not available" }, 409);
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
const response = await captureDiffContext({
|
|
2099
|
+
filePath: body.filePath,
|
|
2100
|
+
oldPath: body.oldPath,
|
|
2101
|
+
oldRef: source.oldRef,
|
|
2102
|
+
newRef: source.newRef,
|
|
2103
|
+
oldStart: body.oldStart,
|
|
2104
|
+
newStart: body.newStart,
|
|
2105
|
+
lineCount: body.lineCount,
|
|
2106
|
+
repoRoot
|
|
2107
|
+
});
|
|
2108
|
+
return c.json(response);
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
return c.json({ error: `context is unavailable: ${formatError(error)}` }, 409);
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
1919
2113
|
app.post("/api/reviews/:id/files/open", async (c) => {
|
|
1920
2114
|
const id = c.req.param("id");
|
|
1921
2115
|
const existing = await reviewStore.get(id);
|
|
@@ -1927,11 +2121,11 @@ function createApp(origin2, options = {}) {
|
|
|
1927
2121
|
return parsed.response;
|
|
1928
2122
|
}
|
|
1929
2123
|
const { filePath, turnId } = parsed.body;
|
|
1930
|
-
if (!filePath || filePath.includes("\0") ||
|
|
2124
|
+
if (!filePath || filePath.includes("\0") || path7.isAbsolute(filePath)) {
|
|
1931
2125
|
return c.json({ error: "filePath must be a repo-relative path" }, 400);
|
|
1932
2126
|
}
|
|
1933
|
-
const repoRoot =
|
|
1934
|
-
const requestedAbsolutePath =
|
|
2127
|
+
const repoRoot = path7.resolve(existing.diff.cwd);
|
|
2128
|
+
const requestedAbsolutePath = path7.resolve(repoRoot, filePath);
|
|
1935
2129
|
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
1936
2130
|
return c.json({ error: "filePath must stay within the review cwd" }, 400);
|
|
1937
2131
|
}
|
|
@@ -2047,13 +2241,13 @@ function createApp(origin2, options = {}) {
|
|
|
2047
2241
|
}
|
|
2048
2242
|
async function serveAsset(c) {
|
|
2049
2243
|
const requestPath = new URL(c.req.url).pathname.replace(/^\/assets\//, "");
|
|
2050
|
-
const normalized =
|
|
2051
|
-
const assetPath =
|
|
2244
|
+
const normalized = path7.normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
2245
|
+
const assetPath = path7.join(webRoot, "assets", normalized);
|
|
2052
2246
|
try {
|
|
2053
|
-
const body = await
|
|
2247
|
+
const body = await readFile5(assetPath);
|
|
2054
2248
|
return new Response(body, {
|
|
2055
2249
|
headers: {
|
|
2056
|
-
"content-type": mimeTypes[
|
|
2250
|
+
"content-type": mimeTypes[path7.extname(assetPath)] ?? "application/octet-stream"
|
|
2057
2251
|
}
|
|
2058
2252
|
});
|
|
2059
2253
|
} catch (error) {
|
|
@@ -2065,7 +2259,7 @@ async function serveAsset(c) {
|
|
|
2065
2259
|
}
|
|
2066
2260
|
async function serveIndex() {
|
|
2067
2261
|
try {
|
|
2068
|
-
const body = await
|
|
2262
|
+
const body = await readFile5(path7.join(webRoot, "index.html"));
|
|
2069
2263
|
return new Response(body, {
|
|
2070
2264
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2071
2265
|
});
|
|
@@ -2079,7 +2273,7 @@ async function serveIndex() {
|
|
|
2079
2273
|
function serveRootFile(fileName, contentType) {
|
|
2080
2274
|
return async () => {
|
|
2081
2275
|
try {
|
|
2082
|
-
const body = await
|
|
2276
|
+
const body = await readFile5(path7.join(webRoot, fileName));
|
|
2083
2277
|
return new Response(body, {
|
|
2084
2278
|
headers: { "content-type": contentType }
|
|
2085
2279
|
});
|
|
@@ -2111,8 +2305,63 @@ async function readJsonBody(c, guard, label) {
|
|
|
2111
2305
|
}
|
|
2112
2306
|
}
|
|
2113
2307
|
function isPathWithin(parentPath, childPath) {
|
|
2114
|
-
const relative =
|
|
2115
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2308
|
+
const relative = path7.relative(parentPath, childPath);
|
|
2309
|
+
return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
|
|
2310
|
+
}
|
|
2311
|
+
async function resolveContextSource(diffPayload, source) {
|
|
2312
|
+
if (source.mode === "turn") {
|
|
2313
|
+
return {
|
|
2314
|
+
ok: true,
|
|
2315
|
+
files: diffPayload.files,
|
|
2316
|
+
oldRef: diffPayload.base.sha,
|
|
2317
|
+
newRef: diffPayload.scope.comparison.sha
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
const commitDiffs = diffPayload.commitDiffs ?? [];
|
|
2321
|
+
if (commitDiffs.length === 0) {
|
|
2322
|
+
return {
|
|
2323
|
+
ok: false,
|
|
2324
|
+
status: 409,
|
|
2325
|
+
error: "commit context is only available for branch reviews"
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
if (source.mode === "commit") {
|
|
2329
|
+
const commitDiff = commitDiffs.find((diff) => diff.commit.sha === source.sha);
|
|
2330
|
+
if (!commitDiff) {
|
|
2331
|
+
return { ok: false, status: 404, error: "commit must be part of this review" };
|
|
2332
|
+
}
|
|
2333
|
+
return {
|
|
2334
|
+
ok: true,
|
|
2335
|
+
files: commitDiff.files,
|
|
2336
|
+
oldRef: `${source.sha}^`,
|
|
2337
|
+
newRef: source.sha
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
const fromIndex = commitDiffs.findIndex((diff) => diff.commit.sha === source.fromSha);
|
|
2341
|
+
const toIndex = commitDiffs.findIndex((diff) => diff.commit.sha === source.toSha);
|
|
2342
|
+
if (fromIndex < 0 || toIndex < 0) {
|
|
2343
|
+
return { ok: false, status: 404, error: "commit range must use commits from this review" };
|
|
2344
|
+
}
|
|
2345
|
+
if (fromIndex > toIndex) {
|
|
2346
|
+
return { ok: false, status: 400, error: "fromSha must come before or match toSha" };
|
|
2347
|
+
}
|
|
2348
|
+
const rangeDiff = source.fromSha === source.toSha ? commitDiffs[fromIndex] : await captureCommitRangeDiff(source.fromSha, source.toSha, diffPayload.cwd);
|
|
2349
|
+
return {
|
|
2350
|
+
ok: true,
|
|
2351
|
+
files: rangeDiff.files,
|
|
2352
|
+
oldRef: `${source.fromSha}^`,
|
|
2353
|
+
newRef: source.toSha
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function validateContextPath(repoRoot, filePath, label) {
|
|
2357
|
+
if (!filePath || filePath.includes("\0") || path7.isAbsolute(filePath)) {
|
|
2358
|
+
return `${label} must be a repo-relative path`;
|
|
2359
|
+
}
|
|
2360
|
+
const requestedAbsolutePath = path7.resolve(repoRoot, filePath);
|
|
2361
|
+
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2362
|
+
return `${label} must stay within the review cwd`;
|
|
2363
|
+
}
|
|
2364
|
+
return null;
|
|
2116
2365
|
}
|
|
2117
2366
|
function activeTurnSummary(meta) {
|
|
2118
2367
|
if (!meta.activeTurnId) {
|
|
@@ -2157,23 +2406,39 @@ async function runStartupCleanup(logger = defaultLogger) {
|
|
|
2157
2406
|
|
|
2158
2407
|
// src/server/daemon.ts
|
|
2159
2408
|
var port = Number(process.env.GLOSS_PORT ?? "0");
|
|
2160
|
-
var idleTimeoutMs =
|
|
2409
|
+
var idleTimeoutMs = normalizeIdleTimeoutMs();
|
|
2410
|
+
var daemonPath = fileURLToPath2(import.meta.url);
|
|
2161
2411
|
if (!port) {
|
|
2162
2412
|
throw new Error("GLOSS_PORT is required");
|
|
2163
2413
|
}
|
|
2164
2414
|
var origin = `http://localhost:${port}`;
|
|
2165
2415
|
var eventStreams = /* @__PURE__ */ new Set();
|
|
2166
|
-
var idleTimer = null;
|
|
2167
2416
|
var shuttingDown = false;
|
|
2417
|
+
var idleScheduler = createIdleScheduler({
|
|
2418
|
+
timeoutMs: idleTimeoutMs,
|
|
2419
|
+
hasLiveClients: () => eventStreams.size > 0,
|
|
2420
|
+
isShuttingDown: () => shuttingDown,
|
|
2421
|
+
shutdown: () => shutdown(0)
|
|
2422
|
+
});
|
|
2168
2423
|
var server = serve({
|
|
2169
2424
|
fetch: createApp(origin, {
|
|
2170
2425
|
onReviewActivity: () => {
|
|
2171
|
-
|
|
2426
|
+
idleScheduler.schedule();
|
|
2172
2427
|
},
|
|
2173
2428
|
registerEventStream: (close) => {
|
|
2174
2429
|
eventStreams.add(close);
|
|
2430
|
+
idleScheduler.schedule();
|
|
2175
2431
|
return () => {
|
|
2176
2432
|
eventStreams.delete(close);
|
|
2433
|
+
idleScheduler.schedule();
|
|
2434
|
+
};
|
|
2435
|
+
},
|
|
2436
|
+
health: () => {
|
|
2437
|
+
return {
|
|
2438
|
+
connections: eventStreams.size,
|
|
2439
|
+
cwd: process.cwd(),
|
|
2440
|
+
daemonPath,
|
|
2441
|
+
stateDir: globalStateDir()
|
|
2177
2442
|
};
|
|
2178
2443
|
}
|
|
2179
2444
|
}).fetch,
|
|
@@ -2184,54 +2449,23 @@ await writeServerInfo({
|
|
|
2184
2449
|
port,
|
|
2185
2450
|
version: packageVersion,
|
|
2186
2451
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2187
|
-
stateDir: globalStateDir()
|
|
2452
|
+
stateDir: globalStateDir(),
|
|
2453
|
+
cwd: process.cwd(),
|
|
2454
|
+
daemonPath
|
|
2188
2455
|
});
|
|
2189
2456
|
await runStartupCleanup();
|
|
2190
|
-
|
|
2457
|
+
idleScheduler.schedule();
|
|
2191
2458
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
2192
2459
|
process.on(signal, () => {
|
|
2193
2460
|
void shutdown(0);
|
|
2194
2461
|
});
|
|
2195
2462
|
}
|
|
2196
|
-
async function scheduleIdleShutdown() {
|
|
2197
|
-
if (shuttingDown || idleTimeoutMs <= 0) {
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
const activeReviews = await countActiveReviews();
|
|
2201
|
-
if (activeReviews > 0) {
|
|
2202
|
-
if (idleTimer) {
|
|
2203
|
-
clearTimeout(idleTimer);
|
|
2204
|
-
idleTimer = null;
|
|
2205
|
-
}
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
if (!idleTimer) {
|
|
2209
|
-
idleTimer = setTimeout(() => {
|
|
2210
|
-
idleTimer = null;
|
|
2211
|
-
void shutdownIfIdle();
|
|
2212
|
-
}, idleTimeoutMs);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
async function shutdownIfIdle() {
|
|
2216
|
-
if (await countActiveReviews() > 0) {
|
|
2217
|
-
await scheduleIdleShutdown();
|
|
2218
|
-
return;
|
|
2219
|
-
}
|
|
2220
|
-
await shutdown(0);
|
|
2221
|
-
}
|
|
2222
|
-
async function countActiveReviews() {
|
|
2223
|
-
const reviews = await reviewStore.list();
|
|
2224
|
-
return reviews.filter((review) => review.status === "pending").length;
|
|
2225
|
-
}
|
|
2226
2463
|
async function shutdown(exitCode) {
|
|
2227
2464
|
if (shuttingDown) {
|
|
2228
2465
|
return;
|
|
2229
2466
|
}
|
|
2230
2467
|
shuttingDown = true;
|
|
2231
|
-
|
|
2232
|
-
clearTimeout(idleTimer);
|
|
2233
|
-
idleTimer = null;
|
|
2234
|
-
}
|
|
2468
|
+
idleScheduler.cancel();
|
|
2235
2469
|
for (const close of [...eventStreams]) {
|
|
2236
2470
|
close();
|
|
2237
2471
|
}
|