glassbox 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1282 -440
- package/dist/client/app.global.js +8 -8
- package/dist/client/history.global.js +1 -1
- package/dist/client/styles.css +1 -1
- package/package.json +7 -4
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
|
-
|
|
395
|
+
init_queries();
|
|
396
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
397
397
|
import { tmpdir } from "os";
|
|
398
|
-
import { join as
|
|
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/
|
|
435
|
-
|
|
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/
|
|
470
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
604
|
+
spawnSync2("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
|
|
618
605
|
} else if (os === "linux") {
|
|
619
|
-
|
|
606
|
+
spawnSync2("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
|
|
620
607
|
} else if (os === "win32") {
|
|
621
608
|
const target = winCredTarget(platform);
|
|
622
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
640
|
+
return {};
|
|
649
641
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 = () =>
|
|
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
|
|
1705
|
-
import { Hono as
|
|
1706
|
-
import { dirname, join as
|
|
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,71 +2563,24 @@ async function mockGuidedAnalysisBatch(files) {
|
|
|
2536
2563
|
}));
|
|
2537
2564
|
}
|
|
2538
2565
|
|
|
2539
|
-
// src/routes/ai-
|
|
2566
|
+
// src/routes/ai-analysis.ts
|
|
2540
2567
|
init_queries();
|
|
2541
|
-
var
|
|
2568
|
+
var aiAnalysisRoutes = new Hono();
|
|
2569
|
+
var VALID_SORT_MODES = ["folder", "risk", "narrative", "guided"];
|
|
2570
|
+
var VALID_RISK_DIMENSIONS = ["aggregate", "security", "correctness", "error-handling", "maintainability", "architecture", "performance"];
|
|
2571
|
+
var VALID_SVG_VIEW_MODES = ["code", "rendered"];
|
|
2572
|
+
var VALID_IMAGE_MODES = ["metadata", "side-by-side", "difference", "slice"];
|
|
2573
|
+
var VALID_ANALYSIS_TYPES = ["risk", "narrative", "guided"];
|
|
2542
2574
|
var cancelledAnalyses = /* @__PURE__ */ new Set();
|
|
2543
|
-
|
|
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) => {
|
|
2575
|
+
aiAnalysisRoutes.post("/analyze", async (c) => {
|
|
2596
2576
|
const reviewId = c.req.query("reviewId") ?? "";
|
|
2597
2577
|
const repoRoot = c.get("repoRoot");
|
|
2598
2578
|
const body = await c.req.json();
|
|
2599
2579
|
const analysisType = body.type;
|
|
2600
2580
|
const invalidateCache = body.invalidateCache === true;
|
|
2601
2581
|
debugLog(`POST /analyze: type=${analysisType}, reviewId=${reviewId}`);
|
|
2602
|
-
if (analysisType
|
|
2603
|
-
return c.json({ error:
|
|
2582
|
+
if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
|
|
2583
|
+
return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
|
|
2604
2584
|
}
|
|
2605
2585
|
const testMode = isAIServiceTest();
|
|
2606
2586
|
const config = loadAIConfig();
|
|
@@ -2844,9 +2824,12 @@ async function runBatchedGuidedAnalysis(analysisId, batches, allFiles, config, r
|
|
|
2844
2824
|
"guided"
|
|
2845
2825
|
);
|
|
2846
2826
|
}
|
|
2847
|
-
|
|
2827
|
+
aiAnalysisRoutes.get("/analysis/:type", async (c) => {
|
|
2848
2828
|
const reviewId = c.req.query("reviewId") ?? "";
|
|
2849
2829
|
const analysisType = c.req.param("type");
|
|
2830
|
+
if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
|
|
2831
|
+
return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
|
|
2832
|
+
}
|
|
2850
2833
|
const analysis = await getLatestAnalysis(reviewId, analysisType);
|
|
2851
2834
|
if (analysis === void 0) {
|
|
2852
2835
|
debugLog(`GET /analysis/${analysisType}: no analysis found`);
|
|
@@ -2876,9 +2859,12 @@ aiApiRoutes.get("/analysis/:type", async (c) => {
|
|
|
2876
2859
|
}))
|
|
2877
2860
|
});
|
|
2878
2861
|
});
|
|
2879
|
-
|
|
2862
|
+
aiAnalysisRoutes.get("/analysis/:type/status", async (c) => {
|
|
2880
2863
|
const reviewId = c.req.query("reviewId") ?? "";
|
|
2881
2864
|
const analysisType = c.req.param("type");
|
|
2865
|
+
if (!VALID_ANALYSIS_TYPES.includes(analysisType)) {
|
|
2866
|
+
return c.json({ error: `type must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}` }, 400);
|
|
2867
|
+
}
|
|
2882
2868
|
const analysis = await getLatestAnalysis(reviewId, analysisType);
|
|
2883
2869
|
if (analysis === void 0) {
|
|
2884
2870
|
debugLog(`GET /analysis/${analysisType}/status: no analysis found`);
|
|
@@ -2900,35 +2886,150 @@ aiApiRoutes.get("/analysis/:type/status", async (c) => {
|
|
|
2900
2886
|
progressTotal: analysis.progress_total
|
|
2901
2887
|
});
|
|
2902
2888
|
});
|
|
2903
|
-
|
|
2889
|
+
aiAnalysisRoutes.get("/debug-status", (c) => {
|
|
2904
2890
|
return c.json({ enabled: isDebug() });
|
|
2905
2891
|
});
|
|
2906
|
-
|
|
2892
|
+
aiAnalysisRoutes.post("/debug-log", async (c) => {
|
|
2907
2893
|
if (!isDebug()) return c.json({ ok: true });
|
|
2908
2894
|
const body = await c.req.json();
|
|
2895
|
+
if (typeof body.message !== "string") {
|
|
2896
|
+
return c.json({ error: "message must be a string" }, 400);
|
|
2897
|
+
}
|
|
2909
2898
|
debugLog(`[client] ${body.message}`);
|
|
2910
2899
|
return c.json({ ok: true });
|
|
2911
2900
|
});
|
|
2912
|
-
|
|
2901
|
+
aiAnalysisRoutes.get("/preferences", async (c) => {
|
|
2913
2902
|
const prefs = await getUserPreferences();
|
|
2914
2903
|
return c.json(prefs);
|
|
2915
2904
|
});
|
|
2916
|
-
|
|
2905
|
+
aiAnalysisRoutes.post("/preferences", async (c) => {
|
|
2917
2906
|
const body = await c.req.json();
|
|
2907
|
+
if (body.sort_mode !== void 0 && !VALID_SORT_MODES.includes(body.sort_mode)) {
|
|
2908
|
+
return c.json({ error: `sort_mode must be one of: ${VALID_SORT_MODES.join(", ")}` }, 400);
|
|
2909
|
+
}
|
|
2910
|
+
if (body.risk_sort_dimension !== void 0 && !VALID_RISK_DIMENSIONS.includes(body.risk_sort_dimension)) {
|
|
2911
|
+
return c.json({ error: `risk_sort_dimension must be one of: ${VALID_RISK_DIMENSIONS.join(", ")}` }, 400);
|
|
2912
|
+
}
|
|
2913
|
+
if (body.show_risk_scores !== void 0 && typeof body.show_risk_scores !== "boolean") {
|
|
2914
|
+
return c.json({ error: "show_risk_scores must be a boolean" }, 400);
|
|
2915
|
+
}
|
|
2916
|
+
if (body.ignore_whitespace !== void 0 && typeof body.ignore_whitespace !== "boolean") {
|
|
2917
|
+
return c.json({ error: "ignore_whitespace must be a boolean" }, 400);
|
|
2918
|
+
}
|
|
2919
|
+
if (body.svg_view_mode !== void 0 && !VALID_SVG_VIEW_MODES.includes(body.svg_view_mode)) {
|
|
2920
|
+
return c.json({ error: `svg_view_mode must be one of: ${VALID_SVG_VIEW_MODES.join(", ")}` }, 400);
|
|
2921
|
+
}
|
|
2922
|
+
if (body.last_image_mode !== void 0 && !VALID_IMAGE_MODES.includes(body.last_image_mode)) {
|
|
2923
|
+
return c.json({ error: `last_image_mode must be one of: ${VALID_IMAGE_MODES.join(", ")}` }, 400);
|
|
2924
|
+
}
|
|
2918
2925
|
await saveUserPreferences(body);
|
|
2919
2926
|
return c.json({ ok: true });
|
|
2920
2927
|
});
|
|
2921
2928
|
|
|
2929
|
+
// src/routes/ai-config.ts
|
|
2930
|
+
import { Hono as Hono2 } from "hono";
|
|
2931
|
+
var aiConfigRoutes = new Hono2();
|
|
2932
|
+
var VALID_PLATFORMS = ["anthropic", "openai", "google"];
|
|
2933
|
+
var VALID_KEY_STORAGES = ["keychain", "config"];
|
|
2934
|
+
aiConfigRoutes.get("/config", (c) => {
|
|
2935
|
+
const config = loadAIConfig();
|
|
2936
|
+
return c.json({
|
|
2937
|
+
platform: config.platform,
|
|
2938
|
+
model: config.model,
|
|
2939
|
+
keyConfigured: config.apiKey !== null || isAIServiceTest() || getDemoMode() !== null,
|
|
2940
|
+
keySource: config.keySource,
|
|
2941
|
+
guidedReview: loadGuidedReviewConfig()
|
|
2942
|
+
});
|
|
2943
|
+
});
|
|
2944
|
+
aiConfigRoutes.post("/config", async (c) => {
|
|
2945
|
+
const body = await c.req.json();
|
|
2946
|
+
if (!VALID_PLATFORMS.includes(body.platform)) {
|
|
2947
|
+
return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
|
|
2948
|
+
}
|
|
2949
|
+
if (typeof body.model !== "string" || body.model.trim() === "") {
|
|
2950
|
+
return c.json({ error: "model must be a non-empty string" }, 400);
|
|
2951
|
+
}
|
|
2952
|
+
if (body.guidedReview !== void 0) {
|
|
2953
|
+
const gr = body.guidedReview;
|
|
2954
|
+
if (typeof gr !== "object" || gr === null || Array.isArray(gr)) {
|
|
2955
|
+
return c.json({ error: "guidedReview must be an object" }, 400);
|
|
2956
|
+
}
|
|
2957
|
+
const grObj = gr;
|
|
2958
|
+
if (typeof grObj.enabled !== "boolean") {
|
|
2959
|
+
return c.json({ error: "guidedReview.enabled must be a boolean" }, 400);
|
|
2960
|
+
}
|
|
2961
|
+
if (!Array.isArray(grObj.topics) || !grObj.topics.every((t) => typeof t === "string")) {
|
|
2962
|
+
return c.json({ error: "guidedReview.topics must be an array of strings" }, 400);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
saveAIConfigPreferences(body.platform, body.model);
|
|
2966
|
+
if (body.guidedReview !== void 0) {
|
|
2967
|
+
saveGuidedReviewConfig(body.guidedReview);
|
|
2968
|
+
}
|
|
2969
|
+
return c.json({ ok: true });
|
|
2970
|
+
});
|
|
2971
|
+
aiConfigRoutes.get("/models", (c) => {
|
|
2972
|
+
return c.json({
|
|
2973
|
+
platforms: PLATFORMS,
|
|
2974
|
+
models: MODELS
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
aiConfigRoutes.get("/key-status", (c) => {
|
|
2978
|
+
const platforms = ["anthropic", "openai", "google"];
|
|
2979
|
+
const status = {};
|
|
2980
|
+
for (const platform of platforms) {
|
|
2981
|
+
const { source } = resolveAPIKey(platform);
|
|
2982
|
+
status[platform] = { configured: source !== null, source };
|
|
2983
|
+
}
|
|
2984
|
+
return c.json({
|
|
2985
|
+
status,
|
|
2986
|
+
keychainAvailable: isKeychainAvailable(),
|
|
2987
|
+
keychainLabel: getKeychainLabel(),
|
|
2988
|
+
availablePlatforms: detectAvailablePlatforms()
|
|
2989
|
+
});
|
|
2990
|
+
});
|
|
2991
|
+
aiConfigRoutes.post("/key", async (c) => {
|
|
2992
|
+
const body = await c.req.json();
|
|
2993
|
+
if (!VALID_PLATFORMS.includes(body.platform)) {
|
|
2994
|
+
return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof body.key !== "string" || body.key.trim() === "") {
|
|
2997
|
+
return c.json({ error: "key must be a non-empty string" }, 400);
|
|
2998
|
+
}
|
|
2999
|
+
if (!VALID_KEY_STORAGES.includes(body.storage)) {
|
|
3000
|
+
return c.json({ error: `storage must be one of: ${VALID_KEY_STORAGES.join(", ")}` }, 400);
|
|
3001
|
+
}
|
|
3002
|
+
saveAPIKey(
|
|
3003
|
+
body.platform,
|
|
3004
|
+
body.key,
|
|
3005
|
+
body.storage
|
|
3006
|
+
);
|
|
3007
|
+
return c.json({ ok: true });
|
|
3008
|
+
});
|
|
3009
|
+
aiConfigRoutes.delete("/key", (c) => {
|
|
3010
|
+
const platform = c.req.query("platform") ?? "anthropic";
|
|
3011
|
+
if (!VALID_PLATFORMS.includes(platform)) {
|
|
3012
|
+
return c.json({ error: `platform must be one of: ${VALID_PLATFORMS.join(", ")}` }, 400);
|
|
3013
|
+
}
|
|
3014
|
+
deleteAPIKey(platform);
|
|
3015
|
+
return c.json({ ok: true });
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
// src/routes/ai-api.ts
|
|
3019
|
+
var aiApiRoutes = new Hono3();
|
|
3020
|
+
aiApiRoutes.route("/", aiConfigRoutes);
|
|
3021
|
+
aiApiRoutes.route("/", aiAnalysisRoutes);
|
|
3022
|
+
|
|
2922
3023
|
// src/routes/api.ts
|
|
2923
3024
|
init_queries();
|
|
2924
|
-
import { execFileSync, spawnSync as
|
|
3025
|
+
import { execFileSync, spawnSync as spawnSync7 } from "child_process";
|
|
2925
3026
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
2926
|
-
import { Hono as
|
|
3027
|
+
import { Hono as Hono4 } from "hono";
|
|
2927
3028
|
import { join as join6, resolve as resolve3 } from "path";
|
|
2928
3029
|
|
|
2929
3030
|
// src/export/generate.ts
|
|
2930
3031
|
init_queries();
|
|
2931
|
-
import { spawnSync as
|
|
3032
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
2932
3033
|
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2933
3034
|
import { homedir as homedir2 } from "os";
|
|
2934
3035
|
import { join as join4 } from "path";
|
|
@@ -2947,7 +3048,7 @@ function saveDismissals(data) {
|
|
|
2947
3048
|
writeFileSync3(DISMISS_FILE, JSON.stringify(data), "utf-8");
|
|
2948
3049
|
}
|
|
2949
3050
|
function isGlassboxGitignored(repoRoot) {
|
|
2950
|
-
const result =
|
|
3051
|
+
const result = spawnSync5("git", ["check-ignore", "-q", ".glassbox"], { cwd: repoRoot, stdio: "pipe" });
|
|
2951
3052
|
return result.status === 0;
|
|
2952
3053
|
}
|
|
2953
3054
|
function shouldPromptGitignore(repoRoot) {
|
|
@@ -3071,127 +3172,43 @@ function scheduleAutoExport(reviewId, repoRoot) {
|
|
|
3071
3172
|
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
3072
3173
|
debounceTimer = setTimeout(() => {
|
|
3073
3174
|
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
|
-
}
|
|
3151
|
-
}
|
|
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;
|
|
3157
|
-
}
|
|
3158
|
-
function readWorkingFile(filePath, repoRoot) {
|
|
3159
|
-
try {
|
|
3160
|
-
return readFileSync5(resolve2(repoRoot, filePath));
|
|
3161
|
-
} catch {
|
|
3162
|
-
return null;
|
|
3163
|
-
}
|
|
3175
|
+
void generateReviewExport(reviewId, repoRoot, true);
|
|
3176
|
+
}, DEBOUNCE_MS);
|
|
3164
3177
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
const
|
|
3175
|
-
|
|
3176
|
-
return { data, size: data.length };
|
|
3178
|
+
|
|
3179
|
+
// src/git/image.ts
|
|
3180
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
3181
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3182
|
+
import { resolve as resolve2 } from "path";
|
|
3183
|
+
|
|
3184
|
+
// src/git/image-metadata.ts
|
|
3185
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
3186
|
+
function isImageFile(filePath) {
|
|
3187
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
3188
|
+
return IMAGE_EXTENSIONS.has(ext);
|
|
3177
3189
|
}
|
|
3178
|
-
function
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3190
|
+
function isSvgFile(filePath) {
|
|
3191
|
+
return filePath.slice(filePath.lastIndexOf(".")).toLowerCase() === ".svg";
|
|
3192
|
+
}
|
|
3193
|
+
function getContentType(filePath) {
|
|
3194
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
3195
|
+
switch (ext) {
|
|
3196
|
+
case ".png":
|
|
3197
|
+
return "image/png";
|
|
3198
|
+
case ".jpg":
|
|
3199
|
+
case ".jpeg":
|
|
3200
|
+
return "image/jpeg";
|
|
3201
|
+
case ".gif":
|
|
3202
|
+
return "image/gif";
|
|
3203
|
+
case ".webp":
|
|
3204
|
+
return "image/webp";
|
|
3205
|
+
case ".svg":
|
|
3206
|
+
return "image/svg+xml";
|
|
3207
|
+
default:
|
|
3208
|
+
return "application/octet-stream";
|
|
3189
3209
|
}
|
|
3190
|
-
const data = gitShowFile(ref, filePath, repoRoot);
|
|
3191
|
-
if (!data) return null;
|
|
3192
|
-
return { data, size: data.length };
|
|
3193
3210
|
}
|
|
3194
|
-
|
|
3211
|
+
function extractMetadata(data, filePath) {
|
|
3195
3212
|
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
3196
3213
|
if (ext === ".svg") {
|
|
3197
3214
|
const text = data.toString("utf-8");
|
|
@@ -3233,9 +3250,9 @@ function formatMetadataLines(meta) {
|
|
|
3233
3250
|
lines.push(`Dimensions: ${meta.width} \xD7 ${meta.height}`);
|
|
3234
3251
|
}
|
|
3235
3252
|
lines.push(`File size: ${formatBytes(meta.fileSize)}`);
|
|
3236
|
-
if (meta.colorSpace) lines.push(`Color space: ${meta.colorSpace}`);
|
|
3253
|
+
if (meta.colorSpace !== null) lines.push(`Color space: ${meta.colorSpace}`);
|
|
3237
3254
|
if (meta.channels !== null) lines.push(`Channels: ${meta.channels}`);
|
|
3238
|
-
if (meta.depth) lines.push(`Bit depth: ${meta.depth}`);
|
|
3255
|
+
if (meta.depth !== null) lines.push(`Bit depth: ${meta.depth}`);
|
|
3239
3256
|
if (meta.hasAlpha !== null) lines.push(`Alpha: ${meta.hasAlpha ? "yes" : "no"}`);
|
|
3240
3257
|
if (meta.density !== null) lines.push(`Density: ${meta.density} DPI`);
|
|
3241
3258
|
if (meta.exif) {
|
|
@@ -3376,7 +3393,95 @@ function parseWebp(data) {
|
|
|
3376
3393
|
width = (data[24] | data[25] << 8 | data[26] << 16) + 1;
|
|
3377
3394
|
height = (data[27] | data[28] << 8 | data[29] << 16) + 1;
|
|
3378
3395
|
}
|
|
3379
|
-
return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
|
|
3396
|
+
return { format: "webp", width, height, colorSpace: "srgb", channels: hasAlpha === true ? 4 : 3, depth: null, hasAlpha, density: null, exif: null };
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/git/image.ts
|
|
3400
|
+
function getOldRef(mode) {
|
|
3401
|
+
switch (mode.type) {
|
|
3402
|
+
case "uncommitted":
|
|
3403
|
+
return "HEAD";
|
|
3404
|
+
case "staged":
|
|
3405
|
+
return "HEAD";
|
|
3406
|
+
case "unstaged":
|
|
3407
|
+
return null;
|
|
3408
|
+
// old = index, use ':'
|
|
3409
|
+
case "commit":
|
|
3410
|
+
return `${mode.sha}~1`;
|
|
3411
|
+
case "range":
|
|
3412
|
+
return mode.from;
|
|
3413
|
+
case "branch":
|
|
3414
|
+
return mode.name;
|
|
3415
|
+
case "files":
|
|
3416
|
+
return "HEAD";
|
|
3417
|
+
case "all":
|
|
3418
|
+
return null;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
function getNewRef(mode) {
|
|
3422
|
+
switch (mode.type) {
|
|
3423
|
+
case "uncommitted":
|
|
3424
|
+
return null;
|
|
3425
|
+
// working tree
|
|
3426
|
+
case "staged":
|
|
3427
|
+
return null;
|
|
3428
|
+
// index, but git show : works
|
|
3429
|
+
case "unstaged":
|
|
3430
|
+
return null;
|
|
3431
|
+
// working tree
|
|
3432
|
+
case "commit":
|
|
3433
|
+
return mode.sha;
|
|
3434
|
+
case "range":
|
|
3435
|
+
return mode.to;
|
|
3436
|
+
case "branch":
|
|
3437
|
+
return "HEAD";
|
|
3438
|
+
case "files":
|
|
3439
|
+
return null;
|
|
3440
|
+
case "all":
|
|
3441
|
+
return null;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
function gitShowFile(ref, filePath, repoRoot) {
|
|
3445
|
+
const spec = ref === ":" ? `:${filePath}` : `${ref}:${filePath}`;
|
|
3446
|
+
const result = spawnSync6("git", ["show", spec], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
|
|
3447
|
+
if (result.status !== 0 || result.stdout.length === 0) return null;
|
|
3448
|
+
return result.stdout;
|
|
3449
|
+
}
|
|
3450
|
+
function readWorkingFile(filePath, repoRoot) {
|
|
3451
|
+
try {
|
|
3452
|
+
return readFileSync5(resolve2(repoRoot, filePath));
|
|
3453
|
+
} catch {
|
|
3454
|
+
return null;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
function getOldImage(mode, filePath, oldPath, repoRoot) {
|
|
3458
|
+
const ref = getOldRef(mode);
|
|
3459
|
+
const path = oldPath ?? filePath;
|
|
3460
|
+
if (ref === null) {
|
|
3461
|
+
const data2 = readWorkingFile(path, repoRoot);
|
|
3462
|
+
if (!data2) return null;
|
|
3463
|
+
return { data: data2, size: data2.length };
|
|
3464
|
+
}
|
|
3465
|
+
const actualRef = mode.type === "unstaged" ? ":" : ref;
|
|
3466
|
+
const data = gitShowFile(actualRef, path, repoRoot);
|
|
3467
|
+
if (!data) return null;
|
|
3468
|
+
return { data, size: data.length };
|
|
3469
|
+
}
|
|
3470
|
+
function getNewImage(mode, filePath, repoRoot) {
|
|
3471
|
+
const ref = getNewRef(mode);
|
|
3472
|
+
if (ref === null) {
|
|
3473
|
+
if (mode.type === "staged") {
|
|
3474
|
+
const data3 = gitShowFile(":", filePath, repoRoot);
|
|
3475
|
+
if (!data3) return null;
|
|
3476
|
+
return { data: data3, size: data3.length };
|
|
3477
|
+
}
|
|
3478
|
+
const data2 = readWorkingFile(filePath, repoRoot);
|
|
3479
|
+
if (!data2) return null;
|
|
3480
|
+
return { data: data2, size: data2.length };
|
|
3481
|
+
}
|
|
3482
|
+
const data = gitShowFile(ref, filePath, repoRoot);
|
|
3483
|
+
if (!data) return null;
|
|
3484
|
+
return { data, size: data.length };
|
|
3380
3485
|
}
|
|
3381
3486
|
|
|
3382
3487
|
// src/git/svg-rasterize.ts
|
|
@@ -3411,63 +3516,62 @@ function loadSystemFonts() {
|
|
|
3411
3516
|
return buffers;
|
|
3412
3517
|
}
|
|
3413
3518
|
function getFontCandidates() {
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
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 [];
|
|
3519
|
+
const os = process.platform;
|
|
3520
|
+
if (os === "darwin") {
|
|
3521
|
+
const sys = "/System/Library/Fonts";
|
|
3522
|
+
const sup = "/System/Library/Fonts/Supplemental";
|
|
3523
|
+
return [
|
|
3524
|
+
// Core system fonts (serif, sans-serif, monospace)
|
|
3525
|
+
join5(sys, "Helvetica.ttc"),
|
|
3526
|
+
join5(sys, "Times.ttc"),
|
|
3527
|
+
join5(sys, "Courier.ttc"),
|
|
3528
|
+
join5(sys, "Menlo.ttc"),
|
|
3529
|
+
join5(sys, "SFPro.ttf"),
|
|
3530
|
+
join5(sys, "SFNS.ttf"),
|
|
3531
|
+
join5(sys, "SFNSMono.ttf"),
|
|
3532
|
+
// Supplemental (common named fonts in SVGs)
|
|
3533
|
+
join5(sup, "Arial.ttf"),
|
|
3534
|
+
join5(sup, "Arial Bold.ttf"),
|
|
3535
|
+
join5(sup, "Georgia.ttf"),
|
|
3536
|
+
join5(sup, "Verdana.ttf"),
|
|
3537
|
+
join5(sup, "Tahoma.ttf"),
|
|
3538
|
+
join5(sup, "Trebuchet MS.ttf"),
|
|
3539
|
+
join5(sup, "Impact.ttf"),
|
|
3540
|
+
join5(sup, "Comic Sans MS.ttf"),
|
|
3541
|
+
join5(sup, "Courier New.ttf"),
|
|
3542
|
+
join5(sup, "Times New Roman.ttf")
|
|
3543
|
+
];
|
|
3544
|
+
}
|
|
3545
|
+
if (os === "linux") {
|
|
3546
|
+
return [
|
|
3547
|
+
// DejaVu (most common Linux fallback)
|
|
3548
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
3549
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
3550
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
|
|
3551
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
|
3552
|
+
// Liberation (metric-compatible with Arial/Times/Courier)
|
|
3553
|
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
3554
|
+
"/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
|
|
3555
|
+
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
|
|
3556
|
+
// Noto (common on modern distros)
|
|
3557
|
+
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
|
|
3558
|
+
];
|
|
3470
3559
|
}
|
|
3560
|
+
if (os === "win32") {
|
|
3561
|
+
const winFonts = join5(process.env.WINDIR ?? "C:\\Windows", "Fonts");
|
|
3562
|
+
return [
|
|
3563
|
+
join5(winFonts, "arial.ttf"),
|
|
3564
|
+
join5(winFonts, "arialbd.ttf"),
|
|
3565
|
+
join5(winFonts, "times.ttf"),
|
|
3566
|
+
join5(winFonts, "cour.ttf"),
|
|
3567
|
+
join5(winFonts, "verdana.ttf"),
|
|
3568
|
+
join5(winFonts, "tahoma.ttf"),
|
|
3569
|
+
join5(winFonts, "georgia.ttf"),
|
|
3570
|
+
join5(winFonts, "consola.ttf"),
|
|
3571
|
+
join5(winFonts, "segoeui.ttf")
|
|
3572
|
+
];
|
|
3573
|
+
}
|
|
3574
|
+
return [];
|
|
3471
3575
|
}
|
|
3472
3576
|
function parseSvgDimensions(svg) {
|
|
3473
3577
|
const widthMatch = svg.match(/\bwidth\s*=\s*["']([^"']+)["']/);
|
|
@@ -3831,7 +3935,10 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
|
|
|
3831
3935
|
}
|
|
3832
3936
|
|
|
3833
3937
|
// src/routes/api.ts
|
|
3834
|
-
var apiRoutes = new
|
|
3938
|
+
var apiRoutes = new Hono4();
|
|
3939
|
+
var VALID_CATEGORIES = ["bug", "fix", "style", "pattern-follow", "pattern-avoid", "note", "remember"];
|
|
3940
|
+
var VALID_SIDES = ["old", "new"];
|
|
3941
|
+
var VALID_FILE_STATUSES = ["pending", "reviewed"];
|
|
3835
3942
|
function resolveReviewId(c) {
|
|
3836
3943
|
return c.req.query("reviewId") ?? c.get("reviewId");
|
|
3837
3944
|
}
|
|
@@ -3938,6 +4045,9 @@ apiRoutes.get("/files/:fileId", async (c) => {
|
|
|
3938
4045
|
});
|
|
3939
4046
|
apiRoutes.patch("/files/:fileId/status", async (c) => {
|
|
3940
4047
|
const { status } = await c.req.json();
|
|
4048
|
+
if (!VALID_FILE_STATUSES.includes(status)) {
|
|
4049
|
+
return c.json({ error: `status must be one of: ${VALID_FILE_STATUSES.join(", ")}` }, 400);
|
|
4050
|
+
}
|
|
3941
4051
|
await updateFileStatus(c.req.param("fileId"), status);
|
|
3942
4052
|
return c.json({ ok: true });
|
|
3943
4053
|
});
|
|
@@ -3963,6 +4073,21 @@ function autoExport(c) {
|
|
|
3963
4073
|
}
|
|
3964
4074
|
apiRoutes.post("/annotations", async (c) => {
|
|
3965
4075
|
const body = await c.req.json();
|
|
4076
|
+
if (typeof body.reviewFileId !== "string" || body.reviewFileId === "") {
|
|
4077
|
+
return c.json({ error: "reviewFileId must be a non-empty string" }, 400);
|
|
4078
|
+
}
|
|
4079
|
+
if (typeof body.lineNumber !== "number" || !Number.isInteger(body.lineNumber) || body.lineNumber < 1) {
|
|
4080
|
+
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
4081
|
+
}
|
|
4082
|
+
if (!VALID_SIDES.includes(body.side)) {
|
|
4083
|
+
return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
|
|
4084
|
+
}
|
|
4085
|
+
if (!VALID_CATEGORIES.includes(body.category)) {
|
|
4086
|
+
return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
|
|
4087
|
+
}
|
|
4088
|
+
if (typeof body.content !== "string" || body.content.trim() === "") {
|
|
4089
|
+
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
4090
|
+
}
|
|
3966
4091
|
const annotation = await addAnnotation(
|
|
3967
4092
|
body.reviewFileId,
|
|
3968
4093
|
body.lineNumber,
|
|
@@ -3975,6 +4100,12 @@ apiRoutes.post("/annotations", async (c) => {
|
|
|
3975
4100
|
});
|
|
3976
4101
|
apiRoutes.patch("/annotations/:id", async (c) => {
|
|
3977
4102
|
const { content, category } = await c.req.json();
|
|
4103
|
+
if (typeof content !== "string" || content.trim() === "") {
|
|
4104
|
+
return c.json({ error: "content must be a non-empty string" }, 400);
|
|
4105
|
+
}
|
|
4106
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
4107
|
+
return c.json({ error: `category must be one of: ${VALID_CATEGORIES.join(", ")}` }, 400);
|
|
4108
|
+
}
|
|
3978
4109
|
await updateAnnotation(c.req.param("id"), content, category);
|
|
3979
4110
|
autoExport(c);
|
|
3980
4111
|
return c.json({ ok: true });
|
|
@@ -3986,6 +4117,12 @@ apiRoutes.delete("/annotations/:id", async (c) => {
|
|
|
3986
4117
|
});
|
|
3987
4118
|
apiRoutes.patch("/annotations/:id/move", async (c) => {
|
|
3988
4119
|
const { lineNumber, side } = await c.req.json();
|
|
4120
|
+
if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) {
|
|
4121
|
+
return c.json({ error: "lineNumber must be a positive integer" }, 400);
|
|
4122
|
+
}
|
|
4123
|
+
if (!VALID_SIDES.includes(side)) {
|
|
4124
|
+
return c.json({ error: `side must be one of: ${VALID_SIDES.join(", ")}` }, 400);
|
|
4125
|
+
}
|
|
3989
4126
|
await moveAnnotation(c.req.param("id"), lineNumber, side);
|
|
3990
4127
|
autoExport(c);
|
|
3991
4128
|
return c.json({ ok: true });
|
|
@@ -4034,7 +4171,7 @@ apiRoutes.get("/outline/:fileId", async (c) => {
|
|
|
4034
4171
|
apiRoutes.get("/symbol-definition", async (c) => {
|
|
4035
4172
|
const name = c.req.query("name");
|
|
4036
4173
|
const currentFileId = c.req.query("currentFileId");
|
|
4037
|
-
if (
|
|
4174
|
+
if (name === void 0 || name === "") return c.json({ definitions: [] });
|
|
4038
4175
|
const reviewId = resolveReviewId(c);
|
|
4039
4176
|
const repoRoot = c.get("repoRoot");
|
|
4040
4177
|
const definitions = [];
|
|
@@ -4056,7 +4193,7 @@ apiRoutes.get("/symbol-definition", async (c) => {
|
|
|
4056
4193
|
}
|
|
4057
4194
|
if (definitions.length === 0) {
|
|
4058
4195
|
try {
|
|
4059
|
-
const allFiles =
|
|
4196
|
+
const allFiles = spawnSync7("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).stdout.trim().split("\n").filter(Boolean);
|
|
4060
4197
|
for (const filePath of allFiles) {
|
|
4061
4198
|
if (searchedPaths.has(filePath)) continue;
|
|
4062
4199
|
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
@@ -4091,7 +4228,7 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
|
|
|
4091
4228
|
if (sym.name === targetName) {
|
|
4092
4229
|
out.push({ fileId, filePath, name: sym.name, kind: sym.kind, line: sym.line });
|
|
4093
4230
|
}
|
|
4094
|
-
if (sym.children
|
|
4231
|
+
if (sym.children.length > 0) {
|
|
4095
4232
|
collectDefinitions(sym.children, targetName, fileId, filePath, out);
|
|
4096
4233
|
}
|
|
4097
4234
|
}
|
|
@@ -4134,6 +4271,9 @@ apiRoutes.get("/project-settings", (c) => {
|
|
|
4134
4271
|
apiRoutes.patch("/project-settings", async (c) => {
|
|
4135
4272
|
const repoRoot = c.get("repoRoot");
|
|
4136
4273
|
const body = await c.req.json();
|
|
4274
|
+
if (body.appName !== void 0 && typeof body.appName !== "string") {
|
|
4275
|
+
return c.json({ error: "appName must be a string" }, 400);
|
|
4276
|
+
}
|
|
4137
4277
|
const current = readProjectSettings(repoRoot);
|
|
4138
4278
|
if (body.appName !== void 0) current.appName = body.appName || void 0;
|
|
4139
4279
|
writeProjectSettings(repoRoot, current);
|
|
@@ -4152,10 +4292,8 @@ apiRoutes.get("/image/:fileId/metadata", async (c) => {
|
|
|
4152
4292
|
const status = diff.status ?? "modified";
|
|
4153
4293
|
const oldImage = status !== "added" ? getOldImage(mode, file.file_path, oldPath, repoRoot) : null;
|
|
4154
4294
|
const newImage = status !== "deleted" ? getNewImage(mode, file.file_path, repoRoot) : null;
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4157
|
-
newImage ? extractMetadata(newImage.data, file.file_path) : null
|
|
4158
|
-
]);
|
|
4295
|
+
const oldMeta = oldImage !== null ? extractMetadata(oldImage.data, oldPath ?? file.file_path) : null;
|
|
4296
|
+
const newMeta = newImage !== null ? extractMetadata(newImage.data, file.file_path) : null;
|
|
4159
4297
|
return c.json({
|
|
4160
4298
|
old: oldMeta ? formatMetadataLines(oldMeta) : null,
|
|
4161
4299
|
new: newMeta ? formatMetadataLines(newMeta) : null
|
|
@@ -4178,7 +4316,7 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
|
|
|
4178
4316
|
if (isSvgFile(file.file_path)) {
|
|
4179
4317
|
try {
|
|
4180
4318
|
const png = await rasterizeSvg(image.data);
|
|
4181
|
-
return new Response(png, {
|
|
4319
|
+
return new Response(new Uint8Array(png), {
|
|
4182
4320
|
headers: { "Content-Type": "image/png", "Cache-Control": "no-cache" }
|
|
4183
4321
|
});
|
|
4184
4322
|
} catch {
|
|
@@ -4186,14 +4324,14 @@ apiRoutes.get("/image/:fileId/:side", async (c) => {
|
|
|
4186
4324
|
}
|
|
4187
4325
|
}
|
|
4188
4326
|
const contentType = getContentType(file.file_path);
|
|
4189
|
-
return new Response(image.data, {
|
|
4327
|
+
return new Response(new Uint8Array(image.data), {
|
|
4190
4328
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4191
4329
|
});
|
|
4192
4330
|
});
|
|
4193
4331
|
|
|
4194
4332
|
// src/routes/pages.tsx
|
|
4195
|
-
import { readFileSync as
|
|
4196
|
-
import { Hono as
|
|
4333
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
4334
|
+
import { Hono as Hono5 } from "hono";
|
|
4197
4335
|
import { resolve as resolve4 } from "path";
|
|
4198
4336
|
|
|
4199
4337
|
// src/utils/escapeHtml.ts
|
|
@@ -4508,10 +4646,10 @@ function ImageDiff({ file, diff, fontWarning, baseWidth, baseHeight }) {
|
|
|
4508
4646
|
"data-file-path": file.file_path,
|
|
4509
4647
|
"data-has-old": String(hasOld),
|
|
4510
4648
|
"data-has-new": String(hasNew),
|
|
4511
|
-
...baseWidth ? { "data-base-width": String(baseWidth) } : {},
|
|
4512
|
-
...baseHeight ? { "data-base-height": String(baseHeight) } : {},
|
|
4649
|
+
...baseWidth !== void 0 ? { "data-base-width": String(baseWidth) } : {},
|
|
4650
|
+
...baseHeight !== void 0 ? { "data-base-height": String(baseHeight) } : {},
|
|
4513
4651
|
children: [
|
|
4514
|
-
fontWarning && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
|
|
4652
|
+
fontWarning === true && /* @__PURE__ */ jsx("div", { className: "image-font-warning", children: "This SVG uses text that may render differently depending on locally installed fonts." }),
|
|
4515
4653
|
/* @__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
4654
|
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
4655
|
/* @__PURE__ */ jsx("img", { className: "image-layer image-layer-old", src: `/api/image/${fileId}/old`, alt: "Old version" }),
|
|
@@ -4970,9 +5108,570 @@ function FileList({ files, annotationCounts, staleCounts }) {
|
|
|
4970
5108
|
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
5109
|
}
|
|
4972
5110
|
|
|
5111
|
+
// src/themes/built-in.ts
|
|
5112
|
+
var THEME_VARIABLES = [
|
|
5113
|
+
"bg",
|
|
5114
|
+
"bg-surface",
|
|
5115
|
+
"bg-hover",
|
|
5116
|
+
"bg-active",
|
|
5117
|
+
"text",
|
|
5118
|
+
"text-dim",
|
|
5119
|
+
"text-bright",
|
|
5120
|
+
"accent",
|
|
5121
|
+
"accent-hover",
|
|
5122
|
+
"green",
|
|
5123
|
+
"red",
|
|
5124
|
+
"yellow",
|
|
5125
|
+
"orange",
|
|
5126
|
+
"blue",
|
|
5127
|
+
"purple",
|
|
5128
|
+
"teal",
|
|
5129
|
+
"border",
|
|
5130
|
+
"diff-add-bg",
|
|
5131
|
+
"diff-add-border",
|
|
5132
|
+
"diff-remove-bg",
|
|
5133
|
+
"diff-remove-border",
|
|
5134
|
+
"diff-context-bg",
|
|
5135
|
+
"gutter-bg",
|
|
5136
|
+
"gutter-text"
|
|
5137
|
+
];
|
|
5138
|
+
var dark = {
|
|
5139
|
+
"bg": "#1e1e2e",
|
|
5140
|
+
"bg-surface": "#252536",
|
|
5141
|
+
"bg-hover": "#2d2d44",
|
|
5142
|
+
"bg-active": "#363652",
|
|
5143
|
+
"text": "#cdd6f4",
|
|
5144
|
+
"text-dim": "#8888aa",
|
|
5145
|
+
"text-bright": "#ffffff",
|
|
5146
|
+
"accent": "#89b4fa",
|
|
5147
|
+
"accent-hover": "#74a8fc",
|
|
5148
|
+
"green": "#a6e3a1",
|
|
5149
|
+
"red": "#f38ba8",
|
|
5150
|
+
"yellow": "#f9e2af",
|
|
5151
|
+
"orange": "#fab387",
|
|
5152
|
+
"blue": "#89b4fa",
|
|
5153
|
+
"purple": "#cba6f7",
|
|
5154
|
+
"teal": "#94e2d5",
|
|
5155
|
+
"border": "#363652",
|
|
5156
|
+
"diff-add-bg": "rgba(166, 227, 161, 0.1)",
|
|
5157
|
+
"diff-add-border": "rgba(166, 227, 161, 0.3)",
|
|
5158
|
+
"diff-remove-bg": "rgba(243, 139, 168, 0.1)",
|
|
5159
|
+
"diff-remove-border": "rgba(243, 139, 168, 0.3)",
|
|
5160
|
+
"diff-context-bg": "transparent",
|
|
5161
|
+
"gutter-bg": "#1a1a2e",
|
|
5162
|
+
"gutter-text": "#555577"
|
|
5163
|
+
};
|
|
5164
|
+
var light = {
|
|
5165
|
+
"bg": "#ffffff",
|
|
5166
|
+
"bg-surface": "#f6f8fa",
|
|
5167
|
+
"bg-hover": "#eaeef2",
|
|
5168
|
+
"bg-active": "#dde3e9",
|
|
5169
|
+
"text": "#1f2328",
|
|
5170
|
+
"text-dim": "#656d76",
|
|
5171
|
+
"text-bright": "#000000",
|
|
5172
|
+
"accent": "#0969da",
|
|
5173
|
+
"accent-hover": "#0550ae",
|
|
5174
|
+
"green": "#1a7f37",
|
|
5175
|
+
"red": "#cf222e",
|
|
5176
|
+
"yellow": "#9a6700",
|
|
5177
|
+
"orange": "#bc4c00",
|
|
5178
|
+
"blue": "#0969da",
|
|
5179
|
+
"purple": "#8250df",
|
|
5180
|
+
"teal": "#0e7c6b",
|
|
5181
|
+
"border": "#d0d7de",
|
|
5182
|
+
"diff-add-bg": "rgba(26, 127, 55, 0.08)",
|
|
5183
|
+
"diff-add-border": "rgba(26, 127, 55, 0.25)",
|
|
5184
|
+
"diff-remove-bg": "rgba(207, 34, 46, 0.08)",
|
|
5185
|
+
"diff-remove-border": "rgba(207, 34, 46, 0.25)",
|
|
5186
|
+
"diff-context-bg": "transparent",
|
|
5187
|
+
"gutter-bg": "#f6f8fa",
|
|
5188
|
+
"gutter-text": "#8b949e"
|
|
5189
|
+
};
|
|
5190
|
+
var highContrastDark = {
|
|
5191
|
+
"bg": "#0a0a0a",
|
|
5192
|
+
"bg-surface": "#1a1a1a",
|
|
5193
|
+
"bg-hover": "#2a2a2a",
|
|
5194
|
+
"bg-active": "#3a3a3a",
|
|
5195
|
+
"text": "#f0f0f0",
|
|
5196
|
+
"text-dim": "#b0b0b0",
|
|
5197
|
+
"text-bright": "#ffffff",
|
|
5198
|
+
"accent": "#6db3f2",
|
|
5199
|
+
"accent-hover": "#8ec5f7",
|
|
5200
|
+
"green": "#73e06e",
|
|
5201
|
+
"red": "#ff6b6b",
|
|
5202
|
+
"yellow": "#ffd93d",
|
|
5203
|
+
"orange": "#ffab57",
|
|
5204
|
+
"blue": "#6db3f2",
|
|
5205
|
+
"purple": "#c59eff",
|
|
5206
|
+
"teal": "#5ee6d0",
|
|
5207
|
+
"border": "#555555",
|
|
5208
|
+
"diff-add-bg": "rgba(115, 224, 110, 0.15)",
|
|
5209
|
+
"diff-add-border": "rgba(115, 224, 110, 0.5)",
|
|
5210
|
+
"diff-remove-bg": "rgba(255, 107, 107, 0.15)",
|
|
5211
|
+
"diff-remove-border": "rgba(255, 107, 107, 0.5)",
|
|
5212
|
+
"diff-context-bg": "transparent",
|
|
5213
|
+
"gutter-bg": "#111111",
|
|
5214
|
+
"gutter-text": "#888888"
|
|
5215
|
+
};
|
|
5216
|
+
var highContrastLight = {
|
|
5217
|
+
"bg": "#ffffff",
|
|
5218
|
+
"bg-surface": "#f0f0f0",
|
|
5219
|
+
"bg-hover": "#e0e0e0",
|
|
5220
|
+
"bg-active": "#d0d0d0",
|
|
5221
|
+
"text": "#111111",
|
|
5222
|
+
"text-dim": "#444444",
|
|
5223
|
+
"text-bright": "#000000",
|
|
5224
|
+
"accent": "#0043a8",
|
|
5225
|
+
"accent-hover": "#003080",
|
|
5226
|
+
"green": "#006b1f",
|
|
5227
|
+
"red": "#b80000",
|
|
5228
|
+
"yellow": "#785600",
|
|
5229
|
+
"orange": "#8a3400",
|
|
5230
|
+
"blue": "#0043a8",
|
|
5231
|
+
"purple": "#5b21b6",
|
|
5232
|
+
"teal": "#005e4f",
|
|
5233
|
+
"border": "#767676",
|
|
5234
|
+
"diff-add-bg": "rgba(0, 107, 31, 0.1)",
|
|
5235
|
+
"diff-add-border": "rgba(0, 107, 31, 0.4)",
|
|
5236
|
+
"diff-remove-bg": "rgba(184, 0, 0, 0.1)",
|
|
5237
|
+
"diff-remove-border": "rgba(184, 0, 0, 0.4)",
|
|
5238
|
+
"diff-context-bg": "transparent",
|
|
5239
|
+
"gutter-bg": "#f0f0f0",
|
|
5240
|
+
"gutter-text": "#555555"
|
|
5241
|
+
};
|
|
5242
|
+
var dracula = {
|
|
5243
|
+
"bg": "#282a36",
|
|
5244
|
+
"bg-surface": "#2d303e",
|
|
5245
|
+
"bg-hover": "#343746",
|
|
5246
|
+
"bg-active": "#3e4151",
|
|
5247
|
+
"text": "#f8f8f2",
|
|
5248
|
+
"text-dim": "#8b8da4",
|
|
5249
|
+
"text-bright": "#ffffff",
|
|
5250
|
+
"accent": "#bd93f9",
|
|
5251
|
+
"accent-hover": "#caa5fb",
|
|
5252
|
+
"green": "#50fa7b",
|
|
5253
|
+
"red": "#ff5555",
|
|
5254
|
+
"yellow": "#f1fa8c",
|
|
5255
|
+
"orange": "#ffb86c",
|
|
5256
|
+
"blue": "#8be9fd",
|
|
5257
|
+
"purple": "#bd93f9",
|
|
5258
|
+
"teal": "#8be9fd",
|
|
5259
|
+
"border": "#44475a",
|
|
5260
|
+
"diff-add-bg": "rgba(80, 250, 123, 0.1)",
|
|
5261
|
+
"diff-add-border": "rgba(80, 250, 123, 0.3)",
|
|
5262
|
+
"diff-remove-bg": "rgba(255, 85, 85, 0.1)",
|
|
5263
|
+
"diff-remove-border": "rgba(255, 85, 85, 0.3)",
|
|
5264
|
+
"diff-context-bg": "transparent",
|
|
5265
|
+
"gutter-bg": "#21222c",
|
|
5266
|
+
"gutter-text": "#6272a4"
|
|
5267
|
+
};
|
|
5268
|
+
var tokyoNight = {
|
|
5269
|
+
"bg": "#1a1b26",
|
|
5270
|
+
"bg-surface": "#1f2233",
|
|
5271
|
+
"bg-hover": "#292d42",
|
|
5272
|
+
"bg-active": "#33374e",
|
|
5273
|
+
"text": "#a9b1d6",
|
|
5274
|
+
"text-dim": "#565f89",
|
|
5275
|
+
"text-bright": "#c0caf5",
|
|
5276
|
+
"accent": "#7aa2f7",
|
|
5277
|
+
"accent-hover": "#89b0fa",
|
|
5278
|
+
"green": "#9ece6a",
|
|
5279
|
+
"red": "#f7768e",
|
|
5280
|
+
"yellow": "#e0af68",
|
|
5281
|
+
"orange": "#ff9e64",
|
|
5282
|
+
"blue": "#7aa2f7",
|
|
5283
|
+
"purple": "#bb9af7",
|
|
5284
|
+
"teal": "#73daca",
|
|
5285
|
+
"border": "#2f3351",
|
|
5286
|
+
"diff-add-bg": "rgba(158, 206, 106, 0.1)",
|
|
5287
|
+
"diff-add-border": "rgba(158, 206, 106, 0.3)",
|
|
5288
|
+
"diff-remove-bg": "rgba(247, 118, 142, 0.1)",
|
|
5289
|
+
"diff-remove-border": "rgba(247, 118, 142, 0.3)",
|
|
5290
|
+
"diff-context-bg": "transparent",
|
|
5291
|
+
"gutter-bg": "#16172a",
|
|
5292
|
+
"gutter-text": "#3b4261"
|
|
5293
|
+
};
|
|
5294
|
+
var oneDarkPro = {
|
|
5295
|
+
"bg": "#282c34",
|
|
5296
|
+
"bg-surface": "#2c313a",
|
|
5297
|
+
"bg-hover": "#333842",
|
|
5298
|
+
"bg-active": "#3b4048",
|
|
5299
|
+
"text": "#abb2bf",
|
|
5300
|
+
"text-dim": "#636d83",
|
|
5301
|
+
"text-bright": "#d7dae0",
|
|
5302
|
+
"accent": "#61afef",
|
|
5303
|
+
"accent-hover": "#519fdf",
|
|
5304
|
+
"green": "#98c379",
|
|
5305
|
+
"red": "#e06c75",
|
|
5306
|
+
"yellow": "#e5c07b",
|
|
5307
|
+
"orange": "#d19a66",
|
|
5308
|
+
"blue": "#61afef",
|
|
5309
|
+
"purple": "#c678dd",
|
|
5310
|
+
"teal": "#56b6c2",
|
|
5311
|
+
"border": "#3b4048",
|
|
5312
|
+
"diff-add-bg": "rgba(152, 195, 121, 0.1)",
|
|
5313
|
+
"diff-add-border": "rgba(152, 195, 121, 0.3)",
|
|
5314
|
+
"diff-remove-bg": "rgba(224, 108, 117, 0.1)",
|
|
5315
|
+
"diff-remove-border": "rgba(224, 108, 117, 0.3)",
|
|
5316
|
+
"diff-context-bg": "transparent",
|
|
5317
|
+
"gutter-bg": "#23272e",
|
|
5318
|
+
"gutter-text": "#495162"
|
|
5319
|
+
};
|
|
5320
|
+
var solarizedDark = {
|
|
5321
|
+
"bg": "#002b36",
|
|
5322
|
+
"bg-surface": "#073642",
|
|
5323
|
+
"bg-hover": "#0a4050",
|
|
5324
|
+
"bg-active": "#0d4d5e",
|
|
5325
|
+
"text": "#839496",
|
|
5326
|
+
"text-dim": "#586e75",
|
|
5327
|
+
"text-bright": "#eee8d5",
|
|
5328
|
+
"accent": "#268bd2",
|
|
5329
|
+
"accent-hover": "#1a7cc0",
|
|
5330
|
+
"green": "#859900",
|
|
5331
|
+
"red": "#dc322f",
|
|
5332
|
+
"yellow": "#b58900",
|
|
5333
|
+
"orange": "#cb4b16",
|
|
5334
|
+
"blue": "#268bd2",
|
|
5335
|
+
"purple": "#6c71c4",
|
|
5336
|
+
"teal": "#2aa198",
|
|
5337
|
+
"border": "#0a4050",
|
|
5338
|
+
"diff-add-bg": "rgba(133, 153, 0, 0.12)",
|
|
5339
|
+
"diff-add-border": "rgba(133, 153, 0, 0.3)",
|
|
5340
|
+
"diff-remove-bg": "rgba(220, 50, 47, 0.12)",
|
|
5341
|
+
"diff-remove-border": "rgba(220, 50, 47, 0.3)",
|
|
5342
|
+
"diff-context-bg": "transparent",
|
|
5343
|
+
"gutter-bg": "#002028",
|
|
5344
|
+
"gutter-text": "#4a6568"
|
|
5345
|
+
};
|
|
5346
|
+
var solarizedLight = {
|
|
5347
|
+
"bg": "#fdf6e3",
|
|
5348
|
+
"bg-surface": "#eee8d5",
|
|
5349
|
+
"bg-hover": "#e6dfca",
|
|
5350
|
+
"bg-active": "#ddd6c1",
|
|
5351
|
+
"text": "#657b83",
|
|
5352
|
+
"text-dim": "#93a1a1",
|
|
5353
|
+
"text-bright": "#073642",
|
|
5354
|
+
"accent": "#268bd2",
|
|
5355
|
+
"accent-hover": "#1a7cc0",
|
|
5356
|
+
"green": "#859900",
|
|
5357
|
+
"red": "#dc322f",
|
|
5358
|
+
"yellow": "#b58900",
|
|
5359
|
+
"orange": "#cb4b16",
|
|
5360
|
+
"blue": "#268bd2",
|
|
5361
|
+
"purple": "#6c71c4",
|
|
5362
|
+
"teal": "#2aa198",
|
|
5363
|
+
"border": "#ddd6c1",
|
|
5364
|
+
"diff-add-bg": "rgba(133, 153, 0, 0.1)",
|
|
5365
|
+
"diff-add-border": "rgba(133, 153, 0, 0.25)",
|
|
5366
|
+
"diff-remove-bg": "rgba(220, 50, 47, 0.1)",
|
|
5367
|
+
"diff-remove-border": "rgba(220, 50, 47, 0.25)",
|
|
5368
|
+
"diff-context-bg": "transparent",
|
|
5369
|
+
"gutter-bg": "#eee8d5",
|
|
5370
|
+
"gutter-text": "#93a1a1"
|
|
5371
|
+
};
|
|
5372
|
+
var monokai = {
|
|
5373
|
+
"bg": "#272822",
|
|
5374
|
+
"bg-surface": "#2d2e27",
|
|
5375
|
+
"bg-hover": "#3e3d32",
|
|
5376
|
+
"bg-active": "#49483e",
|
|
5377
|
+
"text": "#f8f8f2",
|
|
5378
|
+
"text-dim": "#75715e",
|
|
5379
|
+
"text-bright": "#ffffff",
|
|
5380
|
+
"accent": "#66d9ef",
|
|
5381
|
+
"accent-hover": "#55c8de",
|
|
5382
|
+
"green": "#a6e22e",
|
|
5383
|
+
"red": "#f92672",
|
|
5384
|
+
"yellow": "#e6db74",
|
|
5385
|
+
"orange": "#fd971f",
|
|
5386
|
+
"blue": "#66d9ef",
|
|
5387
|
+
"purple": "#ae81ff",
|
|
5388
|
+
"teal": "#66d9ef",
|
|
5389
|
+
"border": "#49483e",
|
|
5390
|
+
"diff-add-bg": "rgba(166, 226, 46, 0.1)",
|
|
5391
|
+
"diff-add-border": "rgba(166, 226, 46, 0.3)",
|
|
5392
|
+
"diff-remove-bg": "rgba(249, 38, 114, 0.1)",
|
|
5393
|
+
"diff-remove-border": "rgba(249, 38, 114, 0.3)",
|
|
5394
|
+
"diff-context-bg": "transparent",
|
|
5395
|
+
"gutter-bg": "#222218",
|
|
5396
|
+
"gutter-text": "#575848"
|
|
5397
|
+
};
|
|
5398
|
+
var nord = {
|
|
5399
|
+
"bg": "#2e3440",
|
|
5400
|
+
"bg-surface": "#3b4252",
|
|
5401
|
+
"bg-hover": "#434c5e",
|
|
5402
|
+
"bg-active": "#4c566a",
|
|
5403
|
+
"text": "#d8dee9",
|
|
5404
|
+
"text-dim": "#7b88a1",
|
|
5405
|
+
"text-bright": "#eceff4",
|
|
5406
|
+
"accent": "#88c0d0",
|
|
5407
|
+
"accent-hover": "#81a1c1",
|
|
5408
|
+
"green": "#a3be8c",
|
|
5409
|
+
"red": "#bf616a",
|
|
5410
|
+
"yellow": "#ebcb8b",
|
|
5411
|
+
"orange": "#d08770",
|
|
5412
|
+
"blue": "#81a1c1",
|
|
5413
|
+
"purple": "#b48ead",
|
|
5414
|
+
"teal": "#8fbcbb",
|
|
5415
|
+
"border": "#4c566a",
|
|
5416
|
+
"diff-add-bg": "rgba(163, 190, 140, 0.1)",
|
|
5417
|
+
"diff-add-border": "rgba(163, 190, 140, 0.3)",
|
|
5418
|
+
"diff-remove-bg": "rgba(191, 97, 106, 0.1)",
|
|
5419
|
+
"diff-remove-border": "rgba(191, 97, 106, 0.3)",
|
|
5420
|
+
"diff-context-bg": "transparent",
|
|
5421
|
+
"gutter-bg": "#2a303c",
|
|
5422
|
+
"gutter-text": "#5b6578"
|
|
5423
|
+
};
|
|
5424
|
+
var gruvboxDark = {
|
|
5425
|
+
"bg": "#282828",
|
|
5426
|
+
"bg-surface": "#3c3836",
|
|
5427
|
+
"bg-hover": "#504945",
|
|
5428
|
+
"bg-active": "#665c54",
|
|
5429
|
+
"text": "#ebdbb2",
|
|
5430
|
+
"text-dim": "#928374",
|
|
5431
|
+
"text-bright": "#fbf1c7",
|
|
5432
|
+
"accent": "#83a598",
|
|
5433
|
+
"accent-hover": "#76988b",
|
|
5434
|
+
"green": "#b8bb26",
|
|
5435
|
+
"red": "#fb4934",
|
|
5436
|
+
"yellow": "#fabd2f",
|
|
5437
|
+
"orange": "#fe8019",
|
|
5438
|
+
"blue": "#83a598",
|
|
5439
|
+
"purple": "#d3869b",
|
|
5440
|
+
"teal": "#8ec07c",
|
|
5441
|
+
"border": "#504945",
|
|
5442
|
+
"diff-add-bg": "rgba(184, 187, 38, 0.1)",
|
|
5443
|
+
"diff-add-border": "rgba(184, 187, 38, 0.3)",
|
|
5444
|
+
"diff-remove-bg": "rgba(251, 73, 52, 0.1)",
|
|
5445
|
+
"diff-remove-border": "rgba(251, 73, 52, 0.3)",
|
|
5446
|
+
"diff-context-bg": "transparent",
|
|
5447
|
+
"gutter-bg": "#232323",
|
|
5448
|
+
"gutter-text": "#665c54"
|
|
5449
|
+
};
|
|
5450
|
+
var gruvboxLight = {
|
|
5451
|
+
"bg": "#fbf1c7",
|
|
5452
|
+
"bg-surface": "#f2e5bc",
|
|
5453
|
+
"bg-hover": "#ebdbb2",
|
|
5454
|
+
"bg-active": "#d5c4a1",
|
|
5455
|
+
"text": "#3c3836",
|
|
5456
|
+
"text-dim": "#7c6f64",
|
|
5457
|
+
"text-bright": "#282828",
|
|
5458
|
+
"accent": "#427b58",
|
|
5459
|
+
"accent-hover": "#376b4c",
|
|
5460
|
+
"green": "#79740e",
|
|
5461
|
+
"red": "#9d0006",
|
|
5462
|
+
"yellow": "#b57614",
|
|
5463
|
+
"orange": "#af3a03",
|
|
5464
|
+
"blue": "#076678",
|
|
5465
|
+
"purple": "#8f3f71",
|
|
5466
|
+
"teal": "#427b58",
|
|
5467
|
+
"border": "#d5c4a1",
|
|
5468
|
+
"diff-add-bg": "rgba(121, 116, 14, 0.1)",
|
|
5469
|
+
"diff-add-border": "rgba(121, 116, 14, 0.25)",
|
|
5470
|
+
"diff-remove-bg": "rgba(157, 0, 6, 0.1)",
|
|
5471
|
+
"diff-remove-border": "rgba(157, 0, 6, 0.25)",
|
|
5472
|
+
"diff-context-bg": "transparent",
|
|
5473
|
+
"gutter-bg": "#f2e5bc",
|
|
5474
|
+
"gutter-text": "#928374"
|
|
5475
|
+
};
|
|
5476
|
+
var githubDark = {
|
|
5477
|
+
"bg": "#0d1117",
|
|
5478
|
+
"bg-surface": "#161b22",
|
|
5479
|
+
"bg-hover": "#1c2128",
|
|
5480
|
+
"bg-active": "#262c36",
|
|
5481
|
+
"text": "#c9d1d9",
|
|
5482
|
+
"text-dim": "#8b949e",
|
|
5483
|
+
"text-bright": "#f0f6fc",
|
|
5484
|
+
"accent": "#58a6ff",
|
|
5485
|
+
"accent-hover": "#4090e0",
|
|
5486
|
+
"green": "#3fb950",
|
|
5487
|
+
"red": "#f85149",
|
|
5488
|
+
"yellow": "#d29922",
|
|
5489
|
+
"orange": "#db6d28",
|
|
5490
|
+
"blue": "#58a6ff",
|
|
5491
|
+
"purple": "#bc8cff",
|
|
5492
|
+
"teal": "#39d353",
|
|
5493
|
+
"border": "#30363d",
|
|
5494
|
+
"diff-add-bg": "rgba(63, 185, 80, 0.1)",
|
|
5495
|
+
"diff-add-border": "rgba(63, 185, 80, 0.3)",
|
|
5496
|
+
"diff-remove-bg": "rgba(248, 81, 73, 0.1)",
|
|
5497
|
+
"diff-remove-border": "rgba(248, 81, 73, 0.3)",
|
|
5498
|
+
"diff-context-bg": "transparent",
|
|
5499
|
+
"gutter-bg": "#0a0e14",
|
|
5500
|
+
"gutter-text": "#484f58"
|
|
5501
|
+
};
|
|
5502
|
+
var rosePine = {
|
|
5503
|
+
"bg": "#191724",
|
|
5504
|
+
"bg-surface": "#1f1d2e",
|
|
5505
|
+
"bg-hover": "#26233a",
|
|
5506
|
+
"bg-active": "#2a2740",
|
|
5507
|
+
"text": "#e0def4",
|
|
5508
|
+
"text-dim": "#6e6a86",
|
|
5509
|
+
"text-bright": "#f0efff",
|
|
5510
|
+
"accent": "#c4a7e7",
|
|
5511
|
+
"accent-hover": "#b498d7",
|
|
5512
|
+
"green": "#31748f",
|
|
5513
|
+
"red": "#eb6f92",
|
|
5514
|
+
"yellow": "#f6c177",
|
|
5515
|
+
"orange": "#ea9a97",
|
|
5516
|
+
"blue": "#9ccfd8",
|
|
5517
|
+
"purple": "#c4a7e7",
|
|
5518
|
+
"teal": "#9ccfd8",
|
|
5519
|
+
"border": "#2a2740",
|
|
5520
|
+
"diff-add-bg": "rgba(49, 116, 143, 0.12)",
|
|
5521
|
+
"diff-add-border": "rgba(49, 116, 143, 0.3)",
|
|
5522
|
+
"diff-remove-bg": "rgba(235, 111, 146, 0.12)",
|
|
5523
|
+
"diff-remove-border": "rgba(235, 111, 146, 0.3)",
|
|
5524
|
+
"diff-context-bg": "transparent",
|
|
5525
|
+
"gutter-bg": "#16141f",
|
|
5526
|
+
"gutter-text": "#524f67"
|
|
5527
|
+
};
|
|
5528
|
+
var ayuDark = {
|
|
5529
|
+
"bg": "#0b0e14",
|
|
5530
|
+
"bg-surface": "#0f131a",
|
|
5531
|
+
"bg-hover": "#151a23",
|
|
5532
|
+
"bg-active": "#1c222d",
|
|
5533
|
+
"text": "#bfbdb6",
|
|
5534
|
+
"text-dim": "#636a76",
|
|
5535
|
+
"text-bright": "#e6e1cf",
|
|
5536
|
+
"accent": "#e6b450",
|
|
5537
|
+
"accent-hover": "#d9a740",
|
|
5538
|
+
"green": "#7fd962",
|
|
5539
|
+
"red": "#d95757",
|
|
5540
|
+
"yellow": "#e6b450",
|
|
5541
|
+
"orange": "#ff8f40",
|
|
5542
|
+
"blue": "#59c2ff",
|
|
5543
|
+
"purple": "#d2a6ff",
|
|
5544
|
+
"teal": "#95e6cb",
|
|
5545
|
+
"border": "#1c222d",
|
|
5546
|
+
"diff-add-bg": "rgba(127, 217, 98, 0.1)",
|
|
5547
|
+
"diff-add-border": "rgba(127, 217, 98, 0.3)",
|
|
5548
|
+
"diff-remove-bg": "rgba(217, 87, 87, 0.1)",
|
|
5549
|
+
"diff-remove-border": "rgba(217, 87, 87, 0.3)",
|
|
5550
|
+
"diff-context-bg": "transparent",
|
|
5551
|
+
"gutter-bg": "#080a10",
|
|
5552
|
+
"gutter-text": "#3d424d"
|
|
5553
|
+
};
|
|
5554
|
+
var BUILT_IN_THEMES = [
|
|
5555
|
+
{ id: "dark", name: "Dark", builtIn: true, colors: dark },
|
|
5556
|
+
{ id: "light", name: "Light", builtIn: true, colors: light },
|
|
5557
|
+
{ id: "high-contrast-dark", name: "High Contrast Dark", builtIn: true, colors: highContrastDark },
|
|
5558
|
+
{ id: "high-contrast-light", name: "High Contrast Light", builtIn: true, colors: highContrastLight },
|
|
5559
|
+
{ id: "dracula", name: "Dracula", builtIn: true, colors: dracula },
|
|
5560
|
+
{ id: "tokyo-night", name: "Tokyo Night", builtIn: true, colors: tokyoNight },
|
|
5561
|
+
{ id: "one-dark-pro", name: "One Dark Pro", builtIn: true, colors: oneDarkPro },
|
|
5562
|
+
{ id: "solarized-dark", name: "Solarized Dark", builtIn: true, colors: solarizedDark },
|
|
5563
|
+
{ id: "solarized-light", name: "Solarized Light", builtIn: true, colors: solarizedLight },
|
|
5564
|
+
{ id: "monokai", name: "Monokai", builtIn: true, colors: monokai },
|
|
5565
|
+
{ id: "nord", name: "Nord", builtIn: true, colors: nord },
|
|
5566
|
+
{ id: "gruvbox-dark", name: "Gruvbox Dark", builtIn: true, colors: gruvboxDark },
|
|
5567
|
+
{ id: "gruvbox-light", name: "Gruvbox Light", builtIn: true, colors: gruvboxLight },
|
|
5568
|
+
{ id: "github-dark", name: "GitHub Dark", builtIn: true, colors: githubDark },
|
|
5569
|
+
{ id: "rose-pine", name: "Ros\xE9 Pine", builtIn: true, colors: rosePine },
|
|
5570
|
+
{ id: "ayu-dark", name: "Ayu Dark", builtIn: true, colors: ayuDark }
|
|
5571
|
+
];
|
|
5572
|
+
var DEFAULT_THEME_ID = "dark";
|
|
5573
|
+
function getBuiltInTheme(id) {
|
|
5574
|
+
return BUILT_IN_THEMES.find((t) => t.id === id);
|
|
5575
|
+
}
|
|
5576
|
+
function themeToInlineStyle(colors) {
|
|
5577
|
+
return THEME_VARIABLES.map((v) => `--${v}:${colors[v]}`).join(";");
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
// src/themes/config.ts
|
|
5581
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
5582
|
+
import { homedir as homedir3 } from "os";
|
|
5583
|
+
import { join as join7 } from "path";
|
|
5584
|
+
var CONFIG_DIR2 = join7(homedir3(), ".glassbox");
|
|
5585
|
+
var CONFIG_PATH2 = join7(CONFIG_DIR2, "config.json");
|
|
5586
|
+
var THEMES_DIR = join7(CONFIG_DIR2, "themes");
|
|
5587
|
+
function readConfigFile2() {
|
|
5588
|
+
try {
|
|
5589
|
+
if (existsSync6(CONFIG_PATH2)) {
|
|
5590
|
+
return JSON.parse(readFileSync8(CONFIG_PATH2, "utf-8"));
|
|
5591
|
+
}
|
|
5592
|
+
} catch {
|
|
5593
|
+
}
|
|
5594
|
+
return {};
|
|
5595
|
+
}
|
|
5596
|
+
function writeConfigFile2(config) {
|
|
5597
|
+
mkdirSync5(CONFIG_DIR2, { recursive: true });
|
|
5598
|
+
writeFileSync5(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
5599
|
+
}
|
|
5600
|
+
function getActiveThemeId() {
|
|
5601
|
+
const config = readConfigFile2();
|
|
5602
|
+
const theme = config.theme;
|
|
5603
|
+
const active = theme?.active;
|
|
5604
|
+
return active ?? DEFAULT_THEME_ID;
|
|
5605
|
+
}
|
|
5606
|
+
function setActiveThemeId(id) {
|
|
5607
|
+
const config = readConfigFile2();
|
|
5608
|
+
if (config.theme === void 0) config.theme = {};
|
|
5609
|
+
config.theme.active = id;
|
|
5610
|
+
writeConfigFile2(config);
|
|
5611
|
+
}
|
|
5612
|
+
function loadCustomThemes() {
|
|
5613
|
+
if (!existsSync6(THEMES_DIR)) return [];
|
|
5614
|
+
const themes = [];
|
|
5615
|
+
try {
|
|
5616
|
+
const files = readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json"));
|
|
5617
|
+
for (const file of files) {
|
|
5618
|
+
try {
|
|
5619
|
+
const data = JSON.parse(readFileSync8(join7(THEMES_DIR, file), "utf-8"));
|
|
5620
|
+
if (data.id !== void 0 && data.id !== "" && data.name !== void 0 && data.name !== "" && data.colors !== void 0) {
|
|
5621
|
+
themes.push({ id: data.id, name: data.name, colors: data.colors, builtIn: false, baseTheme: data.baseTheme ?? "" });
|
|
5622
|
+
}
|
|
5623
|
+
} catch {
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
} catch {
|
|
5627
|
+
}
|
|
5628
|
+
return themes;
|
|
5629
|
+
}
|
|
5630
|
+
function saveCustomTheme(theme) {
|
|
5631
|
+
mkdirSync5(THEMES_DIR, { recursive: true });
|
|
5632
|
+
const filePath = join7(THEMES_DIR, `${theme.id}.json`);
|
|
5633
|
+
writeFileSync5(filePath, JSON.stringify(theme, null, 2), "utf-8");
|
|
5634
|
+
}
|
|
5635
|
+
function deleteCustomTheme(id) {
|
|
5636
|
+
const filePath = join7(THEMES_DIR, `${id}.json`);
|
|
5637
|
+
if (existsSync6(filePath)) {
|
|
5638
|
+
unlinkSync2(filePath);
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
function getCustomTheme(id) {
|
|
5642
|
+
const filePath = join7(THEMES_DIR, `${id}.json`);
|
|
5643
|
+
if (!existsSync6(filePath)) return void 0;
|
|
5644
|
+
try {
|
|
5645
|
+
const data = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
5646
|
+
return { ...data, builtIn: false };
|
|
5647
|
+
} catch {
|
|
5648
|
+
return void 0;
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
function getAllThemes() {
|
|
5652
|
+
return [...BUILT_IN_THEMES, ...loadCustomThemes()];
|
|
5653
|
+
}
|
|
5654
|
+
function resolveTheme(id) {
|
|
5655
|
+
return getBuiltInTheme(id) ?? getCustomTheme(id);
|
|
5656
|
+
}
|
|
5657
|
+
function getActiveThemeColors() {
|
|
5658
|
+
const id = getActiveThemeId();
|
|
5659
|
+
const theme = resolveTheme(id);
|
|
5660
|
+
if (theme) return theme.colors;
|
|
5661
|
+
const fallback = getBuiltInTheme(DEFAULT_THEME_ID);
|
|
5662
|
+
if (fallback === void 0) throw new Error(`Default theme '${DEFAULT_THEME_ID}' not found`);
|
|
5663
|
+
return fallback.colors;
|
|
5664
|
+
}
|
|
5665
|
+
function generateThemeId() {
|
|
5666
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
5667
|
+
}
|
|
5668
|
+
|
|
4973
5669
|
// src/components/layout.tsx
|
|
4974
5670
|
function Layout({ title, reviewId, children }) {
|
|
4975
|
-
|
|
5671
|
+
const themeId = getActiveThemeId();
|
|
5672
|
+
const themeColors = getActiveThemeColors();
|
|
5673
|
+
const themeStyle = themeToInlineStyle(themeColors);
|
|
5674
|
+
return /* @__PURE__ */ jsx("html", { lang: "en", style: themeStyle, "data-theme": themeId, children: [
|
|
4976
5675
|
/* @__PURE__ */ jsx("head", { children: [
|
|
4977
5676
|
/* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
|
|
4978
5677
|
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
@@ -5053,7 +5752,7 @@ function ReviewHistory({ reviews, currentReviewId }) {
|
|
|
5053
5752
|
|
|
5054
5753
|
// src/routes/pages.tsx
|
|
5055
5754
|
init_queries();
|
|
5056
|
-
var pageRoutes = new
|
|
5755
|
+
var pageRoutes = new Hono5();
|
|
5057
5756
|
pageRoutes.get("/", async (c) => {
|
|
5058
5757
|
const reviewId = c.get("reviewId");
|
|
5059
5758
|
const review = await getReview(reviewId);
|
|
@@ -5207,11 +5906,11 @@ pageRoutes.get("/file/:fileId", async (c) => {
|
|
|
5207
5906
|
});
|
|
5208
5907
|
pageRoutes.get("/file-raw", (c) => {
|
|
5209
5908
|
const filePath = c.req.query("path");
|
|
5210
|
-
if (
|
|
5909
|
+
if (filePath === void 0 || filePath === "") return c.text("Missing path", 400);
|
|
5211
5910
|
const repoRoot = c.get("repoRoot");
|
|
5212
5911
|
let content;
|
|
5213
5912
|
try {
|
|
5214
|
-
content =
|
|
5913
|
+
content = readFileSync9(resolve4(repoRoot, filePath), "utf-8");
|
|
5215
5914
|
} catch {
|
|
5216
5915
|
return c.text("File not found", 404);
|
|
5217
5916
|
}
|
|
@@ -5234,7 +5933,7 @@ pageRoutes.get("/file-raw", (c) => {
|
|
|
5234
5933
|
}))
|
|
5235
5934
|
}]
|
|
5236
5935
|
};
|
|
5237
|
-
const fakeFile = { id: "", review_id: "", file_path: filePath, status: "reviewed", diff_data: null };
|
|
5936
|
+
const fakeFile = { id: "", review_id: "", file_path: filePath, status: "reviewed", diff_data: null, created_at: "" };
|
|
5238
5937
|
const html = /* @__PURE__ */ jsx(DiffView, { file: fakeFile, diff, annotations: [], mode: "unified" });
|
|
5239
5938
|
return c.html(html.toString());
|
|
5240
5939
|
});
|
|
@@ -5337,10 +6036,144 @@ pageRoutes.get("/history", async (c) => {
|
|
|
5337
6036
|
return c.html(html.toString());
|
|
5338
6037
|
});
|
|
5339
6038
|
|
|
6039
|
+
// src/routes/theme-api.ts
|
|
6040
|
+
import { Hono as Hono6 } from "hono";
|
|
6041
|
+
var themeApiRoutes = new Hono6();
|
|
6042
|
+
function validateColors(colors) {
|
|
6043
|
+
if (typeof colors !== "object" || colors === null || Array.isArray(colors)) {
|
|
6044
|
+
return "colors must be an object";
|
|
6045
|
+
}
|
|
6046
|
+
const validKeys = new Set(THEME_VARIABLES);
|
|
6047
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
6048
|
+
if (!validKeys.has(key)) {
|
|
6049
|
+
return `colors contains unknown key: ${key}`;
|
|
6050
|
+
}
|
|
6051
|
+
if (typeof value !== "string") {
|
|
6052
|
+
return `colors.${key} must be a string`;
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
return null;
|
|
6056
|
+
}
|
|
6057
|
+
themeApiRoutes.get("/", (c) => {
|
|
6058
|
+
const themes = getAllThemes();
|
|
6059
|
+
const activeId = getActiveThemeId();
|
|
6060
|
+
return c.json({
|
|
6061
|
+
themes: themes.map((t) => ({
|
|
6062
|
+
id: t.id,
|
|
6063
|
+
name: t.name,
|
|
6064
|
+
builtIn: t.builtIn,
|
|
6065
|
+
colors: t.colors
|
|
6066
|
+
})),
|
|
6067
|
+
activeId
|
|
6068
|
+
});
|
|
6069
|
+
});
|
|
6070
|
+
themeApiRoutes.get("/active", (c) => {
|
|
6071
|
+
const id = getActiveThemeId();
|
|
6072
|
+
const colors = getActiveThemeColors();
|
|
6073
|
+
return c.json({ id, colors });
|
|
6074
|
+
});
|
|
6075
|
+
themeApiRoutes.post("/active", async (c) => {
|
|
6076
|
+
const body = await c.req.json();
|
|
6077
|
+
if (typeof body.id !== "string" || body.id === "") return c.json({ error: "id must be a non-empty string" }, 400);
|
|
6078
|
+
const theme = resolveTheme(body.id);
|
|
6079
|
+
if (!theme) return c.json({ error: "Theme not found" }, 404);
|
|
6080
|
+
setActiveThemeId(body.id);
|
|
6081
|
+
return c.json({ id: body.id, colors: theme.colors });
|
|
6082
|
+
});
|
|
6083
|
+
themeApiRoutes.post("/", async (c) => {
|
|
6084
|
+
const body = await c.req.json();
|
|
6085
|
+
if (typeof body.sourceId !== "string" || body.sourceId === "") return c.json({ error: "sourceId must be a non-empty string" }, 400);
|
|
6086
|
+
if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
|
|
6087
|
+
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6088
|
+
}
|
|
6089
|
+
const source = resolveTheme(body.sourceId);
|
|
6090
|
+
if (!source) return c.json({ error: "Source theme not found" }, 404);
|
|
6091
|
+
const baseTheme = source.builtIn ? source.id : source.baseTheme;
|
|
6092
|
+
const name = body.name ?? `${source.name} (Copy)`;
|
|
6093
|
+
const newTheme = {
|
|
6094
|
+
id: generateThemeId(),
|
|
6095
|
+
name,
|
|
6096
|
+
builtIn: false,
|
|
6097
|
+
baseTheme,
|
|
6098
|
+
colors: { ...source.colors }
|
|
6099
|
+
};
|
|
6100
|
+
saveCustomTheme(newTheme);
|
|
6101
|
+
return c.json(newTheme, 201);
|
|
6102
|
+
});
|
|
6103
|
+
themeApiRoutes.post("/:id/edit", async (c) => {
|
|
6104
|
+
const id = c.req.param("id");
|
|
6105
|
+
const body = await c.req.json();
|
|
6106
|
+
if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
|
|
6107
|
+
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6108
|
+
}
|
|
6109
|
+
if (body.colors !== void 0) {
|
|
6110
|
+
const colorsError = validateColors(body.colors);
|
|
6111
|
+
if (colorsError !== null) return c.json({ error: colorsError }, 400);
|
|
6112
|
+
}
|
|
6113
|
+
const source = resolveTheme(id);
|
|
6114
|
+
if (!source) return c.json({ error: "Theme not found" }, 404);
|
|
6115
|
+
if (source.builtIn) {
|
|
6116
|
+
const newTheme = {
|
|
6117
|
+
id: generateThemeId(),
|
|
6118
|
+
name: `${source.name} (Customized)`,
|
|
6119
|
+
builtIn: false,
|
|
6120
|
+
baseTheme: source.id,
|
|
6121
|
+
colors: body.colors ? { ...source.colors, ...body.colors } : { ...source.colors }
|
|
6122
|
+
};
|
|
6123
|
+
if (body.name !== void 0 && body.name !== "") newTheme.name = body.name;
|
|
6124
|
+
saveCustomTheme(newTheme);
|
|
6125
|
+
setActiveThemeId(newTheme.id);
|
|
6126
|
+
return c.json({ theme: newTheme, copied: true }, 201);
|
|
6127
|
+
}
|
|
6128
|
+
const updated = {
|
|
6129
|
+
...source,
|
|
6130
|
+
name: body.name ?? source.name,
|
|
6131
|
+
colors: body.colors ? { ...source.colors, ...body.colors } : source.colors
|
|
6132
|
+
};
|
|
6133
|
+
saveCustomTheme(updated);
|
|
6134
|
+
return c.json({ theme: updated, copied: false });
|
|
6135
|
+
});
|
|
6136
|
+
themeApiRoutes.patch("/:id", async (c) => {
|
|
6137
|
+
const id = c.req.param("id");
|
|
6138
|
+
if (getBuiltInTheme(id)) {
|
|
6139
|
+
return c.json({ error: "Cannot edit built-in theme" }, 400);
|
|
6140
|
+
}
|
|
6141
|
+
const existing = resolveTheme(id);
|
|
6142
|
+
if (!existing || existing.builtIn) {
|
|
6143
|
+
return c.json({ error: "Theme not found" }, 404);
|
|
6144
|
+
}
|
|
6145
|
+
const body = await c.req.json();
|
|
6146
|
+
if (body.name !== void 0 && (typeof body.name !== "string" || body.name.trim() === "")) {
|
|
6147
|
+
return c.json({ error: "name must be a non-empty string when provided" }, 400);
|
|
6148
|
+
}
|
|
6149
|
+
if (body.colors !== void 0) {
|
|
6150
|
+
const colorsError = validateColors(body.colors);
|
|
6151
|
+
if (colorsError !== null) return c.json({ error: colorsError }, 400);
|
|
6152
|
+
}
|
|
6153
|
+
const updated = {
|
|
6154
|
+
...existing,
|
|
6155
|
+
name: body.name ?? existing.name,
|
|
6156
|
+
colors: body.colors ? { ...existing.colors, ...body.colors } : existing.colors
|
|
6157
|
+
};
|
|
6158
|
+
saveCustomTheme(updated);
|
|
6159
|
+
return c.json(updated);
|
|
6160
|
+
});
|
|
6161
|
+
themeApiRoutes.delete("/:id", (c) => {
|
|
6162
|
+
const id = c.req.param("id");
|
|
6163
|
+
if (getBuiltInTheme(id)) {
|
|
6164
|
+
return c.json({ error: "Cannot delete built-in theme" }, 400);
|
|
6165
|
+
}
|
|
6166
|
+
deleteCustomTheme(id);
|
|
6167
|
+
if (getActiveThemeId() === id) {
|
|
6168
|
+
setActiveThemeId(BUILT_IN_THEMES[0].id);
|
|
6169
|
+
}
|
|
6170
|
+
return c.json({ ok: true });
|
|
6171
|
+
});
|
|
6172
|
+
|
|
5340
6173
|
// src/server.ts
|
|
5341
|
-
function tryServe(
|
|
6174
|
+
function tryServe(appFetch, port) {
|
|
5342
6175
|
return new Promise((resolve6, reject) => {
|
|
5343
|
-
const server = serve({ fetch:
|
|
6176
|
+
const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
|
|
5344
6177
|
server.on("listening", () => {
|
|
5345
6178
|
resolve6(port);
|
|
5346
6179
|
});
|
|
@@ -5354,7 +6187,7 @@ function tryServe(fetch2, port) {
|
|
|
5354
6187
|
});
|
|
5355
6188
|
}
|
|
5356
6189
|
async function startServer(port, reviewId, repoRoot, options) {
|
|
5357
|
-
const app = new
|
|
6190
|
+
const app = new Hono7();
|
|
5358
6191
|
app.use("*", async (c, next) => {
|
|
5359
6192
|
c.set("reviewId", reviewId);
|
|
5360
6193
|
c.set("currentReviewId", reviewId);
|
|
@@ -5362,24 +6195,25 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
5362
6195
|
await next();
|
|
5363
6196
|
});
|
|
5364
6197
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
5365
|
-
const distDir =
|
|
6198
|
+
const distDir = existsSync7(join8(selfDir, "client", "styles.css")) ? join8(selfDir, "client") : join8(selfDir, "..", "dist", "client");
|
|
5366
6199
|
app.get("/static/styles.css", (c) => {
|
|
5367
|
-
const css =
|
|
6200
|
+
const css = readFileSync10(join8(distDir, "styles.css"), "utf-8");
|
|
5368
6201
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
5369
6202
|
});
|
|
5370
6203
|
app.get("/static/app.js", (c) => {
|
|
5371
|
-
const js =
|
|
6204
|
+
const js = readFileSync10(join8(distDir, "app.global.js"), "utf-8");
|
|
5372
6205
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
5373
6206
|
});
|
|
5374
6207
|
app.get("/static/history.js", (c) => {
|
|
5375
|
-
const js =
|
|
6208
|
+
const js = readFileSync10(join8(distDir, "history.global.js"), "utf-8");
|
|
5376
6209
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
5377
6210
|
});
|
|
5378
6211
|
app.route("/api", apiRoutes);
|
|
5379
6212
|
app.route("/api/ai", aiApiRoutes);
|
|
6213
|
+
app.route("/api/themes", themeApiRoutes);
|
|
5380
6214
|
app.route("/", pageRoutes);
|
|
5381
6215
|
let actualPort = port;
|
|
5382
|
-
if (options?.strictPort) {
|
|
6216
|
+
if (options?.strictPort === true) {
|
|
5383
6217
|
actualPort = await tryServe(app.fetch, port);
|
|
5384
6218
|
} else {
|
|
5385
6219
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
@@ -5401,15 +6235,15 @@ async function startServer(port, reviewId, repoRoot, options) {
|
|
|
5401
6235
|
console.log(`
|
|
5402
6236
|
Glassbox running at ${url}
|
|
5403
6237
|
`);
|
|
5404
|
-
if (
|
|
6238
|
+
if (options?.noOpen !== true) {
|
|
5405
6239
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5406
6240
|
exec(`${openCmd} ${url}`);
|
|
5407
6241
|
}
|
|
5408
6242
|
}
|
|
5409
6243
|
|
|
5410
6244
|
// src/skills.ts
|
|
5411
|
-
import { existsSync as
|
|
5412
|
-
import { join as
|
|
6245
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
6246
|
+
import { join as join9 } from "path";
|
|
5413
6247
|
var SKILL_VERSION = 1;
|
|
5414
6248
|
function versionHeader() {
|
|
5415
6249
|
return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
|
|
@@ -5420,14 +6254,14 @@ function parseVersionHeader(content) {
|
|
|
5420
6254
|
return parseInt(match[1], 10);
|
|
5421
6255
|
}
|
|
5422
6256
|
function updateFile(path, content) {
|
|
5423
|
-
if (
|
|
5424
|
-
const existing =
|
|
6257
|
+
if (existsSync8(path)) {
|
|
6258
|
+
const existing = readFileSync11(path, "utf-8");
|
|
5425
6259
|
const version = parseVersionHeader(existing);
|
|
5426
6260
|
if (version !== null && version >= SKILL_VERSION) {
|
|
5427
6261
|
return false;
|
|
5428
6262
|
}
|
|
5429
6263
|
}
|
|
5430
|
-
|
|
6264
|
+
writeFileSync6(path, content, "utf-8");
|
|
5431
6265
|
return true;
|
|
5432
6266
|
}
|
|
5433
6267
|
function skillBody() {
|
|
@@ -5447,8 +6281,8 @@ function skillBody() {
|
|
|
5447
6281
|
].join("\n");
|
|
5448
6282
|
}
|
|
5449
6283
|
function ensureClaudeSkills(cwd) {
|
|
5450
|
-
const dir =
|
|
5451
|
-
|
|
6284
|
+
const dir = join9(cwd, ".claude", "skills", "glassbox");
|
|
6285
|
+
mkdirSync6(dir, { recursive: true });
|
|
5452
6286
|
const content = [
|
|
5453
6287
|
"---",
|
|
5454
6288
|
"name: glassbox",
|
|
@@ -5460,11 +6294,11 @@ function ensureClaudeSkills(cwd) {
|
|
|
5460
6294
|
skillBody(),
|
|
5461
6295
|
""
|
|
5462
6296
|
].join("\n");
|
|
5463
|
-
return updateFile(
|
|
6297
|
+
return updateFile(join9(dir, "SKILL.md"), content);
|
|
5464
6298
|
}
|
|
5465
6299
|
function ensureCursorRules(cwd) {
|
|
5466
|
-
const rulesDir =
|
|
5467
|
-
|
|
6300
|
+
const rulesDir = join9(cwd, ".cursor", "rules");
|
|
6301
|
+
mkdirSync6(rulesDir, { recursive: true });
|
|
5468
6302
|
const content = [
|
|
5469
6303
|
"---",
|
|
5470
6304
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -5475,11 +6309,11 @@ function ensureCursorRules(cwd) {
|
|
|
5475
6309
|
skillBody(),
|
|
5476
6310
|
""
|
|
5477
6311
|
].join("\n");
|
|
5478
|
-
return updateFile(
|
|
6312
|
+
return updateFile(join9(rulesDir, "glassbox.mdc"), content);
|
|
5479
6313
|
}
|
|
5480
6314
|
function ensureCopilotPrompts(cwd) {
|
|
5481
|
-
const promptsDir =
|
|
5482
|
-
|
|
6315
|
+
const promptsDir = join9(cwd, ".github", "prompts");
|
|
6316
|
+
mkdirSync6(promptsDir, { recursive: true });
|
|
5483
6317
|
const content = [
|
|
5484
6318
|
"---",
|
|
5485
6319
|
"description: Read the latest Glassbox code review and apply all feedback annotations",
|
|
@@ -5489,11 +6323,11 @@ function ensureCopilotPrompts(cwd) {
|
|
|
5489
6323
|
skillBody(),
|
|
5490
6324
|
""
|
|
5491
6325
|
].join("\n");
|
|
5492
|
-
return updateFile(
|
|
6326
|
+
return updateFile(join9(promptsDir, "glassbox.prompt.md"), content);
|
|
5493
6327
|
}
|
|
5494
6328
|
function ensureWindsurfRules(cwd) {
|
|
5495
|
-
const rulesDir =
|
|
5496
|
-
|
|
6329
|
+
const rulesDir = join9(cwd, ".windsurf", "rules");
|
|
6330
|
+
mkdirSync6(rulesDir, { recursive: true });
|
|
5497
6331
|
const content = [
|
|
5498
6332
|
"---",
|
|
5499
6333
|
"trigger: manual",
|
|
@@ -5504,39 +6338,39 @@ function ensureWindsurfRules(cwd) {
|
|
|
5504
6338
|
skillBody(),
|
|
5505
6339
|
""
|
|
5506
6340
|
].join("\n");
|
|
5507
|
-
return updateFile(
|
|
6341
|
+
return updateFile(join9(rulesDir, "glassbox.md"), content);
|
|
5508
6342
|
}
|
|
5509
6343
|
function ensureSkills() {
|
|
5510
6344
|
const cwd = process.cwd();
|
|
5511
6345
|
const platforms = [];
|
|
5512
|
-
if (
|
|
6346
|
+
if (existsSync8(join9(cwd, ".claude"))) {
|
|
5513
6347
|
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
5514
6348
|
}
|
|
5515
|
-
if (
|
|
6349
|
+
if (existsSync8(join9(cwd, ".cursor"))) {
|
|
5516
6350
|
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
5517
6351
|
}
|
|
5518
|
-
if (
|
|
6352
|
+
if (existsSync8(join9(cwd, ".github", "prompts")) || existsSync8(join9(cwd, ".github", "copilot-instructions.md"))) {
|
|
5519
6353
|
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
5520
6354
|
}
|
|
5521
|
-
if (
|
|
6355
|
+
if (existsSync8(join9(cwd, ".windsurf"))) {
|
|
5522
6356
|
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
5523
6357
|
}
|
|
5524
6358
|
return platforms;
|
|
5525
6359
|
}
|
|
5526
6360
|
|
|
5527
6361
|
// src/update-check.ts
|
|
5528
|
-
import { existsSync as
|
|
6362
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
5529
6363
|
import { get } from "https";
|
|
5530
|
-
import { homedir as
|
|
5531
|
-
import { dirname as dirname2, join as
|
|
6364
|
+
import { homedir as homedir4 } from "os";
|
|
6365
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
5532
6366
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5533
|
-
var DATA_DIR =
|
|
5534
|
-
var CHECK_FILE =
|
|
6367
|
+
var DATA_DIR = join10(homedir4(), ".glassbox");
|
|
6368
|
+
var CHECK_FILE = join10(DATA_DIR, "last-update-check");
|
|
5535
6369
|
var PACKAGE_NAME = "glassbox";
|
|
5536
6370
|
function getCurrentVersion() {
|
|
5537
6371
|
try {
|
|
5538
6372
|
const dir = dirname2(fileURLToPath2(import.meta.url));
|
|
5539
|
-
const pkg = JSON.parse(
|
|
6373
|
+
const pkg = JSON.parse(readFileSync12(join10(dir, "..", "package.json"), "utf-8"));
|
|
5540
6374
|
return pkg.version;
|
|
5541
6375
|
} catch {
|
|
5542
6376
|
return "0.0.0";
|
|
@@ -5544,16 +6378,16 @@ function getCurrentVersion() {
|
|
|
5544
6378
|
}
|
|
5545
6379
|
function getLastCheckDate() {
|
|
5546
6380
|
try {
|
|
5547
|
-
if (
|
|
5548
|
-
return
|
|
6381
|
+
if (existsSync9(CHECK_FILE)) {
|
|
6382
|
+
return readFileSync12(CHECK_FILE, "utf-8").trim();
|
|
5549
6383
|
}
|
|
5550
6384
|
} catch {
|
|
5551
6385
|
}
|
|
5552
6386
|
return null;
|
|
5553
6387
|
}
|
|
5554
6388
|
function saveCheckDate() {
|
|
5555
|
-
|
|
5556
|
-
|
|
6389
|
+
mkdirSync7(DATA_DIR, { recursive: true });
|
|
6390
|
+
writeFileSync7(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
|
|
5557
6391
|
}
|
|
5558
6392
|
function isFirstUseToday() {
|
|
5559
6393
|
const last = getLastCheckDate();
|
|
@@ -5636,6 +6470,7 @@ async function checkForUpdates(force) {
|
|
|
5636
6470
|
}
|
|
5637
6471
|
|
|
5638
6472
|
// src/cli.ts
|
|
6473
|
+
import { realpathSync } from "fs";
|
|
5639
6474
|
function printUsage() {
|
|
5640
6475
|
console.log(`
|
|
5641
6476
|
glassbox - Review AI-generated code with annotations
|
|
@@ -5779,13 +6614,13 @@ async function main() {
|
|
|
5779
6614
|
console.log("AI service test mode enabled \u2014 using mock AI responses");
|
|
5780
6615
|
}
|
|
5781
6616
|
if (debug) {
|
|
5782
|
-
console.log(`[debug] Build timestamp: ${"2026-
|
|
6617
|
+
console.log(`[debug] Build timestamp: ${"2026-04-02T11:34:35.783Z"}`);
|
|
5783
6618
|
}
|
|
5784
|
-
if (projectDir) {
|
|
6619
|
+
if (projectDir !== null) {
|
|
5785
6620
|
process.chdir(projectDir);
|
|
5786
6621
|
}
|
|
5787
6622
|
if (dataDir === null) {
|
|
5788
|
-
dataDir =
|
|
6623
|
+
dataDir = join11(process.cwd(), ".glassbox");
|
|
5789
6624
|
}
|
|
5790
6625
|
if (demo !== null) {
|
|
5791
6626
|
const scenario = DEMO_SCENARIOS.find((s) => s.id === demo);
|
|
@@ -5797,13 +6632,13 @@ async function main() {
|
|
|
5797
6632
|
}
|
|
5798
6633
|
process.exit(1);
|
|
5799
6634
|
}
|
|
5800
|
-
dataDir =
|
|
6635
|
+
dataDir = join11(tmpdir(), `glassbox-demo-${demo}-${Date.now()}`);
|
|
5801
6636
|
setDemoMode(demo);
|
|
5802
6637
|
console.log(`
|
|
5803
6638
|
DEMO MODE: ${scenario.label}
|
|
5804
6639
|
`);
|
|
5805
6640
|
}
|
|
5806
|
-
|
|
6641
|
+
mkdirSync8(dataDir, { recursive: true });
|
|
5807
6642
|
if (demo === null) {
|
|
5808
6643
|
acquireLock(dataDir);
|
|
5809
6644
|
}
|
|
@@ -5860,8 +6695,15 @@ async function main() {
|
|
|
5860
6695
|
console.log(`Review ${review.id} created.`);
|
|
5861
6696
|
await startServer(port, review.id, repoRoot, { noOpen, strictPort });
|
|
5862
6697
|
}
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
6698
|
+
var resolvedArg = process.argv[1] !== void 0 ? realpathSync(process.argv[1]) : "";
|
|
6699
|
+
var isDirectRun = resolvedArg.endsWith("cli.js") || resolvedArg.endsWith("cli.ts");
|
|
6700
|
+
if (isDirectRun) {
|
|
6701
|
+
main().catch((err) => {
|
|
6702
|
+
console.error(err);
|
|
6703
|
+
process.exit(1);
|
|
6704
|
+
});
|
|
6705
|
+
}
|
|
6706
|
+
export {
|
|
6707
|
+
parseArgs
|
|
6708
|
+
};
|
|
5867
6709
|
//# sourceMappingURL=cli.js.map
|