glassbox 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -391,11 +391,11 @@ var init_queries = __esm({
391
391
  });
392
392
 
393
393
  // src/cli.ts
394
- init_queries();
395
394
  init_connection();
396
- import { mkdirSync as mkdirSync7 } from "fs";
395
+ init_queries();
396
+ import { mkdirSync as mkdirSync8 } from "fs";
397
397
  import { tmpdir } from "os";
398
- import { join as join10, resolve as resolve5 } from "path";
398
+ import { join as join11, resolve as resolve5 } from "path";
399
399
 
400
400
  // src/debug.ts
401
401
  var debugEnabled = false;
@@ -426,70 +426,15 @@ function debugLog(...args) {
426
426
  }
427
427
 
428
428
  // src/ai/config.ts
429
- import { spawnSync } from "child_process";
430
429
  import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
431
430
  import { homedir } from "os";
432
431
  import { join as join2 } from "path";
433
432
 
434
- // src/ai/models.ts
435
- var PLATFORMS = {
436
- anthropic: "Anthropic",
437
- openai: "OpenAI",
438
- google: "Google"
439
- };
440
- var MODELS = {
441
- anthropic: [
442
- { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", contextWindow: 2e5, isDefault: true },
443
- { id: "claude-haiku-4-20250514", name: "Claude Haiku 4", contextWindow: 2e5, isDefault: false }
444
- ],
445
- openai: [
446
- { id: "gpt-4o", name: "GPT-4o", contextWindow: 128e3, isDefault: true },
447
- { id: "gpt-4o-mini", name: "GPT-4o Mini", contextWindow: 128e3, isDefault: false }
448
- ],
449
- google: [
450
- { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
451
- { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
452
- ]
453
- };
454
- var ENV_KEY_NAMES = {
455
- anthropic: "ANTHROPIC_API_KEY",
456
- openai: "OPENAI_API_KEY",
457
- google: "GEMINI_API_KEY"
458
- };
459
- function getDefaultModel(platform) {
460
- const models = MODELS[platform];
461
- const def = models.find((m) => m.isDefault);
462
- return def ? def.id : models[0].id;
463
- }
464
- function getModelContextWindow(platform, modelId) {
465
- const model = MODELS[platform].find((m) => m.id === modelId);
466
- return model ? model.contextWindow : 128e3;
467
- }
433
+ // src/ai/api-keys.ts
434
+ import { spawnSync as spawnSync2 } from "child_process";
468
435
 
469
- // src/ai/config.ts
470
- var CONFIG_DIR = join2(homedir(), ".glassbox");
471
- var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
472
- function readConfigFile() {
473
- try {
474
- if (existsSync(CONFIG_PATH)) {
475
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
476
- }
477
- } catch {
478
- }
479
- return {};
480
- }
481
- function writeConfigFile(config) {
482
- mkdirSync2(CONFIG_DIR, { recursive: true });
483
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
484
- try {
485
- chmodSync(CONFIG_PATH, 384);
486
- } catch {
487
- }
488
- }
489
- function getKeyFromEnv(platform) {
490
- const envName = ENV_KEY_NAMES[platform];
491
- return process.env[envName] ?? null;
492
- }
436
+ // src/ai/keychain.ts
437
+ import { spawnSync } from "child_process";
493
438
  var WIN_CRED_READ_PS = `
494
439
  Add-Type -TypeDefinition @'
495
440
  using System;
@@ -526,19 +471,19 @@ function getKeyFromKeychain(platform) {
526
471
  try {
527
472
  if (os === "darwin") {
528
473
  const r = spawnSync("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
529
- const result = (r.stdout ?? "").trim();
474
+ const result = r.stdout.trim();
530
475
  return r.status === 0 && result !== "" ? result : null;
531
476
  }
532
477
  if (os === "linux") {
533
478
  const r = spawnSync("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
534
- const result = (r.stdout ?? "").trim();
479
+ const result = r.stdout.trim();
535
480
  return r.status === 0 && result !== "" ? result : null;
536
481
  }
537
482
  if (os === "win32") {
538
483
  const target = winCredTarget(platform);
539
484
  const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
540
485
  const r = spawnSync("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
541
- const result = (r.stdout ?? "").trim();
486
+ const result = r.stdout.trim();
542
487
  return r.status === 0 && result !== "" ? result : null;
543
488
  }
544
489
  } catch {
@@ -546,6 +491,81 @@ function getKeyFromKeychain(platform) {
546
491
  }
547
492
  return null;
548
493
  }
494
+ function saveKeyToKeychain(platform, key) {
495
+ const os = process.platform;
496
+ const account = `${platform}-api-key`;
497
+ if (os === "darwin") {
498
+ spawnSync("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
499
+ spawnSync("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", key]);
500
+ return;
501
+ }
502
+ if (os === "linux") {
503
+ spawnSync("secret-tool", ["store", "--label=Glassbox API Key", "service", "glassbox", "account", account], { input: key, encoding: "utf-8" });
504
+ return;
505
+ }
506
+ if (os === "win32") {
507
+ const target = winCredTarget(platform);
508
+ const escapedKey = key.replace(/'/g, "''");
509
+ const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedKey}'`;
510
+ spawnSync("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
511
+ }
512
+ }
513
+ function isKeychainAvailable() {
514
+ const os = process.platform;
515
+ if (os === "darwin" || os === "win32") return true;
516
+ if (os === "linux") {
517
+ return spawnSync("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
518
+ }
519
+ return false;
520
+ }
521
+ function getKeychainLabel() {
522
+ const os = process.platform;
523
+ if (os === "darwin") return "Keychain";
524
+ if (os === "linux") return "System Keyring";
525
+ if (os === "win32") return "Credential Manager";
526
+ return "System Keychain";
527
+ }
528
+
529
+ // src/ai/models.ts
530
+ var PLATFORMS = {
531
+ anthropic: "Anthropic",
532
+ openai: "OpenAI",
533
+ google: "Google"
534
+ };
535
+ var MODELS = {
536
+ anthropic: [
537
+ { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", contextWindow: 2e5, isDefault: true },
538
+ { id: "claude-haiku-4-20250514", name: "Claude Haiku 4", contextWindow: 2e5, isDefault: false }
539
+ ],
540
+ openai: [
541
+ { id: "gpt-4o", name: "GPT-4o", contextWindow: 128e3, isDefault: true },
542
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", contextWindow: 128e3, isDefault: false }
543
+ ],
544
+ google: [
545
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1e6, isDefault: true },
546
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1e6, isDefault: false }
547
+ ]
548
+ };
549
+ var ENV_KEY_NAMES = {
550
+ anthropic: "ANTHROPIC_API_KEY",
551
+ openai: "OPENAI_API_KEY",
552
+ google: "GEMINI_API_KEY"
553
+ };
554
+ function getDefaultModel(platform) {
555
+ const models = MODELS[platform];
556
+ const def = models.find((m) => m.isDefault);
557
+ return def ? def.id : models[0].id;
558
+ }
559
+ function getModelContextWindow(platform, modelId) {
560
+ const model = MODELS[platform].find((m) => m.id === modelId);
561
+ return model ? model.contextWindow : 128e3;
562
+ }
563
+
564
+ // src/ai/api-keys.ts
565
+ function getKeyFromEnv(platform) {
566
+ const envName = ENV_KEY_NAMES[platform];
567
+ return process.env[envName] ?? null;
568
+ }
549
569
  function getKeyFromConfig(platform) {
550
570
  const config = readConfigFile();
551
571
  const encoded = config.ai?.keys?.[platform];
@@ -565,20 +585,6 @@ function resolveAPIKey(platform) {
565
585
  if (configKey !== null) return { key: configKey, source: "config" };
566
586
  return { key: null, source: null };
567
587
  }
568
- function loadAIConfig() {
569
- const config = readConfigFile();
570
- const platform = config.ai?.platform ?? "anthropic";
571
- const model = config.ai?.model ?? getDefaultModel(platform);
572
- const { key, source } = resolveAPIKey(platform);
573
- return { platform, model, apiKey: key, keySource: source };
574
- }
575
- function saveAIConfigPreferences(platform, model) {
576
- const config = readConfigFile();
577
- if (config.ai === void 0) config.ai = {};
578
- config.ai.platform = platform;
579
- config.ai.model = model;
580
- writeConfigFile(config);
581
- }
582
588
  function saveAPIKey(platform, key, storage) {
583
589
  if (storage === "keychain") {
584
590
  saveKeyToKeychain(platform, key);
@@ -590,36 +596,17 @@ function saveAPIKey(platform, key, storage) {
590
596
  writeConfigFile(config);
591
597
  }
592
598
  }
593
- function saveKeyToKeychain(platform, key) {
594
- const os = process.platform;
595
- const account = `${platform}-api-key`;
596
- if (os === "darwin") {
597
- spawnSync("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
598
- spawnSync("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", key]);
599
- return;
600
- }
601
- if (os === "linux") {
602
- spawnSync("secret-tool", ["store", "--label=Glassbox API Key", "service", "glassbox", "account", account], { input: key, encoding: "utf-8" });
603
- return;
604
- }
605
- if (os === "win32") {
606
- const target = winCredTarget(platform);
607
- const escapedKey = key.replace(/'/g, "''");
608
- const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedKey}'`;
609
- spawnSync("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
610
- }
611
- }
612
599
  function deleteAPIKey(platform) {
613
600
  const os = process.platform;
614
601
  const account = `${platform}-api-key`;
615
602
  try {
616
603
  if (os === "darwin") {
617
- spawnSync("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
604
+ spawnSync2("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
618
605
  } else if (os === "linux") {
619
- spawnSync("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
606
+ spawnSync2("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
620
607
  } else if (os === "win32") {
621
608
  const target = winCredTarget(platform);
622
- spawnSync("powershell", ["-NoProfile", "-Command", "-"], { input: `cmdkey /delete:'${target}'`, encoding: "utf-8" });
609
+ spawnSync2("powershell", ["-NoProfile", "-Command", "-"], { input: `cmdkey /delete:'${target}'`, encoding: "utf-8" });
623
610
  }
624
611
  } catch {
625
612
  }
@@ -639,20 +626,40 @@ function detectAvailablePlatforms() {
639
626
  }
640
627
  return results;
641
628
  }
642
- function isKeychainAvailable() {
643
- const os = process.platform;
644
- if (os === "darwin" || os === "win32") return true;
645
- if (os === "linux") {
646
- return spawnSync("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
629
+
630
+ // src/ai/config.ts
631
+ var CONFIG_DIR = join2(homedir(), ".glassbox");
632
+ var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
633
+ function readConfigFile() {
634
+ try {
635
+ if (existsSync(CONFIG_PATH)) {
636
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
637
+ }
638
+ } catch {
647
639
  }
648
- return false;
640
+ return {};
649
641
  }
650
- function getKeychainLabel() {
651
- const os = process.platform;
652
- if (os === "darwin") return "Keychain";
653
- if (os === "linux") return "System Keyring";
654
- if (os === "win32") return "Credential Manager";
655
- return "System Keychain";
642
+ function writeConfigFile(config) {
643
+ mkdirSync2(CONFIG_DIR, { recursive: true });
644
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
645
+ try {
646
+ chmodSync(CONFIG_PATH, 384);
647
+ } catch {
648
+ }
649
+ }
650
+ function loadAIConfig() {
651
+ const config = readConfigFile();
652
+ const platform = config.ai?.platform ?? "anthropic";
653
+ const model = config.ai?.model ?? getDefaultModel(platform);
654
+ const { key, source } = resolveAPIKey(platform);
655
+ return { platform, model, apiKey: key, keySource: source };
656
+ }
657
+ function saveAIConfigPreferences(platform, model) {
658
+ const config = readConfigFile();
659
+ if (config.ai === void 0) config.ai = {};
660
+ config.ai.platform = platform;
661
+ config.ai.model = model;
662
+ writeConfigFile(config);
656
663
  }
657
664
  function loadGuidedReviewConfig() {
658
665
  const config = readConfigFile();
@@ -1266,11 +1273,14 @@ async function setupAnnotations(fileIdMap) {
1266
1273
  }
1267
1274
 
1268
1275
  // src/git/diff.ts
1269
- import { spawnSync as spawnSync2 } from "child_process";
1276
+ import { spawnSync as spawnSync4 } from "child_process";
1270
1277
  import { readFileSync as readFileSync2 } from "fs";
1271
1278
  import { resolve } from "path";
1279
+
1280
+ // src/git/repo.ts
1281
+ import { spawnSync as spawnSync3 } from "child_process";
1272
1282
  function git(args, cwd) {
1273
- const result = spawnSync2("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1283
+ const result = spawnSync3("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1274
1284
  if (result.status === 0) return result.stdout;
1275
1285
  if (result.stdout !== "") return result.stdout;
1276
1286
  const err = new Error(result.stderr);
@@ -1294,6 +1304,21 @@ function isGitRepo(cwd) {
1294
1304
  return false;
1295
1305
  }
1296
1306
  }
1307
+ function getHeadCommit(cwd) {
1308
+ return spawnSync3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).stdout.trim();
1309
+ }
1310
+
1311
+ // src/git/diff.ts
1312
+ function git2(args, cwd) {
1313
+ const result = spawnSync4("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1314
+ if (result.status === 0) return result.stdout;
1315
+ if (result.stdout !== "") return result.stdout;
1316
+ const err = new Error(result.stderr);
1317
+ err.stdout = result.stdout;
1318
+ err.stderr = result.stderr;
1319
+ err.status = result.status;
1320
+ throw err;
1321
+ }
1297
1322
  function getDiffArgs(mode) {
1298
1323
  switch (mode.type) {
1299
1324
  case "uncommitted":
@@ -1322,13 +1347,13 @@ function getFileDiffs(mode, cwd) {
1322
1347
  const diffArgs = getDiffArgs(mode);
1323
1348
  let rawDiff;
1324
1349
  try {
1325
- rawDiff = git([...diffArgs, "-U3"], repoRoot);
1350
+ rawDiff = git2([...diffArgs, "-U3"], repoRoot);
1326
1351
  } catch {
1327
1352
  rawDiff = "";
1328
1353
  }
1329
1354
  const diffs = parseDiff(rawDiff);
1330
1355
  if (mode.type === "uncommitted") {
1331
- const untracked = git(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
1356
+ const untracked = git2(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
1332
1357
  if (untracked) {
1333
1358
  for (const file of untracked.split("\n").filter(Boolean)) {
1334
1359
  if (!diffs.some((d) => d.filePath === file)) {
@@ -1340,7 +1365,7 @@ function getFileDiffs(mode, cwd) {
1340
1365
  return diffs;
1341
1366
  }
1342
1367
  function getAllFiles(repoRoot) {
1343
- const files = git(["ls-files"], repoRoot).trim().split("\n").filter(Boolean);
1368
+ const files = git2(["ls-files"], repoRoot).trim().split("\n").filter(Boolean);
1344
1369
  return files.map((file) => createNewFileDiff(file, repoRoot));
1345
1370
  }
1346
1371
  function createNewFileDiff(filePath, repoRoot) {
@@ -1469,14 +1494,11 @@ function getFileContent(filePath, ref, cwd) {
1469
1494
  if (ref === "working") {
1470
1495
  return readFileSync2(resolve(repoRoot, filePath), "utf-8");
1471
1496
  }
1472
- return git(["show", `${ref}:${filePath}`], repoRoot);
1497
+ return git2(["show", `${ref}:${filePath}`], repoRoot);
1473
1498
  } catch {
1474
1499
  return "";
1475
1500
  }
1476
1501
  }
1477
- function getHeadCommit(cwd) {
1478
- return spawnSync2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).stdout.trim();
1479
- }
1480
1502
  function parseModeString(modeStr) {
1481
1503
  if (modeStr === "uncommitted") return { type: "uncommitted" };
1482
1504
  if (modeStr === "staged") return { type: "staged" };
@@ -1501,7 +1523,7 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
1501
1523
  args.push("--", filePath);
1502
1524
  let rawDiff;
1503
1525
  try {
1504
- rawDiff = git(args, repoRoot);
1526
+ rawDiff = git2(args, repoRoot);
1505
1527
  } catch {
1506
1528
  rawDiff = "";
1507
1529
  }
@@ -1573,7 +1595,9 @@ function acquireLock(dataDir) {
1573
1595
  }
1574
1596
  }
1575
1597
  writeFileSync2(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
1576
- const cleanup = () => releaseLock();
1598
+ const cleanup = () => {
1599
+ releaseLock();
1600
+ };
1577
1601
  process.on("exit", cleanup);
1578
1602
  process.on("SIGINT", () => {
1579
1603
  cleanup();
@@ -1585,7 +1609,7 @@ function acquireLock(dataDir) {
1585
1609
  });
1586
1610
  }
1587
1611
  function releaseLock() {
1588
- if (lockPath) {
1612
+ if (lockPath !== null) {
1589
1613
  try {
1590
1614
  rmSync2(lockPath, { force: true });
1591
1615
  } catch {
@@ -1701,12 +1725,15 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1701
1725
  // src/server.ts
1702
1726
  import { serve } from "@hono/node-server";
1703
1727
  import { exec } from "child_process";
1704
- import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
1705
- import { Hono as Hono4 } from "hono";
1706
- import { dirname, join as join7 } from "path";
1728
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1729
+ import { Hono as Hono7 } from "hono";
1730
+ import { dirname, join as join8 } from "path";
1707
1731
  import { fileURLToPath } from "url";
1708
1732
 
1709
1733
  // src/routes/ai-api.ts
1734
+ import { Hono as Hono3 } from "hono";
1735
+
1736
+ // src/routes/ai-analysis.ts
1710
1737
  import { Hono } from "hono";
1711
1738
 
1712
1739
  // src/ai/client.ts
@@ -2536,63 +2563,11 @@ async function mockGuidedAnalysisBatch(files) {
2536
2563
  }));
2537
2564
  }
2538
2565
 
2539
- // src/routes/ai-api.ts
2566
+ // src/routes/ai-analysis.ts
2540
2567
  init_queries();
2541
- var aiApiRoutes = new Hono();
2568
+ var aiAnalysisRoutes = new Hono();
2542
2569
  var cancelledAnalyses = /* @__PURE__ */ new Set();
2543
- aiApiRoutes.get("/config", (c) => {
2544
- const config = loadAIConfig();
2545
- return c.json({
2546
- platform: config.platform,
2547
- model: config.model,
2548
- keyConfigured: config.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
2549
- keySource: config.keySource,
2550
- guidedReview: loadGuidedReviewConfig()
2551
- });
2552
- });
2553
- aiApiRoutes.post("/config", async (c) => {
2554
- const body = await c.req.json();
2555
- saveAIConfigPreferences(body.platform, body.model);
2556
- if (body.guidedReview !== void 0) {
2557
- saveGuidedReviewConfig(body.guidedReview);
2558
- }
2559
- return c.json({ ok: true });
2560
- });
2561
- aiApiRoutes.get("/models", (c) => {
2562
- return c.json({
2563
- platforms: PLATFORMS,
2564
- models: MODELS
2565
- });
2566
- });
2567
- aiApiRoutes.get("/key-status", (c) => {
2568
- const platforms = ["anthropic", "openai", "google"];
2569
- const status = {};
2570
- for (const platform of platforms) {
2571
- const { source } = resolveAPIKey(platform);
2572
- status[platform] = { configured: source !== null, source };
2573
- }
2574
- return c.json({
2575
- status,
2576
- keychainAvailable: isKeychainAvailable(),
2577
- keychainLabel: getKeychainLabel(),
2578
- availablePlatforms: detectAvailablePlatforms()
2579
- });
2580
- });
2581
- aiApiRoutes.post("/key", async (c) => {
2582
- const body = await c.req.json();
2583
- saveAPIKey(
2584
- body.platform,
2585
- body.key,
2586
- body.storage
2587
- );
2588
- return c.json({ ok: true });
2589
- });
2590
- aiApiRoutes.delete("/key", (c) => {
2591
- const platform = c.req.query("platform") ?? "anthropic";
2592
- deleteAPIKey(platform);
2593
- return c.json({ ok: true });
2594
- });
2595
- aiApiRoutes.post("/analyze", async (c) => {
2570
+ aiAnalysisRoutes.post("/analyze", async (c) => {
2596
2571
  const reviewId = c.req.query("reviewId") ?? "";
2597
2572
  const repoRoot = c.get("repoRoot");
2598
2573
  const body = await c.req.json();
@@ -2844,7 +2819,7 @@ async function runBatchedGuidedAnalysis(analysisId, batches, allFiles, config, r
2844
2819
  "guided"
2845
2820
  );
2846
2821
  }
2847
- aiApiRoutes.get("/analysis/:type", async (c) => {
2822
+ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2848
2823
  const reviewId = c.req.query("reviewId") ?? "";
2849
2824
  const analysisType = c.req.param("type");
2850
2825
  const analysis = await getLatestAnalysis(reviewId, analysisType);
@@ -2876,7 +2851,7 @@ aiApiRoutes.get("/analysis/:type", async (c) => {
2876
2851
  }))
2877
2852
  });
2878
2853
  });
2879
- aiApiRoutes.get("/analysis/:type/status", async (c) => {
2854
+ aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
2880
2855
  const reviewId = c.req.query("reviewId") ?? "";
2881
2856
  const analysisType = c.req.param("type");
2882
2857
  const analysis = await getLatestAnalysis(reviewId, analysisType);
@@ -2900,35 +2875,96 @@ aiApiRoutes.get("/analysis/:type/status", async (c) => {
2900
2875
  progressTotal: analysis.progress_total
2901
2876
  });
2902
2877
  });
2903
- aiApiRoutes.get("/debug-status", (c) => {
2878
+ aiAnalysisRoutes.get("/debug-status", (c) => {
2904
2879
  return c.json({ enabled: isDebug() });
2905
2880
  });
2906
- aiApiRoutes.post("/debug-log", async (c) => {
2881
+ aiAnalysisRoutes.post("/debug-log", async (c) => {
2907
2882
  if (!isDebug()) return c.json({ ok: true });
2908
2883
  const body = await c.req.json();
2909
2884
  debugLog(`[client] ${body.message}`);
2910
2885
  return c.json({ ok: true });
2911
2886
  });
2912
- aiApiRoutes.get("/preferences", async (c) => {
2887
+ aiAnalysisRoutes.get("/preferences", async (c) => {
2913
2888
  const prefs = await getUserPreferences();
2914
2889
  return c.json(prefs);
2915
2890
  });
2916
- aiApiRoutes.post("/preferences", async (c) => {
2891
+ aiAnalysisRoutes.post("/preferences", async (c) => {
2917
2892
  const body = await c.req.json();
2918
2893
  await saveUserPreferences(body);
2919
2894
  return c.json({ ok: true });
2920
2895
  });
2921
2896
 
2897
+ // src/routes/ai-config.ts
2898
+ import { Hono as Hono2 } from "hono";
2899
+ var aiConfigRoutes = new Hono2();
2900
+ aiConfigRoutes.get("/config", (c) => {
2901
+ const config = loadAIConfig();
2902
+ return c.json({
2903
+ platform: config.platform,
2904
+ model: config.model,
2905
+ keyConfigured: config.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
2906
+ keySource: config.keySource,
2907
+ guidedReview: loadGuidedReviewConfig()
2908
+ });
2909
+ });
2910
+ aiConfigRoutes.post("/config", async (c) => {
2911
+ const body = await c.req.json();
2912
+ saveAIConfigPreferences(body.platform, body.model);
2913
+ if (body.guidedReview !== void 0) {
2914
+ saveGuidedReviewConfig(body.guidedReview);
2915
+ }
2916
+ return c.json({ ok: true });
2917
+ });
2918
+ aiConfigRoutes.get("/models", (c) => {
2919
+ return c.json({
2920
+ platforms: PLATFORMS,
2921
+ models: MODELS
2922
+ });
2923
+ });
2924
+ aiConfigRoutes.get("/key-status", (c) => {
2925
+ const platforms = ["anthropic", "openai", "google"];
2926
+ const status = {};
2927
+ for (const platform of platforms) {
2928
+ const { source } = resolveAPIKey(platform);
2929
+ status[platform] = { configured: source !== null, source };
2930
+ }
2931
+ return c.json({
2932
+ status,
2933
+ keychainAvailable: isKeychainAvailable(),
2934
+ keychainLabel: getKeychainLabel(),
2935
+ availablePlatforms: detectAvailablePlatforms()
2936
+ });
2937
+ });
2938
+ aiConfigRoutes.post("/key", async (c) => {
2939
+ const body = await c.req.json();
2940
+ saveAPIKey(
2941
+ body.platform,
2942
+ body.key,
2943
+ body.storage
2944
+ );
2945
+ return c.json({ ok: true });
2946
+ });
2947
+ aiConfigRoutes.delete("/key", (c) => {
2948
+ const platform = c.req.query("platform") ?? "anthropic";
2949
+ deleteAPIKey(platform);
2950
+ return c.json({ ok: true });
2951
+ });
2952
+
2953
+ // src/routes/ai-api.ts
2954
+ var aiApiRoutes = new Hono3();
2955
+ aiApiRoutes.route("/", aiConfigRoutes);
2956
+ aiApiRoutes.route("/", aiAnalysisRoutes);
2957
+
2922
2958
  // src/routes/api.ts
2923
2959
  init_queries();
2924
- import { execFileSync, spawnSync as spawnSync5 } from "child_process";
2960
+ import { execFileSync, spawnSync as spawnSync7 } from "child_process";
2925
2961
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2926
- import { Hono as Hono2 } from "hono";
2962
+ import { Hono as Hono4 } from "hono";
2927
2963
  import { join as join6, resolve as resolve3 } from "path";
2928
2964
 
2929
2965
  // src/export/generate.ts
2930
2966
  init_queries();
2931
- import { spawnSync as spawnSync3 } from "child_process";
2967
+ import { spawnSync as spawnSync5 } from "child_process";
2932
2968
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2933
2969
  import { homedir as homedir2 } from "os";
2934
2970
  import { join as join4 } from "path";
@@ -2947,7 +2983,7 @@ function saveDismissals(data) {
2947
2983
  writeFileSync3(DISMISS_FILE, JSON.stringify(data), "utf-8");
2948
2984
  }
2949
2985
  function isGlassboxGitignored(repoRoot) {
2950
- const result = spawnSync3("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
2986
+ const result = spawnSync5("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
2951
2987
  return result.status === 0;
2952
2988
  }
2953
2989
  function shouldPromptGitignore(repoRoot) {
@@ -3061,137 +3097,53 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
3061
3097
  writeFileSync3(latestPath, content, "utf-8");
3062
3098
  return latestPath;
3063
3099
  }
3064
- return archivePath;
3065
- }
3066
-
3067
- // src/export/auto-export.ts
3068
- var debounceTimer = null;
3069
- var DEBOUNCE_MS = 2e3;
3070
- function scheduleAutoExport(reviewId, repoRoot) {
3071
- if (debounceTimer !== null) clearTimeout(debounceTimer);
3072
- debounceTimer = setTimeout(() => {
3073
- debounceTimer = null;
3074
- void generateReviewExport(reviewId, repoRoot, true);
3075
- }, DEBOUNCE_MS);
3076
- }
3077
-
3078
- // src/git/image.ts
3079
- import { spawnSync as spawnSync4 } from "child_process";
3080
- import { readFileSync as readFileSync5 } from "fs";
3081
- import { resolve as resolve2 } from "path";
3082
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
3083
- function isImageFile(filePath) {
3084
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3085
- return IMAGE_EXTENSIONS.has(ext);
3086
- }
3087
- function isSvgFile(filePath) {
3088
- return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3089
- }
3090
- function getContentType(filePath) {
3091
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3092
- switch (ext) {
3093
- case ".png":
3094
- return "image/png";
3095
- case ".jpg":
3096
- case ".jpeg":
3097
- return "image/jpeg";
3098
- case ".gif":
3099
- return "image/gif";
3100
- case ".webp":
3101
- return "image/webp";
3102
- case ".svg":
3103
- return "image/svg+xml";
3104
- default:
3105
- return "application/octet-stream";
3106
- }
3107
- }
3108
- function getOldRef(mode) {
3109
- switch (mode.type) {
3110
- case "uncommitted":
3111
- return "HEAD";
3112
- case "staged":
3113
- return "HEAD";
3114
- case "unstaged":
3115
- return null;
3116
- // old = index, use ':'
3117
- case "commit":
3118
- return `${mode.sha}~1`;
3119
- case "range":
3120
- return mode.from;
3121
- case "branch":
3122
- return mode.name;
3123
- case "files":
3124
- return "HEAD";
3125
- case "all":
3126
- return null;
3127
- }
3128
- }
3129
- function getNewRef(mode) {
3130
- switch (mode.type) {
3131
- case "uncommitted":
3132
- return null;
3133
- // working tree
3134
- case "staged":
3135
- return null;
3136
- // index, but git show : works
3137
- case "unstaged":
3138
- return null;
3139
- // working tree
3140
- case "commit":
3141
- return mode.sha;
3142
- case "range":
3143
- return mode.to;
3144
- case "branch":
3145
- return "HEAD";
3146
- case "files":
3147
- return null;
3148
- case "all":
3149
- return null;
3150
- }
3100
+ return archivePath;
3151
3101
  }
3152
- function gitShowFile(ref, filePath, repoRoot) {
3153
- const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
3154
- const result = spawnSync4("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
3155
- if (result.status !== 0 || result.stdout.length === 0) return null;
3156
- return result.stdout;
3102
+
3103
+ // src/export/auto-export.ts
3104
+ var debounceTimer = null;
3105
+ var DEBOUNCE_MS = 2e3;
3106
+ function scheduleAutoExport(reviewId, repoRoot) {
3107
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
3108
+ debounceTimer = setTimeout(() => {
3109
+ debounceTimer = null;
3110
+ void generateReviewExport(reviewId, repoRoot, true);
3111
+ }, DEBOUNCE_MS);
3157
3112
  }
3158
- function readWorkingFile(filePath, repoRoot) {
3159
- try {
3160
- return readFileSync5(resolve2(repoRoot, filePath));
3161
- } catch {
3162
- return null;
3163
- }
3113
+
3114
+ // src/git/image.ts
3115
+ import { spawnSync as spawnSync6 } from "child_process";
3116
+ import { readFileSync as readFileSync5 } from "fs";
3117
+ import { resolve as resolve2 } from "path";
3118
+
3119
+ // src/git/image-metadata.ts
3120
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
3121
+ function isImageFile(filePath) {
3122
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3123
+ return IMAGE_EXTENSIONS.has(ext);
3164
3124
  }
3165
- function getOldImage(mode, filePath, oldPath, repoRoot) {
3166
- const ref = getOldRef(mode);
3167
- const path = oldPath ?? filePath;
3168
- if (ref === null) {
3169
- const data2 = readWorkingFile(path, repoRoot);
3170
- if (!data2) return null;
3171
- return { data: data2, size: data2.length };
3172
- }
3173
- const actualRef = mode.type === "unstaged" ? ":" : ref;
3174
- const data = gitShowFile(actualRef, path, repoRoot);
3175
- if (!data) return null;
3176
- return { data, size: data.length };
3125
+ function isSvgFile(filePath) {
3126
+ return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3177
3127
  }
3178
- function getNewImage(mode, filePath, repoRoot) {
3179
- const ref = getNewRef(mode);
3180
- if (ref === null) {
3181
- if (mode.type === "staged") {
3182
- const data3 = gitShowFile(":", filePath, repoRoot);
3183
- if (!data3) return null;
3184
- return { data: data3, size: data3.length };
3185
- }
3186
- const data2 = readWorkingFile(filePath, repoRoot);
3187
- if (!data2) return null;
3188
- return { data: data2, size: data2.length };
3128
+ function getContentType(filePath) {
3129
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3130
+ switch (ext) {
3131
+ case ".png":
3132
+ return "image/png";
3133
+ case ".jpg":
3134
+ case ".jpeg":
3135
+ return "image/jpeg";
3136
+ case ".gif":
3137
+ return "image/gif";
3138
+ case ".webp":
3139
+ return "image/webp";
3140
+ case ".svg":
3141
+ return "image/svg+xml";
3142
+ default:
3143
+ return "application/octet-stream";
3189
3144
  }
3190
- const data = gitShowFile(ref, filePath, repoRoot);
3191
- if (!data) return null;
3192
- return { data, size: data.length };
3193
3145
  }
3194
- async function extractMetadata(data, filePath) {
3146
+ function extractMetadata(data, filePath) {
3195
3147
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3196
3148
  if (ext === ".svg") {
3197
3149
  const text = data.toString("utf-8");
@@ -3233,9 +3185,9 @@ function formatMetadataLines(meta) {
3233
3185
  lines.push(`Dimensions: ${meta.width} \xD7 ${meta.height}`);
3234
3186
  }
3235
3187
  lines.push(`File size: ${formatBytes(meta.fileSize)}`);
3236
- if (meta.colorSpace) lines.push(`Color space: ${meta.colorSpace}`);
3188
+ if (meta.colorSpace !== null) lines.push(`Color space: ${meta.colorSpace}`);
3237
3189
  if (meta.channels !== null) lines.push(`Channels: ${meta.channels}`);
3238
- if (meta.depth) lines.push(`Bit depth: ${meta.depth}`);
3190
+ if (meta.depth !== null) lines.push(`Bit depth: ${meta.depth}`);
3239
3191
  if (meta.hasAlpha !== null) lines.push(`Alpha: ${meta.hasAlpha ? "yes" : "no"}`);
3240
3192
  if (meta.density !== null) lines.push(`Density: ${meta.density} DPI`);
3241
3193
  if (meta.exif) {
@@ -3376,7 +3328,95 @@ function parseWebp(data) {
3376
3328
  width = (data[24] | data[25] << 8 | data[26] << 16) + 1;
3377
3329
  height = (data[27] | data[28] << 8 | data[29] << 16) + 1;
3378
3330
  }
3379
- return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
3331
+ return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha === true ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
3332
+ }
3333
+
3334
+ // src/git/image.ts
3335
+ function getOldRef(mode) {
3336
+ switch (mode.type) {
3337
+ case "uncommitted":
3338
+ return "HEAD";
3339
+ case "staged":
3340
+ return "HEAD";
3341
+ case "unstaged":
3342
+ return null;
3343
+ // old = index, use ':'
3344
+ case "commit":
3345
+ return `${mode.sha}~1`;
3346
+ case "range":
3347
+ return mode.from;
3348
+ case "branch":
3349
+ return mode.name;
3350
+ case "files":
3351
+ return "HEAD";
3352
+ case "all":
3353
+ return null;
3354
+ }
3355
+ }
3356
+ function getNewRef(mode) {
3357
+ switch (mode.type) {
3358
+ case "uncommitted":
3359
+ return null;
3360
+ // working tree
3361
+ case "staged":
3362
+ return null;
3363
+ // index, but git show : works
3364
+ case "unstaged":
3365
+ return null;
3366
+ // working tree
3367
+ case "commit":
3368
+ return mode.sha;
3369
+ case "range":
3370
+ return mode.to;
3371
+ case "branch":
3372
+ return "HEAD";
3373
+ case "files":
3374
+ return null;
3375
+ case "all":
3376
+ return null;
3377
+ }
3378
+ }
3379
+ function gitShowFile(ref, filePath, repoRoot) {
3380
+ const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
3381
+ const result = spawnSync6("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
3382
+ if (result.status !== 0 || result.stdout.length === 0) return null;
3383
+ return result.stdout;
3384
+ }
3385
+ function readWorkingFile(filePath, repoRoot) {
3386
+ try {
3387
+ return readFileSync5(resolve2(repoRoot, filePath));
3388
+ } catch {
3389
+ return null;
3390
+ }
3391
+ }
3392
+ function getOldImage(mode, filePath, oldPath, repoRoot) {
3393
+ const ref = getOldRef(mode);
3394
+ const path = oldPath ?? filePath;
3395
+ if (ref === null) {
3396
+ const data2 = readWorkingFile(path, repoRoot);
3397
+ if (!data2) return null;
3398
+ return { data: data2, size: data2.length };
3399
+ }
3400
+ const actualRef = mode.type === "unstaged" ? ":" : ref;
3401
+ const data = gitShowFile(actualRef, path, repoRoot);
3402
+ if (!data) return null;
3403
+ return { data, size: data.length };
3404
+ }
3405
+ function getNewImage(mode, filePath, repoRoot) {
3406
+ const ref = getNewRef(mode);
3407
+ if (ref === null) {
3408
+ if (mode.type === "staged") {
3409
+ const data3 = gitShowFile(":", filePath, repoRoot);
3410
+ if (!data3) return null;
3411
+ return { data: data3, size: data3.length };
3412
+ }
3413
+ const data2 = readWorkingFile(filePath, repoRoot);
3414
+ if (!data2) return null;
3415
+ return { data: data2, size: data2.length };
3416
+ }
3417
+ const data = gitShowFile(ref, filePath, repoRoot);
3418
+ if (!data) return null;
3419
+ return { data, size: data.length };
3380
3420
  }
3381
3421
 
3382
3422
  // src/git/svg-rasterize.ts
@@ -3411,63 +3451,62 @@ function loadSystemFonts() {
3411
3451
  return buffers;
3412
3452
  }
3413
3453
  function getFontCandidates() {
3414
- switch (process.platform) {
3415
- case "darwin": {
3416
- const sys = "/System/Library/Fonts";
3417
- const sup = "/System/Library/Fonts/Supplemental";
3418
- return [
3419
- // Core system fonts (serif, sans-serif, monospace)
3420
- join5(sys, "Helvetica.ttc"),
3421
- join5(sys, "Times.ttc"),
3422
- join5(sys, "Courier.ttc"),
3423
- join5(sys, "Menlo.ttc"),
3424
- join5(sys, "SFPro.ttf"),
3425
- join5(sys, "SFNS.ttf"),
3426
- join5(sys, "SFNSMono.ttf"),
3427
- // Supplemental (common named fonts in SVGs)
3428
- join5(sup, "Arial.ttf"),
3429
- join5(sup, "Arial Bold.ttf"),
3430
- join5(sup, "Georgia.ttf"),
3431
- join5(sup, "Verdana.ttf"),
3432
- join5(sup, "Tahoma.ttf"),
3433
- join5(sup, "Trebuchet MS.ttf"),
3434
- join5(sup, "Impact.ttf"),
3435
- join5(sup, "Comic Sans MS.ttf"),
3436
- join5(sup, "Courier New.ttf"),
3437
- join5(sup, "Times New Roman.ttf")
3438
- ];
3439
- }
3440
- case "linux":
3441
- return [
3442
- // DejaVu (most common Linux fallback)
3443
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3444
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
3445
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
3446
- "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
3447
- // Liberation (metric-compatible with Arial/Times/Courier)
3448
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3449
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
3450
- "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
3451
- // Noto (common on modern distros)
3452
- "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
3453
- ];
3454
- case "win32": {
3455
- const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
3456
- return [
3457
- join5(winFonts, "arial.ttf"),
3458
- join5(winFonts, "arialbd.ttf"),
3459
- join5(winFonts, "times.ttf"),
3460
- join5(winFonts, "cour.ttf"),
3461
- join5(winFonts, "verdana.ttf"),
3462
- join5(winFonts, "tahoma.ttf"),
3463
- join5(winFonts, "georgia.ttf"),
3464
- join5(winFonts, "consola.ttf"),
3465
- join5(winFonts, "segoeui.ttf")
3466
- ];
3467
- }
3468
- default:
3469
- return [];
3454
+ const os = process.platform;
3455
+ if (os === "darwin") {
3456
+ const sys = "/System/Library/Fonts";
3457
+ const sup = "/System/Library/Fonts/Supplemental";
3458
+ return [
3459
+ // Core system fonts (serif, sans-serif, monospace)
3460
+ join5(sys, "Helvetica.ttc"),
3461
+ join5(sys, "Times.ttc"),
3462
+ join5(sys, "Courier.ttc"),
3463
+ join5(sys, "Menlo.ttc"),
3464
+ join5(sys, "SFPro.ttf"),
3465
+ join5(sys, "SFNS.ttf"),
3466
+ join5(sys, "SFNSMono.ttf"),
3467
+ // Supplemental (common named fonts in SVGs)
3468
+ join5(sup, "Arial.ttf"),
3469
+ join5(sup, "Arial Bold.ttf"),
3470
+ join5(sup, "Georgia.ttf"),
3471
+ join5(sup, "Verdana.ttf"),
3472
+ join5(sup, "Tahoma.ttf"),
3473
+ join5(sup, "Trebuchet MS.ttf"),
3474
+ join5(sup, "Impact.ttf"),
3475
+ join5(sup, "Comic Sans MS.ttf"),
3476
+ join5(sup, "Courier New.ttf"),
3477
+ join5(sup, "Times New Roman.ttf")
3478
+ ];
3479
+ }
3480
+ if (os === "linux") {
3481
+ return [
3482
+ // DejaVu (most common Linux fallback)
3483
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3484
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
3485
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
3486
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
3487
+ // Liberation (metric-compatible with Arial/Times/Courier)
3488
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3489
+ "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
3490
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
3491
+ // Noto (common on modern distros)
3492
+ "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
3493
+ ];
3494
+ }
3495
+ if (os === "win32") {
3496
+ const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
3497
+ return [
3498
+ join5(winFonts, "arial.ttf"),
3499
+ join5(winFonts, "arialbd.ttf"),
3500
+ join5(winFonts, "times.ttf"),
3501
+ join5(winFonts, "cour.ttf"),
3502
+ join5(winFonts, "verdana.ttf"),
3503
+ join5(winFonts, "tahoma.ttf"),
3504
+ join5(winFonts, "georgia.ttf"),
3505
+ join5(winFonts, "consola.ttf"),
3506
+ join5(winFonts, "segoeui.ttf")
3507
+ ];
3470
3508
  }
3509
+ return [];
3471
3510
  }
3472
3511
  function parseSvgDimensions(svg) {
3473
3512
  const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
@@ -3831,7 +3870,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
3831
3870
  }
3832
3871
 
3833
3872
  // src/routes/api.ts
3834
- var apiRoutes = new Hono2();
3873
+ var apiRoutes = new Hono4();
3835
3874
  function resolveReviewId(c) {
3836
3875
  return c.req.query("reviewId") ?? c.get("reviewId");
3837
3876
  }
@@ -4034,7 +4073,7 @@ apiRoutes.get("/outline/:fileId", async (c) => {
4034
4073
  apiRoutes.get("/symbol-definition", async (c) => {
4035
4074
  const name = c.req.query("name");
4036
4075
  const currentFileId = c.req.query("currentFileId");
4037
- if (!name) return c.json({ definitions: [] });
4076
+ if (name === void 0 || name === "") return c.json({ definitions: [] });
4038
4077
  const reviewId = resolveReviewId(c);
4039
4078
  const repoRoot = c.get("repoRoot");
4040
4079
  const definitions = [];
@@ -4056,7 +4095,7 @@ apiRoutes.get("/symbol-definition", async (c) => {
4056
4095
  }
4057
4096
  if (definitions.length === 0) {
4058
4097
  try {
4059
- const allFiles = spawnSync5("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
4098
+ const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
4060
4099
  for (const filePath of allFiles) {
4061
4100
  if (searchedPaths.has(filePath)) continue;
4062
4101
  const ext = filePath.slice(filePath.lastIndexOf("."));
@@ -4091,7 +4130,7 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
4091
4130
  if (sym.name === targetName) {
4092
4131
  out.push({ fileId, filePath, name: sym.name, kind: sym.kind, line: sym.line });
4093
4132
  }
4094
- if (sym.children?.length > 0) {
4133
+ if (sym.children.length > 0) {
4095
4134
  collectDefinitions(sym.children, targetName, fileId, filePath, out);
4096
4135
  }
4097
4136
  }
@@ -4152,10 +4191,8 @@ apiRoutes.get("/image/:fileId/metadata", async (c) => {
4152
4191
  const status = diff.status ?? "modified";
4153
4192
  const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
4154
4193
  const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
4155
- const [oldMeta, newMeta] = await Promise.all([
4156
- oldImage ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null,
4157
- newImage ? extractMetadata(newImage.data, file.file_path) : null
4158
- ]);
4194
+ const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
4195
+ const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
4159
4196
  return c.json({
4160
4197
  old: oldMeta ? formatMetadataLines(oldMeta) : null,
4161
4198
  new: newMeta ? formatMetadataLines(newMeta) : null
@@ -4192,8 +4229,8 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
4192
4229
  });
4193
4230
 
4194
4231
  // src/routes/pages.tsx
4195
- import { readFileSync as readFileSync8 } from "fs";
4196
- import { Hono as Hono3 } from "hono";
4232
+ import { readFileSync as readFileSync9 } from "fs";
4233
+ import { Hono as Hono5 } from "hono";
4197
4234
  import { resolve as resolve4 } from "path";
4198
4235
 
4199
4236
  // src/utils/escapeHtml.ts
@@ -4508,10 +4545,10 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4508
4545
  "data-file-path": file.file_path,
4509
4546
  "data-has-old": String(hasOld),
4510
4547
  "data-has-new": String(hasNew),
4511
- ...baseWidth ? { "data-base-width": String(baseWidth) } : {},
4512
- ...baseHeight ? { "data-base-height": String(baseHeight) } : {},
4548
+ ...baseWidth !== void 0 ? { "data-base-width": String(baseWidth) } : {},
4549
+ ...baseHeight !== void 0 ? { "data-base-height": String(baseHeight) } : {},
4513
4550
  children: [
4514
- fontWarning && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4551
+ fontWarning === true && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
4515
4552
  /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-metadata", "data-panel": "metadata", children: /* @__PURE__ */ jsx("div", { className: "image-metadata-loading", children: "Loading metadata..." }) }),
4516
4553
  hasComparison && /* @__PURE__ */ jsx("div", { className: "image-diff-panel image-diff-visual", "data-panel": "difference", children: /* @__PURE__ */ jsx("div", { className: "image-visual-canvas", "data-zoomable": "true", children: /* @__PURE__ */ jsx("div", { className: "image-zoom-wrap", children: [
4517
4554
  /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
@@ -4970,9 +5007,570 @@ function FileList({ files, annotationCounts, staleCounts }) {
4970
5007
  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 }) }) });
4971
5008
  }
4972
5009
 
5010
+ // src/themes/built-in.ts
5011
+ var THEME_VARIABLES = [
5012
+ "bg",
5013
+ "bg-surface",
5014
+ "bg-hover",
5015
+ "bg-active",
5016
+ "text",
5017
+ "text-dim",
5018
+ "text-bright",
5019
+ "accent",
5020
+ "accent-hover",
5021
+ "green",
5022
+ "red",
5023
+ "yellow",
5024
+ "orange",
5025
+ "blue",
5026
+ "purple",
5027
+ "teal",
5028
+ "border",
5029
+ "diff-add-bg",
5030
+ "diff-add-border",
5031
+ "diff-remove-bg",
5032
+ "diff-remove-border",
5033
+ "diff-context-bg",
5034
+ "gutter-bg",
5035
+ "gutter-text"
5036
+ ];
5037
+ var dark = {
5038
+ "bg": "#1e1e2e",
5039
+ "bg-surface": "#252536",
5040
+ "bg-hover": "#2d2d44",
5041
+ "bg-active": "#363652",
5042
+ "text": "#cdd6f4",
5043
+ "text-dim": "#8888aa",
5044
+ "text-bright": "#ffffff",
5045
+ "accent": "#89b4fa",
5046
+ "accent-hover": "#74a8fc",
5047
+ "green": "#a6e3a1",
5048
+ "red": "#f38ba8",
5049
+ "yellow": "#f9e2af",
5050
+ "orange": "#fab387",
5051
+ "blue": "#89b4fa",
5052
+ "purple": "#cba6f7",
5053
+ "teal": "#94e2d5",
5054
+ "border": "#363652",
5055
+ "diff-add-bg": "rgba(166, 227, 161, 0.1)",
5056
+ "diff-add-border": "rgba(166, 227, 161, 0.3)",
5057
+ "diff-remove-bg": "rgba(243, 139, 168, 0.1)",
5058
+ "diff-remove-border": "rgba(243, 139, 168, 0.3)",
5059
+ "diff-context-bg": "transparent",
5060
+ "gutter-bg": "#1a1a2e",
5061
+ "gutter-text": "#555577"
5062
+ };
5063
+ var light = {
5064
+ "bg": "#ffffff",
5065
+ "bg-surface": "#f6f8fa",
5066
+ "bg-hover": "#eaeef2",
5067
+ "bg-active": "#dde3e9",
5068
+ "text": "#1f2328",
5069
+ "text-dim": "#656d76",
5070
+ "text-bright": "#000000",
5071
+ "accent": "#0969da",
5072
+ "accent-hover": "#0550ae",
5073
+ "green": "#1a7f37",
5074
+ "red": "#cf222e",
5075
+ "yellow": "#9a6700",
5076
+ "orange": "#bc4c00",
5077
+ "blue": "#0969da",
5078
+ "purple": "#8250df",
5079
+ "teal": "#0e7c6b",
5080
+ "border": "#d0d7de",
5081
+ "diff-add-bg": "rgba(26, 127, 55, 0.08)",
5082
+ "diff-add-border": "rgba(26, 127, 55, 0.25)",
5083
+ "diff-remove-bg": "rgba(207, 34, 46, 0.08)",
5084
+ "diff-remove-border": "rgba(207, 34, 46, 0.25)",
5085
+ "diff-context-bg": "transparent",
5086
+ "gutter-bg": "#f6f8fa",
5087
+ "gutter-text": "#8b949e"
5088
+ };
5089
+ var highContrastDark = {
5090
+ "bg": "#0a0a0a",
5091
+ "bg-surface": "#1a1a1a",
5092
+ "bg-hover": "#2a2a2a",
5093
+ "bg-active": "#3a3a3a",
5094
+ "text": "#f0f0f0",
5095
+ "text-dim": "#b0b0b0",
5096
+ "text-bright": "#ffffff",
5097
+ "accent": "#6db3f2",
5098
+ "accent-hover": "#8ec5f7",
5099
+ "green": "#73e06e",
5100
+ "red": "#ff6b6b",
5101
+ "yellow": "#ffd93d",
5102
+ "orange": "#ffab57",
5103
+ "blue": "#6db3f2",
5104
+ "purple": "#c59eff",
5105
+ "teal": "#5ee6d0",
5106
+ "border": "#555555",
5107
+ "diff-add-bg": "rgba(115, 224, 110, 0.15)",
5108
+ "diff-add-border": "rgba(115, 224, 110, 0.5)",
5109
+ "diff-remove-bg": "rgba(255, 107, 107, 0.15)",
5110
+ "diff-remove-border": "rgba(255, 107, 107, 0.5)",
5111
+ "diff-context-bg": "transparent",
5112
+ "gutter-bg": "#111111",
5113
+ "gutter-text": "#888888"
5114
+ };
5115
+ var highContrastLight = {
5116
+ "bg": "#ffffff",
5117
+ "bg-surface": "#f0f0f0",
5118
+ "bg-hover": "#e0e0e0",
5119
+ "bg-active": "#d0d0d0",
5120
+ "text": "#111111",
5121
+ "text-dim": "#444444",
5122
+ "text-bright": "#000000",
5123
+ "accent": "#0043a8",
5124
+ "accent-hover": "#003080",
5125
+ "green": "#006b1f",
5126
+ "red": "#b80000",
5127
+ "yellow": "#785600",
5128
+ "orange": "#8a3400",
5129
+ "blue": "#0043a8",
5130
+ "purple": "#5b21b6",
5131
+ "teal": "#005e4f",
5132
+ "border": "#767676",
5133
+ "diff-add-bg": "rgba(0, 107, 31, 0.1)",
5134
+ "diff-add-border": "rgba(0, 107, 31, 0.4)",
5135
+ "diff-remove-bg": "rgba(184, 0, 0, 0.1)",
5136
+ "diff-remove-border": "rgba(184, 0, 0, 0.4)",
5137
+ "diff-context-bg": "transparent",
5138
+ "gutter-bg": "#f0f0f0",
5139
+ "gutter-text": "#555555"
5140
+ };
5141
+ var dracula = {
5142
+ "bg": "#282a36",
5143
+ "bg-surface": "#2d303e",
5144
+ "bg-hover": "#343746",
5145
+ "bg-active": "#3e4151",
5146
+ "text": "#f8f8f2",
5147
+ "text-dim": "#8b8da4",
5148
+ "text-bright": "#ffffff",
5149
+ "accent": "#bd93f9",
5150
+ "accent-hover": "#caa5fb",
5151
+ "green": "#50fa7b",
5152
+ "red": "#ff5555",
5153
+ "yellow": "#f1fa8c",
5154
+ "orange": "#ffb86c",
5155
+ "blue": "#8be9fd",
5156
+ "purple": "#bd93f9",
5157
+ "teal": "#8be9fd",
5158
+ "border": "#44475a",
5159
+ "diff-add-bg": "rgba(80, 250, 123, 0.1)",
5160
+ "diff-add-border": "rgba(80, 250, 123, 0.3)",
5161
+ "diff-remove-bg": "rgba(255, 85, 85, 0.1)",
5162
+ "diff-remove-border": "rgba(255, 85, 85, 0.3)",
5163
+ "diff-context-bg": "transparent",
5164
+ "gutter-bg": "#21222c",
5165
+ "gutter-text": "#6272a4"
5166
+ };
5167
+ var tokyoNight = {
5168
+ "bg": "#1a1b26",
5169
+ "bg-surface": "#1f2233",
5170
+ "bg-hover": "#292d42",
5171
+ "bg-active": "#33374e",
5172
+ "text": "#a9b1d6",
5173
+ "text-dim": "#565f89",
5174
+ "text-bright": "#c0caf5",
5175
+ "accent": "#7aa2f7",
5176
+ "accent-hover": "#89b0fa",
5177
+ "green": "#9ece6a",
5178
+ "red": "#f7768e",
5179
+ "yellow": "#e0af68",
5180
+ "orange": "#ff9e64",
5181
+ "blue": "#7aa2f7",
5182
+ "purple": "#bb9af7",
5183
+ "teal": "#73daca",
5184
+ "border": "#2f3351",
5185
+ "diff-add-bg": "rgba(158, 206, 106, 0.1)",
5186
+ "diff-add-border": "rgba(158, 206, 106, 0.3)",
5187
+ "diff-remove-bg": "rgba(247, 118, 142, 0.1)",
5188
+ "diff-remove-border": "rgba(247, 118, 142, 0.3)",
5189
+ "diff-context-bg": "transparent",
5190
+ "gutter-bg": "#16172a",
5191
+ "gutter-text": "#3b4261"
5192
+ };
5193
+ var oneDarkPro = {
5194
+ "bg": "#282c34",
5195
+ "bg-surface": "#2c313a",
5196
+ "bg-hover": "#333842",
5197
+ "bg-active": "#3b4048",
5198
+ "text": "#abb2bf",
5199
+ "text-dim": "#636d83",
5200
+ "text-bright": "#d7dae0",
5201
+ "accent": "#61afef",
5202
+ "accent-hover": "#519fdf",
5203
+ "green": "#98c379",
5204
+ "red": "#e06c75",
5205
+ "yellow": "#e5c07b",
5206
+ "orange": "#d19a66",
5207
+ "blue": "#61afef",
5208
+ "purple": "#c678dd",
5209
+ "teal": "#56b6c2",
5210
+ "border": "#3b4048",
5211
+ "diff-add-bg": "rgba(152, 195, 121, 0.1)",
5212
+ "diff-add-border": "rgba(152, 195, 121, 0.3)",
5213
+ "diff-remove-bg": "rgba(224, 108, 117, 0.1)",
5214
+ "diff-remove-border": "rgba(224, 108, 117, 0.3)",
5215
+ "diff-context-bg": "transparent",
5216
+ "gutter-bg": "#23272e",
5217
+ "gutter-text": "#495162"
5218
+ };
5219
+ var solarizedDark = {
5220
+ "bg": "#002b36",
5221
+ "bg-surface": "#073642",
5222
+ "bg-hover": "#0a4050",
5223
+ "bg-active": "#0d4d5e",
5224
+ "text": "#839496",
5225
+ "text-dim": "#586e75",
5226
+ "text-bright": "#eee8d5",
5227
+ "accent": "#268bd2",
5228
+ "accent-hover": "#1a7cc0",
5229
+ "green": "#859900",
5230
+ "red": "#dc322f",
5231
+ "yellow": "#b58900",
5232
+ "orange": "#cb4b16",
5233
+ "blue": "#268bd2",
5234
+ "purple": "#6c71c4",
5235
+ "teal": "#2aa198",
5236
+ "border": "#0a4050",
5237
+ "diff-add-bg": "rgba(133, 153, 0, 0.12)",
5238
+ "diff-add-border": "rgba(133, 153, 0, 0.3)",
5239
+ "diff-remove-bg": "rgba(220, 50, 47, 0.12)",
5240
+ "diff-remove-border": "rgba(220, 50, 47, 0.3)",
5241
+ "diff-context-bg": "transparent",
5242
+ "gutter-bg": "#002028",
5243
+ "gutter-text": "#4a6568"
5244
+ };
5245
+ var solarizedLight = {
5246
+ "bg": "#fdf6e3",
5247
+ "bg-surface": "#eee8d5",
5248
+ "bg-hover": "#e6dfca",
5249
+ "bg-active": "#ddd6c1",
5250
+ "text": "#657b83",
5251
+ "text-dim": "#93a1a1",
5252
+ "text-bright": "#073642",
5253
+ "accent": "#268bd2",
5254
+ "accent-hover": "#1a7cc0",
5255
+ "green": "#859900",
5256
+ "red": "#dc322f",
5257
+ "yellow": "#b58900",
5258
+ "orange": "#cb4b16",
5259
+ "blue": "#268bd2",
5260
+ "purple": "#6c71c4",
5261
+ "teal": "#2aa198",
5262
+ "border": "#ddd6c1",
5263
+ "diff-add-bg": "rgba(133, 153, 0, 0.1)",
5264
+ "diff-add-border": "rgba(133, 153, 0, 0.25)",
5265
+ "diff-remove-bg": "rgba(220, 50, 47, 0.1)",
5266
+ "diff-remove-border": "rgba(220, 50, 47, 0.25)",
5267
+ "diff-context-bg": "transparent",
5268
+ "gutter-bg": "#eee8d5",
5269
+ "gutter-text": "#93a1a1"
5270
+ };
5271
+ var monokai = {
5272
+ "bg": "#272822",
5273
+ "bg-surface": "#2d2e27",
5274
+ "bg-hover": "#3e3d32",
5275
+ "bg-active": "#49483e",
5276
+ "text": "#f8f8f2",
5277
+ "text-dim": "#75715e",
5278
+ "text-bright": "#ffffff",
5279
+ "accent": "#66d9ef",
5280
+ "accent-hover": "#55c8de",
5281
+ "green": "#a6e22e",
5282
+ "red": "#f92672",
5283
+ "yellow": "#e6db74",
5284
+ "orange": "#fd971f",
5285
+ "blue": "#66d9ef",
5286
+ "purple": "#ae81ff",
5287
+ "teal": "#66d9ef",
5288
+ "border": "#49483e",
5289
+ "diff-add-bg": "rgba(166, 226, 46, 0.1)",
5290
+ "diff-add-border": "rgba(166, 226, 46, 0.3)",
5291
+ "diff-remove-bg": "rgba(249, 38, 114, 0.1)",
5292
+ "diff-remove-border": "rgba(249, 38, 114, 0.3)",
5293
+ "diff-context-bg": "transparent",
5294
+ "gutter-bg": "#222218",
5295
+ "gutter-text": "#575848"
5296
+ };
5297
+ var nord = {
5298
+ "bg": "#2e3440",
5299
+ "bg-surface": "#3b4252",
5300
+ "bg-hover": "#434c5e",
5301
+ "bg-active": "#4c566a",
5302
+ "text": "#d8dee9",
5303
+ "text-dim": "#7b88a1",
5304
+ "text-bright": "#eceff4",
5305
+ "accent": "#88c0d0",
5306
+ "accent-hover": "#81a1c1",
5307
+ "green": "#a3be8c",
5308
+ "red": "#bf616a",
5309
+ "yellow": "#ebcb8b",
5310
+ "orange": "#d08770",
5311
+ "blue": "#81a1c1",
5312
+ "purple": "#b48ead",
5313
+ "teal": "#8fbcbb",
5314
+ "border": "#4c566a",
5315
+ "diff-add-bg": "rgba(163, 190, 140, 0.1)",
5316
+ "diff-add-border": "rgba(163, 190, 140, 0.3)",
5317
+ "diff-remove-bg": "rgba(191, 97, 106, 0.1)",
5318
+ "diff-remove-border": "rgba(191, 97, 106, 0.3)",
5319
+ "diff-context-bg": "transparent",
5320
+ "gutter-bg": "#2a303c",
5321
+ "gutter-text": "#5b6578"
5322
+ };
5323
+ var gruvboxDark = {
5324
+ "bg": "#282828",
5325
+ "bg-surface": "#3c3836",
5326
+ "bg-hover": "#504945",
5327
+ "bg-active": "#665c54",
5328
+ "text": "#ebdbb2",
5329
+ "text-dim": "#928374",
5330
+ "text-bright": "#fbf1c7",
5331
+ "accent": "#83a598",
5332
+ "accent-hover": "#76988b",
5333
+ "green": "#b8bb26",
5334
+ "red": "#fb4934",
5335
+ "yellow": "#fabd2f",
5336
+ "orange": "#fe8019",
5337
+ "blue": "#83a598",
5338
+ "purple": "#d3869b",
5339
+ "teal": "#8ec07c",
5340
+ "border": "#504945",
5341
+ "diff-add-bg": "rgba(184, 187, 38, 0.1)",
5342
+ "diff-add-border": "rgba(184, 187, 38, 0.3)",
5343
+ "diff-remove-bg": "rgba(251, 73, 52, 0.1)",
5344
+ "diff-remove-border": "rgba(251, 73, 52, 0.3)",
5345
+ "diff-context-bg": "transparent",
5346
+ "gutter-bg": "#232323",
5347
+ "gutter-text": "#665c54"
5348
+ };
5349
+ var gruvboxLight = {
5350
+ "bg": "#fbf1c7",
5351
+ "bg-surface": "#f2e5bc",
5352
+ "bg-hover": "#ebdbb2",
5353
+ "bg-active": "#d5c4a1",
5354
+ "text": "#3c3836",
5355
+ "text-dim": "#7c6f64",
5356
+ "text-bright": "#282828",
5357
+ "accent": "#427b58",
5358
+ "accent-hover": "#376b4c",
5359
+ "green": "#79740e",
5360
+ "red": "#9d0006",
5361
+ "yellow": "#b57614",
5362
+ "orange": "#af3a03",
5363
+ "blue": "#076678",
5364
+ "purple": "#8f3f71",
5365
+ "teal": "#427b58",
5366
+ "border": "#d5c4a1",
5367
+ "diff-add-bg": "rgba(121, 116, 14, 0.1)",
5368
+ "diff-add-border": "rgba(121, 116, 14, 0.25)",
5369
+ "diff-remove-bg": "rgba(157, 0, 6, 0.1)",
5370
+ "diff-remove-border": "rgba(157, 0, 6, 0.25)",
5371
+ "diff-context-bg": "transparent",
5372
+ "gutter-bg": "#f2e5bc",
5373
+ "gutter-text": "#928374"
5374
+ };
5375
+ var githubDark = {
5376
+ "bg": "#0d1117",
5377
+ "bg-surface": "#161b22",
5378
+ "bg-hover": "#1c2128",
5379
+ "bg-active": "#262c36",
5380
+ "text": "#c9d1d9",
5381
+ "text-dim": "#8b949e",
5382
+ "text-bright": "#f0f6fc",
5383
+ "accent": "#58a6ff",
5384
+ "accent-hover": "#4090e0",
5385
+ "green": "#3fb950",
5386
+ "red": "#f85149",
5387
+ "yellow": "#d29922",
5388
+ "orange": "#db6d28",
5389
+ "blue": "#58a6ff",
5390
+ "purple": "#bc8cff",
5391
+ "teal": "#39d353",
5392
+ "border": "#30363d",
5393
+ "diff-add-bg": "rgba(63, 185, 80, 0.1)",
5394
+ "diff-add-border": "rgba(63, 185, 80, 0.3)",
5395
+ "diff-remove-bg": "rgba(248, 81, 73, 0.1)",
5396
+ "diff-remove-border": "rgba(248, 81, 73, 0.3)",
5397
+ "diff-context-bg": "transparent",
5398
+ "gutter-bg": "#0a0e14",
5399
+ "gutter-text": "#484f58"
5400
+ };
5401
+ var rosePine = {
5402
+ "bg": "#191724",
5403
+ "bg-surface": "#1f1d2e",
5404
+ "bg-hover": "#26233a",
5405
+ "bg-active": "#2a2740",
5406
+ "text": "#e0def4",
5407
+ "text-dim": "#6e6a86",
5408
+ "text-bright": "#f0efff",
5409
+ "accent": "#c4a7e7",
5410
+ "accent-hover": "#b498d7",
5411
+ "green": "#31748f",
5412
+ "red": "#eb6f92",
5413
+ "yellow": "#f6c177",
5414
+ "orange": "#ea9a97",
5415
+ "blue": "#9ccfd8",
5416
+ "purple": "#c4a7e7",
5417
+ "teal": "#9ccfd8",
5418
+ "border": "#2a2740",
5419
+ "diff-add-bg": "rgba(49, 116, 143, 0.12)",
5420
+ "diff-add-border": "rgba(49, 116, 143, 0.3)",
5421
+ "diff-remove-bg": "rgba(235, 111, 146, 0.12)",
5422
+ "diff-remove-border": "rgba(235, 111, 146, 0.3)",
5423
+ "diff-context-bg": "transparent",
5424
+ "gutter-bg": "#16141f",
5425
+ "gutter-text": "#524f67"
5426
+ };
5427
+ var ayuDark = {
5428
+ "bg": "#0b0e14",
5429
+ "bg-surface": "#0f131a",
5430
+ "bg-hover": "#151a23",
5431
+ "bg-active": "#1c222d",
5432
+ "text": "#bfbdb6",
5433
+ "text-dim": "#636a76",
5434
+ "text-bright": "#e6e1cf",
5435
+ "accent": "#e6b450",
5436
+ "accent-hover": "#d9a740",
5437
+ "green": "#7fd962",
5438
+ "red": "#d95757",
5439
+ "yellow": "#e6b450",
5440
+ "orange": "#ff8f40",
5441
+ "blue": "#59c2ff",
5442
+ "purple": "#d2a6ff",
5443
+ "teal": "#95e6cb",
5444
+ "border": "#1c222d",
5445
+ "diff-add-bg": "rgba(127, 217, 98, 0.1)",
5446
+ "diff-add-border": "rgba(127, 217, 98, 0.3)",
5447
+ "diff-remove-bg": "rgba(217, 87, 87, 0.1)",
5448
+ "diff-remove-border": "rgba(217, 87, 87, 0.3)",
5449
+ "diff-context-bg": "transparent",
5450
+ "gutter-bg": "#080a10",
5451
+ "gutter-text": "#3d424d"
5452
+ };
5453
+ var BUILT_IN_THEMES = [
5454
+ { id: "dark", name: "Dark", builtIn: true, colors: dark },
5455
+ { id: "light", name: "Light", builtIn: true, colors: light },
5456
+ { id: "high-contrast-dark", name: "High Contrast Dark", builtIn: true, colors: highContrastDark },
5457
+ { id: "high-contrast-light", name: "High Contrast Light", builtIn: true, colors: highContrastLight },
5458
+ { id: "dracula", name: "Dracula", builtIn: true, colors: dracula },
5459
+ { id: "tokyo-night", name: "Tokyo Night", builtIn: true, colors: tokyoNight },
5460
+ { id: "one-dark-pro", name: "One Dark Pro", builtIn: true, colors: oneDarkPro },
5461
+ { id: "solarized-dark", name: "Solarized Dark", builtIn: true, colors: solarizedDark },
5462
+ { id: "solarized-light", name: "Solarized Light", builtIn: true, colors: solarizedLight },
5463
+ { id: "monokai", name: "Monokai", builtIn: true, colors: monokai },
5464
+ { id: "nord", name: "Nord", builtIn: true, colors: nord },
5465
+ { id: "gruvbox-dark", name: "Gruvbox Dark", builtIn: true, colors: gruvboxDark },
5466
+ { id: "gruvbox-light", name: "Gruvbox Light", builtIn: true, colors: gruvboxLight },
5467
+ { id: "github-dark", name: "GitHub Dark", builtIn: true, colors: githubDark },
5468
+ { id: "rose-pine", name: "Ros\xE9 Pine", builtIn: true, colors: rosePine },
5469
+ { id: "ayu-dark", name: "Ayu Dark", builtIn: true, colors: ayuDark }
5470
+ ];
5471
+ var DEFAULT_THEME_ID = "dark";
5472
+ function getBuiltInTheme(id) {
5473
+ return BUILT_IN_THEMES.find((t) => t.id === id);
5474
+ }
5475
+ function themeToInlineStyle(colors) {
5476
+ return THEME_VARIABLES.map((v) => `--${v}:${colors[v]}`).join(";");
5477
+ }
5478
+
5479
+ // src/themes/config.ts
5480
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
5481
+ import { homedir as homedir3 } from "os";
5482
+ import { join as join7 } from "path";
5483
+ var CONFIG_DIR2 = join7(homedir3(), ".glassbox");
5484
+ var CONFIG_PATH2 = join7(CONFIG_DIR2, "config.json");
5485
+ var THEMES_DIR = join7(CONFIG_DIR2, "themes");
5486
+ function readConfigFile2() {
5487
+ try {
5488
+ if (existsSync6(CONFIG_PATH2)) {
5489
+ return JSON.parse(readFileSync8(CONFIG_PATH2, "utf-8"));
5490
+ }
5491
+ } catch {
5492
+ }
5493
+ return {};
5494
+ }
5495
+ function writeConfigFile2(config) {
5496
+ mkdirSync5(CONFIG_DIR2, { recursive: true });
5497
+ writeFileSync5(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
5498
+ }
5499
+ function getActiveThemeId() {
5500
+ const config = readConfigFile2();
5501
+ const theme = config.theme;
5502
+ const active = theme?.active;
5503
+ return active ?? DEFAULT_THEME_ID;
5504
+ }
5505
+ function setActiveThemeId(id) {
5506
+ const config = readConfigFile2();
5507
+ if (config.theme === void 0) config.theme = {};
5508
+ config.theme.active = id;
5509
+ writeConfigFile2(config);
5510
+ }
5511
+ function loadCustomThemes() {
5512
+ if (!existsSync6(THEMES_DIR)) return [];
5513
+ const themes = [];
5514
+ try {
5515
+ const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
5516
+ for (const file of files) {
5517
+ try {
5518
+ const data = JSON.parse(readFileSync8(join7(THEMES_DIR, file), "utf-8"));
5519
+ if (data.id !== void 0 && data.id !== "" && data.name !== void 0 && data.name !== "" && data.colors !== void 0) {
5520
+ themes.push({ id: data.id, name: data.name, colors: data.colors, builtIn: false, baseTheme: data.baseTheme ?? "" });
5521
+ }
5522
+ } catch {
5523
+ }
5524
+ }
5525
+ } catch {
5526
+ }
5527
+ return themes;
5528
+ }
5529
+ function saveCustomTheme(theme) {
5530
+ mkdirSync5(THEMES_DIR, { recursive: true });
5531
+ const filePath = join7(THEMES_DIR, `${theme.id}.json`);
5532
+ writeFileSync5(filePath, JSON.stringify(theme, null, 2), "utf-8");
5533
+ }
5534
+ function deleteCustomTheme(id) {
5535
+ const filePath = join7(THEMES_DIR, `${id}.json`);
5536
+ if (existsSync6(filePath)) {
5537
+ unlinkSync2(filePath);
5538
+ }
5539
+ }
5540
+ function getCustomTheme(id) {
5541
+ const filePath = join7(THEMES_DIR, `${id}.json`);
5542
+ if (!existsSync6(filePath)) return void 0;
5543
+ try {
5544
+ const data = JSON.parse(readFileSync8(filePath, "utf-8"));
5545
+ return { ...data, builtIn: false };
5546
+ } catch {
5547
+ return void 0;
5548
+ }
5549
+ }
5550
+ function getAllThemes() {
5551
+ return [...BUILT_IN_THEMES, ...loadCustomThemes()];
5552
+ }
5553
+ function resolveTheme(id) {
5554
+ return getBuiltInTheme(id) ?? getCustomTheme(id);
5555
+ }
5556
+ function getActiveThemeColors() {
5557
+ const id = getActiveThemeId();
5558
+ const theme = resolveTheme(id);
5559
+ if (theme) return theme.colors;
5560
+ const fallback = getBuiltInTheme(DEFAULT_THEME_ID);
5561
+ if (fallback === void 0) throw new Error(`Default theme '${DEFAULT_THEME_ID}' not found`);
5562
+ return fallback.colors;
5563
+ }
5564
+ function generateThemeId() {
5565
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
5566
+ }
5567
+
4973
5568
  // src/components/layout.tsx
4974
5569
  function Layout({ title, reviewId, children }) {
4975
- return /* @__PURE__ */ jsx("html", { lang: "en", children: [
5570
+ const themeId = getActiveThemeId();
5571
+ const themeColors = getActiveThemeColors();
5572
+ const themeStyle = themeToInlineStyle(themeColors);
5573
+ return /* @__PURE__ */ jsx("html", { lang: "en", style: themeStyle, "data-theme": themeId, children: [
4976
5574
  /* @__PURE__ */ jsx("head", { children: [
4977
5575
  /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
4978
5576
  /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
@@ -5053,7 +5651,7 @@ function ReviewHistory({ reviews, currentReviewId }) {
5053
5651
 
5054
5652
  // src/routes/pages.tsx
5055
5653
  init_queries();
5056
- var pageRoutes = new Hono3();
5654
+ var pageRoutes = new Hono5();
5057
5655
  pageRoutes.get("/", async (c) => {
5058
5656
  const reviewId = c.get("reviewId");
5059
5657
  const review = await getReview(reviewId);
@@ -5207,11 +5805,11 @@ pageRoutes.get("/file/:fileId", async (c) => {
5207
5805
  });
5208
5806
  pageRoutes.get("/file-raw", (c) => {
5209
5807
  const filePath = c.req.query("path");
5210
- if (!filePath) return c.text("Missing path", 400);
5808
+ if (filePath === void 0 || filePath === "") return c.text("Missing path", 400);
5211
5809
  const repoRoot = c.get("repoRoot");
5212
5810
  let content;
5213
5811
  try {
5214
- content = readFileSync8(resolve4(repoRoot, filePath), "utf-8");
5812
+ content = readFileSync9(resolve4(repoRoot, filePath), "utf-8");
5215
5813
  } catch {
5216
5814
  return c.text("File not found", 404);
5217
5815
  }
@@ -5234,7 +5832,7 @@ pageRoutes.get("/file-raw", (c) => {
5234
5832
  }))
5235
5833
  }]
5236
5834
  };
5237
- const fakeFile = { id: "", review_id: "", file_path: filePath, status: "reviewed", diff_data: null };
5835
+ const fakeFile = { id: "", review_id: "", file_path: filePath, status: "reviewed", diff_data: null, created_at: "" };
5238
5836
  const html = /* @__PURE__ */ jsx(DiffView, { file: fakeFile, diff, annotations: [], mode: "unified" });
5239
5837
  return c.html(html.toString());
5240
5838
  });
@@ -5337,6 +5935,108 @@ pageRoutes.get("/history", async (c) => {
5337
5935
  return c.html(html.toString());
5338
5936
  });
5339
5937
 
5938
+ // src/routes/theme-api.ts
5939
+ import { Hono as Hono6 } from "hono";
5940
+ var themeApiRoutes = new Hono6();
5941
+ themeApiRoutes.get("/", (c) => {
5942
+ const themes = getAllThemes();
5943
+ const activeId = getActiveThemeId();
5944
+ return c.json({
5945
+ themes: themes.map((t) => ({
5946
+ id: t.id,
5947
+ name: t.name,
5948
+ builtIn: t.builtIn,
5949
+ colors: t.colors
5950
+ })),
5951
+ activeId
5952
+ });
5953
+ });
5954
+ themeApiRoutes.get("/active", (c) => {
5955
+ const id = getActiveThemeId();
5956
+ const colors = getActiveThemeColors();
5957
+ return c.json({ id, colors });
5958
+ });
5959
+ themeApiRoutes.post("/active", async (c) => {
5960
+ const body = await c.req.json();
5961
+ if (!body.id) return c.json({ error: "Missing theme id" }, 400);
5962
+ const theme = resolveTheme(body.id);
5963
+ if (!theme) return c.json({ error: "Theme not found" }, 404);
5964
+ setActiveThemeId(body.id);
5965
+ return c.json({ id: body.id, colors: theme.colors });
5966
+ });
5967
+ themeApiRoutes.post("/", async (c) => {
5968
+ const body = await c.req.json();
5969
+ if (!body.sourceId) return c.json({ error: "Missing sourceId" }, 400);
5970
+ const source = resolveTheme(body.sourceId);
5971
+ if (!source) return c.json({ error: "Source theme not found" }, 404);
5972
+ const baseTheme = source.builtIn ? source.id : source.baseTheme;
5973
+ const name = body.name ?? `${source.name} (Copy)`;
5974
+ const newTheme = {
5975
+ id: generateThemeId(),
5976
+ name,
5977
+ builtIn: false,
5978
+ baseTheme,
5979
+ colors: { ...source.colors }
5980
+ };
5981
+ saveCustomTheme(newTheme);
5982
+ return c.json(newTheme, 201);
5983
+ });
5984
+ themeApiRoutes.post("/:id/edit", async (c) => {
5985
+ const id = c.req.param("id");
5986
+ const body = await c.req.json();
5987
+ const source = resolveTheme(id);
5988
+ if (!source) return c.json({ error: "Theme not found" }, 404);
5989
+ if (source.builtIn) {
5990
+ const newTheme = {
5991
+ id: generateThemeId(),
5992
+ name: `${source.name} (Customized)`,
5993
+ builtIn: false,
5994
+ baseTheme: source.id,
5995
+ colors: body.colors ? { ...source.colors, ...body.colors } : { ...source.colors }
5996
+ };
5997
+ if (body.name !== void 0 && body.name !== "") newTheme.name = body.name;
5998
+ saveCustomTheme(newTheme);
5999
+ setActiveThemeId(newTheme.id);
6000
+ return c.json({ theme: newTheme, copied: true }, 201);
6001
+ }
6002
+ const updated = {
6003
+ ...source,
6004
+ name: body.name ?? source.name,
6005
+ colors: body.colors ? { ...source.colors, ...body.colors } : source.colors
6006
+ };
6007
+ saveCustomTheme(updated);
6008
+ return c.json({ theme: updated, copied: false });
6009
+ });
6010
+ themeApiRoutes.patch("/:id", async (c) => {
6011
+ const id = c.req.param("id");
6012
+ if (getBuiltInTheme(id)) {
6013
+ return c.json({ error: "Cannot edit built-in theme" }, 400);
6014
+ }
6015
+ const existing = resolveTheme(id);
6016
+ if (!existing || existing.builtIn) {
6017
+ return c.json({ error: "Theme not found" }, 404);
6018
+ }
6019
+ const body = await c.req.json();
6020
+ const updated = {
6021
+ ...existing,
6022
+ name: body.name ?? existing.name,
6023
+ colors: body.colors ? { ...existing.colors, ...body.colors } : existing.colors
6024
+ };
6025
+ saveCustomTheme(updated);
6026
+ return c.json(updated);
6027
+ });
6028
+ themeApiRoutes.delete("/:id", (c) => {
6029
+ const id = c.req.param("id");
6030
+ if (getBuiltInTheme(id)) {
6031
+ return c.json({ error: "Cannot delete built-in theme" }, 400);
6032
+ }
6033
+ deleteCustomTheme(id);
6034
+ if (getActiveThemeId() === id) {
6035
+ setActiveThemeId(BUILT_IN_THEMES[0].id);
6036
+ }
6037
+ return c.json({ ok: true });
6038
+ });
6039
+
5340
6040
  // src/server.ts
5341
6041
  function tryServe(fetch2, port) {
5342
6042
  return new Promise((resolve6, reject) => {
@@ -5354,7 +6054,7 @@ function tryServe(fetch2, port) {
5354
6054
  });
5355
6055
  }
5356
6056
  async function startServer(port, reviewId, repoRoot, options) {
5357
- const app = new Hono4();
6057
+ const app = new Hono7();
5358
6058
  app.use("*", async (c, next) => {
5359
6059
  c.set("reviewId", reviewId);
5360
6060
  c.set("currentReviewId", reviewId);
@@ -5362,24 +6062,25 @@ async function startServer(port, reviewId, repoRoot, options) {
5362
6062
  await next();
5363
6063
  });
5364
6064
  const selfDir = dirname(fileURLToPath(import.meta.url));
5365
- const distDir = existsSync6(join7(selfDir, "client", "styles.css")) ? join7(selfDir, "client") : join7(selfDir, "..", "dist", "client");
6065
+ const distDir = existsSync7(join8(selfDir, "client", "styles.css")) ? join8(selfDir, "client") : join8(selfDir, "..", "dist", "client");
5366
6066
  app.get("/static/styles.css", (c) => {
5367
- const css = readFileSync9(join7(distDir, "styles.css"), "utf-8");
6067
+ const css = readFileSync10(join8(distDir, "styles.css"), "utf-8");
5368
6068
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
5369
6069
  });
5370
6070
  app.get("/static/app.js", (c) => {
5371
- const js = readFileSync9(join7(distDir, "app.global.js"), "utf-8");
6071
+ const js = readFileSync10(join8(distDir, "app.global.js"), "utf-8");
5372
6072
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
5373
6073
  });
5374
6074
  app.get("/static/history.js", (c) => {
5375
- const js = readFileSync9(join7(distDir, "history.global.js"), "utf-8");
6075
+ const js = readFileSync10(join8(distDir, "history.global.js"), "utf-8");
5376
6076
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
5377
6077
  });
5378
6078
  app.route("/api", apiRoutes);
5379
6079
  app.route("/api/ai", aiApiRoutes);
6080
+ app.route("/api/themes", themeApiRoutes);
5380
6081
  app.route("/", pageRoutes);
5381
6082
  let actualPort = port;
5382
- if (options?.strictPort) {
6083
+ if (options?.strictPort === true) {
5383
6084
  actualPort = await tryServe(app.fetch, port);
5384
6085
  } else {
5385
6086
  for (let attempt = 0; attempt < 20; attempt++) {
@@ -5401,15 +6102,15 @@ async function startServer(port, reviewId, repoRoot, options) {
5401
6102
  console.log(`
5402
6103
  Glassbox running at ${url}
5403
6104
  `);
5404
- if (!options?.noOpen) {
6105
+ if (options?.noOpen !== true) {
5405
6106
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5406
6107
  exec(`${openCmd} ${url}`);
5407
6108
  }
5408
6109
  }
5409
6110
 
5410
6111
  // src/skills.ts
5411
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
5412
- import { join as join8 } from "path";
6112
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
6113
+ import { join as join9 } from "path";
5413
6114
  var SKILL_VERSION = 1;
5414
6115
  function versionHeader() {
5415
6116
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -5420,14 +6121,14 @@ function parseVersionHeader(content) {
5420
6121
  return parseInt(match[1], 10);
5421
6122
  }
5422
6123
  function updateFile(path, content) {
5423
- if (existsSync7(path)) {
5424
- const existing = readFileSync10(path, "utf-8");
6124
+ if (existsSync8(path)) {
6125
+ const existing = readFileSync11(path, "utf-8");
5425
6126
  const version = parseVersionHeader(existing);
5426
6127
  if (version !== null && version >= SKILL_VERSION) {
5427
6128
  return false;
5428
6129
  }
5429
6130
  }
5430
- writeFileSync5(path, content, "utf-8");
6131
+ writeFileSync6(path, content, "utf-8");
5431
6132
  return true;
5432
6133
  }
5433
6134
  function skillBody() {
@@ -5447,8 +6148,8 @@ function skillBody() {
5447
6148
  ].join("\n");
5448
6149
  }
5449
6150
  function ensureClaudeSkills(cwd) {
5450
- const dir = join8(cwd, ".claude", "skills", "glassbox");
5451
- mkdirSync5(dir, { recursive: true });
6151
+ const dir = join9(cwd, ".claude", "skills", "glassbox");
6152
+ mkdirSync6(dir, { recursive: true });
5452
6153
  const content = [
5453
6154
  "---",
5454
6155
  "name: glassbox",
@@ -5460,11 +6161,11 @@ function ensureClaudeSkills(cwd) {
5460
6161
  skillBody(),
5461
6162
  ""
5462
6163
  ].join("\n");
5463
- return updateFile(join8(dir, "SKILL.md"), content);
6164
+ return updateFile(join9(dir, "SKILL.md"), content);
5464
6165
  }
5465
6166
  function ensureCursorRules(cwd) {
5466
- const rulesDir = join8(cwd, ".cursor", "rules");
5467
- mkdirSync5(rulesDir, { recursive: true });
6167
+ const rulesDir = join9(cwd, ".cursor", "rules");
6168
+ mkdirSync6(rulesDir, { recursive: true });
5468
6169
  const content = [
5469
6170
  "---",
5470
6171
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -5475,11 +6176,11 @@ function ensureCursorRules(cwd) {
5475
6176
  skillBody(),
5476
6177
  ""
5477
6178
  ].join("\n");
5478
- return updateFile(join8(rulesDir, "glassbox.mdc"), content);
6179
+ return updateFile(join9(rulesDir, "glassbox.mdc"), content);
5479
6180
  }
5480
6181
  function ensureCopilotPrompts(cwd) {
5481
- const promptsDir = join8(cwd, ".github", "prompts");
5482
- mkdirSync5(promptsDir, { recursive: true });
6182
+ const promptsDir = join9(cwd, ".github", "prompts");
6183
+ mkdirSync6(promptsDir, { recursive: true });
5483
6184
  const content = [
5484
6185
  "---",
5485
6186
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -5489,11 +6190,11 @@ function ensureCopilotPrompts(cwd) {
5489
6190
  skillBody(),
5490
6191
  ""
5491
6192
  ].join("\n");
5492
- return updateFile(join8(promptsDir, "glassbox.prompt.md"), content);
6193
+ return updateFile(join9(promptsDir, "glassbox.prompt.md"), content);
5493
6194
  }
5494
6195
  function ensureWindsurfRules(cwd) {
5495
- const rulesDir = join8(cwd, ".windsurf", "rules");
5496
- mkdirSync5(rulesDir, { recursive: true });
6196
+ const rulesDir = join9(cwd, ".windsurf", "rules");
6197
+ mkdirSync6(rulesDir, { recursive: true });
5497
6198
  const content = [
5498
6199
  "---",
5499
6200
  "trigger: manual",
@@ -5504,39 +6205,39 @@ function ensureWindsurfRules(cwd) {
5504
6205
  skillBody(),
5505
6206
  ""
5506
6207
  ].join("\n");
5507
- return updateFile(join8(rulesDir, "glassbox.md"), content);
6208
+ return updateFile(join9(rulesDir, "glassbox.md"), content);
5508
6209
  }
5509
6210
  function ensureSkills() {
5510
6211
  const cwd = process.cwd();
5511
6212
  const platforms = [];
5512
- if (existsSync7(join8(cwd, ".claude"))) {
6213
+ if (existsSync8(join9(cwd, ".claude"))) {
5513
6214
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
5514
6215
  }
5515
- if (existsSync7(join8(cwd, ".cursor"))) {
6216
+ if (existsSync8(join9(cwd, ".cursor"))) {
5516
6217
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
5517
6218
  }
5518
- if (existsSync7(join8(cwd, ".github", "prompts")) || existsSync7(join8(cwd, ".github", "copilot-instructions.md"))) {
6219
+ if (existsSync8(join9(cwd, ".github", "prompts")) || existsSync8(join9(cwd, ".github", "copilot-instructions.md"))) {
5519
6220
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
5520
6221
  }
5521
- if (existsSync7(join8(cwd, ".windsurf"))) {
6222
+ if (existsSync8(join9(cwd, ".windsurf"))) {
5522
6223
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
5523
6224
  }
5524
6225
  return platforms;
5525
6226
  }
5526
6227
 
5527
6228
  // src/update-check.ts
5528
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
6229
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
5529
6230
  import { get } from "https";
5530
- import { homedir as homedir3 } from "os";
5531
- import { dirname as dirname2, join as join9 } from "path";
6231
+ import { homedir as homedir4 } from "os";
6232
+ import { dirname as dirname2, join as join10 } from "path";
5532
6233
  import { fileURLToPath as fileURLToPath2 } from "url";
5533
- var DATA_DIR = join9(homedir3(), ".glassbox");
5534
- var CHECK_FILE = join9(DATA_DIR, "last-update-check");
6234
+ var DATA_DIR = join10(homedir4(), ".glassbox");
6235
+ var CHECK_FILE = join10(DATA_DIR, "last-update-check");
5535
6236
  var PACKAGE_NAME = "glassbox";
5536
6237
  function getCurrentVersion() {
5537
6238
  try {
5538
6239
  const dir = dirname2(fileURLToPath2(import.meta.url));
5539
- const pkg = JSON.parse(readFileSync11(join9(dir, "..", "package.json"), "utf-8"));
6240
+ const pkg = JSON.parse(readFileSync12(join10(dir, "..", "package.json"), "utf-8"));
5540
6241
  return pkg.version;
5541
6242
  } catch {
5542
6243
  return "0.0.0";
@@ -5544,16 +6245,16 @@ function getCurrentVersion() {
5544
6245
  }
5545
6246
  function getLastCheckDate() {
5546
6247
  try {
5547
- if (existsSync8(CHECK_FILE)) {
5548
- return readFileSync11(CHECK_FILE, "utf-8").trim();
6248
+ if (existsSync9(CHECK_FILE)) {
6249
+ return readFileSync12(CHECK_FILE, "utf-8").trim();
5549
6250
  }
5550
6251
  } catch {
5551
6252
  }
5552
6253
  return null;
5553
6254
  }
5554
6255
  function saveCheckDate() {
5555
- mkdirSync6(DATA_DIR, { recursive: true });
5556
- writeFileSync6(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
6256
+ mkdirSync7(DATA_DIR, { recursive: true });
6257
+ writeFileSync7(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
5557
6258
  }
5558
6259
  function isFirstUseToday() {
5559
6260
  const last = getLastCheckDate();
@@ -5779,13 +6480,13 @@ async function main() {
5779
6480
  console.log("AI service test mode enabled \u2014 using mock AI responses");
5780
6481
  }
5781
6482
  if (debug) {
5782
- console.log(`[debug] Build timestamp: ${"2026-03-27T03:39:52.371Z"}`);
6483
+ console.log(`[debug] Build timestamp: ${"2026-04-02T08:06:45.351Z"}`);
5783
6484
  }
5784
- if (projectDir) {
6485
+ if (projectDir !== null) {
5785
6486
  process.chdir(projectDir);
5786
6487
  }
5787
6488
  if (dataDir === null) {
5788
- dataDir = join10(process.cwd(), ".glassbox");
6489
+ dataDir = join11(process.cwd(), ".glassbox");
5789
6490
  }
5790
6491
  if (demo !== null) {
5791
6492
  const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
@@ -5797,13 +6498,13 @@ async function main() {
5797
6498
  }
5798
6499
  process.exit(1);
5799
6500
  }
5800
- dataDir = join10(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
6501
+ dataDir = join11(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5801
6502
  setDemoMode(demo);
5802
6503
  console.log(`
5803
6504
  DEMO MODE: ${scenario.label}
5804
6505
  `);
5805
6506
  }
5806
- mkdirSync7(dataDir, { recursive: true });
6507
+ mkdirSync8(dataDir, { recursive: true });
5807
6508
  if (demo === null) {
5808
6509
  acquireLock(dataDir);
5809
6510
  }
@@ -5860,8 +6561,14 @@ async function main() {
5860
6561
  console.log(`Review ${review.id} created.`);
5861
6562
  await startServer(port, review.id, repoRoot, { noOpen, strictPort });
5862
6563
  }
5863
- main().catch((err) => {
5864
- console.error(err);
5865
- process.exit(1);
5866
- });
6564
+ var isDirectRun = process.argv[1]?.endsWith("cli.js") || process.argv[1]?.endsWith("cli.ts");
6565
+ if (isDirectRun) {
6566
+ main().catch((err) => {
6567
+ console.error(err);
6568
+ process.exit(1);
6569
+ });
6570
+ }
6571
+ export {
6572
+ parseArgs
6573
+ };
5867
6574
  //# sourceMappingURL=cli.js.map