getgloss 0.8.0 → 0.8.1
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 -0
- package/dist/cli/index.js +504 -229
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +281 -15
- package/dist/server/daemon.js.map +1 -1
- package/dist/web/setup.md +3 -0
- package/package.json +1 -1
- package/skill/SKILL.md +52 -54
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import openBrowser from "open";
|
|
6
6
|
|
|
7
|
+
// src/shared/cleanup.ts
|
|
8
|
+
import { readdir, readFile, rm } from "fs/promises";
|
|
9
|
+
|
|
10
|
+
// src/shared/errors.ts
|
|
11
|
+
function formatError(error) {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
function isFileNotFound(error) {
|
|
15
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
// src/shared/paths.ts
|
|
8
19
|
import { mkdir } from "fs/promises";
|
|
9
20
|
import { homedir } from "os";
|
|
@@ -12,7 +23,7 @@ import path from "path";
|
|
|
12
23
|
// package.json
|
|
13
24
|
var package_default = {
|
|
14
25
|
name: "getgloss",
|
|
15
|
-
version: "0.8.
|
|
26
|
+
version: "0.8.1",
|
|
16
27
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
17
28
|
type: "module",
|
|
18
29
|
packageManager: "pnpm@10.33.2",
|
|
@@ -150,14 +161,435 @@ function globalReviewDiffFile(reviewId) {
|
|
|
150
161
|
function globalReviewFeedbackFile(reviewId) {
|
|
151
162
|
return path.join(globalReviewDir(reviewId), "feedback.json");
|
|
152
163
|
}
|
|
153
|
-
function globalReviewMarkdownFile(reviewId) {
|
|
154
|
-
return path.join(globalReviewDir(reviewId), "feedback.md");
|
|
164
|
+
function globalReviewMarkdownFile(reviewId) {
|
|
165
|
+
return path.join(globalReviewDir(reviewId), "feedback.md");
|
|
166
|
+
}
|
|
167
|
+
function globalReviewResolvedFile(reviewId) {
|
|
168
|
+
return path.join(globalReviewDir(reviewId), "resolved.json");
|
|
169
|
+
}
|
|
170
|
+
async function ensureDir(dir) {
|
|
171
|
+
await mkdir(dir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/shared/types.ts
|
|
175
|
+
var SIDES = ["L", "R"];
|
|
176
|
+
var REVIEW_STATUSES = ["pending", "submitted", "cancelled", "resolved"];
|
|
177
|
+
var DIFF_LINE_TYPES = ["context", "add", "delete"];
|
|
178
|
+
var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
179
|
+
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
180
|
+
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
181
|
+
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
182
|
+
var REVIEW_UPDATE_REASONS = [
|
|
183
|
+
"review-resolved",
|
|
184
|
+
"comment-resolved",
|
|
185
|
+
"comment-reopened",
|
|
186
|
+
"turn-created"
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// src/shared/validation.ts
|
|
190
|
+
function parseJson(raw, guard, label) {
|
|
191
|
+
const parsed = JSON.parse(raw);
|
|
192
|
+
return parseJsonValue(parsed, guard, label);
|
|
193
|
+
}
|
|
194
|
+
function parseJsonValue(value, guard, label) {
|
|
195
|
+
if (!guard(value)) {
|
|
196
|
+
throw new Error(`Invalid ${label}`);
|
|
197
|
+
}
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
function isServerInfo(value) {
|
|
201
|
+
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
|
|
202
|
+
}
|
|
203
|
+
function isHealthResponse(value) {
|
|
204
|
+
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews);
|
|
205
|
+
}
|
|
206
|
+
function isClearReviewsResult(value) {
|
|
207
|
+
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);
|
|
208
|
+
}
|
|
209
|
+
function isCreateReviewResponse(value) {
|
|
210
|
+
return isRecord(value) && hasReviewRegistrationFields(value) && isOptional(value.turn, isReviewTurnSummary);
|
|
211
|
+
}
|
|
212
|
+
function isCreateReviewTurnResponse(value) {
|
|
213
|
+
return isRecord(value) && hasReviewRegistrationFields(value) && isReviewTurnSummary(value.turn) && isBoolean(value.reused);
|
|
214
|
+
}
|
|
215
|
+
function isListReviewsResponse(value) {
|
|
216
|
+
return isRecord(value) && isArrayOf(value.reviews, isReviewMeta);
|
|
217
|
+
}
|
|
218
|
+
function isOpenResult(value) {
|
|
219
|
+
return isRecord(value) && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isString(value.url) && isNumber(value.files) && isOptionalNumber(value.comments) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.artifactDir);
|
|
220
|
+
}
|
|
221
|
+
function isResolveResult(value) {
|
|
222
|
+
return isRecord(value) && value.ok === true && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.comments) && isString(value.path) && isResolutionBundle(value.resolution);
|
|
223
|
+
}
|
|
224
|
+
function isReviewRecord(value) {
|
|
225
|
+
return isRecord(value) && isReviewMeta(value.meta) && isArrayOf(value.turns, isReviewTurn) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
226
|
+
}
|
|
227
|
+
function isStoredReviewMeta(value) {
|
|
228
|
+
return isRecord(value) && isString(value.id) && isString(value.cwd) && isBaseRef(value.base) && isNullableString(value.branch) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isOptionalString(value.artifactDir) && isOptionalString(value.activeTurnId) && isOptional(
|
|
229
|
+
value.turns,
|
|
230
|
+
(turns) => isArrayOf(turns, isReviewTurnSummary)
|
|
231
|
+
) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath);
|
|
232
|
+
}
|
|
233
|
+
function isReviewMeta(value) {
|
|
234
|
+
return isStoredReviewMeta(value) && isString(value.artifactDir);
|
|
235
|
+
}
|
|
236
|
+
function hasReviewRegistrationFields(value) {
|
|
237
|
+
return isReviewMeta(value.meta) && isString(value.url);
|
|
238
|
+
}
|
|
239
|
+
function isDiffPayload(value) {
|
|
240
|
+
return isRecord(value) && isBaseRef(value.base) && isNullableString(value.branch) && isString(value.cwd) && isDiffScope(value.scope) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile) && isOptional(
|
|
241
|
+
value.commitDiffs,
|
|
242
|
+
(commitDiffs) => isArrayOf(commitDiffs, isCommitDiff)
|
|
243
|
+
) && isString(value.capturedAt);
|
|
244
|
+
}
|
|
245
|
+
function isFeedbackBundle(value) {
|
|
246
|
+
return isRecord(value) && value.version === 1 && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isString(value.timestamp) && isBaseRef(value.base) && isNullableString(value.branch) && isOptional(value.reviewScope, isReviewScope) && isArrayOf(value.comments, isComment);
|
|
247
|
+
}
|
|
248
|
+
function isResolutionBundle(value) {
|
|
249
|
+
return isRecord(value) && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isResolutionStatus(value.status) && isNullableString(value.summary) && isNullableString(value.resolvedAt) && isArrayOf(value.comments, isResolvedComment);
|
|
250
|
+
}
|
|
251
|
+
function isReviewEvent(value) {
|
|
252
|
+
if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
switch (value.type) {
|
|
256
|
+
case "review.opened":
|
|
257
|
+
case "review.cancelled":
|
|
258
|
+
return true;
|
|
259
|
+
case "review.turn.created":
|
|
260
|
+
return isString(value.turnId) && isNumber(value.turnIndex) && isBoolean(value.reused);
|
|
261
|
+
case "review.submitted":
|
|
262
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
|
|
263
|
+
case "review.updated":
|
|
264
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
|
|
265
|
+
default:
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isReviewTurnMeta(value) {
|
|
270
|
+
return isRecord(value) && isString(value.id) && isNumber(value.index) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isString(value.artifactDir) && isString(value.diffPath) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.resolvedPath);
|
|
271
|
+
}
|
|
272
|
+
function isReviewTurnSummary(value) {
|
|
273
|
+
if (!isRecord(value) || !isReviewTurnMeta(value)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return isString(value.capturedAt) && isDiffStats(value.stats) && isResolutionCounts(value.comments);
|
|
277
|
+
}
|
|
278
|
+
function isReviewTurn(value) {
|
|
279
|
+
if (!isRecord(value) || !isReviewTurnMeta(value)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
283
|
+
}
|
|
284
|
+
function isClearReviewEntry(value) {
|
|
285
|
+
return isRecord(value) && isString(value.reviewId) && isReviewStatus(value.status) && isString(value.artifactDir) && isString(value.lastActivityAt);
|
|
286
|
+
}
|
|
287
|
+
function isClearReviewSkipped(value) {
|
|
288
|
+
return isRecord(value) && isString(value.reviewId) && isString(value.artifactDir) && isString(value.reason);
|
|
289
|
+
}
|
|
290
|
+
function isDiffScope(value) {
|
|
291
|
+
return isRecord(value) && isOneOf(value.mode, DIFF_SCOPE_MODES) && isNullableString(value.requestedBase) && isBaseRef(value.base) && isDiffRef(value.comparison) && (value.fallbackReason === null || isOneOf(value.fallbackReason, DIFF_FALLBACK_REASONS));
|
|
292
|
+
}
|
|
293
|
+
function isDiffRef(value) {
|
|
294
|
+
return isRecord(value) && isString(value.ref) && isNullableString(value.sha);
|
|
295
|
+
}
|
|
296
|
+
function isBaseRef(value) {
|
|
297
|
+
return isRecord(value) && isString(value.ref) && isString(value.sha);
|
|
298
|
+
}
|
|
299
|
+
function isDiffStats(value) {
|
|
300
|
+
return isRecord(value) && isNumber(value.files) && isNumber(value.additions) && isNumber(value.deletions);
|
|
301
|
+
}
|
|
302
|
+
function isDiffCommit(value) {
|
|
303
|
+
return isRecord(value) && isString(value.sha) && isString(value.shortSha) && isString(value.subject) && isString(value.authorName) && isString(value.authorEmail) && isString(value.authoredAt) && isString(value.committedAt);
|
|
304
|
+
}
|
|
305
|
+
function isCommitDiff(value) {
|
|
306
|
+
return isRecord(value) && isDiffCommit(value.commit) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile);
|
|
307
|
+
}
|
|
308
|
+
function isReviewScope(value) {
|
|
309
|
+
if (!isRecord(value) || !isOneOf(value.mode, REVIEW_SCOPE_MODES)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
switch (value.mode) {
|
|
313
|
+
case "all":
|
|
314
|
+
return true;
|
|
315
|
+
case "single":
|
|
316
|
+
return isString(value.sha);
|
|
317
|
+
case "range":
|
|
318
|
+
return isString(value.fromSha) && isString(value.toSha);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function isDiffFile(value) {
|
|
322
|
+
return isRecord(value) && isString(value.path) && isNullableString(value.oldPath) && isNumber(value.additions) && isNumber(value.deletions) && isBoolean(value.isBinary) && isBoolean(value.isDeleted) && isBoolean(value.isNew) && isBoolean(value.isRenamed) && isNullableString(value.language) && isArrayOf(value.hunks, isDiffHunk);
|
|
323
|
+
}
|
|
324
|
+
function isDiffHunk(value) {
|
|
325
|
+
return isRecord(value) && isNumber(value.oldStart) && isNumber(value.oldLines) && isNumber(value.newStart) && isNumber(value.newLines) && isString(value.header) && isArrayOf(value.lines, isDiffLine);
|
|
326
|
+
}
|
|
327
|
+
function isDiffLine(value) {
|
|
328
|
+
return isRecord(value) && isOneOf(value.type, DIFF_LINE_TYPES) && isNullableNumber(value.oldLine) && isNullableNumber(value.newLine) && isString(value.content);
|
|
329
|
+
}
|
|
330
|
+
function isComment(value) {
|
|
331
|
+
return isRecord(value) && isString(value.id) && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, SIDES) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
|
|
332
|
+
}
|
|
333
|
+
function isResolvedComment(value) {
|
|
334
|
+
return isRecord(value) && isString(value.commentId) && value.status === "resolved" && isOptionalString(value.summary) && isString(value.resolvedAt);
|
|
335
|
+
}
|
|
336
|
+
function isResolutionCounts(value) {
|
|
337
|
+
return isRecord(value) && isNumber(value.total) && isNumber(value.resolved) && isNumber(value.open);
|
|
338
|
+
}
|
|
339
|
+
function isReviewStatus(value) {
|
|
340
|
+
return isOneOf(value, REVIEW_STATUSES);
|
|
341
|
+
}
|
|
342
|
+
function isResolutionStatus(value) {
|
|
343
|
+
return isOneOf(value, RESOLUTION_STATUSES);
|
|
344
|
+
}
|
|
345
|
+
function isReviewUpdateReason(value) {
|
|
346
|
+
return isOneOf(value, REVIEW_UPDATE_REASONS);
|
|
347
|
+
}
|
|
348
|
+
function isRecord(value) {
|
|
349
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
350
|
+
}
|
|
351
|
+
function isArrayOf(value, guard) {
|
|
352
|
+
return Array.isArray(value) && value.every(guard);
|
|
353
|
+
}
|
|
354
|
+
function isOptional(value, guard) {
|
|
355
|
+
return value === void 0 || guard(value);
|
|
356
|
+
}
|
|
357
|
+
function isString(value) {
|
|
358
|
+
return typeof value === "string";
|
|
359
|
+
}
|
|
360
|
+
function isOptionalString(value) {
|
|
361
|
+
return value === void 0 || isString(value);
|
|
362
|
+
}
|
|
363
|
+
function isNullableString(value) {
|
|
364
|
+
return value === null || isString(value);
|
|
365
|
+
}
|
|
366
|
+
function isNumber(value) {
|
|
367
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
368
|
+
}
|
|
369
|
+
function isOptionalNumber(value) {
|
|
370
|
+
return value === void 0 || isNumber(value);
|
|
371
|
+
}
|
|
372
|
+
function isNullableNumber(value) {
|
|
373
|
+
return value === null || isNumber(value);
|
|
374
|
+
}
|
|
375
|
+
function isBoolean(value) {
|
|
376
|
+
return typeof value === "boolean";
|
|
377
|
+
}
|
|
378
|
+
function isOneOf(value, options) {
|
|
379
|
+
return typeof value === "string" && options.includes(value);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/shared/cleanup.ts
|
|
383
|
+
var DEFAULT_REVIEW_RETENTION_DAYS = 30;
|
|
384
|
+
var clearableStatuses = /* @__PURE__ */ new Set(["submitted", "resolved", "cancelled"]);
|
|
385
|
+
var millisecondsPerDay = 24 * 60 * 60 * 1e3;
|
|
386
|
+
async function clearReviewArtifacts(options = {}) {
|
|
387
|
+
const olderThanDays = normalizeRetentionDays(options.olderThanDays);
|
|
388
|
+
const dryRun = options.dryRun === true;
|
|
389
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
390
|
+
const cutoff = new Date(now.getTime() - olderThanDays * millisecondsPerDay);
|
|
391
|
+
const reviewsDir = globalReviewsDir();
|
|
392
|
+
const candidates = [];
|
|
393
|
+
const deleted = [];
|
|
394
|
+
const skipped = [];
|
|
395
|
+
let entries;
|
|
396
|
+
try {
|
|
397
|
+
entries = await readdir(reviewsDir, { withFileTypes: true });
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isFileNotFound(error)) {
|
|
400
|
+
return cleanupResult({
|
|
401
|
+
reviewsDir,
|
|
402
|
+
cutoff,
|
|
403
|
+
olderThanDays,
|
|
404
|
+
dryRun,
|
|
405
|
+
candidates,
|
|
406
|
+
deleted,
|
|
407
|
+
skipped
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`Could not read reviews directory at ${reviewsDir}: ${formatError(error)}`, {
|
|
411
|
+
cause: error
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (!entry.isDirectory()) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const reviewId = entry.name;
|
|
419
|
+
const artifactDir = globalReviewDir(reviewId);
|
|
420
|
+
const candidate = await cleanupCandidate(reviewId, artifactDir, cutoff, skipped);
|
|
421
|
+
if (!candidate) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
candidates.push(candidate);
|
|
425
|
+
if (!dryRun) {
|
|
426
|
+
await rm(artifactDir, { recursive: true, force: true });
|
|
427
|
+
deleted.push(candidate);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return cleanupResult({ reviewsDir, cutoff, olderThanDays, dryRun, candidates, deleted, skipped });
|
|
431
|
+
}
|
|
432
|
+
function normalizeRetentionDays(value) {
|
|
433
|
+
const days = value ?? DEFAULT_REVIEW_RETENTION_DAYS;
|
|
434
|
+
if (!Number.isInteger(days) || days < 0) {
|
|
435
|
+
throw new Error("olderThanDays must be a non-negative integer");
|
|
436
|
+
}
|
|
437
|
+
return days;
|
|
438
|
+
}
|
|
439
|
+
async function cleanupCandidate(reviewId, artifactDir, cutoff, skipped) {
|
|
440
|
+
let raw;
|
|
441
|
+
try {
|
|
442
|
+
raw = await readFile(globalReviewMetaFile(reviewId), "utf8");
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (isFileNotFound(error)) {
|
|
445
|
+
skipped.push({ reviewId, artifactDir, reason: "missing metadata" });
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
skipped.push({ reviewId, artifactDir, reason: `unreadable metadata: ${formatError(error)}` });
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
let meta;
|
|
452
|
+
try {
|
|
453
|
+
meta = parseJson(raw, isStoredReviewMeta, "review metadata");
|
|
454
|
+
} catch (error) {
|
|
455
|
+
skipped.push({ reviewId, artifactDir, reason: `invalid metadata: ${formatError(error)}` });
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (meta.id !== reviewId) {
|
|
459
|
+
skipped.push({ reviewId, artifactDir, reason: `metadata id mismatch: ${meta.id}` });
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (!clearableStatuses.has(meta.status)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const turnState = await persistedTurnCleanupState(reviewId, artifactDir, skipped);
|
|
466
|
+
if (turnState === "preserve") {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const lastActivityAt = latestTimestamp([
|
|
470
|
+
...metadataTimestamps(meta),
|
|
471
|
+
...turnState === "none" ? [] : turnState.timestamps
|
|
472
|
+
]);
|
|
473
|
+
if (!lastActivityAt) {
|
|
474
|
+
skipped.push({ reviewId, artifactDir, reason: "missing valid activity timestamp" });
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (Date.parse(lastActivityAt) >= cutoff.getTime()) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
reviewId,
|
|
482
|
+
status: meta.status,
|
|
483
|
+
artifactDir,
|
|
484
|
+
lastActivityAt
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function persistedTurnCleanupState(reviewId, artifactDir, skipped) {
|
|
488
|
+
let entries;
|
|
489
|
+
try {
|
|
490
|
+
entries = await readdir(globalReviewTurnsDir(reviewId), { withFileTypes: true });
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (isFileNotFound(error)) {
|
|
493
|
+
return "none";
|
|
494
|
+
}
|
|
495
|
+
skipped.push({
|
|
496
|
+
reviewId,
|
|
497
|
+
artifactDir,
|
|
498
|
+
reason: `unreadable turns directory: ${formatError(error)}`
|
|
499
|
+
});
|
|
500
|
+
return "preserve";
|
|
501
|
+
}
|
|
502
|
+
const turnDirs = entries.filter((entry) => entry.isDirectory());
|
|
503
|
+
if (turnDirs.length === 0) {
|
|
504
|
+
return "none";
|
|
505
|
+
}
|
|
506
|
+
const timestamps = [];
|
|
507
|
+
for (const entry of turnDirs) {
|
|
508
|
+
const turn = await readPersistedTurnMeta(reviewId, entry.name, artifactDir, skipped);
|
|
509
|
+
if (!turn) {
|
|
510
|
+
return "preserve";
|
|
511
|
+
}
|
|
512
|
+
if (turn.status === "pending" || !clearableStatuses.has(turn.status)) {
|
|
513
|
+
return "preserve";
|
|
514
|
+
}
|
|
515
|
+
timestamps.push(turn.createdAt, turn.submittedAt, turn.resolvedAt);
|
|
516
|
+
}
|
|
517
|
+
return { timestamps };
|
|
518
|
+
}
|
|
519
|
+
async function readPersistedTurnMeta(reviewId, turnDirName, artifactDir, skipped) {
|
|
520
|
+
let raw;
|
|
521
|
+
try {
|
|
522
|
+
raw = await readFile(globalReviewTurnMetaFile(reviewId, turnDirName), "utf8");
|
|
523
|
+
} catch (error) {
|
|
524
|
+
skipped.push({
|
|
525
|
+
reviewId,
|
|
526
|
+
artifactDir,
|
|
527
|
+
reason: `${isFileNotFound(error) ? "missing" : "unreadable"} turn metadata for ${turnDirName}${isFileNotFound(error) ? "" : `: ${formatError(error)}`}`
|
|
528
|
+
});
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const turn = parseJson(raw, isReviewTurnMeta, "review turn metadata");
|
|
533
|
+
if (turn.id !== turnDirName) {
|
|
534
|
+
skipped.push({
|
|
535
|
+
reviewId,
|
|
536
|
+
artifactDir,
|
|
537
|
+
reason: `turn metadata id mismatch for ${turnDirName}: ${turn.id}`
|
|
538
|
+
});
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
return turn;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
skipped.push({
|
|
544
|
+
reviewId,
|
|
545
|
+
artifactDir,
|
|
546
|
+
reason: `invalid turn metadata for ${turnDirName}: ${formatError(error)}`
|
|
547
|
+
});
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
155
550
|
}
|
|
156
|
-
function
|
|
157
|
-
return
|
|
551
|
+
function metadataTimestamps(meta) {
|
|
552
|
+
return [
|
|
553
|
+
meta.createdAt,
|
|
554
|
+
meta.submittedAt,
|
|
555
|
+
meta.resolvedAt,
|
|
556
|
+
...(meta.turns ?? []).flatMap((turn) => [
|
|
557
|
+
turn.createdAt,
|
|
558
|
+
turn.capturedAt,
|
|
559
|
+
turn.submittedAt,
|
|
560
|
+
turn.resolvedAt
|
|
561
|
+
])
|
|
562
|
+
];
|
|
158
563
|
}
|
|
159
|
-
|
|
160
|
-
|
|
564
|
+
function latestTimestamp(timestamps) {
|
|
565
|
+
const latest = Math.max(
|
|
566
|
+
...timestamps.map((timestamp) => timestamp ? Date.parse(timestamp) : Number.NaN).filter((timestamp) => Number.isFinite(timestamp))
|
|
567
|
+
);
|
|
568
|
+
return Number.isFinite(latest) ? new Date(latest).toISOString() : null;
|
|
569
|
+
}
|
|
570
|
+
function cleanupResult({
|
|
571
|
+
reviewsDir,
|
|
572
|
+
cutoff,
|
|
573
|
+
olderThanDays,
|
|
574
|
+
dryRun,
|
|
575
|
+
candidates,
|
|
576
|
+
deleted,
|
|
577
|
+
skipped
|
|
578
|
+
}) {
|
|
579
|
+
return {
|
|
580
|
+
reviewsDir,
|
|
581
|
+
cutoff: cutoff.toISOString(),
|
|
582
|
+
olderThanDays,
|
|
583
|
+
dryRun,
|
|
584
|
+
candidates,
|
|
585
|
+
deleted,
|
|
586
|
+
skipped,
|
|
587
|
+
counts: {
|
|
588
|
+
candidates: candidates.length,
|
|
589
|
+
deleted: deleted.length,
|
|
590
|
+
skipped: skipped.length
|
|
591
|
+
}
|
|
592
|
+
};
|
|
161
593
|
}
|
|
162
594
|
|
|
163
595
|
// src/cli/git.ts
|
|
@@ -552,26 +984,18 @@ async function assertGitAvailable() {
|
|
|
552
984
|
// src/cli/lifecycle.ts
|
|
553
985
|
import { execFile, spawn } from "child_process";
|
|
554
986
|
import { closeSync, existsSync, openSync } from "fs";
|
|
555
|
-
import { rm as
|
|
987
|
+
import { rm as rm3 } from "fs/promises";
|
|
556
988
|
import { userInfo } from "os";
|
|
557
989
|
import { fileURLToPath } from "url";
|
|
558
990
|
import { promisify } from "util";
|
|
559
991
|
import getPort from "get-port";
|
|
560
992
|
|
|
561
993
|
// src/shared/server-info.ts
|
|
562
|
-
import { readFile } from "fs/promises";
|
|
563
|
-
|
|
564
|
-
// src/shared/errors.ts
|
|
565
|
-
function formatError(error) {
|
|
566
|
-
return error instanceof Error ? error.message : String(error);
|
|
567
|
-
}
|
|
568
|
-
function isFileNotFound(error) {
|
|
569
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
570
|
-
}
|
|
994
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
571
995
|
|
|
572
996
|
// src/shared/json.ts
|
|
573
997
|
import { randomUUID } from "crypto";
|
|
574
|
-
import { rename, rm, writeFile } from "fs/promises";
|
|
998
|
+
import { rename, rm as rm2, writeFile } from "fs/promises";
|
|
575
999
|
import path3 from "path";
|
|
576
1000
|
function serializeJson(value) {
|
|
577
1001
|
return `${JSON.stringify(value, null, 2)}
|
|
@@ -589,215 +1013,16 @@ async function writeTextFile(filePath, value) {
|
|
|
589
1013
|
await writeFile(tempPath, value);
|
|
590
1014
|
await rename(tempPath, filePath);
|
|
591
1015
|
} catch (error) {
|
|
592
|
-
await
|
|
1016
|
+
await rm2(tempPath, { force: true }).catch(() => void 0);
|
|
593
1017
|
throw error;
|
|
594
1018
|
}
|
|
595
1019
|
}
|
|
596
1020
|
|
|
597
|
-
// src/shared/types.ts
|
|
598
|
-
var SIDES = ["L", "R"];
|
|
599
|
-
var REVIEW_STATUSES = ["pending", "submitted", "cancelled", "resolved"];
|
|
600
|
-
var DIFF_LINE_TYPES = ["context", "add", "delete"];
|
|
601
|
-
var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
602
|
-
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
603
|
-
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
604
|
-
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
605
|
-
var REVIEW_UPDATE_REASONS = [
|
|
606
|
-
"review-resolved",
|
|
607
|
-
"comment-resolved",
|
|
608
|
-
"comment-reopened",
|
|
609
|
-
"turn-created"
|
|
610
|
-
];
|
|
611
|
-
|
|
612
|
-
// src/shared/validation.ts
|
|
613
|
-
function parseJson(raw, guard, label) {
|
|
614
|
-
const parsed = JSON.parse(raw);
|
|
615
|
-
return parseJsonValue(parsed, guard, label);
|
|
616
|
-
}
|
|
617
|
-
function parseJsonValue(value, guard, label) {
|
|
618
|
-
if (!guard(value)) {
|
|
619
|
-
throw new Error(`Invalid ${label}`);
|
|
620
|
-
}
|
|
621
|
-
return value;
|
|
622
|
-
}
|
|
623
|
-
function isServerInfo(value) {
|
|
624
|
-
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir);
|
|
625
|
-
}
|
|
626
|
-
function isHealthResponse(value) {
|
|
627
|
-
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews);
|
|
628
|
-
}
|
|
629
|
-
function isCreateReviewResponse(value) {
|
|
630
|
-
return isRecord(value) && hasReviewRegistrationFields(value) && isOptional(value.turn, isReviewTurnSummary);
|
|
631
|
-
}
|
|
632
|
-
function isCreateReviewTurnResponse(value) {
|
|
633
|
-
return isRecord(value) && hasReviewRegistrationFields(value) && isReviewTurnSummary(value.turn) && isBoolean(value.reused);
|
|
634
|
-
}
|
|
635
|
-
function isListReviewsResponse(value) {
|
|
636
|
-
return isRecord(value) && isArrayOf(value.reviews, isReviewMeta);
|
|
637
|
-
}
|
|
638
|
-
function isOpenResult(value) {
|
|
639
|
-
return isRecord(value) && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isString(value.url) && isNumber(value.files) && isOptionalNumber(value.comments) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.artifactDir);
|
|
640
|
-
}
|
|
641
|
-
function isResolveResult(value) {
|
|
642
|
-
return isRecord(value) && value.ok === true && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.comments) && isString(value.path) && isResolutionBundle(value.resolution);
|
|
643
|
-
}
|
|
644
|
-
function isReviewRecord(value) {
|
|
645
|
-
return isRecord(value) && isReviewMeta(value.meta) && isArrayOf(value.turns, isReviewTurn) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
646
|
-
}
|
|
647
|
-
function isStoredReviewMeta(value) {
|
|
648
|
-
return isRecord(value) && isString(value.id) && isString(value.cwd) && isBaseRef(value.base) && isNullableString(value.branch) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isOptionalString(value.artifactDir) && isOptionalString(value.activeTurnId) && isOptional(
|
|
649
|
-
value.turns,
|
|
650
|
-
(turns) => isArrayOf(turns, isReviewTurnSummary)
|
|
651
|
-
) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath);
|
|
652
|
-
}
|
|
653
|
-
function isReviewMeta(value) {
|
|
654
|
-
return isStoredReviewMeta(value) && isString(value.artifactDir);
|
|
655
|
-
}
|
|
656
|
-
function hasReviewRegistrationFields(value) {
|
|
657
|
-
return isReviewMeta(value.meta) && isString(value.url);
|
|
658
|
-
}
|
|
659
|
-
function isDiffPayload(value) {
|
|
660
|
-
return isRecord(value) && isBaseRef(value.base) && isNullableString(value.branch) && isString(value.cwd) && isDiffScope(value.scope) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile) && isOptional(
|
|
661
|
-
value.commitDiffs,
|
|
662
|
-
(commitDiffs) => isArrayOf(commitDiffs, isCommitDiff)
|
|
663
|
-
) && isString(value.capturedAt);
|
|
664
|
-
}
|
|
665
|
-
function isFeedbackBundle(value) {
|
|
666
|
-
return isRecord(value) && value.version === 1 && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isString(value.timestamp) && isBaseRef(value.base) && isNullableString(value.branch) && isOptional(value.reviewScope, isReviewScope) && isArrayOf(value.comments, isComment);
|
|
667
|
-
}
|
|
668
|
-
function isResolutionBundle(value) {
|
|
669
|
-
return isRecord(value) && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isResolutionStatus(value.status) && isNullableString(value.summary) && isNullableString(value.resolvedAt) && isArrayOf(value.comments, isResolvedComment);
|
|
670
|
-
}
|
|
671
|
-
function isReviewEvent(value) {
|
|
672
|
-
if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
switch (value.type) {
|
|
676
|
-
case "review.opened":
|
|
677
|
-
case "review.cancelled":
|
|
678
|
-
return true;
|
|
679
|
-
case "review.turn.created":
|
|
680
|
-
return isString(value.turnId) && isNumber(value.turnIndex) && isBoolean(value.reused);
|
|
681
|
-
case "review.submitted":
|
|
682
|
-
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
|
|
683
|
-
case "review.updated":
|
|
684
|
-
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
|
|
685
|
-
default:
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
function isReviewTurnMeta(value) {
|
|
690
|
-
return isRecord(value) && isString(value.id) && isNumber(value.index) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isString(value.artifactDir) && isString(value.diffPath) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.resolvedPath);
|
|
691
|
-
}
|
|
692
|
-
function isReviewTurnSummary(value) {
|
|
693
|
-
if (!isRecord(value) || !isReviewTurnMeta(value)) {
|
|
694
|
-
return false;
|
|
695
|
-
}
|
|
696
|
-
return isString(value.capturedAt) && isDiffStats(value.stats) && isResolutionCounts(value.comments);
|
|
697
|
-
}
|
|
698
|
-
function isReviewTurn(value) {
|
|
699
|
-
if (!isRecord(value) || !isReviewTurnMeta(value)) {
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
return isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
703
|
-
}
|
|
704
|
-
function isDiffScope(value) {
|
|
705
|
-
return isRecord(value) && isOneOf(value.mode, DIFF_SCOPE_MODES) && isNullableString(value.requestedBase) && isBaseRef(value.base) && isDiffRef(value.comparison) && (value.fallbackReason === null || isOneOf(value.fallbackReason, DIFF_FALLBACK_REASONS));
|
|
706
|
-
}
|
|
707
|
-
function isDiffRef(value) {
|
|
708
|
-
return isRecord(value) && isString(value.ref) && isNullableString(value.sha);
|
|
709
|
-
}
|
|
710
|
-
function isBaseRef(value) {
|
|
711
|
-
return isRecord(value) && isString(value.ref) && isString(value.sha);
|
|
712
|
-
}
|
|
713
|
-
function isDiffStats(value) {
|
|
714
|
-
return isRecord(value) && isNumber(value.files) && isNumber(value.additions) && isNumber(value.deletions);
|
|
715
|
-
}
|
|
716
|
-
function isDiffCommit(value) {
|
|
717
|
-
return isRecord(value) && isString(value.sha) && isString(value.shortSha) && isString(value.subject) && isString(value.authorName) && isString(value.authorEmail) && isString(value.authoredAt) && isString(value.committedAt);
|
|
718
|
-
}
|
|
719
|
-
function isCommitDiff(value) {
|
|
720
|
-
return isRecord(value) && isDiffCommit(value.commit) && isDiffStats(value.stats) && isString(value.rawDiff) && isArrayOf(value.files, isDiffFile);
|
|
721
|
-
}
|
|
722
|
-
function isReviewScope(value) {
|
|
723
|
-
if (!isRecord(value) || !isOneOf(value.mode, REVIEW_SCOPE_MODES)) {
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
switch (value.mode) {
|
|
727
|
-
case "all":
|
|
728
|
-
return true;
|
|
729
|
-
case "single":
|
|
730
|
-
return isString(value.sha);
|
|
731
|
-
case "range":
|
|
732
|
-
return isString(value.fromSha) && isString(value.toSha);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
function isDiffFile(value) {
|
|
736
|
-
return isRecord(value) && isString(value.path) && isNullableString(value.oldPath) && isNumber(value.additions) && isNumber(value.deletions) && isBoolean(value.isBinary) && isBoolean(value.isDeleted) && isBoolean(value.isNew) && isBoolean(value.isRenamed) && isNullableString(value.language) && isArrayOf(value.hunks, isDiffHunk);
|
|
737
|
-
}
|
|
738
|
-
function isDiffHunk(value) {
|
|
739
|
-
return isRecord(value) && isNumber(value.oldStart) && isNumber(value.oldLines) && isNumber(value.newStart) && isNumber(value.newLines) && isString(value.header) && isArrayOf(value.lines, isDiffLine);
|
|
740
|
-
}
|
|
741
|
-
function isDiffLine(value) {
|
|
742
|
-
return isRecord(value) && isOneOf(value.type, DIFF_LINE_TYPES) && isNullableNumber(value.oldLine) && isNullableNumber(value.newLine) && isString(value.content);
|
|
743
|
-
}
|
|
744
|
-
function isComment(value) {
|
|
745
|
-
return isRecord(value) && isString(value.id) && isString(value.filePath) && isNumber(value.startLine) && isNumber(value.endLine) && isOneOf(value.side, SIDES) && isString(value.body) && isString(value.originalSnippet) && isString(value.createdAt);
|
|
746
|
-
}
|
|
747
|
-
function isResolvedComment(value) {
|
|
748
|
-
return isRecord(value) && isString(value.commentId) && value.status === "resolved" && isOptionalString(value.summary) && isString(value.resolvedAt);
|
|
749
|
-
}
|
|
750
|
-
function isResolutionCounts(value) {
|
|
751
|
-
return isRecord(value) && isNumber(value.total) && isNumber(value.resolved) && isNumber(value.open);
|
|
752
|
-
}
|
|
753
|
-
function isReviewStatus(value) {
|
|
754
|
-
return isOneOf(value, REVIEW_STATUSES);
|
|
755
|
-
}
|
|
756
|
-
function isResolutionStatus(value) {
|
|
757
|
-
return isOneOf(value, RESOLUTION_STATUSES);
|
|
758
|
-
}
|
|
759
|
-
function isReviewUpdateReason(value) {
|
|
760
|
-
return isOneOf(value, REVIEW_UPDATE_REASONS);
|
|
761
|
-
}
|
|
762
|
-
function isRecord(value) {
|
|
763
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
764
|
-
}
|
|
765
|
-
function isArrayOf(value, guard) {
|
|
766
|
-
return Array.isArray(value) && value.every(guard);
|
|
767
|
-
}
|
|
768
|
-
function isOptional(value, guard) {
|
|
769
|
-
return value === void 0 || guard(value);
|
|
770
|
-
}
|
|
771
|
-
function isString(value) {
|
|
772
|
-
return typeof value === "string";
|
|
773
|
-
}
|
|
774
|
-
function isOptionalString(value) {
|
|
775
|
-
return value === void 0 || isString(value);
|
|
776
|
-
}
|
|
777
|
-
function isNullableString(value) {
|
|
778
|
-
return value === null || isString(value);
|
|
779
|
-
}
|
|
780
|
-
function isNumber(value) {
|
|
781
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
782
|
-
}
|
|
783
|
-
function isOptionalNumber(value) {
|
|
784
|
-
return value === void 0 || isNumber(value);
|
|
785
|
-
}
|
|
786
|
-
function isNullableNumber(value) {
|
|
787
|
-
return value === null || isNumber(value);
|
|
788
|
-
}
|
|
789
|
-
function isBoolean(value) {
|
|
790
|
-
return typeof value === "boolean";
|
|
791
|
-
}
|
|
792
|
-
function isOneOf(value, options) {
|
|
793
|
-
return typeof value === "string" && options.includes(value);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
1021
|
// src/shared/server-info.ts
|
|
797
1022
|
async function readServerInfo() {
|
|
798
1023
|
let raw;
|
|
799
1024
|
try {
|
|
800
|
-
raw = await
|
|
1025
|
+
raw = await readFile2(globalServerFile(), "utf8");
|
|
801
1026
|
} catch (error) {
|
|
802
1027
|
if (isFileNotFound(error)) {
|
|
803
1028
|
return null;
|
|
@@ -845,6 +1070,14 @@ var ServerClient = class {
|
|
|
845
1070
|
async listReviews() {
|
|
846
1071
|
return this.get("/api/reviews", isListReviewsResponse, "review list response");
|
|
847
1072
|
}
|
|
1073
|
+
async clearReviews(request) {
|
|
1074
|
+
return this.post(
|
|
1075
|
+
"/api/maintenance/clear-reviews",
|
|
1076
|
+
request,
|
|
1077
|
+
isClearReviewsResult,
|
|
1078
|
+
"clear reviews response"
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
848
1081
|
async getFeedback(reviewId) {
|
|
849
1082
|
return this.get(`/api/reviews/${reviewId}/feedback`, isFeedbackBundle, "feedback response");
|
|
850
1083
|
}
|
|
@@ -1065,7 +1298,7 @@ async function stopServer(options = {}) {
|
|
|
1065
1298
|
stoppedPids.push(pid);
|
|
1066
1299
|
}
|
|
1067
1300
|
}
|
|
1068
|
-
await
|
|
1301
|
+
await rm3(globalServerFile(), { force: true });
|
|
1069
1302
|
return { stopped: stoppedPids.length > 0, info: info2, stoppedPids };
|
|
1070
1303
|
}
|
|
1071
1304
|
const info = await readServerInfo();
|
|
@@ -1135,7 +1368,7 @@ async function waitForPidExit(pid, timeoutMs) {
|
|
|
1135
1368
|
async function removeServerInfoForPid(pid) {
|
|
1136
1369
|
const current = await readServerInfo().catch(() => null);
|
|
1137
1370
|
if (!current || current.pid === pid) {
|
|
1138
|
-
await
|
|
1371
|
+
await rm3(globalServerFile(), { force: true });
|
|
1139
1372
|
}
|
|
1140
1373
|
}
|
|
1141
1374
|
async function isGlossDaemonPid(pid) {
|
|
@@ -1175,7 +1408,7 @@ function isGlossDaemonCommand(command) {
|
|
|
1175
1408
|
|
|
1176
1409
|
// src/server/store.ts
|
|
1177
1410
|
import { createHash } from "crypto";
|
|
1178
|
-
import { readdir, readFile as
|
|
1411
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
1179
1412
|
import path4 from "path";
|
|
1180
1413
|
import { ulid } from "ulid";
|
|
1181
1414
|
|
|
@@ -1395,6 +1628,15 @@ var ReviewStore = class {
|
|
|
1395
1628
|
await this.loadAllReviews();
|
|
1396
1629
|
return Array.from(this.reviews.values()).map((record) => record.meta).toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1397
1630
|
}
|
|
1631
|
+
async clearReviewArtifacts(options = {}) {
|
|
1632
|
+
const result = await clearReviewArtifacts(options);
|
|
1633
|
+
if (!result.dryRun) {
|
|
1634
|
+
for (const review of result.deleted) {
|
|
1635
|
+
this.reviews.delete(review.reviewId);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
}
|
|
1398
1640
|
async get(id) {
|
|
1399
1641
|
return this.reviews.get(id) ?? await this.loadKnownReview(id);
|
|
1400
1642
|
}
|
|
@@ -1623,7 +1865,7 @@ var ReviewStore = class {
|
|
|
1623
1865
|
async loadAllReviews() {
|
|
1624
1866
|
let entries;
|
|
1625
1867
|
try {
|
|
1626
|
-
entries = await
|
|
1868
|
+
entries = await readdir2(globalReviewsDir(), { withFileTypes: true });
|
|
1627
1869
|
} catch (error) {
|
|
1628
1870
|
if (isFileNotFound(error)) {
|
|
1629
1871
|
return;
|
|
@@ -1647,7 +1889,7 @@ var ReviewStore = class {
|
|
|
1647
1889
|
const metaPath = globalReviewMetaFile(id);
|
|
1648
1890
|
let metaRaw;
|
|
1649
1891
|
try {
|
|
1650
|
-
metaRaw = await
|
|
1892
|
+
metaRaw = await readFile3(metaPath, "utf8");
|
|
1651
1893
|
} catch (error) {
|
|
1652
1894
|
if (isFileNotFound(error)) {
|
|
1653
1895
|
return this.loadReviewFromTurnsOnly(id);
|
|
@@ -1701,7 +1943,7 @@ var ReviewStore = class {
|
|
|
1701
1943
|
async loadPersistedTurns(id) {
|
|
1702
1944
|
let entries;
|
|
1703
1945
|
try {
|
|
1704
|
-
entries = await
|
|
1946
|
+
entries = await readdir2(globalReviewTurnsDir(id), { withFileTypes: true });
|
|
1705
1947
|
} catch (error) {
|
|
1706
1948
|
if (isFileNotFound(error)) {
|
|
1707
1949
|
return [];
|
|
@@ -1729,8 +1971,8 @@ var ReviewStore = class {
|
|
|
1729
1971
|
let diffRaw;
|
|
1730
1972
|
try {
|
|
1731
1973
|
[metaRaw, diffRaw] = await Promise.all([
|
|
1732
|
-
|
|
1733
|
-
|
|
1974
|
+
readFile3(metaPath, "utf8"),
|
|
1975
|
+
readFile3(diffPath, "utf8")
|
|
1734
1976
|
]);
|
|
1735
1977
|
} catch (error) {
|
|
1736
1978
|
if (isFileNotFound(error)) {
|
|
@@ -1760,7 +2002,7 @@ var ReviewStore = class {
|
|
|
1760
2002
|
const diffPath = globalReviewDiffFile(id);
|
|
1761
2003
|
let diffRaw;
|
|
1762
2004
|
try {
|
|
1763
|
-
diffRaw = await
|
|
2005
|
+
diffRaw = await readFile3(diffPath, "utf8");
|
|
1764
2006
|
} catch (error) {
|
|
1765
2007
|
if (isFileNotFound(error)) {
|
|
1766
2008
|
return null;
|
|
@@ -1975,7 +2217,7 @@ function requiredPath(value, label) {
|
|
|
1975
2217
|
async function readOptionalJsonFile(filePath, guard, label) {
|
|
1976
2218
|
let raw;
|
|
1977
2219
|
try {
|
|
1978
|
-
raw = await
|
|
2220
|
+
raw = await readFile3(filePath, "utf8");
|
|
1979
2221
|
} catch (error) {
|
|
1980
2222
|
if (isFileNotFound(error)) {
|
|
1981
2223
|
return void 0;
|
|
@@ -2140,6 +2382,19 @@ program.command("stop").description("Stop the managed background server").option
|
|
|
2140
2382
|
options.all && result.stoppedPids ? `Stopped ${result.stoppedPids.length} Gloss daemon(s)` : result.stopped ? "Gloss server stopped" : "Gloss server was not running"
|
|
2141
2383
|
);
|
|
2142
2384
|
});
|
|
2385
|
+
program.command("clear").description("Delete old completed review artifacts").option(
|
|
2386
|
+
"--older-than <days>",
|
|
2387
|
+
"delete completed reviews older than this many days",
|
|
2388
|
+
parseOlderThanDays,
|
|
2389
|
+
DEFAULT_REVIEW_RETENTION_DAYS
|
|
2390
|
+
).option("--dry-run", "print cleanup candidates without deleting them").action(async (options) => {
|
|
2391
|
+
const globals = program.opts();
|
|
2392
|
+
const result = await clearReviews({
|
|
2393
|
+
olderThanDays: options.olderThan,
|
|
2394
|
+
dryRun: options.dryRun === true
|
|
2395
|
+
});
|
|
2396
|
+
globals.json ? printJson(result) : printPlain(formatClearResult(result));
|
|
2397
|
+
});
|
|
2143
2398
|
program.command("resolve").argument("<reviewId>", "review id").description("Mark a submitted review or one feedback comment as resolved").option("--comment <commentId>", "resolve one submitted feedback comment").option("--summary <text>", "brief summary of the fixes applied").option("--turn <idOrIndex>", "resolve a specific turn for whole-review resolution").action(
|
|
2144
2399
|
async (reviewId, options) => {
|
|
2145
2400
|
const globals = program.opts();
|
|
@@ -2245,6 +2500,26 @@ async function baseForExistingReview(client, reviewId) {
|
|
|
2245
2500
|
const record = await client.getReview(reviewId);
|
|
2246
2501
|
return record.diff.scope.mode === "explicit" ? record.diff.scope.requestedBase ?? record.diff.base.ref : null;
|
|
2247
2502
|
}
|
|
2503
|
+
async function clearReviews(options) {
|
|
2504
|
+
const info = await readServerInfo();
|
|
2505
|
+
if (info && await isServerResponsive(info)) {
|
|
2506
|
+
return new ServerClient(serverUrl(info)).clearReviews(options);
|
|
2507
|
+
}
|
|
2508
|
+
return clearReviewArtifacts(options);
|
|
2509
|
+
}
|
|
2510
|
+
function parseOlderThanDays(value) {
|
|
2511
|
+
const days = Number(value);
|
|
2512
|
+
if (!Number.isInteger(days) || days < 0) {
|
|
2513
|
+
throw new Error("--older-than must be a non-negative integer");
|
|
2514
|
+
}
|
|
2515
|
+
return days;
|
|
2516
|
+
}
|
|
2517
|
+
function formatClearResult(result) {
|
|
2518
|
+
const action = result.dryRun ? "Would delete" : "Deleted";
|
|
2519
|
+
const count = result.dryRun ? result.counts.candidates : result.counts.deleted;
|
|
2520
|
+
const skipped = result.counts.skipped > 0 ? `; skipped ${result.counts.skipped} invalid review artifact(s)` : "";
|
|
2521
|
+
return `${action} ${count} review artifact(s) older than ${result.olderThanDays} day(s) from ${result.reviewsDir}${skipped}`;
|
|
2522
|
+
}
|
|
2248
2523
|
function isWatchTimeout(error) {
|
|
2249
2524
|
return error instanceof Error && /^watch timed out after/.test(error.message);
|
|
2250
2525
|
}
|