glassbox 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +433 -426
- package/dist/client/app.global.js +8 -8
- package/package.json +9 -2
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/server.ts
|
|
4
|
-
import { Hono as Hono3 } from "hono";
|
|
5
|
-
import { serve } from "@hono/node-server";
|
|
6
|
-
import { exec } from "child_process";
|
|
7
|
-
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
8
|
-
import { join as join3, dirname } from "path";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
|
|
11
|
-
// src/routes/api.ts
|
|
12
|
-
import { Hono } from "hono";
|
|
13
|
-
|
|
14
3
|
// src/db/connection.ts
|
|
15
4
|
import { PGlite } from "@electric-sql/pglite";
|
|
16
|
-
import { join } from "path";
|
|
17
|
-
import { homedir } from "os";
|
|
18
5
|
import { mkdirSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
19
8
|
var dataDir = join(homedir(), ".glassbox", "data");
|
|
20
9
|
mkdirSync(dataDir, { recursive: true });
|
|
21
10
|
var db = null;
|
|
@@ -101,7 +90,7 @@ async function getReview(id) {
|
|
|
101
90
|
}
|
|
102
91
|
async function listReviews(repoPath) {
|
|
103
92
|
const db2 = await getDb();
|
|
104
|
-
if (repoPath) {
|
|
93
|
+
if (repoPath !== void 0 && repoPath !== "") {
|
|
105
94
|
const result2 = await db2.query(
|
|
106
95
|
"SELECT * FROM reviews WHERE repo_path = $1 ORDER BY created_at DESC",
|
|
107
96
|
[repoPath]
|
|
@@ -266,157 +255,16 @@ async function getStaleCountsForReview(reviewId) {
|
|
|
266
255
|
return counts;
|
|
267
256
|
}
|
|
268
257
|
|
|
269
|
-
// src/export/generate.ts
|
|
270
|
-
import { mkdirSync as mkdirSync2, writeFileSync, readFileSync, unlinkSync, existsSync, appendFileSync } from "fs";
|
|
271
|
-
import { join as join2 } from "path";
|
|
272
|
-
import { homedir as homedir2 } from "os";
|
|
273
|
-
import { execSync } from "child_process";
|
|
274
|
-
var DISMISS_FILE = join2(homedir2(), ".glassbox", "gitignore-dismissed.json");
|
|
275
|
-
var DISMISS_DAYS = 30;
|
|
276
|
-
function loadDismissals() {
|
|
277
|
-
try {
|
|
278
|
-
return JSON.parse(readFileSync(DISMISS_FILE, "utf-8"));
|
|
279
|
-
} catch {
|
|
280
|
-
return {};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function saveDismissals(data) {
|
|
284
|
-
const dir = join2(homedir2(), ".glassbox");
|
|
285
|
-
mkdirSync2(dir, { recursive: true });
|
|
286
|
-
writeFileSync(DISMISS_FILE, JSON.stringify(data), "utf-8");
|
|
287
|
-
}
|
|
288
|
-
function isGlassboxGitignored(repoRoot) {
|
|
289
|
-
try {
|
|
290
|
-
execSync("git check-ignore -q .glassbox", { cwd: repoRoot, stdio: "pipe" });
|
|
291
|
-
return true;
|
|
292
|
-
} catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
function shouldPromptGitignore(repoRoot) {
|
|
297
|
-
if (isGlassboxGitignored(repoRoot)) return false;
|
|
298
|
-
const dismissals = loadDismissals();
|
|
299
|
-
const dismissed = dismissals[repoRoot];
|
|
300
|
-
if (dismissed) {
|
|
301
|
-
const daysSince = (Date.now() - dismissed) / (1e3 * 60 * 60 * 24);
|
|
302
|
-
if (daysSince < DISMISS_DAYS) return false;
|
|
303
|
-
}
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
function addGlassboxToGitignore(repoRoot) {
|
|
307
|
-
const gitignorePath = join2(repoRoot, ".gitignore");
|
|
308
|
-
if (existsSync(gitignorePath)) {
|
|
309
|
-
const content = readFileSync(gitignorePath, "utf-8");
|
|
310
|
-
if (!content.endsWith("\n")) {
|
|
311
|
-
appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
|
|
312
|
-
} else {
|
|
313
|
-
appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
writeFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
function dismissGitignorePrompt(repoRoot) {
|
|
320
|
-
const dismissals = loadDismissals();
|
|
321
|
-
dismissals[repoRoot] = Date.now();
|
|
322
|
-
saveDismissals(dismissals);
|
|
323
|
-
}
|
|
324
|
-
function deleteReviewExport(reviewId, repoRoot) {
|
|
325
|
-
const exportDir = join2(repoRoot, ".glassbox");
|
|
326
|
-
const archivePath = join2(exportDir, `review-${reviewId}.md`);
|
|
327
|
-
if (existsSync(archivePath)) unlinkSync(archivePath);
|
|
328
|
-
}
|
|
329
|
-
async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
330
|
-
const review = await getReview(reviewId);
|
|
331
|
-
if (!review) throw new Error("Review not found");
|
|
332
|
-
const files = await getReviewFiles(reviewId);
|
|
333
|
-
const annotations = await getAnnotationsForReview(reviewId);
|
|
334
|
-
const exportDir = join2(repoRoot, ".glassbox");
|
|
335
|
-
mkdirSync2(exportDir, { recursive: true });
|
|
336
|
-
const byFile = {};
|
|
337
|
-
for (const a of annotations) {
|
|
338
|
-
if (!byFile[a.file_path]) byFile[a.file_path] = [];
|
|
339
|
-
byFile[a.file_path].push(a);
|
|
340
|
-
}
|
|
341
|
-
const lines = [];
|
|
342
|
-
lines.push("# Code Review");
|
|
343
|
-
lines.push("");
|
|
344
|
-
lines.push(`- **Repository**: ${review.repo_name}`);
|
|
345
|
-
lines.push(`- **Review mode**: ${review.mode}${review.mode_args ? ` (${review.mode_args})` : ""}`);
|
|
346
|
-
lines.push(`- **Review ID**: ${review.id}`);
|
|
347
|
-
lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
348
|
-
lines.push(`- **Files reviewed**: ${files.filter((f) => f.status === "reviewed").length}/${files.length}`);
|
|
349
|
-
lines.push(`- **Total annotations**: ${annotations.length}`);
|
|
350
|
-
lines.push("");
|
|
351
|
-
const categoryCounts = {};
|
|
352
|
-
for (const a of annotations) {
|
|
353
|
-
categoryCounts[a.category] = (categoryCounts[a.category] || 0) + 1;
|
|
354
|
-
}
|
|
355
|
-
if (Object.keys(categoryCounts).length > 0) {
|
|
356
|
-
lines.push("## Annotation Summary");
|
|
357
|
-
lines.push("");
|
|
358
|
-
for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
|
|
359
|
-
lines.push(`- **${cat}**: ${count}`);
|
|
360
|
-
}
|
|
361
|
-
lines.push("");
|
|
362
|
-
}
|
|
363
|
-
const rememberItems = annotations.filter((a) => a.category === "remember");
|
|
364
|
-
if (rememberItems.length > 0) {
|
|
365
|
-
lines.push("## Items to Remember");
|
|
366
|
-
lines.push("");
|
|
367
|
-
lines.push("> These annotations are flagged for long-term retention. AI tools should consider updating");
|
|
368
|
-
lines.push("> project configuration (CLAUDE.md, .cursorrules, etc.) with these preferences/rules.");
|
|
369
|
-
lines.push("");
|
|
370
|
-
for (const item of rememberItems) {
|
|
371
|
-
lines.push(`- **${item.file_path}:${item.line_number}** - ${item.content}`);
|
|
372
|
-
}
|
|
373
|
-
lines.push("");
|
|
374
|
-
}
|
|
375
|
-
lines.push("## File Annotations");
|
|
376
|
-
lines.push("");
|
|
377
|
-
for (const filePath of Object.keys(byFile).sort()) {
|
|
378
|
-
const fileAnns = byFile[filePath];
|
|
379
|
-
lines.push(`### ${filePath}`);
|
|
380
|
-
lines.push("");
|
|
381
|
-
for (const a of fileAnns) {
|
|
382
|
-
lines.push(`- **Line ${a.line_number}** [${a.category}]: ${a.content}`);
|
|
383
|
-
}
|
|
384
|
-
lines.push("");
|
|
385
|
-
}
|
|
386
|
-
lines.push("---");
|
|
387
|
-
lines.push("");
|
|
388
|
-
lines.push("## Instructions for AI Tools");
|
|
389
|
-
lines.push("");
|
|
390
|
-
lines.push("When processing this code review:");
|
|
391
|
-
lines.push("");
|
|
392
|
-
lines.push("1. **bug** and **fix** annotations indicate code that needs to be changed. Apply the suggested fixes.");
|
|
393
|
-
lines.push("2. **style** annotations indicate stylistic preferences. Apply them to the indicated lines and similar patterns nearby.");
|
|
394
|
-
lines.push("3. **pattern-follow** annotations highlight good patterns. Continue using these patterns in new code.");
|
|
395
|
-
lines.push("4. **pattern-avoid** annotations highlight anti-patterns. Refactor the indicated code and avoid the pattern elsewhere.");
|
|
396
|
-
lines.push("5. **remember** annotations are rules/preferences to persist. Update the project's AI configuration file (e.g., CLAUDE.md) with these.");
|
|
397
|
-
lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
|
|
398
|
-
lines.push("");
|
|
399
|
-
const content = lines.join("\n");
|
|
400
|
-
const archivePath = join2(exportDir, `review-${review.id}.md`);
|
|
401
|
-
writeFileSync(archivePath, content, "utf-8");
|
|
402
|
-
if (isCurrent) {
|
|
403
|
-
const latestPath = join2(exportDir, "latest-review.md");
|
|
404
|
-
writeFileSync(latestPath, content, "utf-8");
|
|
405
|
-
return latestPath;
|
|
406
|
-
}
|
|
407
|
-
return archivePath;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
258
|
// src/git/diff.ts
|
|
411
|
-
import { execSync
|
|
412
|
-
import { readFileSync
|
|
259
|
+
import { execSync } from "child_process";
|
|
260
|
+
import { readFileSync } from "fs";
|
|
413
261
|
import { resolve } from "path";
|
|
414
262
|
function git(args, cwd) {
|
|
415
263
|
try {
|
|
416
|
-
return
|
|
264
|
+
return execSync(`git ${args}`, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
417
265
|
} catch (e) {
|
|
418
266
|
const err = e;
|
|
419
|
-
if (err.stdout) return err.stdout;
|
|
267
|
+
if (err.stdout !== void 0 && err.stdout !== "") return err.stdout;
|
|
420
268
|
throw e;
|
|
421
269
|
}
|
|
422
270
|
}
|
|
@@ -425,7 +273,7 @@ function getRepoRoot(cwd) {
|
|
|
425
273
|
}
|
|
426
274
|
function getRepoName(cwd) {
|
|
427
275
|
const root = getRepoRoot(cwd);
|
|
428
|
-
return root.split("/").pop()
|
|
276
|
+
return root.split("/").pop() ?? "unknown";
|
|
429
277
|
}
|
|
430
278
|
function isGitRepo(cwd) {
|
|
431
279
|
try {
|
|
@@ -488,7 +336,7 @@ function getAllFiles(repoRoot) {
|
|
|
488
336
|
function createNewFileDiff(filePath, repoRoot) {
|
|
489
337
|
let content;
|
|
490
338
|
try {
|
|
491
|
-
const buf =
|
|
339
|
+
const buf = readFileSync(resolve(repoRoot, filePath));
|
|
492
340
|
const checkLen = Math.min(buf.length, 8192);
|
|
493
341
|
for (let i = 0; i < checkLen; i++) {
|
|
494
342
|
if (buf[i] === 0) {
|
|
@@ -547,7 +395,7 @@ function parseDiff(raw2) {
|
|
|
547
395
|
let status = "modified";
|
|
548
396
|
if (header.includes("new file mode")) status = "added";
|
|
549
397
|
else if (header.includes("deleted file mode")) status = "deleted";
|
|
550
|
-
else if (oldPath) status = "renamed";
|
|
398
|
+
else if (oldPath !== null) status = "renamed";
|
|
551
399
|
const isBinary = header.includes("Binary file");
|
|
552
400
|
if (isBinary) {
|
|
553
401
|
files.push({ filePath, oldPath, status, hunks: [], isBinary: true });
|
|
@@ -564,12 +412,13 @@ function parseHunks(raw2) {
|
|
|
564
412
|
let match;
|
|
565
413
|
const hunkStarts = [];
|
|
566
414
|
while ((match = hunkRegex.exec(raw2)) !== null) {
|
|
415
|
+
const groups = match;
|
|
567
416
|
hunkStarts.push({
|
|
568
417
|
index: match.index + match[0].length,
|
|
569
418
|
oldStart: parseInt(match[1], 10),
|
|
570
|
-
oldCount:
|
|
419
|
+
oldCount: groups[2] !== void 0 ? parseInt(groups[2], 10) : 1,
|
|
571
420
|
newStart: parseInt(match[3], 10),
|
|
572
|
-
newCount:
|
|
421
|
+
newCount: groups[4] !== void 0 ? parseInt(groups[4], 10) : 1
|
|
573
422
|
});
|
|
574
423
|
}
|
|
575
424
|
for (let i = 0; i < hunkStarts.length; i++) {
|
|
@@ -608,7 +457,7 @@ function getFileContent(filePath, ref, cwd) {
|
|
|
608
457
|
const repoRoot = getRepoRoot(cwd);
|
|
609
458
|
try {
|
|
610
459
|
if (ref === "working") {
|
|
611
|
-
return
|
|
460
|
+
return execSync(`cat "${resolve(repoRoot, filePath)}"`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
612
461
|
}
|
|
613
462
|
return git(`show ${ref}:${filePath}`, repoRoot);
|
|
614
463
|
} catch {
|
|
@@ -616,7 +465,7 @@ function getFileContent(filePath, ref, cwd) {
|
|
|
616
465
|
}
|
|
617
466
|
}
|
|
618
467
|
function getHeadCommit(cwd) {
|
|
619
|
-
return
|
|
468
|
+
return execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
620
469
|
}
|
|
621
470
|
function getModeString(mode) {
|
|
622
471
|
switch (mode.type) {
|
|
@@ -648,11 +497,269 @@ function getModeArgs(mode) {
|
|
|
648
497
|
return mode.name;
|
|
649
498
|
case "files":
|
|
650
499
|
return mode.patterns.join(",");
|
|
651
|
-
|
|
500
|
+
case "uncommitted":
|
|
501
|
+
case "staged":
|
|
502
|
+
case "unstaged":
|
|
503
|
+
case "all":
|
|
652
504
|
return void 0;
|
|
653
505
|
}
|
|
654
506
|
}
|
|
655
507
|
|
|
508
|
+
// src/review-update.ts
|
|
509
|
+
function findLineContent(diff, lineNumber, side) {
|
|
510
|
+
for (const hunk of diff.hunks) {
|
|
511
|
+
for (const line of hunk.lines) {
|
|
512
|
+
if (side === "old" && line.oldNum === lineNumber) return line.content;
|
|
513
|
+
if (side === "new" && line.newNum === lineNumber) return line.content;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
function findMatchingLine(diff, content, origLineNum, side, radius = 10) {
|
|
519
|
+
let bestMatch = null;
|
|
520
|
+
let bestDistance = Infinity;
|
|
521
|
+
for (const hunk of diff.hunks) {
|
|
522
|
+
for (const line of hunk.lines) {
|
|
523
|
+
if (line.content !== content) continue;
|
|
524
|
+
const lineNum = side === "old" ? line.oldNum : line.newNum;
|
|
525
|
+
if (lineNum == null) continue;
|
|
526
|
+
const distance = Math.abs(lineNum - origLineNum);
|
|
527
|
+
if (distance <= radius && distance < bestDistance) {
|
|
528
|
+
bestDistance = distance;
|
|
529
|
+
bestMatch = { lineNumber: lineNum, side };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return bestMatch;
|
|
534
|
+
}
|
|
535
|
+
async function migrateAnnotations(annotations, oldDiff, newDiff) {
|
|
536
|
+
let staleCount = 0;
|
|
537
|
+
for (const annotation of annotations) {
|
|
538
|
+
const oldContent = findLineContent(oldDiff, annotation.line_number, annotation.side);
|
|
539
|
+
if (oldContent === null) {
|
|
540
|
+
if (!annotation.is_stale) {
|
|
541
|
+
await markAnnotationStale(annotation.id, null);
|
|
542
|
+
staleCount++;
|
|
543
|
+
}
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const match = findMatchingLine(newDiff, oldContent, annotation.line_number, annotation.side);
|
|
547
|
+
if (match) {
|
|
548
|
+
if (match.lineNumber !== annotation.line_number || match.side !== annotation.side) {
|
|
549
|
+
await moveAnnotation(annotation.id, match.lineNumber, match.side);
|
|
550
|
+
} else if (annotation.is_stale) {
|
|
551
|
+
await markAnnotationCurrent(annotation.id);
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
if (!annotation.is_stale) {
|
|
555
|
+
await markAnnotationStale(annotation.id, oldContent);
|
|
556
|
+
staleCount++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return staleCount;
|
|
561
|
+
}
|
|
562
|
+
async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
563
|
+
const existingFiles = await getReviewFiles(reviewId);
|
|
564
|
+
const existingByPath = /* @__PURE__ */ new Map();
|
|
565
|
+
for (const f of existingFiles) {
|
|
566
|
+
existingByPath.set(f.file_path, f);
|
|
567
|
+
}
|
|
568
|
+
const newDiffsByPath = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const d of newDiffs) {
|
|
570
|
+
newDiffsByPath.set(d.filePath, d);
|
|
571
|
+
}
|
|
572
|
+
let updated = 0;
|
|
573
|
+
let added = 0;
|
|
574
|
+
let stale = 0;
|
|
575
|
+
for (const [path, existingFile] of existingByPath) {
|
|
576
|
+
const newDiff = newDiffsByPath.get(path);
|
|
577
|
+
if (newDiff) {
|
|
578
|
+
const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
|
|
579
|
+
const annotations = await getAnnotationsForFile(existingFile.id);
|
|
580
|
+
if (annotations.length > 0) {
|
|
581
|
+
stale += await migrateAnnotations(annotations, oldDiff, newDiff);
|
|
582
|
+
}
|
|
583
|
+
await updateFileDiff(existingFile.id, JSON.stringify(newDiff));
|
|
584
|
+
updated++;
|
|
585
|
+
} else {
|
|
586
|
+
const annotations = await getAnnotationsForFile(existingFile.id);
|
|
587
|
+
if (annotations.length === 0) {
|
|
588
|
+
await deleteReviewFile(existingFile.id);
|
|
589
|
+
} else {
|
|
590
|
+
const oldDiff = JSON.parse(existingFile.diff_data ?? "{}");
|
|
591
|
+
for (const a of annotations) {
|
|
592
|
+
if (!a.is_stale) {
|
|
593
|
+
const content = findLineContent(oldDiff, a.line_number, a.side);
|
|
594
|
+
await markAnnotationStale(a.id, content);
|
|
595
|
+
stale++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const [path, diff] of newDiffsByPath) {
|
|
602
|
+
if (!existingByPath.has(path)) {
|
|
603
|
+
await addReviewFile(reviewId, path, JSON.stringify(diff));
|
|
604
|
+
added++;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
await updateReviewHead(reviewId, headCommit);
|
|
608
|
+
return { updated, added, stale };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/server.ts
|
|
612
|
+
import { serve } from "@hono/node-server";
|
|
613
|
+
import { exec } from "child_process";
|
|
614
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
615
|
+
import { Hono as Hono3 } from "hono";
|
|
616
|
+
import { dirname, join as join3 } from "path";
|
|
617
|
+
import { fileURLToPath } from "url";
|
|
618
|
+
|
|
619
|
+
// src/routes/api.ts
|
|
620
|
+
import { Hono } from "hono";
|
|
621
|
+
|
|
622
|
+
// src/export/generate.ts
|
|
623
|
+
import { execSync as execSync2 } from "child_process";
|
|
624
|
+
import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
625
|
+
import { homedir as homedir2 } from "os";
|
|
626
|
+
import { join as join2 } from "path";
|
|
627
|
+
var DISMISS_FILE = join2(homedir2(), ".glassbox", "gitignore-dismissed.json");
|
|
628
|
+
var DISMISS_DAYS = 30;
|
|
629
|
+
function loadDismissals() {
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(readFileSync2(DISMISS_FILE, "utf-8"));
|
|
632
|
+
} catch {
|
|
633
|
+
return {};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function saveDismissals(data) {
|
|
637
|
+
const dir = join2(homedir2(), ".glassbox");
|
|
638
|
+
mkdirSync2(dir, { recursive: true });
|
|
639
|
+
writeFileSync(DISMISS_FILE, JSON.stringify(data), "utf-8");
|
|
640
|
+
}
|
|
641
|
+
function isGlassboxGitignored(repoRoot) {
|
|
642
|
+
try {
|
|
643
|
+
execSync2("git check-ignore -q .glassbox", { cwd: repoRoot, stdio: "pipe" });
|
|
644
|
+
return true;
|
|
645
|
+
} catch {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function shouldPromptGitignore(repoRoot) {
|
|
650
|
+
if (isGlassboxGitignored(repoRoot)) return false;
|
|
651
|
+
const dismissals = loadDismissals();
|
|
652
|
+
const dismissed = dismissals[repoRoot];
|
|
653
|
+
if (dismissed) {
|
|
654
|
+
const daysSince = (Date.now() - dismissed) / (1e3 * 60 * 60 * 24);
|
|
655
|
+
if (daysSince < DISMISS_DAYS) return false;
|
|
656
|
+
}
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
function addGlassboxToGitignore(repoRoot) {
|
|
660
|
+
const gitignorePath = join2(repoRoot, ".gitignore");
|
|
661
|
+
if (existsSync(gitignorePath)) {
|
|
662
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
663
|
+
if (!content.endsWith("\n")) {
|
|
664
|
+
appendFileSync(gitignorePath, "\n.glassbox/\n", "utf-8");
|
|
665
|
+
} else {
|
|
666
|
+
appendFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
writeFileSync(gitignorePath, ".glassbox/\n", "utf-8");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function dismissGitignorePrompt(repoRoot) {
|
|
673
|
+
const dismissals = loadDismissals();
|
|
674
|
+
dismissals[repoRoot] = Date.now();
|
|
675
|
+
saveDismissals(dismissals);
|
|
676
|
+
}
|
|
677
|
+
function deleteReviewExport(reviewId, repoRoot) {
|
|
678
|
+
const exportDir = join2(repoRoot, ".glassbox");
|
|
679
|
+
const archivePath = join2(exportDir, `review-${reviewId}.md`);
|
|
680
|
+
if (existsSync(archivePath)) unlinkSync(archivePath);
|
|
681
|
+
}
|
|
682
|
+
async function generateReviewExport(reviewId, repoRoot, isCurrent) {
|
|
683
|
+
const review = await getReview(reviewId);
|
|
684
|
+
if (!review) throw new Error("Review not found");
|
|
685
|
+
const files = await getReviewFiles(reviewId);
|
|
686
|
+
const annotations = await getAnnotationsForReview(reviewId);
|
|
687
|
+
const exportDir = join2(repoRoot, ".glassbox");
|
|
688
|
+
mkdirSync2(exportDir, { recursive: true });
|
|
689
|
+
const byFile = {};
|
|
690
|
+
for (const a of annotations) {
|
|
691
|
+
if (!(a.file_path in byFile)) byFile[a.file_path] = [];
|
|
692
|
+
byFile[a.file_path].push(a);
|
|
693
|
+
}
|
|
694
|
+
const lines = [];
|
|
695
|
+
lines.push("# Code Review");
|
|
696
|
+
lines.push("");
|
|
697
|
+
lines.push(`- **Repository**: ${review.repo_name}`);
|
|
698
|
+
lines.push(`- **Review mode**: ${review.mode}${review.mode_args !== null && review.mode_args !== "" ? ` (${review.mode_args})` : ""}`);
|
|
699
|
+
lines.push(`- **Review ID**: ${review.id}`);
|
|
700
|
+
lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
701
|
+
lines.push(`- **Files reviewed**: ${files.filter((f) => f.status === "reviewed").length}/${files.length}`);
|
|
702
|
+
lines.push(`- **Total annotations**: ${annotations.length}`);
|
|
703
|
+
lines.push("");
|
|
704
|
+
const categoryCounts = {};
|
|
705
|
+
for (const a of annotations) {
|
|
706
|
+
categoryCounts[a.category] = (categoryCounts[a.category] || 0) + 1;
|
|
707
|
+
}
|
|
708
|
+
if (Object.keys(categoryCounts).length > 0) {
|
|
709
|
+
lines.push("## Annotation Summary");
|
|
710
|
+
lines.push("");
|
|
711
|
+
for (const [cat, count] of Object.entries(categoryCounts).sort((a, b) => b[1] - a[1])) {
|
|
712
|
+
lines.push(`- **${cat}**: ${count}`);
|
|
713
|
+
}
|
|
714
|
+
lines.push("");
|
|
715
|
+
}
|
|
716
|
+
const rememberItems = annotations.filter((a) => a.category === "remember");
|
|
717
|
+
if (rememberItems.length > 0) {
|
|
718
|
+
lines.push("## Items to Remember");
|
|
719
|
+
lines.push("");
|
|
720
|
+
lines.push("> These annotations are flagged for long-term retention. AI tools should consider updating");
|
|
721
|
+
lines.push("> project configuration (CLAUDE.md, .cursorrules, etc.) with these preferences/rules.");
|
|
722
|
+
lines.push("");
|
|
723
|
+
for (const item of rememberItems) {
|
|
724
|
+
lines.push(`- **${item.file_path}:${item.line_number}** - ${item.content}`);
|
|
725
|
+
}
|
|
726
|
+
lines.push("");
|
|
727
|
+
}
|
|
728
|
+
lines.push("## File Annotations");
|
|
729
|
+
lines.push("");
|
|
730
|
+
for (const filePath of Object.keys(byFile).sort()) {
|
|
731
|
+
const fileAnns = byFile[filePath];
|
|
732
|
+
lines.push(`### ${filePath}`);
|
|
733
|
+
lines.push("");
|
|
734
|
+
for (const a of fileAnns) {
|
|
735
|
+
lines.push(`- **Line ${a.line_number}** [${a.category}]: ${a.content}`);
|
|
736
|
+
}
|
|
737
|
+
lines.push("");
|
|
738
|
+
}
|
|
739
|
+
lines.push("---");
|
|
740
|
+
lines.push("");
|
|
741
|
+
lines.push("## Instructions for AI Tools");
|
|
742
|
+
lines.push("");
|
|
743
|
+
lines.push("When processing this code review:");
|
|
744
|
+
lines.push("");
|
|
745
|
+
lines.push("1. **bug** and **fix** annotations indicate code that needs to be changed. Apply the suggested fixes.");
|
|
746
|
+
lines.push("2. **style** annotations indicate stylistic preferences. Apply them to the indicated lines and similar patterns nearby.");
|
|
747
|
+
lines.push("3. **pattern-follow** annotations highlight good patterns. Continue using these patterns in new code.");
|
|
748
|
+
lines.push("4. **pattern-avoid** annotations highlight anti-patterns. Refactor the indicated code and avoid the pattern elsewhere.");
|
|
749
|
+
lines.push("5. **remember** annotations are rules/preferences to persist. Update the project's AI configuration file (e.g., CLAUDE.md) with these.");
|
|
750
|
+
lines.push("6. **note** annotations are informational context. Consider them but they may not require code changes.");
|
|
751
|
+
lines.push("");
|
|
752
|
+
const content = lines.join("\n");
|
|
753
|
+
const archivePath = join2(exportDir, `review-${review.id}.md`);
|
|
754
|
+
writeFileSync(archivePath, content, "utf-8");
|
|
755
|
+
if (isCurrent) {
|
|
756
|
+
const latestPath = join2(exportDir, "latest-review.md");
|
|
757
|
+
writeFileSync(latestPath, content, "utf-8");
|
|
758
|
+
return latestPath;
|
|
759
|
+
}
|
|
760
|
+
return archivePath;
|
|
761
|
+
}
|
|
762
|
+
|
|
656
763
|
// src/outline/parser.ts
|
|
657
764
|
var BRACE_LANGS = /* @__PURE__ */ new Set([
|
|
658
765
|
"javascript",
|
|
@@ -715,7 +822,7 @@ function langFromPath(filePath) {
|
|
|
715
822
|
}
|
|
716
823
|
function parseOutline(content, filePath) {
|
|
717
824
|
const lang = langFromPath(filePath);
|
|
718
|
-
if (
|
|
825
|
+
if (lang === null) return [];
|
|
719
826
|
if (BRACE_LANGS.has(lang)) return parseBraces(content, lang);
|
|
720
827
|
if (INDENT_LANGS.has(lang)) return parseIndent(content, lang);
|
|
721
828
|
return [];
|
|
@@ -809,7 +916,6 @@ function parseBraces(content, lang) {
|
|
|
809
916
|
let braceDepth = 0;
|
|
810
917
|
let inString = false;
|
|
811
918
|
let stringChar = "";
|
|
812
|
-
let inLineComment = false;
|
|
813
919
|
let inBlockComment = false;
|
|
814
920
|
let inTemplateLiteral = false;
|
|
815
921
|
const { top: topFuncPatterns, method: methodFuncPatterns } = getFuncPatterns(lang);
|
|
@@ -848,7 +954,6 @@ function parseBraces(content, lang) {
|
|
|
848
954
|
for (let j = 0; j < line.length; j++) {
|
|
849
955
|
const ch = line[j];
|
|
850
956
|
const next = line[j + 1];
|
|
851
|
-
if (inLineComment) break;
|
|
852
957
|
if (inBlockComment) {
|
|
853
958
|
if (ch === "*" && next === "/") {
|
|
854
959
|
inBlockComment = false;
|
|
@@ -877,7 +982,6 @@ function parseBraces(content, lang) {
|
|
|
877
982
|
continue;
|
|
878
983
|
}
|
|
879
984
|
if (ch === "/" && next === "/") {
|
|
880
|
-
inLineComment = true;
|
|
881
985
|
break;
|
|
882
986
|
}
|
|
883
987
|
if (ch === "/" && next === "*") {
|
|
@@ -899,11 +1003,12 @@ function parseBraces(content, lang) {
|
|
|
899
1003
|
braceDepth--;
|
|
900
1004
|
while (stack.length > 0 && stack[stack.length - 1].depth >= braceDepth) {
|
|
901
1005
|
const closed = stack.pop();
|
|
902
|
-
closed
|
|
1006
|
+
if (closed !== void 0) {
|
|
1007
|
+
closed.symbol.endLine = lineNum;
|
|
1008
|
+
}
|
|
903
1009
|
}
|
|
904
1010
|
}
|
|
905
1011
|
}
|
|
906
|
-
inLineComment = false;
|
|
907
1012
|
}
|
|
908
1013
|
const lastLine = lines.length;
|
|
909
1014
|
for (const item of stack) {
|
|
@@ -950,7 +1055,9 @@ function parseIndent(content, lang) {
|
|
|
950
1055
|
function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
|
|
951
1056
|
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
952
1057
|
const closed = stack.pop();
|
|
953
|
-
closed
|
|
1058
|
+
if (closed !== void 0) {
|
|
1059
|
+
closed.symbol.endLine = lineIdx;
|
|
1060
|
+
}
|
|
954
1061
|
}
|
|
955
1062
|
let endLine = lines.length;
|
|
956
1063
|
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
@@ -974,7 +1081,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
|
|
|
974
1081
|
// src/routes/api.ts
|
|
975
1082
|
var apiRoutes = new Hono();
|
|
976
1083
|
function resolveReviewId(c) {
|
|
977
|
-
return c.req.query("reviewId")
|
|
1084
|
+
return c.req.query("reviewId") ?? c.get("reviewId");
|
|
978
1085
|
}
|
|
979
1086
|
apiRoutes.get("/reviews", async (c) => {
|
|
980
1087
|
const repoRoot = c.get("repoRoot");
|
|
@@ -996,12 +1103,12 @@ apiRoutes.post("/review/complete", async (c) => {
|
|
|
996
1103
|
const gitignorePrompt = shouldPromptGitignore(repoRoot);
|
|
997
1104
|
return c.json({ status: "completed", exportPath, isCurrent, reviewId, gitignorePrompt });
|
|
998
1105
|
});
|
|
999
|
-
apiRoutes.post("/gitignore/add",
|
|
1106
|
+
apiRoutes.post("/gitignore/add", (c) => {
|
|
1000
1107
|
const repoRoot = c.get("repoRoot");
|
|
1001
1108
|
addGlassboxToGitignore(repoRoot);
|
|
1002
1109
|
return c.json({ ok: true });
|
|
1003
1110
|
});
|
|
1004
|
-
apiRoutes.post("/gitignore/dismiss",
|
|
1111
|
+
apiRoutes.post("/gitignore/dismiss", (c) => {
|
|
1005
1112
|
const repoRoot = c.get("repoRoot");
|
|
1006
1113
|
dismissGitignorePrompt(repoRoot);
|
|
1007
1114
|
return c.json({ ok: true });
|
|
@@ -1114,7 +1221,7 @@ apiRoutes.get("/outline/:fileId", async (c) => {
|
|
|
1114
1221
|
const repoRoot = c.get("repoRoot");
|
|
1115
1222
|
const file = await getReviewFile(c.req.param("fileId"));
|
|
1116
1223
|
if (!file) return c.json({ error: "Not found" }, 404);
|
|
1117
|
-
const diff = JSON.parse(file.diff_data
|
|
1224
|
+
const diff = JSON.parse(file.diff_data ?? "{}");
|
|
1118
1225
|
const isDeleted = diff.status === "deleted";
|
|
1119
1226
|
let content = "";
|
|
1120
1227
|
try {
|
|
@@ -1133,8 +1240,8 @@ apiRoutes.get("/context/:fileId", async (c) => {
|
|
|
1133
1240
|
const repoRoot = c.get("repoRoot");
|
|
1134
1241
|
const file = await getReviewFile(c.req.param("fileId"));
|
|
1135
1242
|
if (!file) return c.json({ error: "Not found" }, 404);
|
|
1136
|
-
const startLine = parseInt(c.req.query("start")
|
|
1137
|
-
const endLine = parseInt(c.req.query("end")
|
|
1243
|
+
const startLine = parseInt(c.req.query("start") ?? "1", 10);
|
|
1244
|
+
const endLine = parseInt(c.req.query("end") ?? "20", 10);
|
|
1138
1245
|
const content = getFileContent(file.file_path, "working", repoRoot);
|
|
1139
1246
|
const allLines = content.split("\n");
|
|
1140
1247
|
const clampedStart = Math.max(1, startLine);
|
|
@@ -1195,126 +1302,27 @@ function renderChildren(children) {
|
|
|
1195
1302
|
}
|
|
1196
1303
|
function renderAttr(key, value) {
|
|
1197
1304
|
if (value == null || value === false) return "";
|
|
1198
|
-
if (value === true) return ` ${key}`;
|
|
1199
|
-
const name = key === "className" ? "class" : key === "htmlFor" ? "for" : key;
|
|
1200
|
-
let strValue;
|
|
1201
|
-
if (value instanceof SafeHtml) {
|
|
1202
|
-
strValue = value.__html;
|
|
1203
|
-
} else if (typeof value === "number") {
|
|
1204
|
-
strValue = String(value);
|
|
1205
|
-
} else if (typeof value === "string") {
|
|
1206
|
-
strValue = escapeAttr(value);
|
|
1207
|
-
} else {
|
|
1208
|
-
strValue = "";
|
|
1209
|
-
}
|
|
1210
|
-
return ` ${name}="${strValue}"`;
|
|
1211
|
-
}
|
|
1212
|
-
function jsx(tag, props) {
|
|
1213
|
-
if (typeof tag === "function") return tag(props);
|
|
1214
|
-
const { children, ...attrs } = props;
|
|
1215
|
-
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
1216
|
-
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
1217
|
-
const childStr = children != null ? renderChildren(children) : "";
|
|
1218
|
-
return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// src/components/layout.tsx
|
|
1222
|
-
function Layout({ title, reviewId, children }) {
|
|
1223
|
-
return /* @__PURE__ */ jsx("html", { lang: "en", children: [
|
|
1224
|
-
/* @__PURE__ */ jsx("head", { children: [
|
|
1225
|
-
/* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
|
|
1226
|
-
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
1227
|
-
/* @__PURE__ */ jsx("title", { children: title }),
|
|
1228
|
-
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
|
|
1229
|
-
] }),
|
|
1230
|
-
/* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
|
|
1231
|
-
children,
|
|
1232
|
-
/* @__PURE__ */ jsx("script", { src: "/static/app.js" })
|
|
1233
|
-
] })
|
|
1234
|
-
] });
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// src/components/fileList.tsx
|
|
1238
|
-
function buildFileTree(files) {
|
|
1239
|
-
const root = { name: "", children: [], files: [] };
|
|
1240
|
-
for (const f of files) {
|
|
1241
|
-
const parts = f.file_path.split("/");
|
|
1242
|
-
let node = root;
|
|
1243
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1244
|
-
let child = node.children.find((c) => c.name === parts[i]);
|
|
1245
|
-
if (!child) {
|
|
1246
|
-
child = { name: parts[i], children: [], files: [] };
|
|
1247
|
-
node.children.push(child);
|
|
1248
|
-
}
|
|
1249
|
-
node = child;
|
|
1250
|
-
}
|
|
1251
|
-
node.files.push(f);
|
|
1252
|
-
}
|
|
1253
|
-
compressTree(root);
|
|
1254
|
-
return root;
|
|
1255
|
-
}
|
|
1256
|
-
function compressTree(node) {
|
|
1257
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
1258
|
-
let child = node.children[i];
|
|
1259
|
-
while (child.children.length === 1 && child.files.length === 0) {
|
|
1260
|
-
const grandchild = child.children[0];
|
|
1261
|
-
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
1262
|
-
node.children[i] = child;
|
|
1263
|
-
}
|
|
1264
|
-
compressTree(child);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
function countFiles(node) {
|
|
1268
|
-
let count = node.files.length;
|
|
1269
|
-
for (const child of node.children) count += countFiles(child);
|
|
1270
|
-
return count;
|
|
1271
|
-
}
|
|
1272
|
-
function hasStale(node, staleCounts) {
|
|
1273
|
-
for (const f of node.files) {
|
|
1274
|
-
if (staleCounts[f.id]) return true;
|
|
1275
|
-
}
|
|
1276
|
-
for (const child of node.children) {
|
|
1277
|
-
if (hasStale(child, staleCounts)) return true;
|
|
1305
|
+
if (value === true) return ` ${key}`;
|
|
1306
|
+
const name = key === "className" ? "class" : key === "htmlFor" ? "for" : key;
|
|
1307
|
+
let strValue;
|
|
1308
|
+
if (value instanceof SafeHtml) {
|
|
1309
|
+
strValue = value.__html;
|
|
1310
|
+
} else if (typeof value === "number") {
|
|
1311
|
+
strValue = String(value);
|
|
1312
|
+
} else if (typeof value === "string") {
|
|
1313
|
+
strValue = escapeAttr(value);
|
|
1314
|
+
} else {
|
|
1315
|
+
strValue = "";
|
|
1278
1316
|
}
|
|
1279
|
-
return
|
|
1280
|
-
}
|
|
1281
|
-
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
1282
|
-
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
1283
|
-
return /* @__PURE__ */ jsx("div", { children: [
|
|
1284
|
-
sortedChildren.map((child) => {
|
|
1285
|
-
const total = countFiles(child);
|
|
1286
|
-
const isCollapsible = total > 1;
|
|
1287
|
-
const stale = hasStale(child, staleCounts);
|
|
1288
|
-
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
1289
|
-
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1290
|
-
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
1291
|
-
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
1292
|
-
child.name,
|
|
1293
|
-
"/"
|
|
1294
|
-
] }),
|
|
1295
|
-
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
1296
|
-
] }),
|
|
1297
|
-
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
1298
|
-
] });
|
|
1299
|
-
}),
|
|
1300
|
-
node.files.map((f) => {
|
|
1301
|
-
const diff = JSON.parse(f.diff_data || "{}");
|
|
1302
|
-
const count = annotationCounts[f.id] || 0;
|
|
1303
|
-
const stale = staleCounts[f.id] || 0;
|
|
1304
|
-
const fileName = f.file_path.split("/").pop();
|
|
1305
|
-
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1306
|
-
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
1307
|
-
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
1308
|
-
/* @__PURE__ */ jsx("span", { className: `file-status ${diff.status || ""}`, children: diff.status || "" }),
|
|
1309
|
-
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
1310
|
-
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
1311
|
-
] });
|
|
1312
|
-
})
|
|
1313
|
-
] });
|
|
1317
|
+
return ` ${name}="${strValue}"`;
|
|
1314
1318
|
}
|
|
1315
|
-
function
|
|
1316
|
-
|
|
1317
|
-
|
|
1319
|
+
function jsx(tag, props) {
|
|
1320
|
+
if (typeof tag === "function") return tag(props);
|
|
1321
|
+
const { children, ...attrs } = props;
|
|
1322
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
1323
|
+
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
1324
|
+
const childStr = children != null ? renderChildren(children) : "";
|
|
1325
|
+
return new SafeHtml(`<${tag}${attrStr}>${childStr}</${tag}>`);
|
|
1318
1326
|
}
|
|
1319
1327
|
|
|
1320
1328
|
// src/components/diffView.tsx
|
|
@@ -1322,7 +1330,7 @@ function DiffView({ file, diff, annotations, mode }) {
|
|
|
1322
1330
|
const annotationsByLine = {};
|
|
1323
1331
|
for (const a of annotations) {
|
|
1324
1332
|
const key = `${a.line_number}:${a.side}`;
|
|
1325
|
-
if (!annotationsByLine
|
|
1333
|
+
if (!(key in annotationsByLine)) annotationsByLine[key] = [];
|
|
1326
1334
|
annotationsByLine[key].push(a);
|
|
1327
1335
|
}
|
|
1328
1336
|
return /* @__PURE__ */ jsx("div", { className: "diff-view", "data-file-id": file.id, "data-file-path": file.file_path, children: [
|
|
@@ -1360,8 +1368,8 @@ function SplitDiff({ hunks, annotationsByLine }) {
|
|
|
1360
1368
|
}
|
|
1361
1369
|
),
|
|
1362
1370
|
pairs.map((pair) => {
|
|
1363
|
-
const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`]
|
|
1364
|
-
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`]
|
|
1371
|
+
const leftAnns = pair.left ? annotationsByLine[`${pair.left.oldNum}:old`] ?? [] : [];
|
|
1372
|
+
const rightAnns = pair.right ? annotationsByLine[`${pair.right.newNum}:new`] ?? [] : [];
|
|
1365
1373
|
const allAnns = [...leftAnns, ...rightAnns];
|
|
1366
1374
|
return /* @__PURE__ */ jsx("div", { children: [
|
|
1367
1375
|
/* @__PURE__ */ jsx("div", { className: "split-row", children: [
|
|
@@ -1422,10 +1430,8 @@ function pairLines(lines) {
|
|
|
1422
1430
|
right: j < adds.length ? adds[j] : null
|
|
1423
1431
|
});
|
|
1424
1432
|
}
|
|
1425
|
-
} else if (line.type === "add") {
|
|
1426
|
-
pairs.push({ left: null, right: line });
|
|
1427
|
-
i++;
|
|
1428
1433
|
} else {
|
|
1434
|
+
pairs.push({ left: null, right: line });
|
|
1429
1435
|
i++;
|
|
1430
1436
|
}
|
|
1431
1437
|
}
|
|
@@ -1458,7 +1464,7 @@ function UnifiedDiff({ hunks, annotationsByLine }) {
|
|
|
1458
1464
|
hunk.lines.map((line) => {
|
|
1459
1465
|
const lineNum = line.type === "remove" ? line.oldNum : line.newNum;
|
|
1460
1466
|
const side = line.type === "remove" ? "old" : "new";
|
|
1461
|
-
const anns = annotationsByLine[`${lineNum}:${side}`]
|
|
1467
|
+
const anns = annotationsByLine[`${lineNum}:${side}`] ?? [];
|
|
1462
1468
|
return /* @__PURE__ */ jsx("div", { children: [
|
|
1463
1469
|
/* @__PURE__ */ jsx(
|
|
1464
1470
|
"div",
|
|
@@ -1499,6 +1505,105 @@ function AnnotationRows({ annotations }) {
|
|
|
1499
1505
|
)) });
|
|
1500
1506
|
}
|
|
1501
1507
|
|
|
1508
|
+
// src/components/fileList.tsx
|
|
1509
|
+
function buildFileTree(files) {
|
|
1510
|
+
const root = { name: "", children: [], files: [] };
|
|
1511
|
+
for (const f of files) {
|
|
1512
|
+
const parts = f.file_path.split("/");
|
|
1513
|
+
let node = root;
|
|
1514
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1515
|
+
let child = node.children.find((c) => c.name === parts[i]);
|
|
1516
|
+
if (!child) {
|
|
1517
|
+
child = { name: parts[i], children: [], files: [] };
|
|
1518
|
+
node.children.push(child);
|
|
1519
|
+
}
|
|
1520
|
+
node = child;
|
|
1521
|
+
}
|
|
1522
|
+
node.files.push(f);
|
|
1523
|
+
}
|
|
1524
|
+
compressTree(root);
|
|
1525
|
+
return root;
|
|
1526
|
+
}
|
|
1527
|
+
function compressTree(node) {
|
|
1528
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1529
|
+
let child = node.children[i];
|
|
1530
|
+
while (child.children.length === 1 && child.files.length === 0) {
|
|
1531
|
+
const grandchild = child.children[0];
|
|
1532
|
+
child = { name: child.name + "/" + grandchild.name, children: grandchild.children, files: grandchild.files };
|
|
1533
|
+
node.children[i] = child;
|
|
1534
|
+
}
|
|
1535
|
+
compressTree(child);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function countFiles(node) {
|
|
1539
|
+
let count = node.files.length;
|
|
1540
|
+
for (const child of node.children) count += countFiles(child);
|
|
1541
|
+
return count;
|
|
1542
|
+
}
|
|
1543
|
+
function hasStale(node, staleCounts) {
|
|
1544
|
+
for (const f of node.files) {
|
|
1545
|
+
if (staleCounts[f.id]) return true;
|
|
1546
|
+
}
|
|
1547
|
+
for (const child of node.children) {
|
|
1548
|
+
if (hasStale(child, staleCounts)) return true;
|
|
1549
|
+
}
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
function TreeView({ node, depth, annotationCounts, staleCounts }) {
|
|
1553
|
+
const sortedChildren = [...node.children].sort((a, b) => a.name.localeCompare(b.name));
|
|
1554
|
+
return /* @__PURE__ */ jsx("div", { children: [
|
|
1555
|
+
sortedChildren.map((child) => {
|
|
1556
|
+
const total = countFiles(child);
|
|
1557
|
+
const isCollapsible = total > 1;
|
|
1558
|
+
const stale = hasStale(child, staleCounts);
|
|
1559
|
+
return /* @__PURE__ */ jsx("div", { className: "folder-group", children: [
|
|
1560
|
+
/* @__PURE__ */ jsx("div", { className: `folder-header${isCollapsible ? " collapsible" : ""}`, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1561
|
+
isCollapsible ? /* @__PURE__ */ jsx("span", { className: "folder-arrow", children: "\u25BE" }) : /* @__PURE__ */ jsx("span", { className: "folder-arrow-spacer" }),
|
|
1562
|
+
/* @__PURE__ */ jsx("span", { className: "folder-name", children: [
|
|
1563
|
+
child.name,
|
|
1564
|
+
"/"
|
|
1565
|
+
] }),
|
|
1566
|
+
stale ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null
|
|
1567
|
+
] }),
|
|
1568
|
+
/* @__PURE__ */ jsx("div", { className: "folder-content", children: /* @__PURE__ */ jsx(TreeView, { node: child, depth: depth + 1, annotationCounts, staleCounts }) })
|
|
1569
|
+
] });
|
|
1570
|
+
}),
|
|
1571
|
+
node.files.map((f) => {
|
|
1572
|
+
const diff = JSON.parse(f.diff_data ?? "{}");
|
|
1573
|
+
const count = annotationCounts[f.id] || 0;
|
|
1574
|
+
const stale = staleCounts[f.id] || 0;
|
|
1575
|
+
const fileName = f.file_path.split("/").pop() ?? "";
|
|
1576
|
+
return /* @__PURE__ */ jsx("div", { className: "file-item", "data-file-id": f.id, style: `padding-left:${16 + depth * 12}px`, children: [
|
|
1577
|
+
/* @__PURE__ */ jsx("span", { className: `status-dot ${f.status}` }),
|
|
1578
|
+
/* @__PURE__ */ jsx("span", { className: "file-name", title: f.file_path, children: fileName }),
|
|
1579
|
+
/* @__PURE__ */ jsx("span", { className: `file-status ${diff.status ?? ""}`, children: diff.status ?? "" }),
|
|
1580
|
+
stale > 0 ? /* @__PURE__ */ jsx("span", { className: "stale-dot" }) : null,
|
|
1581
|
+
count > 0 ? /* @__PURE__ */ jsx("span", { className: "annotation-count", children: count }) : null
|
|
1582
|
+
] });
|
|
1583
|
+
})
|
|
1584
|
+
] });
|
|
1585
|
+
}
|
|
1586
|
+
function FileList({ files, annotationCounts, staleCounts }) {
|
|
1587
|
+
const tree = buildFileTree(files);
|
|
1588
|
+
return /* @__PURE__ */ jsx("div", { className: "file-list", children: /* @__PURE__ */ jsx("div", { className: "file-list-items", children: /* @__PURE__ */ jsx(TreeView, { node: tree, depth: 0, annotationCounts, staleCounts }) }) });
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/components/layout.tsx
|
|
1592
|
+
function Layout({ title, reviewId, children }) {
|
|
1593
|
+
return /* @__PURE__ */ jsx("html", { lang: "en", children: [
|
|
1594
|
+
/* @__PURE__ */ jsx("head", { children: [
|
|
1595
|
+
/* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
|
|
1596
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
1597
|
+
/* @__PURE__ */ jsx("title", { children: title }),
|
|
1598
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "/static/styles.css" })
|
|
1599
|
+
] }),
|
|
1600
|
+
/* @__PURE__ */ jsx("body", { "data-review-id": reviewId, children: [
|
|
1601
|
+
children,
|
|
1602
|
+
/* @__PURE__ */ jsx("script", { src: "/static/app.js" })
|
|
1603
|
+
] })
|
|
1604
|
+
] });
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1502
1607
|
// src/components/reviewHistory.tsx
|
|
1503
1608
|
function titleCase(s) {
|
|
1504
1609
|
return s.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -1506,15 +1611,15 @@ function titleCase(s) {
|
|
|
1506
1611
|
var trashIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
|
|
1507
1612
|
function shortenArgs(args) {
|
|
1508
1613
|
const shaPattern = /\b([0-9a-f]{7,40})\b/gi;
|
|
1509
|
-
|
|
1614
|
+
const result = { hasLong: false };
|
|
1510
1615
|
const short = args.replace(shaPattern, (match) => {
|
|
1511
1616
|
if (match.length > 8) {
|
|
1512
|
-
hasLong = true;
|
|
1617
|
+
result.hasLong = true;
|
|
1513
1618
|
return match.slice(0, 7);
|
|
1514
1619
|
}
|
|
1515
1620
|
return match;
|
|
1516
1621
|
});
|
|
1517
|
-
return { short, full: hasLong ? args : "" };
|
|
1622
|
+
return { short, full: result.hasLong ? args : "" };
|
|
1518
1623
|
}
|
|
1519
1624
|
function ReviewHistory({ reviews, currentReviewId }) {
|
|
1520
1625
|
const hasOtherReviews = reviews.some((r) => r.id !== currentReviewId);
|
|
@@ -1525,9 +1630,9 @@ function ReviewHistory({ reviews, currentReviewId }) {
|
|
|
1525
1630
|
const isCurrent = r.id === currentReviewId;
|
|
1526
1631
|
const href = isCurrent ? "/" : `/review/${r.id}`;
|
|
1527
1632
|
let argsDisplay = null;
|
|
1528
|
-
if (r.mode_args) {
|
|
1633
|
+
if (r.mode_args !== null && r.mode_args !== "") {
|
|
1529
1634
|
const { short, full } = shortenArgs(r.mode_args);
|
|
1530
|
-
argsDisplay = full ? /* @__PURE__ */ jsx("span", { title: full, children: [
|
|
1635
|
+
argsDisplay = full !== "" ? /* @__PURE__ */ jsx("span", { title: full, children: [
|
|
1531
1636
|
": ",
|
|
1532
1637
|
short
|
|
1533
1638
|
] }) : /* @__PURE__ */ jsx("span", { children: [
|
|
@@ -1660,7 +1765,7 @@ pageRoutes.get("/", async (c) => {
|
|
|
1660
1765
|
/* @__PURE__ */ jsx("h2", { children: review.repo_name }),
|
|
1661
1766
|
/* @__PURE__ */ jsx("span", { className: "review-mode", children: [
|
|
1662
1767
|
review.mode,
|
|
1663
|
-
review.mode_args ? `: ${review.mode_args}` : ""
|
|
1768
|
+
review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
|
|
1664
1769
|
] })
|
|
1665
1770
|
] }),
|
|
1666
1771
|
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
@@ -1701,7 +1806,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
|
|
|
1701
1806
|
const file = await getReviewFile(fileId);
|
|
1702
1807
|
if (!file) return c.text("File not found", 404);
|
|
1703
1808
|
const annotations = await getAnnotationsForFile(fileId);
|
|
1704
|
-
const diff = JSON.parse(file.diff_data
|
|
1809
|
+
const diff = JSON.parse(file.diff_data ?? "{}");
|
|
1705
1810
|
const html = /* @__PURE__ */ jsx(DiffView, { file, diff, annotations, mode });
|
|
1706
1811
|
return c.html(html.toString());
|
|
1707
1812
|
});
|
|
@@ -1725,7 +1830,7 @@ pageRoutes.get("/review/:reviewId", async (c) => {
|
|
|
1725
1830
|
/* @__PURE__ */ jsx("h2", { children: review.repo_name }),
|
|
1726
1831
|
/* @__PURE__ */ jsx("span", { className: "review-mode", children: [
|
|
1727
1832
|
review.mode,
|
|
1728
|
-
review.mode_args ? `: ${review.mode_args}` : ""
|
|
1833
|
+
review.mode_args !== null && review.mode_args !== "" ? `: ${review.mode_args}` : ""
|
|
1729
1834
|
] })
|
|
1730
1835
|
] }),
|
|
1731
1836
|
/* @__PURE__ */ jsx("div", { className: "file-filter", children: /* @__PURE__ */ jsx("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
|
|
@@ -1773,7 +1878,9 @@ pageRoutes.get("/history", async (c) => {
|
|
|
1773
1878
|
function tryServe(fetch, port) {
|
|
1774
1879
|
return new Promise((resolve2, reject) => {
|
|
1775
1880
|
const server = serve({ fetch, port });
|
|
1776
|
-
server.on("listening", () =>
|
|
1881
|
+
server.on("listening", () => {
|
|
1882
|
+
resolve2(port);
|
|
1883
|
+
});
|
|
1777
1884
|
server.on("error", (err) => {
|
|
1778
1885
|
if (err.code === "EADDRINUSE") {
|
|
1779
1886
|
reject(err);
|
|
@@ -1809,7 +1916,7 @@ async function startServer(port, reviewId, repoRoot) {
|
|
|
1809
1916
|
actualPort = await tryServe(app.fetch, port + attempt);
|
|
1810
1917
|
break;
|
|
1811
1918
|
} catch (err) {
|
|
1812
|
-
if (err.code === "EADDRINUSE" && attempt < 19) {
|
|
1919
|
+
if (err instanceof Error && err.code === "EADDRINUSE" && attempt < 19) {
|
|
1813
1920
|
continue;
|
|
1814
1921
|
}
|
|
1815
1922
|
throw err;
|
|
@@ -1827,10 +1934,10 @@ async function startServer(port, reviewId, repoRoot) {
|
|
|
1827
1934
|
}
|
|
1828
1935
|
|
|
1829
1936
|
// src/update-check.ts
|
|
1830
|
-
import {
|
|
1831
|
-
import { join as join4, dirname as dirname2 } from "path";
|
|
1832
|
-
import { homedir as homedir3 } from "os";
|
|
1937
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1833
1938
|
import { get } from "https";
|
|
1939
|
+
import { homedir as homedir3 } from "os";
|
|
1940
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1834
1941
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1835
1942
|
var DATA_DIR = join4(homedir3(), ".glassbox");
|
|
1836
1943
|
var CHECK_FILE = join4(DATA_DIR, "last-update-check");
|
|
@@ -1859,7 +1966,7 @@ function saveCheckDate() {
|
|
|
1859
1966
|
}
|
|
1860
1967
|
function isFirstUseToday() {
|
|
1861
1968
|
const last = getLastCheckDate();
|
|
1862
|
-
if (
|
|
1969
|
+
if (last === null) return true;
|
|
1863
1970
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1864
1971
|
return last !== today;
|
|
1865
1972
|
}
|
|
@@ -1872,7 +1979,7 @@ function fetchLatestVersion() {
|
|
|
1872
1979
|
}
|
|
1873
1980
|
let data = "";
|
|
1874
1981
|
res.on("data", (chunk) => {
|
|
1875
|
-
data += chunk;
|
|
1982
|
+
data += chunk.toString();
|
|
1876
1983
|
});
|
|
1877
1984
|
res.on("end", () => {
|
|
1878
1985
|
try {
|
|
@@ -1882,7 +1989,9 @@ function fetchLatestVersion() {
|
|
|
1882
1989
|
}
|
|
1883
1990
|
});
|
|
1884
1991
|
});
|
|
1885
|
-
req.on("error", () =>
|
|
1992
|
+
req.on("error", () => {
|
|
1993
|
+
resolve2(null);
|
|
1994
|
+
});
|
|
1886
1995
|
req.on("timeout", () => {
|
|
1887
1996
|
req.destroy();
|
|
1888
1997
|
resolve2(null);
|
|
@@ -1916,7 +2025,7 @@ async function checkForUpdates(force) {
|
|
|
1916
2025
|
const current = getCurrentVersion();
|
|
1917
2026
|
const latest = await fetchLatestVersion();
|
|
1918
2027
|
saveCheckDate();
|
|
1919
|
-
if (
|
|
2028
|
+
if (latest === null || compareVersions(current, latest) >= 0) return;
|
|
1920
2029
|
const cmd = detectUpgradeCommand();
|
|
1921
2030
|
const updateLine = `Update available: ${current} \u2192 ${latest}`;
|
|
1922
2031
|
const cmdLine = `Run: ${cmd}`;
|
|
@@ -1935,109 +2044,6 @@ async function checkForUpdates(force) {
|
|
|
1935
2044
|
console.log("");
|
|
1936
2045
|
}
|
|
1937
2046
|
|
|
1938
|
-
// src/review-update.ts
|
|
1939
|
-
function findLineContent(diff, lineNumber, side) {
|
|
1940
|
-
for (const hunk of diff.hunks || []) {
|
|
1941
|
-
for (const line of hunk.lines) {
|
|
1942
|
-
if (side === "old" && line.oldNum === lineNumber) return line.content;
|
|
1943
|
-
if (side === "new" && line.newNum === lineNumber) return line.content;
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
return null;
|
|
1947
|
-
}
|
|
1948
|
-
function findMatchingLine(diff, content, origLineNum, side, radius = 10) {
|
|
1949
|
-
let bestMatch = null;
|
|
1950
|
-
let bestDistance = Infinity;
|
|
1951
|
-
for (const hunk of diff.hunks || []) {
|
|
1952
|
-
for (const line of hunk.lines) {
|
|
1953
|
-
if (line.content !== content) continue;
|
|
1954
|
-
const lineNum = side === "old" ? line.oldNum : line.newNum;
|
|
1955
|
-
if (lineNum == null) continue;
|
|
1956
|
-
const distance = Math.abs(lineNum - origLineNum);
|
|
1957
|
-
if (distance <= radius && distance < bestDistance) {
|
|
1958
|
-
bestDistance = distance;
|
|
1959
|
-
bestMatch = { lineNumber: lineNum, side };
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
return bestMatch;
|
|
1964
|
-
}
|
|
1965
|
-
async function migrateAnnotations(annotations, oldDiff, newDiff) {
|
|
1966
|
-
let staleCount = 0;
|
|
1967
|
-
for (const annotation of annotations) {
|
|
1968
|
-
const oldContent = findLineContent(oldDiff, annotation.line_number, annotation.side);
|
|
1969
|
-
if (!oldContent) {
|
|
1970
|
-
if (!annotation.is_stale) {
|
|
1971
|
-
await markAnnotationStale(annotation.id, null);
|
|
1972
|
-
staleCount++;
|
|
1973
|
-
}
|
|
1974
|
-
continue;
|
|
1975
|
-
}
|
|
1976
|
-
const match = findMatchingLine(newDiff, oldContent, annotation.line_number, annotation.side);
|
|
1977
|
-
if (match) {
|
|
1978
|
-
if (match.lineNumber !== annotation.line_number || match.side !== annotation.side) {
|
|
1979
|
-
await moveAnnotation(annotation.id, match.lineNumber, match.side);
|
|
1980
|
-
} else if (annotation.is_stale) {
|
|
1981
|
-
await markAnnotationCurrent(annotation.id);
|
|
1982
|
-
}
|
|
1983
|
-
} else {
|
|
1984
|
-
if (!annotation.is_stale) {
|
|
1985
|
-
await markAnnotationStale(annotation.id, oldContent);
|
|
1986
|
-
staleCount++;
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
return staleCount;
|
|
1991
|
-
}
|
|
1992
|
-
async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
|
|
1993
|
-
const existingFiles = await getReviewFiles(reviewId);
|
|
1994
|
-
const existingByPath = /* @__PURE__ */ new Map();
|
|
1995
|
-
for (const f of existingFiles) {
|
|
1996
|
-
existingByPath.set(f.file_path, f);
|
|
1997
|
-
}
|
|
1998
|
-
const newDiffsByPath = /* @__PURE__ */ new Map();
|
|
1999
|
-
for (const d of newDiffs) {
|
|
2000
|
-
newDiffsByPath.set(d.filePath, d);
|
|
2001
|
-
}
|
|
2002
|
-
let updated = 0;
|
|
2003
|
-
let added = 0;
|
|
2004
|
-
let stale = 0;
|
|
2005
|
-
for (const [path, existingFile] of existingByPath) {
|
|
2006
|
-
const newDiff = newDiffsByPath.get(path);
|
|
2007
|
-
if (newDiff) {
|
|
2008
|
-
const oldDiff = JSON.parse(existingFile.diff_data || "{}");
|
|
2009
|
-
const annotations = await getAnnotationsForFile(existingFile.id);
|
|
2010
|
-
if (annotations.length > 0) {
|
|
2011
|
-
stale += await migrateAnnotations(annotations, oldDiff, newDiff);
|
|
2012
|
-
}
|
|
2013
|
-
await updateFileDiff(existingFile.id, JSON.stringify(newDiff));
|
|
2014
|
-
updated++;
|
|
2015
|
-
} else {
|
|
2016
|
-
const annotations = await getAnnotationsForFile(existingFile.id);
|
|
2017
|
-
if (annotations.length === 0) {
|
|
2018
|
-
await deleteReviewFile(existingFile.id);
|
|
2019
|
-
} else {
|
|
2020
|
-
const oldDiff = JSON.parse(existingFile.diff_data || "{}");
|
|
2021
|
-
for (const a of annotations) {
|
|
2022
|
-
if (!a.is_stale) {
|
|
2023
|
-
const content = findLineContent(oldDiff, a.line_number, a.side);
|
|
2024
|
-
await markAnnotationStale(a.id, content);
|
|
2025
|
-
stale++;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
for (const [path, diff] of newDiffsByPath) {
|
|
2032
|
-
if (!existingByPath.has(path)) {
|
|
2033
|
-
await addReviewFile(reviewId, path, JSON.stringify(diff));
|
|
2034
|
-
added++;
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
await updateReviewHead(reviewId, headCommit);
|
|
2038
|
-
return { updated, added, stale };
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
2047
|
// src/cli.ts
|
|
2042
2048
|
function printUsage() {
|
|
2043
2049
|
console.log(`
|
|
@@ -2084,6 +2090,7 @@ function parseArgs(argv) {
|
|
|
2084
2090
|
case "-h":
|
|
2085
2091
|
printUsage();
|
|
2086
2092
|
process.exit(0);
|
|
2093
|
+
// falls through
|
|
2087
2094
|
case "--uncommitted":
|
|
2088
2095
|
mode = { type: "uncommitted" };
|
|
2089
2096
|
break;
|
|
@@ -2141,7 +2148,7 @@ async function main() {
|
|
|
2141
2148
|
}
|
|
2142
2149
|
const { mode, port, resume, forceUpdateCheck, debug } = parsed;
|
|
2143
2150
|
if (debug) {
|
|
2144
|
-
console.log(`Build timestamp: ${"2026-03-
|
|
2151
|
+
console.log(`Build timestamp: ${"2026-03-06T05:23:12.429Z"}`);
|
|
2145
2152
|
}
|
|
2146
2153
|
await checkForUpdates(forceUpdateCheck);
|
|
2147
2154
|
const cwd = process.cwd();
|