glassbox 0.5.1 → 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 { execSync } 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;
@@ -525,33 +470,102 @@ function getKeyFromKeychain(platform) {
525
470
  const account = `${platform}-api-key`;
526
471
  try {
527
472
  if (os === "darwin") {
528
- const result = execSync(
529
- `security find-generic-password -s glassbox -a "${account}" -w 2>/dev/null`,
530
- { encoding: "utf-8" }
531
- ).trim();
532
- return result !== "" ? result : null;
473
+ const r = spawnSync("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
474
+ const result = r.stdout.trim();
475
+ return r.status === 0 && result !== "" ? result : null;
533
476
  }
534
477
  if (os === "linux") {
535
- const result = execSync(
536
- `secret-tool lookup service glassbox account "${account}" 2>/dev/null`,
537
- { encoding: "utf-8" }
538
- ).trim();
539
- return result !== "" ? result : null;
478
+ const r = spawnSync("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
479
+ const result = r.stdout.trim();
480
+ return r.status === 0 && result !== "" ? result : null;
540
481
  }
541
482
  if (os === "win32") {
542
483
  const target = winCredTarget(platform);
543
484
  const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
544
- const result = execSync("powershell -NoProfile -Command -", {
545
- input: script,
546
- encoding: "utf-8"
547
- }).trim();
548
- return result !== "" ? result : null;
485
+ const r = spawnSync("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
486
+ const result = r.stdout.trim();
487
+ return r.status === 0 && result !== "" ? result : null;
549
488
  }
550
489
  } catch {
551
490
  return null;
552
491
  }
553
492
  return null;
554
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
+ }
555
569
  function getKeyFromConfig(platform) {
556
570
  const config = readConfigFile();
557
571
  const encoded = config.ai?.keys?.[platform];
@@ -571,20 +585,6 @@ function resolveAPIKey(platform) {
571
585
  if (configKey !== null) return { key: configKey, source: "config" };
572
586
  return { key: null, source: null };
573
587
  }
574
- function loadAIConfig() {
575
- const config = readConfigFile();
576
- const platform = config.ai?.platform ?? "anthropic";
577
- const model = config.ai?.model ?? getDefaultModel(platform);
578
- const { key, source } = resolveAPIKey(platform);
579
- return { platform, model, apiKey: key, keySource: source };
580
- }
581
- function saveAIConfigPreferences(platform, model) {
582
- const config = readConfigFile();
583
- if (config.ai === void 0) config.ai = {};
584
- config.ai.platform = platform;
585
- config.ai.model = model;
586
- writeConfigFile(config);
587
- }
588
588
  function saveAPIKey(platform, key, storage) {
589
589
  if (storage === "keychain") {
590
590
  saveKeyToKeychain(platform, key);
@@ -596,50 +596,17 @@ function saveAPIKey(platform, key, storage) {
596
596
  writeConfigFile(config);
597
597
  }
598
598
  }
599
- function saveKeyToKeychain(platform, key) {
600
- const os = process.platform;
601
- const account = `${platform}-api-key`;
602
- if (os === "darwin") {
603
- try {
604
- execSync(`security delete-generic-password -s glassbox -a "${account}" 2>/dev/null`);
605
- } catch {
606
- }
607
- execSync(
608
- `security add-generic-password -s glassbox -a "${account}" -w "${key.replace(/"/g, '\\"')}"`
609
- );
610
- return;
611
- }
612
- if (os === "linux") {
613
- execSync(
614
- `secret-tool store --label='Glassbox API Key' service glassbox account "${account}"`,
615
- { input: key, encoding: "utf-8" }
616
- );
617
- return;
618
- }
619
- if (os === "win32") {
620
- const target = winCredTarget(platform);
621
- const escapedKey = key.replace(/'/g, "''");
622
- const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedKey}'`;
623
- execSync("powershell -NoProfile -Command -", {
624
- input: script,
625
- encoding: "utf-8"
626
- });
627
- }
628
- }
629
599
  function deleteAPIKey(platform) {
630
600
  const os = process.platform;
631
601
  const account = `${platform}-api-key`;
632
602
  try {
633
603
  if (os === "darwin") {
634
- execSync(`security delete-generic-password -s glassbox -a "${account}" 2>/dev/null`);
604
+ spawnSync2("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
635
605
  } else if (os === "linux") {
636
- execSync(`secret-tool clear service glassbox account "${account}" 2>/dev/null`);
606
+ spawnSync2("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
637
607
  } else if (os === "win32") {
638
608
  const target = winCredTarget(platform);
639
- execSync("powershell -NoProfile -Command -", {
640
- input: `cmdkey /delete:'${target}'`,
641
- encoding: "utf-8"
642
- });
609
+ spawnSync2("powershell", ["-NoProfile", "-Command", "-"], { input: `cmdkey /delete:'${target}'`, encoding: "utf-8" });
643
610
  }
644
611
  } catch {
645
612
  }
@@ -659,25 +626,40 @@ function detectAvailablePlatforms() {
659
626
  }
660
627
  return results;
661
628
  }
662
- function isKeychainAvailable() {
663
- const os = process.platform;
664
- if (os === "darwin" || os === "win32") return true;
665
- if (os === "linux") {
666
- try {
667
- execSync("which secret-tool 2>/dev/null", { encoding: "utf-8" });
668
- return true;
669
- } catch {
670
- return false;
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"));
671
637
  }
638
+ } catch {
672
639
  }
673
- return false;
640
+ return {};
674
641
  }
675
- function getKeychainLabel() {
676
- const os = process.platform;
677
- if (os === "darwin") return "Keychain";
678
- if (os === "linux") return "System Keyring";
679
- if (os === "win32") return "Credential Manager";
680
- 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);
681
663
  }
682
664
  function loadGuidedReviewConfig() {
683
665
  const config = readConfigFile();
@@ -1291,20 +1273,24 @@ async function setupAnnotations(fileIdMap) {
1291
1273
  }
1292
1274
 
1293
1275
  // src/git/diff.ts
1294
- import { execSync as execSync2 } from "child_process";
1276
+ import { spawnSync as spawnSync4 } from "child_process";
1295
1277
  import { readFileSync as readFileSync2 } from "fs";
1296
1278
  import { resolve } from "path";
1279
+
1280
+ // src/git/repo.ts
1281
+ import { spawnSync as spawnSync3 } from "child_process";
1297
1282
  function git(args, cwd) {
1298
- try {
1299
- return execSync2(`git ${args}`, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1300
- } catch (e) {
1301
- const err = e;
1302
- if (err.stdout !== void 0 && err.stdout !== "") return err.stdout;
1303
- throw e;
1304
- }
1283
+ const result = spawnSync3("git", args, { cwd, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
1284
+ if (result.status === 0) return result.stdout;
1285
+ if (result.stdout !== "") return result.stdout;
1286
+ const err = new Error(result.stderr);
1287
+ err.stdout = result.stdout;
1288
+ err.stderr = result.stderr;
1289
+ err.status = result.status;
1290
+ throw err;
1305
1291
  }
1306
1292
  function getRepoRoot(cwd) {
1307
- return git("rev-parse --show-toplevel", cwd).trim();
1293
+ return git(["rev-parse", "--show-toplevel"], cwd).trim();
1308
1294
  }
1309
1295
  function getRepoName(cwd) {
1310
1296
  const root = getRepoRoot(cwd);
@@ -1312,31 +1298,45 @@ function getRepoName(cwd) {
1312
1298
  }
1313
1299
  function isGitRepo(cwd) {
1314
1300
  try {
1315
- git("rev-parse --is-inside-work-tree", cwd);
1301
+ git(["rev-parse", "--is-inside-work-tree"], cwd);
1316
1302
  return true;
1317
1303
  } catch {
1318
1304
  return false;
1319
1305
  }
1320
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
+ }
1321
1322
  function getDiffArgs(mode) {
1322
1323
  switch (mode.type) {
1323
1324
  case "uncommitted":
1324
- return "diff HEAD";
1325
+ return ["diff", "HEAD"];
1325
1326
  case "staged":
1326
- return "diff --cached";
1327
+ return ["diff", "--cached"];
1327
1328
  case "unstaged":
1328
- return "diff";
1329
+ return ["diff"];
1329
1330
  case "commit":
1330
- return `diff ${mode.sha}~1 ${mode.sha}`;
1331
+ return ["diff", `${mode.sha}~1`, mode.sha];
1331
1332
  case "range":
1332
- return `diff ${mode.from} ${mode.to}`;
1333
- case "branch": {
1334
- return `diff ${mode.name}...HEAD`;
1335
- }
1333
+ return ["diff", mode.from, mode.to];
1334
+ case "branch":
1335
+ return ["diff", `${mode.name}...HEAD`];
1336
1336
  case "files":
1337
- return `diff HEAD -- ${mode.patterns.join(" ")}`;
1337
+ return ["diff", "HEAD", "--", ...mode.patterns];
1338
1338
  case "all":
1339
- return "diff --no-index /dev/null .";
1339
+ return ["diff", "--no-index", "/dev/null", "."];
1340
1340
  }
1341
1341
  }
1342
1342
  function getFileDiffs(mode, cwd) {
@@ -1347,13 +1347,13 @@ function getFileDiffs(mode, cwd) {
1347
1347
  const diffArgs = getDiffArgs(mode);
1348
1348
  let rawDiff;
1349
1349
  try {
1350
- rawDiff = git(`${diffArgs} -U3`, repoRoot);
1350
+ rawDiff = git2([...diffArgs, "-U3"], repoRoot);
1351
1351
  } catch {
1352
1352
  rawDiff = "";
1353
1353
  }
1354
1354
  const diffs = parseDiff(rawDiff);
1355
1355
  if (mode.type === "uncommitted") {
1356
- const untracked = git("ls-files --others --exclude-standard", repoRoot).trim();
1356
+ const untracked = git2(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
1357
1357
  if (untracked) {
1358
1358
  for (const file of untracked.split("\n").filter(Boolean)) {
1359
1359
  if (!diffs.some((d) => d.filePath === file)) {
@@ -1365,7 +1365,7 @@ function getFileDiffs(mode, cwd) {
1365
1365
  return diffs;
1366
1366
  }
1367
1367
  function getAllFiles(repoRoot) {
1368
- const files = git("ls-files", repoRoot).trim().split("\n").filter(Boolean);
1368
+ const files = git2(["ls-files"], repoRoot).trim().split("\n").filter(Boolean);
1369
1369
  return files.map((file) => createNewFileDiff(file, repoRoot));
1370
1370
  }
1371
1371
  function createNewFileDiff(filePath, repoRoot) {
@@ -1492,16 +1492,13 @@ function getFileContent(filePath, ref, cwd) {
1492
1492
  const repoRoot = getRepoRoot(cwd);
1493
1493
  try {
1494
1494
  if (ref === "working") {
1495
- return execSync2(`cat "${resolve(repoRoot, filePath)}"`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
1495
+ return readFileSync2(resolve(repoRoot, filePath), "utf-8");
1496
1496
  }
1497
- return git(`show ${ref}:${filePath}`, repoRoot);
1497
+ return git2(["show", `${ref}:${filePath}`], repoRoot);
1498
1498
  } catch {
1499
1499
  return "";
1500
1500
  }
1501
1501
  }
1502
- function getHeadCommit(cwd) {
1503
- return execSync2("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
1504
- }
1505
1502
  function parseModeString(modeStr) {
1506
1503
  if (modeStr === "uncommitted") return { type: "uncommitted" };
1507
1504
  if (modeStr === "staged") return { type: "staged" };
@@ -1521,9 +1518,12 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
1521
1518
  return createNewFileDiff(filePath, repoRoot);
1522
1519
  }
1523
1520
  const diffArgs = getDiffArgs(mode);
1521
+ const args = [...diffArgs, "-U3"];
1522
+ if (extraFlags) args.push(...extraFlags.split(" ").filter(Boolean));
1523
+ args.push("--", filePath);
1524
1524
  let rawDiff;
1525
1525
  try {
1526
- rawDiff = git(`${diffArgs} -U3 ${extraFlags} -- ${filePath}`, repoRoot);
1526
+ rawDiff = git2(args, repoRoot);
1527
1527
  } catch {
1528
1528
  rawDiff = "";
1529
1529
  }
@@ -1595,7 +1595,9 @@ function acquireLock(dataDir) {
1595
1595
  }
1596
1596
  }
1597
1597
  writeFileSync2(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
1598
- const cleanup = () => releaseLock();
1598
+ const cleanup = () => {
1599
+ releaseLock();
1600
+ };
1599
1601
  process.on("exit", cleanup);
1600
1602
  process.on("SIGINT", () => {
1601
1603
  cleanup();
@@ -1607,7 +1609,7 @@ function acquireLock(dataDir) {
1607
1609
  });
1608
1610
  }
1609
1611
  function releaseLock() {
1610
- if (lockPath) {
1612
+ if (lockPath !== null) {
1611
1613
  try {
1612
1614
  rmSync2(lockPath, { force: true });
1613
1615
  } catch {
@@ -1723,12 +1725,15 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
1723
1725
  // src/server.ts
1724
1726
  import { serve } from "@hono/node-server";
1725
1727
  import { exec } from "child_process";
1726
- import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
1727
- import { Hono as Hono4 } from "hono";
1728
- 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";
1729
1731
  import { fileURLToPath } from "url";
1730
1732
 
1731
1733
  // src/routes/ai-api.ts
1734
+ import { Hono as Hono3 } from "hono";
1735
+
1736
+ // src/routes/ai-analysis.ts
1732
1737
  import { Hono } from "hono";
1733
1738
 
1734
1739
  // src/ai/client.ts
@@ -2558,63 +2563,11 @@ async function mockGuidedAnalysisBatch(files) {
2558
2563
  }));
2559
2564
  }
2560
2565
 
2561
- // src/routes/ai-api.ts
2566
+ // src/routes/ai-analysis.ts
2562
2567
  init_queries();
2563
- var aiApiRoutes = new Hono();
2568
+ var aiAnalysisRoutes = new Hono();
2564
2569
  var cancelledAnalyses = /* @__PURE__ */ new Set();
2565
- aiApiRoutes.get("/config", (c) => {
2566
- const config = loadAIConfig();
2567
- return c.json({
2568
- platform: config.platform,
2569
- model: config.model,
2570
- keyConfigured: config.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
2571
- keySource: config.keySource,
2572
- guidedReview: loadGuidedReviewConfig()
2573
- });
2574
- });
2575
- aiApiRoutes.post("/config", async (c) => {
2576
- const body = await c.req.json();
2577
- saveAIConfigPreferences(body.platform, body.model);
2578
- if (body.guidedReview !== void 0) {
2579
- saveGuidedReviewConfig(body.guidedReview);
2580
- }
2581
- return c.json({ ok: true });
2582
- });
2583
- aiApiRoutes.get("/models", (c) => {
2584
- return c.json({
2585
- platforms: PLATFORMS,
2586
- models: MODELS
2587
- });
2588
- });
2589
- aiApiRoutes.get("/key-status", (c) => {
2590
- const platforms = ["anthropic", "openai", "google"];
2591
- const status = {};
2592
- for (const platform of platforms) {
2593
- const { source } = resolveAPIKey(platform);
2594
- status[platform] = { configured: source !== null, source };
2595
- }
2596
- return c.json({
2597
- status,
2598
- keychainAvailable: isKeychainAvailable(),
2599
- keychainLabel: getKeychainLabel(),
2600
- availablePlatforms: detectAvailablePlatforms()
2601
- });
2602
- });
2603
- aiApiRoutes.post("/key", async (c) => {
2604
- const body = await c.req.json();
2605
- saveAPIKey(
2606
- body.platform,
2607
- body.key,
2608
- body.storage
2609
- );
2610
- return c.json({ ok: true });
2611
- });
2612
- aiApiRoutes.delete("/key", (c) => {
2613
- const platform = c.req.query("platform") ?? "anthropic";
2614
- deleteAPIKey(platform);
2615
- return c.json({ ok: true });
2616
- });
2617
- aiApiRoutes.post("/analyze", async (c) => {
2570
+ aiAnalysisRoutes.post("/analyze", async (c) => {
2618
2571
  const reviewId = c.req.query("reviewId") ?? "";
2619
2572
  const repoRoot = c.get("repoRoot");
2620
2573
  const body = await c.req.json();
@@ -2866,7 +2819,7 @@ async function runBatchedGuidedAnalysis(analysisId, batches, allFiles, config, r
2866
2819
  "guided"
2867
2820
  );
2868
2821
  }
2869
- aiApiRoutes.get("/analysis/:type", async (c) => {
2822
+ aiAnalysisRoutes.get("/analysis/:type", async (c) => {
2870
2823
  const reviewId = c.req.query("reviewId") ?? "";
2871
2824
  const analysisType = c.req.param("type");
2872
2825
  const analysis = await getLatestAnalysis(reviewId, analysisType);
@@ -2898,7 +2851,7 @@ aiApiRoutes.get("/analysis/:type", async (c) => {
2898
2851
  }))
2899
2852
  });
2900
2853
  });
2901
- aiApiRoutes.get("/analysis/:type/status", async (c) => {
2854
+ aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
2902
2855
  const reviewId = c.req.query("reviewId") ?? "";
2903
2856
  const analysisType = c.req.param("type");
2904
2857
  const analysis = await getLatestAnalysis(reviewId, analysisType);
@@ -2922,35 +2875,96 @@ aiApiRoutes.get("/analysis/:type/status", async (c) => {
2922
2875
  progressTotal: analysis.progress_total
2923
2876
  });
2924
2877
  });
2925
- aiApiRoutes.get("/debug-status", (c) => {
2878
+ aiAnalysisRoutes.get("/debug-status", (c) => {
2926
2879
  return c.json({ enabled: isDebug() });
2927
2880
  });
2928
- aiApiRoutes.post("/debug-log", async (c) => {
2881
+ aiAnalysisRoutes.post("/debug-log", async (c) => {
2929
2882
  if (!isDebug()) return c.json({ ok: true });
2930
2883
  const body = await c.req.json();
2931
2884
  debugLog(`[client] ${body.message}`);
2932
2885
  return c.json({ ok: true });
2933
2886
  });
2934
- aiApiRoutes.get("/preferences", async (c) => {
2887
+ aiAnalysisRoutes.get("/preferences", async (c) => {
2935
2888
  const prefs = await getUserPreferences();
2936
2889
  return c.json(prefs);
2937
2890
  });
2938
- aiApiRoutes.post("/preferences", async (c) => {
2891
+ aiAnalysisRoutes.post("/preferences", async (c) => {
2939
2892
  const body = await c.req.json();
2940
2893
  await saveUserPreferences(body);
2941
2894
  return c.json({ ok: true });
2942
2895
  });
2943
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
+
2944
2958
  // src/routes/api.ts
2945
2959
  init_queries();
2946
- import { execSync as execSync5 } from "child_process";
2960
+ import { execFileSync, spawnSync as spawnSync7 } from "child_process";
2947
2961
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2948
- import { Hono as Hono2 } from "hono";
2962
+ import { Hono as Hono4 } from "hono";
2949
2963
  import { join as join6, resolve as resolve3 } from "path";
2950
2964
 
2951
2965
  // src/export/generate.ts
2952
2966
  init_queries();
2953
- import { execSync as execSync3 } from "child_process";
2967
+ import { spawnSync as spawnSync5 } from "child_process";
2954
2968
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2955
2969
  import { homedir as homedir2 } from "os";
2956
2970
  import { join as join4 } from "path";
@@ -2969,12 +2983,8 @@ function saveDismissals(data) {
2969
2983
  writeFileSync3(DISMISS_FILE, JSON.stringify(data), "utf-8");
2970
2984
  }
2971
2985
  function isGlassboxGitignored(repoRoot) {
2972
- try {
2973
- execSync3("git check-ignore -q .glassbox", { cwd: repoRoot, stdio: "pipe" });
2974
- return true;
2975
- } catch {
2976
- return false;
2977
- }
2986
+ const result = spawnSync5("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
2987
+ return result.status === 0;
2978
2988
  }
2979
2989
  function shouldPromptGitignore(repoRoot) {
2980
2990
  if (isGlassboxGitignored(repoRoot)) return false;
@@ -3084,142 +3094,56 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
3084
3094
  writeFileSync3(archivePath, content, "utf-8");
3085
3095
  if (isCurrent) {
3086
3096
  const latestPath = join4(exportDir, "latest-review.md");
3087
- writeFileSync3(latestPath, content, "utf-8");
3088
- return latestPath;
3089
- }
3090
- return archivePath;
3091
- }
3092
-
3093
- // src/export/auto-export.ts
3094
- var debounceTimer = null;
3095
- var DEBOUNCE_MS = 2e3;
3096
- function scheduleAutoExport(reviewId, repoRoot) {
3097
- if (debounceTimer !== null) clearTimeout(debounceTimer);
3098
- debounceTimer = setTimeout(() => {
3099
- debounceTimer = null;
3100
- void generateReviewExport(reviewId, repoRoot, true);
3101
- }, DEBOUNCE_MS);
3102
- }
3103
-
3104
- // src/git/image.ts
3105
- import { execSync as execSync4 } from "child_process";
3106
- import { readFileSync as readFileSync5 } from "fs";
3107
- import { resolve as resolve2 } from "path";
3108
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
3109
- function isImageFile(filePath) {
3110
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3111
- return IMAGE_EXTENSIONS.has(ext);
3112
- }
3113
- function isSvgFile(filePath) {
3114
- return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3115
- }
3116
- function getContentType(filePath) {
3117
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3118
- switch (ext) {
3119
- case ".png":
3120
- return "image/png";
3121
- case ".jpg":
3122
- case ".jpeg":
3123
- return "image/jpeg";
3124
- case ".gif":
3125
- return "image/gif";
3126
- case ".webp":
3127
- return "image/webp";
3128
- case ".svg":
3129
- return "image/svg+xml";
3130
- default:
3131
- return "application/octet-stream";
3132
- }
3133
- }
3134
- function getOldRef(mode) {
3135
- switch (mode.type) {
3136
- case "uncommitted":
3137
- return "HEAD";
3138
- case "staged":
3139
- return "HEAD";
3140
- case "unstaged":
3141
- return null;
3142
- // old = index, use ':'
3143
- case "commit":
3144
- return `${mode.sha}~1`;
3145
- case "range":
3146
- return mode.from;
3147
- case "branch":
3148
- return mode.name;
3149
- case "files":
3150
- return "HEAD";
3151
- case "all":
3152
- return null;
3153
- }
3154
- }
3155
- function getNewRef(mode) {
3156
- switch (mode.type) {
3157
- case "uncommitted":
3158
- return null;
3159
- // working tree
3160
- case "staged":
3161
- return null;
3162
- // index, but git show : works
3163
- case "unstaged":
3164
- return null;
3165
- // working tree
3166
- case "commit":
3167
- return mode.sha;
3168
- case "range":
3169
- return mode.to;
3170
- case "branch":
3171
- return "HEAD";
3172
- case "files":
3173
- return null;
3174
- case "all":
3175
- return null;
3097
+ writeFileSync3(latestPath, content, "utf-8");
3098
+ return latestPath;
3176
3099
  }
3100
+ return archivePath;
3177
3101
  }
3178
- function gitShowFile(ref, filePath, repoRoot) {
3179
- try {
3180
- const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
3181
- return execSync4(`git show "${spec}"`, { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
3182
- } catch {
3183
- return null;
3184
- }
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);
3185
3112
  }
3186
- function readWorkingFile(filePath, repoRoot) {
3187
- try {
3188
- return readFileSync5(resolve2(repoRoot, filePath));
3189
- } catch {
3190
- return null;
3191
- }
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);
3192
3124
  }
3193
- function getOldImage(mode, filePath, oldPath, repoRoot) {
3194
- const ref = getOldRef(mode);
3195
- const path = oldPath ?? filePath;
3196
- if (ref === null) {
3197
- const data2 = readWorkingFile(path, repoRoot);
3198
- if (!data2) return null;
3199
- return { data: data2, size: data2.length };
3200
- }
3201
- const actualRef = mode.type === "unstaged" ? ":" : ref;
3202
- const data = gitShowFile(actualRef, path, repoRoot);
3203
- if (!data) return null;
3204
- return { data, size: data.length };
3125
+ function isSvgFile(filePath) {
3126
+ return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
3205
3127
  }
3206
- function getNewImage(mode, filePath, repoRoot) {
3207
- const ref = getNewRef(mode);
3208
- if (ref === null) {
3209
- if (mode.type === "staged") {
3210
- const data3 = gitShowFile(":", filePath, repoRoot);
3211
- if (!data3) return null;
3212
- return { data: data3, size: data3.length };
3213
- }
3214
- const data2 = readWorkingFile(filePath, repoRoot);
3215
- if (!data2) return null;
3216
- 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";
3217
3144
  }
3218
- const data = gitShowFile(ref, filePath, repoRoot);
3219
- if (!data) return null;
3220
- return { data, size: data.length };
3221
3145
  }
3222
- async function extractMetadata(data, filePath) {
3146
+ function extractMetadata(data, filePath) {
3223
3147
  const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3224
3148
  if (ext === ".svg") {
3225
3149
  const text = data.toString("utf-8");
@@ -3261,9 +3185,9 @@ function formatMetadataLines(meta) {
3261
3185
  lines.push(`Dimensions: ${meta.width} \xD7 ${meta.height}`);
3262
3186
  }
3263
3187
  lines.push(`File size: ${formatBytes(meta.fileSize)}`);
3264
- if (meta.colorSpace) lines.push(`Color space: ${meta.colorSpace}`);
3188
+ if (meta.colorSpace !== null) lines.push(`Color space: ${meta.colorSpace}`);
3265
3189
  if (meta.channels !== null) lines.push(`Channels: ${meta.channels}`);
3266
- if (meta.depth) lines.push(`Bit depth: ${meta.depth}`);
3190
+ if (meta.depth !== null) lines.push(`Bit depth: ${meta.depth}`);
3267
3191
  if (meta.hasAlpha !== null) lines.push(`Alpha: ${meta.hasAlpha ? "yes" : "no"}`);
3268
3192
  if (meta.density !== null) lines.push(`Density: ${meta.density} DPI`);
3269
3193
  if (meta.exif) {
@@ -3404,7 +3328,95 @@ function parseWebp(data) {
3404
3328
  width = (data[24] | data[25] << 8 | data[26] << 16) + 1;
3405
3329
  height = (data[27] | data[28] << 8 | data[29] << 16) + 1;
3406
3330
  }
3407
- 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 };
3408
3420
  }
3409
3421
 
3410
3422
  // src/git/svg-rasterize.ts
@@ -3439,63 +3451,62 @@ function loadSystemFonts() {
3439
3451
  return buffers;
3440
3452
  }
3441
3453
  function getFontCandidates() {
3442
- switch (process.platform) {
3443
- case "darwin": {
3444
- const sys = "/System/Library/Fonts";
3445
- const sup = "/System/Library/Fonts/Supplemental";
3446
- return [
3447
- // Core system fonts (serif, sans-serif, monospace)
3448
- join5(sys, "Helvetica.ttc"),
3449
- join5(sys, "Times.ttc"),
3450
- join5(sys, "Courier.ttc"),
3451
- join5(sys, "Menlo.ttc"),
3452
- join5(sys, "SFPro.ttf"),
3453
- join5(sys, "SFNS.ttf"),
3454
- join5(sys, "SFNSMono.ttf"),
3455
- // Supplemental (common named fonts in SVGs)
3456
- join5(sup, "Arial.ttf"),
3457
- join5(sup, "Arial Bold.ttf"),
3458
- join5(sup, "Georgia.ttf"),
3459
- join5(sup, "Verdana.ttf"),
3460
- join5(sup, "Tahoma.ttf"),
3461
- join5(sup, "Trebuchet MS.ttf"),
3462
- join5(sup, "Impact.ttf"),
3463
- join5(sup, "Comic Sans MS.ttf"),
3464
- join5(sup, "Courier New.ttf"),
3465
- join5(sup, "Times New Roman.ttf")
3466
- ];
3467
- }
3468
- case "linux":
3469
- return [
3470
- // DejaVu (most common Linux fallback)
3471
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
3472
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
3473
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
3474
- "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
3475
- // Liberation (metric-compatible with Arial/Times/Courier)
3476
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
3477
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
3478
- "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
3479
- // Noto (common on modern distros)
3480
- "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
3481
- ];
3482
- case "win32": {
3483
- const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
3484
- return [
3485
- join5(winFonts, "arial.ttf"),
3486
- join5(winFonts, "arialbd.ttf"),
3487
- join5(winFonts, "times.ttf"),
3488
- join5(winFonts, "cour.ttf"),
3489
- join5(winFonts, "verdana.ttf"),
3490
- join5(winFonts, "tahoma.ttf"),
3491
- join5(winFonts, "georgia.ttf"),
3492
- join5(winFonts, "consola.ttf"),
3493
- join5(winFonts, "segoeui.ttf")
3494
- ];
3495
- }
3496
- default:
3497
- 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
+ ];
3498
3508
  }
3509
+ return [];
3499
3510
  }
3500
3511
  function parseSvgDimensions(svg) {
3501
3512
  const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
@@ -3859,7 +3870,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
3859
3870
  }
3860
3871
 
3861
3872
  // src/routes/api.ts
3862
- var apiRoutes = new Hono2();
3873
+ var apiRoutes = new Hono4();
3863
3874
  function resolveReviewId(c) {
3864
3875
  return c.req.query("reviewId") ?? c.get("reviewId");
3865
3876
  }
@@ -3976,11 +3987,11 @@ apiRoutes.post("/files/:fileId/reveal", async (c) => {
3976
3987
  const fullPath = resolve3(repoRoot, file.file_path);
3977
3988
  try {
3978
3989
  if (process.platform === "darwin") {
3979
- execSync5(`open -R "${fullPath}"`);
3990
+ execFileSync("open", ["-R", fullPath]);
3980
3991
  } else if (process.platform === "win32") {
3981
- execSync5(`explorer /select,"${fullPath}"`);
3992
+ execFileSync("explorer", ["/select," + fullPath]);
3982
3993
  } else {
3983
- execSync5(`xdg-open "${resolve3(fullPath, "..")}"`);
3994
+ execFileSync("xdg-open", [resolve3(fullPath, "..")]);
3984
3995
  }
3985
3996
  } catch {
3986
3997
  }
@@ -4062,7 +4073,7 @@ apiRoutes.get("/outline/:fileId", async (c) => {
4062
4073
  apiRoutes.get("/symbol-definition", async (c) => {
4063
4074
  const name = c.req.query("name");
4064
4075
  const currentFileId = c.req.query("currentFileId");
4065
- if (!name) return c.json({ definitions: [] });
4076
+ if (name === void 0 || name === "") return c.json({ definitions: [] });
4066
4077
  const reviewId = resolveReviewId(c);
4067
4078
  const repoRoot = c.get("repoRoot");
4068
4079
  const definitions = [];
@@ -4084,7 +4095,7 @@ apiRoutes.get("/symbol-definition", async (c) => {
4084
4095
  }
4085
4096
  if (definitions.length === 0) {
4086
4097
  try {
4087
- const allFiles = execSync5("git ls-files", { cwd: repoRoot, encoding: "utf-8" }).trim().split("\n").filter(Boolean);
4098
+ const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
4088
4099
  for (const filePath of allFiles) {
4089
4100
  if (searchedPaths.has(filePath)) continue;
4090
4101
  const ext = filePath.slice(filePath.lastIndexOf("."));
@@ -4119,7 +4130,7 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
4119
4130
  if (sym.name === targetName) {
4120
4131
  out.push({ fileId, filePath, name: sym.name, kind: sym.kind, line: sym.line });
4121
4132
  }
4122
- if (sym.children?.length > 0) {
4133
+ if (sym.children.length > 0) {
4123
4134
  collectDefinitions(sym.children, targetName, fileId, filePath, out);
4124
4135
  }
4125
4136
  }
@@ -4180,10 +4191,8 @@ apiRoutes.get("/image/:fileId/metadata", async (c) => {
4180
4191
  const status = diff.status ?? "modified";
4181
4192
  const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
4182
4193
  const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
4183
- const [oldMeta, newMeta] = await Promise.all([
4184
- oldImage ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null,
4185
- newImage ? extractMetadata(newImage.data, file.file_path) : null
4186
- ]);
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;
4187
4196
  return c.json({
4188
4197
  old: oldMeta ? formatMetadataLines(oldMeta) : null,
4189
4198
  new: newMeta ? formatMetadataLines(newMeta) : null
@@ -4220,8 +4229,8 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
4220
4229
  });
4221
4230
 
4222
4231
  // src/routes/pages.tsx
4223
- import { readFileSync as readFileSync8 } from "fs";
4224
- import { Hono as Hono3 } from "hono";
4232
+ import { readFileSync as readFileSync9 } from "fs";
4233
+ import { Hono as Hono5 } from "hono";
4225
4234
  import { resolve as resolve4 } from "path";
4226
4235
 
4227
4236
  // src/utils/escapeHtml.ts
@@ -4536,10 +4545,10 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
4536
4545
  "data-file-path": file.file_path,
4537
4546
  "data-has-old": String(hasOld),
4538
4547
  "data-has-new": String(hasNew),
4539
- ...baseWidth ? { "data-base-width": String(baseWidth) } : {},
4540
- ...baseHeight ? { "data-base-height": String(baseHeight) } : {},
4548
+ ...baseWidth !== void 0 ? { "data-base-width": String(baseWidth) } : {},
4549
+ ...baseHeight !== void 0 ? { "data-base-height": String(baseHeight) } : {},
4541
4550
  children: [
4542
- 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." }),
4543
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..." }) }),
4544
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: [
4545
4554
  /* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
@@ -4998,9 +5007,570 @@ function FileList({ files, annotationCounts, staleCounts }) {
4998
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 }) }) });
4999
5008
  }
5000
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
+
5001
5568
  // src/components/layout.tsx
5002
5569
  function Layout({ title, reviewId, children }) {
5003
- 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: [
5004
5574
  /* @__PURE__ */ jsx("head", { children: [
5005
5575
  /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
5006
5576
  /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
@@ -5081,7 +5651,7 @@ function ReviewHistory({ reviews, currentReviewId }) {
5081
5651
 
5082
5652
  // src/routes/pages.tsx
5083
5653
  init_queries();
5084
- var pageRoutes = new Hono3();
5654
+ var pageRoutes = new Hono5();
5085
5655
  pageRoutes.get("/", async (c) => {
5086
5656
  const reviewId = c.get("reviewId");
5087
5657
  const review = await getReview(reviewId);
@@ -5235,11 +5805,11 @@ pageRoutes.get("/file/:fileId", async (c) => {
5235
5805
  });
5236
5806
  pageRoutes.get("/file-raw", (c) => {
5237
5807
  const filePath = c.req.query("path");
5238
- if (!filePath) return c.text("Missing path", 400);
5808
+ if (filePath === void 0 || filePath === "") return c.text("Missing path", 400);
5239
5809
  const repoRoot = c.get("repoRoot");
5240
5810
  let content;
5241
5811
  try {
5242
- content = readFileSync8(resolve4(repoRoot, filePath), "utf-8");
5812
+ content = readFileSync9(resolve4(repoRoot, filePath), "utf-8");
5243
5813
  } catch {
5244
5814
  return c.text("File not found", 404);
5245
5815
  }
@@ -5262,7 +5832,7 @@ pageRoutes.get("/file-raw", (c) => {
5262
5832
  }))
5263
5833
  }]
5264
5834
  };
5265
- 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: "" };
5266
5836
  const html = /* @__PURE__ */ jsx(DiffView, { file: fakeFile, diff, annotations: [], mode: "unified" });
5267
5837
  return c.html(html.toString());
5268
5838
  });
@@ -5365,10 +5935,112 @@ pageRoutes.get("/history", async (c) => {
5365
5935
  return c.html(html.toString());
5366
5936
  });
5367
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
+
5368
6040
  // src/server.ts
5369
6041
  function tryServe(fetch2, port) {
5370
6042
  return new Promise((resolve6, reject) => {
5371
- const server = serve({ fetch: fetch2, port });
6043
+ const server = serve({ fetch: fetch2, port, hostname: "127.0.0.1" });
5372
6044
  server.on("listening", () => {
5373
6045
  resolve6(port);
5374
6046
  });
@@ -5382,7 +6054,7 @@ function tryServe(fetch2, port) {
5382
6054
  });
5383
6055
  }
5384
6056
  async function startServer(port, reviewId, repoRoot, options) {
5385
- const app = new Hono4();
6057
+ const app = new Hono7();
5386
6058
  app.use("*", async (c, next) => {
5387
6059
  c.set("reviewId", reviewId);
5388
6060
  c.set("currentReviewId", reviewId);
@@ -5390,24 +6062,25 @@ async function startServer(port, reviewId, repoRoot, options) {
5390
6062
  await next();
5391
6063
  });
5392
6064
  const selfDir = dirname(fileURLToPath(import.meta.url));
5393
- 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");
5394
6066
  app.get("/static/styles.css", (c) => {
5395
- const css = readFileSync9(join7(distDir, "styles.css"), "utf-8");
6067
+ const css = readFileSync10(join8(distDir, "styles.css"), "utf-8");
5396
6068
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
5397
6069
  });
5398
6070
  app.get("/static/app.js", (c) => {
5399
- const js = readFileSync9(join7(distDir, "app.global.js"), "utf-8");
6071
+ const js = readFileSync10(join8(distDir, "app.global.js"), "utf-8");
5400
6072
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
5401
6073
  });
5402
6074
  app.get("/static/history.js", (c) => {
5403
- const js = readFileSync9(join7(distDir, "history.global.js"), "utf-8");
6075
+ const js = readFileSync10(join8(distDir, "history.global.js"), "utf-8");
5404
6076
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
5405
6077
  });
5406
6078
  app.route("/api", apiRoutes);
5407
6079
  app.route("/api/ai", aiApiRoutes);
6080
+ app.route("/api/themes", themeApiRoutes);
5408
6081
  app.route("/", pageRoutes);
5409
6082
  let actualPort = port;
5410
- if (options?.strictPort) {
6083
+ if (options?.strictPort === true) {
5411
6084
  actualPort = await tryServe(app.fetch, port);
5412
6085
  } else {
5413
6086
  for (let attempt = 0; attempt < 20; attempt++) {
@@ -5429,15 +6102,15 @@ async function startServer(port, reviewId, repoRoot, options) {
5429
6102
  console.log(`
5430
6103
  Glassbox running at ${url}
5431
6104
  `);
5432
- if (!options?.noOpen) {
6105
+ if (options?.noOpen !== true) {
5433
6106
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5434
6107
  exec(`${openCmd} ${url}`);
5435
6108
  }
5436
6109
  }
5437
6110
 
5438
6111
  // src/skills.ts
5439
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
5440
- 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";
5441
6114
  var SKILL_VERSION = 1;
5442
6115
  function versionHeader() {
5443
6116
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -5448,14 +6121,14 @@ function parseVersionHeader(content) {
5448
6121
  return parseInt(match[1], 10);
5449
6122
  }
5450
6123
  function updateFile(path, content) {
5451
- if (existsSync7(path)) {
5452
- const existing = readFileSync10(path, "utf-8");
6124
+ if (existsSync8(path)) {
6125
+ const existing = readFileSync11(path, "utf-8");
5453
6126
  const version = parseVersionHeader(existing);
5454
6127
  if (version !== null && version >= SKILL_VERSION) {
5455
6128
  return false;
5456
6129
  }
5457
6130
  }
5458
- writeFileSync5(path, content, "utf-8");
6131
+ writeFileSync6(path, content, "utf-8");
5459
6132
  return true;
5460
6133
  }
5461
6134
  function skillBody() {
@@ -5475,8 +6148,8 @@ function skillBody() {
5475
6148
  ].join("\n");
5476
6149
  }
5477
6150
  function ensureClaudeSkills(cwd) {
5478
- const dir = join8(cwd, ".claude", "skills", "glassbox");
5479
- mkdirSync5(dir, { recursive: true });
6151
+ const dir = join9(cwd, ".claude", "skills", "glassbox");
6152
+ mkdirSync6(dir, { recursive: true });
5480
6153
  const content = [
5481
6154
  "---",
5482
6155
  "name: glassbox",
@@ -5488,11 +6161,11 @@ function ensureClaudeSkills(cwd) {
5488
6161
  skillBody(),
5489
6162
  ""
5490
6163
  ].join("\n");
5491
- return updateFile(join8(dir, "SKILL.md"), content);
6164
+ return updateFile(join9(dir, "SKILL.md"), content);
5492
6165
  }
5493
6166
  function ensureCursorRules(cwd) {
5494
- const rulesDir = join8(cwd, ".cursor", "rules");
5495
- mkdirSync5(rulesDir, { recursive: true });
6167
+ const rulesDir = join9(cwd, ".cursor", "rules");
6168
+ mkdirSync6(rulesDir, { recursive: true });
5496
6169
  const content = [
5497
6170
  "---",
5498
6171
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -5503,11 +6176,11 @@ function ensureCursorRules(cwd) {
5503
6176
  skillBody(),
5504
6177
  ""
5505
6178
  ].join("\n");
5506
- return updateFile(join8(rulesDir, "glassbox.mdc"), content);
6179
+ return updateFile(join9(rulesDir, "glassbox.mdc"), content);
5507
6180
  }
5508
6181
  function ensureCopilotPrompts(cwd) {
5509
- const promptsDir = join8(cwd, ".github", "prompts");
5510
- mkdirSync5(promptsDir, { recursive: true });
6182
+ const promptsDir = join9(cwd, ".github", "prompts");
6183
+ mkdirSync6(promptsDir, { recursive: true });
5511
6184
  const content = [
5512
6185
  "---",
5513
6186
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -5517,11 +6190,11 @@ function ensureCopilotPrompts(cwd) {
5517
6190
  skillBody(),
5518
6191
  ""
5519
6192
  ].join("\n");
5520
- return updateFile(join8(promptsDir, "glassbox.prompt.md"), content);
6193
+ return updateFile(join9(promptsDir, "glassbox.prompt.md"), content);
5521
6194
  }
5522
6195
  function ensureWindsurfRules(cwd) {
5523
- const rulesDir = join8(cwd, ".windsurf", "rules");
5524
- mkdirSync5(rulesDir, { recursive: true });
6196
+ const rulesDir = join9(cwd, ".windsurf", "rules");
6197
+ mkdirSync6(rulesDir, { recursive: true });
5525
6198
  const content = [
5526
6199
  "---",
5527
6200
  "trigger: manual",
@@ -5532,39 +6205,39 @@ function ensureWindsurfRules(cwd) {
5532
6205
  skillBody(),
5533
6206
  ""
5534
6207
  ].join("\n");
5535
- return updateFile(join8(rulesDir, "glassbox.md"), content);
6208
+ return updateFile(join9(rulesDir, "glassbox.md"), content);
5536
6209
  }
5537
6210
  function ensureSkills() {
5538
6211
  const cwd = process.cwd();
5539
6212
  const platforms = [];
5540
- if (existsSync7(join8(cwd, ".claude"))) {
6213
+ if (existsSync8(join9(cwd, ".claude"))) {
5541
6214
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
5542
6215
  }
5543
- if (existsSync7(join8(cwd, ".cursor"))) {
6216
+ if (existsSync8(join9(cwd, ".cursor"))) {
5544
6217
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
5545
6218
  }
5546
- 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"))) {
5547
6220
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
5548
6221
  }
5549
- if (existsSync7(join8(cwd, ".windsurf"))) {
6222
+ if (existsSync8(join9(cwd, ".windsurf"))) {
5550
6223
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
5551
6224
  }
5552
6225
  return platforms;
5553
6226
  }
5554
6227
 
5555
6228
  // src/update-check.ts
5556
- 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";
5557
6230
  import { get } from "https";
5558
- import { homedir as homedir3 } from "os";
5559
- 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";
5560
6233
  import { fileURLToPath as fileURLToPath2 } from "url";
5561
- var DATA_DIR = join9(homedir3(), ".glassbox");
5562
- 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");
5563
6236
  var PACKAGE_NAME = "glassbox";
5564
6237
  function getCurrentVersion() {
5565
6238
  try {
5566
6239
  const dir = dirname2(fileURLToPath2(import.meta.url));
5567
- const pkg = JSON.parse(readFileSync11(join9(dir, "..", "package.json"), "utf-8"));
6240
+ const pkg = JSON.parse(readFileSync12(join10(dir, "..", "package.json"), "utf-8"));
5568
6241
  return pkg.version;
5569
6242
  } catch {
5570
6243
  return "0.0.0";
@@ -5572,16 +6245,16 @@ function getCurrentVersion() {
5572
6245
  }
5573
6246
  function getLastCheckDate() {
5574
6247
  try {
5575
- if (existsSync8(CHECK_FILE)) {
5576
- return readFileSync11(CHECK_FILE, "utf-8").trim();
6248
+ if (existsSync9(CHECK_FILE)) {
6249
+ return readFileSync12(CHECK_FILE, "utf-8").trim();
5577
6250
  }
5578
6251
  } catch {
5579
6252
  }
5580
6253
  return null;
5581
6254
  }
5582
6255
  function saveCheckDate() {
5583
- mkdirSync6(DATA_DIR, { recursive: true });
5584
- 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");
5585
6258
  }
5586
6259
  function isFirstUseToday() {
5587
6260
  const last = getLastCheckDate();
@@ -5807,13 +6480,13 @@ async function main() {
5807
6480
  console.log("AI service test mode enabled \u2014 using mock AI responses");
5808
6481
  }
5809
6482
  if (debug) {
5810
- console.log(`[debug] Build timestamp: ${"2026-03-26T23:51:09.647Z"}`);
6483
+ console.log(`[debug] Build timestamp: ${"2026-04-02T08:06:45.351Z"}`);
5811
6484
  }
5812
- if (projectDir) {
6485
+ if (projectDir !== null) {
5813
6486
  process.chdir(projectDir);
5814
6487
  }
5815
6488
  if (dataDir === null) {
5816
- dataDir = join10(process.cwd(), ".glassbox");
6489
+ dataDir = join11(process.cwd(), ".glassbox");
5817
6490
  }
5818
6491
  if (demo !== null) {
5819
6492
  const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
@@ -5825,13 +6498,13 @@ async function main() {
5825
6498
  }
5826
6499
  process.exit(1);
5827
6500
  }
5828
- dataDir = join10(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
6501
+ dataDir = join11(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
5829
6502
  setDemoMode(demo);
5830
6503
  console.log(`
5831
6504
  DEMO MODE: ${scenario.label}
5832
6505
  `);
5833
6506
  }
5834
- mkdirSync7(dataDir, { recursive: true });
6507
+ mkdirSync8(dataDir, { recursive: true });
5835
6508
  if (demo === null) {
5836
6509
  acquireLock(dataDir);
5837
6510
  }
@@ -5888,8 +6561,14 @@ async function main() {
5888
6561
  console.log(`Review ${review.id} created.`);
5889
6562
  await startServer(port, review.id, repoRoot, { noOpen, strictPort });
5890
6563
  }
5891
- main().catch((err) => {
5892
- console.error(err);
5893
- process.exit(1);
5894
- });
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
+ };
5895
6574
  //# sourceMappingURL=cli.js.map