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 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 as execSync2 } from "child_process";
412
- import { readFileSync as readFileSync2 } from "fs";
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 execSync2(`git ${args}`, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
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() || "unknown";
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 = readFileSync2(resolve(repoRoot, filePath));
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: match[2] != null ? parseInt(match[2], 10) : 1,
419
+ oldCount: groups[2] !== void 0 ? parseInt(groups[2], 10) : 1,
571
420
  newStart: parseInt(match[3], 10),
572
- newCount: match[4] != null ? parseInt(match[4], 10) : 1
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 execSync2(`cat "${resolve(repoRoot, filePath)}"`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
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 execSync2("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
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
- default:
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 (!lang) return [];
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.symbol.endLine = lineNum;
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.symbol.endLine = lineIdx;
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") || c.get("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", async (c) => {
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", async (c) => {
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") || "1", 10);
1137
- const endLine = parseInt(c.req.query("end") || "20", 10);
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 false;
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 FileList({ files, annotationCounts, staleCounts }) {
1316
- const tree = buildFileTree(files);
1317
- 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 }) }) });
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[key]) annotationsByLine[key] = [];
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
- let hasLong = false;
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", () => resolve2(port));
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 { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
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 (!last) return true;
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", () => resolve2(null));
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 (!latest || compareVersions(current, latest) >= 0) return;
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-06T01:38:17.060Z"}`);
2151
+ console.log(`Build timestamp: ${"2026-03-06T05:23:12.429Z"}`);
2145
2152
  }
2146
2153
  await checkForUpdates(forceUpdateCheck);
2147
2154
  const cwd = process.cwd();