glassbox 0.19.0 → 0.20.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11937,21 +11937,21 @@ var init_to_json_schema = __esm({
11937
11937
  // node_modules/zod/v4/core/json-schema-processors.js
11938
11938
  function toJSONSchema(input, params) {
11939
11939
  if ("_idmap" in input) {
11940
- const registry2 = input;
11940
+ const registry3 = input;
11941
11941
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
11942
11942
  const defs = {};
11943
- for (const entry of registry2._idmap.entries()) {
11943
+ for (const entry of registry3._idmap.entries()) {
11944
11944
  const [_, schema] = entry;
11945
11945
  process2(schema, ctx2);
11946
11946
  }
11947
11947
  const schemas = {};
11948
11948
  const external = {
11949
- registry: registry2,
11949
+ registry: registry3,
11950
11950
  uri: params?.uri,
11951
11951
  defs
11952
11952
  };
11953
11953
  ctx2.external = external;
11954
- for (const entry of registry2._idmap.entries()) {
11954
+ for (const entry of registry3._idmap.entries()) {
11955
11955
  const [key, schema] = entry;
11956
11956
  extractDefs(ctx2, schema);
11957
11957
  schemas[key] = finalize(ctx2, schema);
@@ -17254,6 +17254,178 @@ var init_debug = __esm({
17254
17254
  }
17255
17255
  });
17256
17256
 
17257
+ // src/global-config.ts
17258
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
17259
+ import { homedir } from "os";
17260
+ import { join as join4 } from "path";
17261
+ function resolveGlobalConfigDir() {
17262
+ const override = process.env.GLASSBOX_CONFIG_DIR;
17263
+ if (override !== void 0 && override.trim() !== "") return override;
17264
+ return join4(homedir(), ".glassbox");
17265
+ }
17266
+ function readGlobalConfig() {
17267
+ try {
17268
+ if (existsSync4(GLOBAL_CONFIG_PATH)) {
17269
+ const raw2 = JSON.parse(readFileSync3(GLOBAL_CONFIG_PATH, "utf-8"));
17270
+ const parsed = GlobalConfigSchema.safeParse(raw2);
17271
+ if (parsed.success) return parsed.data;
17272
+ }
17273
+ } catch {
17274
+ }
17275
+ return {};
17276
+ }
17277
+ function writeGlobalConfig(config2) {
17278
+ mkdirSync5(GLOBAL_CONFIG_DIR, { recursive: true });
17279
+ writeFileSync3(GLOBAL_CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
17280
+ try {
17281
+ chmodSync(GLOBAL_CONFIG_PATH, 384);
17282
+ } catch {
17283
+ }
17284
+ }
17285
+ function updateGlobalConfig(mutator) {
17286
+ const cfg = readGlobalConfig();
17287
+ const result = mutator(cfg);
17288
+ writeGlobalConfig(result === void 0 ? cfg : result);
17289
+ }
17290
+ var GlobalConfigSchema, GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH;
17291
+ var init_global_config = __esm({
17292
+ "src/global-config.ts"() {
17293
+ "use strict";
17294
+ init_zod();
17295
+ GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
17296
+ GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
17297
+ GLOBAL_CONFIG_PATH = join4(GLOBAL_CONFIG_DIR, "config.json");
17298
+ }
17299
+ });
17300
+
17301
+ // src/ai/keychain.ts
17302
+ import { spawnSync as spawnSync5 } from "child_process";
17303
+ function winTargetForAccount(account) {
17304
+ return `glassbox-${account}`;
17305
+ }
17306
+ function winCredTarget(platform) {
17307
+ return winTargetForAccount(`${platform}-api-key`);
17308
+ }
17309
+ function getSecretFromKeychain(account) {
17310
+ const os = process.platform;
17311
+ try {
17312
+ if (os === "darwin") {
17313
+ const r = spawnSync5("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
17314
+ const result = r.stdout.trim();
17315
+ return r.status === 0 && result !== "" ? result : null;
17316
+ }
17317
+ if (os === "linux") {
17318
+ const r = spawnSync5("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
17319
+ const result = r.stdout.trim();
17320
+ return r.status === 0 && result !== "" ? result : null;
17321
+ }
17322
+ if (os === "win32") {
17323
+ const target = winTargetForAccount(account);
17324
+ const list = spawnSync5("cmdkey", ["/list:" + target], { encoding: "utf-8" });
17325
+ if (list.status !== 0 || (list.stdout || "").includes("* NONE *")) return null;
17326
+ const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
17327
+ const r = spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
17328
+ const result = r.stdout.trim();
17329
+ return r.status === 0 && result !== "" ? result : null;
17330
+ }
17331
+ } catch {
17332
+ return null;
17333
+ }
17334
+ return null;
17335
+ }
17336
+ function getKeyFromKeychain(platform) {
17337
+ return getSecretFromKeychain(`${platform}-api-key`);
17338
+ }
17339
+ function assertSpawnOk(label, r) {
17340
+ if (r.error) throw new Error(`${label} failed: ${r.error.message}`);
17341
+ if (r.status !== 0) {
17342
+ const detail = (String(r.stderr) || String(r.stdout)).trim();
17343
+ throw new Error(`${label} failed (exit ${String(r.status)})${detail ? `: ${detail}` : ""}`);
17344
+ }
17345
+ }
17346
+ function saveSecretToKeychain(account, value, label = "Glassbox") {
17347
+ const os = process.platform;
17348
+ if (os === "darwin") {
17349
+ spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
17350
+ assertSpawnOk("Keychain write", spawnSync5("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", value], { encoding: "utf-8" }));
17351
+ return;
17352
+ }
17353
+ if (os === "linux") {
17354
+ assertSpawnOk("System keyring write", spawnSync5("secret-tool", ["store", `--label=${label}`, "service", "glassbox", "account", account], { input: value, encoding: "utf-8" }));
17355
+ return;
17356
+ }
17357
+ if (os === "win32") {
17358
+ const target = winTargetForAccount(account);
17359
+ const escapedValue = value.replace(/'/g, "''");
17360
+ const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedValue}'`;
17361
+ assertSpawnOk("Credential Manager write", spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" }));
17362
+ }
17363
+ }
17364
+ function deleteSecretFromKeychain(account) {
17365
+ const os = process.platform;
17366
+ try {
17367
+ if (os === "darwin") {
17368
+ spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
17369
+ } else if (os === "linux") {
17370
+ spawnSync5("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
17371
+ } else if (os === "win32") {
17372
+ spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: `cmdkey /delete:'${winTargetForAccount(account)}'`, encoding: "utf-8" });
17373
+ }
17374
+ } catch {
17375
+ }
17376
+ }
17377
+ function saveKeyToKeychain(platform, key) {
17378
+ saveSecretToKeychain(`${platform}-api-key`, key, "Glassbox API Key");
17379
+ }
17380
+ function isKeychainAvailable() {
17381
+ const os = process.platform;
17382
+ if (os === "darwin" || os === "win32") return true;
17383
+ if (os === "linux") {
17384
+ return spawnSync5("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
17385
+ }
17386
+ return false;
17387
+ }
17388
+ function getKeychainLabel() {
17389
+ const os = process.platform;
17390
+ if (os === "darwin") return "Keychain";
17391
+ if (os === "linux") return "System Keyring";
17392
+ if (os === "win32") return "Credential Manager";
17393
+ return "System Keychain";
17394
+ }
17395
+ var WIN_CRED_READ_PS;
17396
+ var init_keychain = __esm({
17397
+ "src/ai/keychain.ts"() {
17398
+ "use strict";
17399
+ WIN_CRED_READ_PS = `
17400
+ Add-Type -TypeDefinition @'
17401
+ using System;
17402
+ using System.Runtime.InteropServices;
17403
+ public class CredHelper {
17404
+ [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
17405
+ static extern bool CredRead(string t, int type, int f, out IntPtr p);
17406
+ [DllImport("advapi32")]
17407
+ static extern void CredFree(IntPtr p);
17408
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
17409
+ struct CRED {
17410
+ public int Flags; public int Type; public string TargetName; public string Comment;
17411
+ public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob;
17412
+ public int Persist; public int AttributeCount; public IntPtr Attributes;
17413
+ public string TargetAlias; public string UserName;
17414
+ }
17415
+ public static string Read(string target) {
17416
+ IntPtr ptr;
17417
+ if (!CredRead(target, 1, 0, out ptr)) return "";
17418
+ CRED c = (CRED)Marshal.PtrToStructure(ptr, typeof(CRED));
17419
+ string r = Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
17420
+ CredFree(ptr);
17421
+ return r;
17422
+ }
17423
+ }
17424
+ '@
17425
+ `;
17426
+ }
17427
+ });
17428
+
17257
17429
  // src/git/image-blobs.ts
17258
17430
  var image_blobs_exports = {};
17259
17431
  __export(image_blobs_exports, {
@@ -17266,33 +17438,1004 @@ import { join as join5 } from "path";
17266
17438
  function blobDir(dataDir) {
17267
17439
  return join5(dataDir, "image-blobs");
17268
17440
  }
17269
- function blobName(fileId, side) {
17270
- return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
17441
+ function blobName(fileId, side) {
17442
+ return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
17443
+ }
17444
+ function writeImageBlob(dataDir, fileId, side, bytes) {
17445
+ if (bytes.length === 0) return;
17446
+ const dir = blobDir(dataDir);
17447
+ mkdirSync6(dir, { recursive: true });
17448
+ writeFileSync4(join5(dir, blobName(fileId, side)), bytes);
17449
+ }
17450
+ function readImageBlob(dataDir, fileId, side) {
17451
+ const path = join5(blobDir(dataDir), blobName(fileId, side));
17452
+ if (!existsSync5(path)) return null;
17453
+ try {
17454
+ return readFileSync4(path);
17455
+ } catch {
17456
+ return null;
17457
+ }
17458
+ }
17459
+ function clearImageBlobs(dataDir) {
17460
+ try {
17461
+ rmSync3(blobDir(dataDir), { recursive: true, force: true });
17462
+ } catch {
17463
+ }
17464
+ }
17465
+ var init_image_blobs = __esm({
17466
+ "src/git/image-blobs.ts"() {
17467
+ "use strict";
17468
+ }
17469
+ });
17470
+
17471
+ // src/feature-flags.ts
17472
+ var PLUGINS_ENABLED;
17473
+ var init_feature_flags = __esm({
17474
+ "src/feature-flags.ts"() {
17475
+ "use strict";
17476
+ PLUGINS_ENABLED = process.env.GLASSBOX_PLUGINS_DISABLED === "1" ? false : true;
17477
+ }
17478
+ });
17479
+
17480
+ // src/api/_runner.ts
17481
+ function currentReviewId() {
17482
+ if (typeof document === "undefined") return "";
17483
+ return document.body.dataset.reviewId ?? "";
17484
+ }
17485
+ async function apiCall(responseSchema, path, opts = {}) {
17486
+ const separator = path.includes("?") ? "&" : "?";
17487
+ const url2 = "/api" + path + separator + "reviewId=" + encodeURIComponent(currentReviewId());
17488
+ const res = await fetch(url2, {
17489
+ headers: { "Content-Type": "application/json" },
17490
+ method: opts.method,
17491
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
17492
+ });
17493
+ const json2 = await res.json();
17494
+ const result = responseSchema.safeParse(json2);
17495
+ if (!result.success) {
17496
+ const summary = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
17497
+ throw new Error(`API response from ${path} failed validation: ${summary}`);
17498
+ }
17499
+ return result.data;
17500
+ }
17501
+ function qs(params) {
17502
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
17503
+ if (entries.length === 0) return "";
17504
+ const usp = new URLSearchParams();
17505
+ for (const [k, v] of entries) usp.set(k, String(v));
17506
+ return "?" + usp.toString();
17507
+ }
17508
+ var OkResponseSchema;
17509
+ var init_runner = __esm({
17510
+ "src/api/_runner.ts"() {
17511
+ "use strict";
17512
+ init_zod();
17513
+ OkResponseSchema = external_exports.object({ ok: external_exports.literal(true) });
17514
+ }
17515
+ });
17516
+
17517
+ // src/api/project-settings.ts
17518
+ var project_settings_exports = {};
17519
+ __export(project_settings_exports, {
17520
+ GetProjectSettingsRespSchema: () => GetProjectSettingsRespSchema,
17521
+ ProjectSettingsSchema: () => ProjectSettingsSchema,
17522
+ UpdateProjectSettingsReqSchema: () => UpdateProjectSettingsReqSchema,
17523
+ UpdateProjectSettingsRespSchema: () => UpdateProjectSettingsRespSchema,
17524
+ getProjectSettings: () => getProjectSettings,
17525
+ updateProjectSettings: () => updateProjectSettings
17526
+ });
17527
+ async function getProjectSettings() {
17528
+ return apiCall(GetProjectSettingsRespSchema, "/project-settings");
17529
+ }
17530
+ async function updateProjectSettings(req) {
17531
+ return apiCall(UpdateProjectSettingsRespSchema, "/project-settings", { method: "PATCH", body: req });
17532
+ }
17533
+ var ProjectSettingsSchema, GetProjectSettingsRespSchema, UpdateProjectSettingsReqSchema, UpdateProjectSettingsRespSchema;
17534
+ var init_project_settings = __esm({
17535
+ "src/api/project-settings.ts"() {
17536
+ "use strict";
17537
+ init_zod();
17538
+ init_runner();
17539
+ ProjectSettingsSchema = external_exports.object({
17540
+ appName: external_exports.string().optional(),
17541
+ /** Content-plugin ids disabled for THIS project (doc 29 FR-29.16). A plugin is
17542
+ * enabled unless it appears here or in the global disabled list. */
17543
+ disabledPlugins: external_exports.array(external_exports.string()).optional(),
17544
+ /** Per-project plugin preference values (doc 29 FR-29.12): pluginId → key → value. */
17545
+ pluginSettings: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.string())).optional()
17546
+ });
17547
+ GetProjectSettingsRespSchema = ProjectSettingsSchema;
17548
+ UpdateProjectSettingsReqSchema = ProjectSettingsSchema.partial();
17549
+ UpdateProjectSettingsRespSchema = ProjectSettingsSchema;
17550
+ }
17551
+ });
17552
+
17553
+ // src/project-settings-store.ts
17554
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
17555
+ import { join as join10 } from "path";
17556
+ function settingsPath(repoRoot) {
17557
+ return join10(repoRoot, ".glassbox", "settings.json");
17558
+ }
17559
+ function readProjectSettings(repoRoot) {
17560
+ try {
17561
+ const path = settingsPath(repoRoot);
17562
+ if (existsSync10(path)) {
17563
+ const raw2 = JSON.parse(readFileSync9(path, "utf-8"));
17564
+ const parsed = ProjectSettingsSchema.safeParse(raw2);
17565
+ if (parsed.success) return parsed.data;
17566
+ }
17567
+ } catch {
17568
+ }
17569
+ return {};
17570
+ }
17571
+ function updateProjectSettings2(repoRoot, mutator) {
17572
+ const current = readProjectSettings(repoRoot);
17573
+ const result = mutator(current);
17574
+ const next = result === void 0 ? current : result;
17575
+ const dir = join10(repoRoot, ".glassbox");
17576
+ mkdirSync8(dir, { recursive: true });
17577
+ writeFileSync9(join10(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
17578
+ }
17579
+ var init_project_settings_store = __esm({
17580
+ "src/project-settings-store.ts"() {
17581
+ "use strict";
17582
+ init_project_settings();
17583
+ }
17584
+ });
17585
+
17586
+ // src/plugins/enablement.ts
17587
+ function toStringArray(v) {
17588
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
17589
+ }
17590
+ function readGlobalDisabled() {
17591
+ return toStringArray(readGlobalConfig().disabledPlugins);
17592
+ }
17593
+ function setGlobalDisabled(id, disabled) {
17594
+ updateGlobalConfig((cfg) => {
17595
+ const cur = toStringArray(cfg.disabledPlugins);
17596
+ const next = disabled ? [.../* @__PURE__ */ new Set([...cur, id])] : cur.filter((x) => x !== id);
17597
+ return { ...cfg, disabledPlugins: next };
17598
+ });
17599
+ }
17600
+ function readProjectDisabled(repoRoot) {
17601
+ return toStringArray(readProjectSettings(repoRoot).disabledPlugins);
17602
+ }
17603
+ function setProjectDisabled(repoRoot, id, disabled) {
17604
+ updateProjectSettings2(repoRoot, (s) => {
17605
+ const cur = toStringArray(s.disabledPlugins);
17606
+ s.disabledPlugins = disabled ? [.../* @__PURE__ */ new Set([...cur, id])] : cur.filter((x) => x !== id);
17607
+ });
17608
+ }
17609
+ function disabledScope(id, lists) {
17610
+ if (lists.globalDisabled.includes(id)) return "global";
17611
+ if (lists.projectDisabled.includes(id)) return "project";
17612
+ return null;
17613
+ }
17614
+ function readEnablementLists(repoRoot) {
17615
+ return { globalDisabled: readGlobalDisabled(), projectDisabled: readProjectDisabled(repoRoot) };
17616
+ }
17617
+ var init_enablement = __esm({
17618
+ "src/plugins/enablement.ts"() {
17619
+ "use strict";
17620
+ init_global_config();
17621
+ init_project_settings_store();
17622
+ }
17623
+ });
17624
+
17625
+ // src/plugins/manifest.ts
17626
+ function parseManifest(raw2) {
17627
+ const parsed = PluginManifestSchema.safeParse(raw2);
17628
+ return parsed.success ? parsed.data : null;
17629
+ }
17630
+ var ContentTypeSchema, PluginPreferenceSchema, ConfigLabelColorSchema, ConfigLayoutItemSchema, PluginRequirementSchema, PluginProvisionStepSchema, PluginInstallSchema, PluginManifestSchema;
17631
+ var init_manifest2 = __esm({
17632
+ "src/plugins/manifest.ts"() {
17633
+ "use strict";
17634
+ init_zod();
17635
+ ContentTypeSchema = external_exports.object({
17636
+ extensions: external_exports.array(external_exports.string()).optional(),
17637
+ mimeTypes: external_exports.array(external_exports.string()).optional()
17638
+ }).loose();
17639
+ PluginPreferenceSchema = external_exports.object({
17640
+ key: external_exports.string().min(1),
17641
+ label: external_exports.string().min(1),
17642
+ type: external_exports.enum(["string", "number", "boolean", "select"]),
17643
+ /** Stored as a string; the plugin coerces. */
17644
+ default: external_exports.string().optional(),
17645
+ description: external_exports.string().optional(),
17646
+ /** `select` options (value === label unless an object form is used later). */
17647
+ options: external_exports.array(external_exports.string()).optional(),
17648
+ /** Per-project vs global storage (default global). Ignored for secrets
17649
+ * (always keychain). */
17650
+ scope: external_exports.enum(["global", "project"]).optional(),
17651
+ /** Secret prefs are stored in the OS keychain, never in config (GB-1054). */
17652
+ secret: external_exports.boolean().optional()
17653
+ }).loose();
17654
+ ConfigLabelColorSchema = external_exports.enum(["default", "success", "error", "warning", "transient"]);
17655
+ ConfigLayoutItemSchema = external_exports.lazy(
17656
+ () => external_exports.object({
17657
+ type: external_exports.enum(["preference", "divider", "spacer", "label", "button", "group"]),
17658
+ key: external_exports.string().optional(),
17659
+ id: external_exports.string().optional(),
17660
+ text: external_exports.string().optional(),
17661
+ color: ConfigLabelColorSchema.optional(),
17662
+ label: external_exports.string().optional(),
17663
+ action: external_exports.string().optional(),
17664
+ style: external_exports.string().optional(),
17665
+ title: external_exports.string().optional(),
17666
+ collapsed: external_exports.boolean().optional(),
17667
+ items: external_exports.array(ConfigLayoutItemSchema).optional()
17668
+ }).loose()
17669
+ );
17670
+ PluginRequirementSchema = external_exports.object({
17671
+ id: external_exports.string().min(1),
17672
+ label: external_exports.string().min(1),
17673
+ /** Executable whose presence proves the requirement (run with `checkArgs`). */
17674
+ command: external_exports.string().min(1),
17675
+ /** Args for the presence check (default `['--version']`). */
17676
+ checkArgs: external_exports.array(external_exports.string()).optional(),
17677
+ /** Human remediation shown when the requirement is missing. */
17678
+ hint: external_exports.string().min(1),
17679
+ docUrl: external_exports.string().optional()
17680
+ }).loose();
17681
+ PluginProvisionStepSchema = external_exports.union([
17682
+ external_exports.object({
17683
+ kind: external_exports.literal("fetch"),
17684
+ url: external_exports.string().min(1),
17685
+ /** Destination filename inside the install dir. */
17686
+ dest: external_exports.string().min(1),
17687
+ /** Optional expected sha-256 (hex) of the downloaded bytes. */
17688
+ sha256: external_exports.string().optional()
17689
+ }).loose(),
17690
+ external_exports.object({
17691
+ kind: external_exports.literal("npm-install"),
17692
+ packages: external_exports.array(external_exports.string().min(1)).min(1),
17693
+ /** Requirement id whose command must be present to run this (default `npm`). */
17694
+ requires: external_exports.string().optional(),
17695
+ note: external_exports.string().optional()
17696
+ }).loose()
17697
+ ]);
17698
+ PluginInstallSchema = external_exports.object({
17699
+ requirements: external_exports.array(PluginRequirementSchema).optional(),
17700
+ provision: external_exports.array(PluginProvisionStepSchema).optional(),
17701
+ /** Fallback command to run by hand when auto-provisioning can't complete. */
17702
+ cliHint: external_exports.string().optional()
17703
+ }).loose();
17704
+ PluginManifestSchema = external_exports.object({
17705
+ id: external_exports.string().min(1),
17706
+ name: external_exports.string().min(1),
17707
+ version: external_exports.string().min(1),
17708
+ /** Entry module relative to the plugin dir; defaults to `index.js`. */
17709
+ entry: external_exports.string().min(1).optional(),
17710
+ description: external_exports.string().optional(),
17711
+ author: external_exports.string().optional(),
17712
+ /** The content types this plugin handles (informational for the UI; the
17713
+ * authoritative match lives in the registered renderer/differ). */
17714
+ contentTypes: external_exports.array(ContentTypeSchema).optional(),
17715
+ /** User-configurable preferences (doc 29 FR-29.12); rendered in Settings. */
17716
+ preferences: external_exports.array(PluginPreferenceSchema).optional(),
17717
+ /** Optional arrangement of the preferences into groups/dividers/labels/
17718
+ * buttons (doc 29 FR-29.18). Omitted → flat preference list. */
17719
+ configLayout: external_exports.array(ConfigLayoutItemSchema).optional(),
17720
+ /** `false` marks a **separately-installable** plugin (e.g. one with a system
17721
+ * requirement like Java): it's built + may ship in the bundle, but
17722
+ * `installBundledPlugins` does NOT auto-install it — the user opts in
17723
+ * (GB-1046). Defaults to auto-install (true) when omitted. */
17724
+ autoInstall: external_exports.boolean().optional(),
17725
+ /** How to install an opt-in plugin from the UI (doc 29 §29.2, GB-1069):
17726
+ * system requirements + auto-run provisioning steps. Absent → self-contained. */
17727
+ install: PluginInstallSchema.optional()
17728
+ }).loose();
17729
+ }
17730
+ });
17731
+
17732
+ // src/plugins/registry.ts
17733
+ import { extname } from "path";
17734
+ function matchSpecificity(match, input) {
17735
+ if (match.sniff && input.bytes.length > 0) {
17736
+ try {
17737
+ if (match.sniff(input.bytes)) return 3;
17738
+ } catch {
17739
+ }
17740
+ }
17741
+ if (match.mimeTypes && input.mime !== void 0 && match.mimeTypes.includes(input.mime)) return 2;
17742
+ const ext = extname(input.path).toLowerCase();
17743
+ if (ext !== "" && match.extensions?.some((e) => e.toLowerCase() === ext) === true) return 1;
17744
+ return 0;
17745
+ }
17746
+ function pathMatches(match, path, mime) {
17747
+ const ext = extname(path).toLowerCase();
17748
+ if (ext !== "" && match.extensions?.some((e) => e.toLowerCase() === ext) === true) return true;
17749
+ if (mime !== void 0 && match.mimeTypes?.includes(mime) === true) return true;
17750
+ return false;
17751
+ }
17752
+ function pickBest(handlers, input) {
17753
+ let best;
17754
+ let bestScore = 0;
17755
+ for (const h of handlers) {
17756
+ const spec = matchSpecificity(h.match, input);
17757
+ if (spec === 0) continue;
17758
+ const score = (h.priority ?? 0) * 10 + spec;
17759
+ if (score > bestScore) {
17760
+ bestScore = score;
17761
+ best = h;
17762
+ }
17763
+ }
17764
+ return best;
17765
+ }
17766
+ var ContentPluginRegistry;
17767
+ var init_registry = __esm({
17768
+ "src/plugins/registry.ts"() {
17769
+ "use strict";
17770
+ ContentPluginRegistry = class {
17771
+ renderers = [];
17772
+ differs = [];
17773
+ imageDecoders = [];
17774
+ addRenderers(rs) {
17775
+ if (rs) this.renderers.push(...rs);
17776
+ }
17777
+ addDiffers(ds) {
17778
+ if (ds) this.differs.push(...ds);
17779
+ }
17780
+ addImageDecoders(ds) {
17781
+ if (ds) this.imageDecoders.push(...ds);
17782
+ }
17783
+ /** The best-matching renderer for a single content blob, or undefined. */
17784
+ findRenderer(input) {
17785
+ return pickBest(this.renderers, input);
17786
+ }
17787
+ /** The best-matching differ for a pair; matching keys off the new side. */
17788
+ findDiffer(input) {
17789
+ return pickBest(this.differs, input.new);
17790
+ }
17791
+ /** The best-matching image decoder (bytes → RGBA) for a blob, or undefined. */
17792
+ findImageDecoder(input) {
17793
+ return pickBest(this.imageDecoders, input);
17794
+ }
17795
+ /** Cheap path pre-check: could any renderer/differ handle this path (by ext /
17796
+ * MIME)? Skips sniff so no bytes are read. */
17797
+ mightHandleByPath(path, mime) {
17798
+ for (const h of this.renderers) if (pathMatches(h.match, path, mime)) return true;
17799
+ for (const h of this.differs) if (pathMatches(h.match, path, mime)) return true;
17800
+ return false;
17801
+ }
17802
+ get rendererCount() {
17803
+ return this.renderers.length;
17804
+ }
17805
+ get differCount() {
17806
+ return this.differs.length;
17807
+ }
17808
+ get imageDecoderCount() {
17809
+ return this.imageDecoders.length;
17810
+ }
17811
+ };
17812
+ }
17813
+ });
17814
+
17815
+ // src/plugins/settings.ts
17816
+ function secretAccount(pluginId, key) {
17817
+ return `plugin-${pluginId}-${key}`;
17818
+ }
17819
+ function coerceSettingsMap(raw2) {
17820
+ const out = {};
17821
+ if (raw2 === null || typeof raw2 !== "object") return out;
17822
+ for (const [pluginId, byKey] of Object.entries(raw2)) {
17823
+ if (byKey === null || typeof byKey !== "object") continue;
17824
+ const entry = {};
17825
+ for (const [k, v] of Object.entries(byKey)) {
17826
+ if (typeof v === "string") entry[k] = v;
17827
+ }
17828
+ out[pluginId] = entry;
17829
+ }
17830
+ return out;
17831
+ }
17832
+ function prefFor(manifest, key) {
17833
+ return manifest.preferences?.find((p) => p.key === key);
17834
+ }
17835
+ function scopeOf(pref) {
17836
+ return pref?.scope === "project" ? "project" : "global";
17837
+ }
17838
+ function readGlobalSettings() {
17839
+ return coerceSettingsMap(readGlobalConfig().pluginSettings);
17840
+ }
17841
+ function readProjectSettingsMap(repoRoot) {
17842
+ return coerceSettingsMap(readProjectSettings(repoRoot).pluginSettings);
17843
+ }
17844
+ function lookup(map2, pluginId, key) {
17845
+ return map2[pluginId]?.[key];
17846
+ }
17847
+ function readPluginSetting(manifest, repoRoot, key) {
17848
+ const pref = prefFor(manifest, key);
17849
+ if (pref?.secret === true) {
17850
+ return getSecretFromKeychain(secretAccount(manifest.id, key)) ?? pref.default ?? null;
17851
+ }
17852
+ const map2 = scopeOf(pref) === "project" ? readProjectSettingsMap(repoRoot) : readGlobalSettings();
17853
+ const stored = lookup(map2, manifest.id, key);
17854
+ if (stored !== void 0) return stored;
17855
+ return pref?.default ?? null;
17856
+ }
17857
+ function writePluginSetting(manifest, repoRoot, key, value) {
17858
+ const pref = prefFor(manifest, key);
17859
+ if (pref?.secret === true) {
17860
+ if (value === "") deleteSecretFromKeychain(secretAccount(manifest.id, key));
17861
+ else saveSecretToKeychain(secretAccount(manifest.id, key), value, `Glassbox plugin: ${manifest.id}`);
17862
+ return;
17863
+ }
17864
+ if (scopeOf(pref) === "project") {
17865
+ updateProjectSettings2(repoRoot, (s) => {
17866
+ const map2 = coerceSettingsMap(s.pluginSettings);
17867
+ map2[manifest.id] = { ...map2[manifest.id], [key]: value };
17868
+ s.pluginSettings = map2;
17869
+ });
17870
+ } else {
17871
+ updateGlobalConfig((cfg) => {
17872
+ const map2 = coerceSettingsMap(cfg.pluginSettings);
17873
+ map2[manifest.id] = { ...map2[manifest.id], [key]: value };
17874
+ return { ...cfg, pluginSettings: map2 };
17875
+ });
17876
+ }
17877
+ }
17878
+ function readPluginPreferenceDisplay(manifest, repoRoot) {
17879
+ const values = {};
17880
+ const secretConfigured = [];
17881
+ for (const p of manifest.preferences ?? []) {
17882
+ if (p.secret === true) {
17883
+ const stored = getSecretFromKeychain(secretAccount(manifest.id, p.key));
17884
+ if (stored !== null && stored !== "") secretConfigured.push(p.key);
17885
+ values[p.key] = "";
17886
+ } else {
17887
+ values[p.key] = readPluginSetting(manifest, repoRoot, p.key) ?? "";
17888
+ }
17889
+ }
17890
+ return { values, secretConfigured };
17891
+ }
17892
+ var init_settings = __esm({
17893
+ "src/plugins/settings.ts"() {
17894
+ "use strict";
17895
+ init_keychain();
17896
+ init_global_config();
17897
+ init_project_settings_store();
17898
+ }
17899
+ });
17900
+
17901
+ // src/plugins/loader.ts
17902
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
17903
+ import { join as join11 } from "path";
17904
+ import { pathToFileURL } from "url";
17905
+ function getConfigLabelOverride(pluginId, labelId) {
17906
+ return configLabelOverrides.get(`${pluginId}:${labelId}`);
17907
+ }
17908
+ function clearConfigLabelOverrides(pluginId) {
17909
+ const prefix = `${pluginId}:`;
17910
+ for (const key of configLabelOverrides.keys()) {
17911
+ if (key.startsWith(prefix)) configLabelOverrides.delete(key);
17912
+ }
17913
+ }
17914
+ function getPluginUIElements() {
17915
+ return Array.from(pluginUIElements.entries()).map(([pluginId, elements]) => ({ pluginId, elements }));
17916
+ }
17917
+ function clearPluginUIElements(pluginId) {
17918
+ pluginUIElements.delete(pluginId);
17919
+ }
17920
+ function clearAllPluginUIElements() {
17921
+ pluginUIElements.clear();
17922
+ }
17923
+ function pluginsDir() {
17924
+ return join11(GLOBAL_CONFIG_DIR, "plugins");
17925
+ }
17926
+ function discoverPluginDirs(root = pluginsDir()) {
17927
+ if (!existsSync11(root)) return [];
17928
+ let entries;
17929
+ try {
17930
+ entries = readdirSync2(root);
17931
+ } catch {
17932
+ return [];
17933
+ }
17934
+ const out = [];
17935
+ for (const name of entries) {
17936
+ if (name.startsWith(".")) continue;
17937
+ const dir = join11(root, name);
17938
+ try {
17939
+ if (statSync3(dir).isDirectory()) out.push(dir);
17940
+ } catch {
17941
+ }
17942
+ }
17943
+ return out.sort();
17944
+ }
17945
+ function readManifest(dir) {
17946
+ const manifestPath = join11(dir, "manifest.json");
17947
+ if (existsSync11(manifestPath)) {
17948
+ try {
17949
+ return parseManifest(JSON.parse(readFileSync10(manifestPath, "utf-8")));
17950
+ } catch {
17951
+ return null;
17952
+ }
17953
+ }
17954
+ const pkgPath = join11(dir, "package.json");
17955
+ if (existsSync11(pkgPath)) {
17956
+ try {
17957
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
17958
+ const gb = pkg.glassbox;
17959
+ if (gb !== null && typeof gb === "object") {
17960
+ return parseManifest({ id: pkg.name, version: pkg.version, entry: pkg.main, ...gb });
17961
+ }
17962
+ } catch {
17963
+ return null;
17964
+ }
17965
+ }
17966
+ return null;
17967
+ }
17968
+ function makeContext(manifest, repoRoot) {
17969
+ const id = manifest.id;
17970
+ return {
17971
+ log: (level, message) => {
17972
+ const line = `[plugin:${id}] ${message}`;
17973
+ if (level === "error") console.error(line);
17974
+ else if (level === "warn") console.warn(line);
17975
+ else console.log(line);
17976
+ },
17977
+ getSetting: (key) => Promise.resolve(readPluginSetting(manifest, repoRoot, key)),
17978
+ setSetting: (key, value) => {
17979
+ writePluginSetting(manifest, repoRoot, key, value);
17980
+ return Promise.resolve();
17981
+ },
17982
+ updateConfigLabel: (labelId, text, color) => {
17983
+ configLabelOverrides.set(`${id}:${labelId}`, { text, color });
17984
+ },
17985
+ registerUI: (elements) => {
17986
+ pluginUIElements.set(id, elements);
17987
+ }
17988
+ };
17989
+ }
17990
+ async function loadPluginDir(dir, registry3, isEnabled = ALWAYS_ENABLED, repoRoot = "") {
17991
+ const manifest = readManifest(dir);
17992
+ if (manifest === null) {
17993
+ return { id: dir, dir, manifest: null, status: "error", error: "invalid or missing manifest" };
17994
+ }
17995
+ const enablement = isEnabled(manifest.id);
17996
+ if (enablement.disabled) {
17997
+ return { id: manifest.id, dir, manifest, status: "disabled", disabledScope: enablement.scope };
17998
+ }
17999
+ try {
18000
+ const entry = join11(dir, manifest.entry ?? "index.js");
18001
+ if (!existsSync11(entry)) throw new Error(`entry not found: ${manifest.entry ?? "index.js"}`);
18002
+ const mod = await import(pathToFileURL(entry).href);
18003
+ const plugin = mod.default ?? (typeof mod.activate === "function" ? { activate: mod.activate, onAction: mod.onAction } : void 0);
18004
+ if (plugin === void 0 || typeof plugin.activate !== "function") {
18005
+ throw new Error("plugin exports no activate()");
18006
+ }
18007
+ const context = makeContext(manifest, repoRoot);
18008
+ const registration = await plugin.activate(context) ?? void 0;
18009
+ if (registration !== void 0) {
18010
+ registry3.addRenderers(registration.renderers);
18011
+ registry3.addDiffers(registration.differs);
18012
+ registry3.addImageDecoders(registration.imageDecoders);
18013
+ }
18014
+ return { id: manifest.id, dir, manifest, status: "loaded", registration, instance: plugin, context };
18015
+ } catch (e) {
18016
+ return { id: manifest.id, dir, manifest, status: "error", error: e instanceof Error ? e.message : String(e) };
18017
+ }
18018
+ }
18019
+ async function loadAllPlugins(root = pluginsDir(), isEnabled = ALWAYS_ENABLED, repoRoot = "") {
18020
+ const registry3 = new ContentPluginRegistry();
18021
+ if (!PLUGINS_ENABLED) return { registry: registry3, loaded: [] };
18022
+ const loaded = [];
18023
+ for (const dir of discoverPluginDirs(root)) {
18024
+ loaded.push(await loadPluginDir(dir, registry3, isEnabled, repoRoot));
18025
+ }
18026
+ return { registry: registry3, loaded };
18027
+ }
18028
+ var configLabelOverrides, pluginUIElements, ALWAYS_ENABLED;
18029
+ var init_loader = __esm({
18030
+ "src/plugins/loader.ts"() {
18031
+ "use strict";
18032
+ init_feature_flags();
18033
+ init_global_config();
18034
+ init_manifest2();
18035
+ init_registry();
18036
+ init_settings();
18037
+ configLabelOverrides = /* @__PURE__ */ new Map();
18038
+ pluginUIElements = /* @__PURE__ */ new Map();
18039
+ ALWAYS_ENABLED = () => ({ disabled: false });
18040
+ }
18041
+ });
18042
+
18043
+ // src/plugins/install.ts
18044
+ import { createHash as createHash3 } from "crypto";
18045
+ import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync11, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
18046
+ import { dirname as dirname7, join as join12 } from "path";
18047
+ import { fileURLToPath as fileURLToPath2 } from "url";
18048
+ function dismissedPathFor(userDir) {
18049
+ return join12(dirname7(userDir), DISMISSED_FILE);
18050
+ }
18051
+ function bundledPluginsDir() {
18052
+ const override = process.env.GLASSBOX_BUNDLED_PLUGINS_DIR;
18053
+ if (override !== void 0 && override.trim() !== "") return override;
18054
+ const sibling = join12(dirname7(fileURLToPath2(import.meta.url)), "plugins");
18055
+ if (existsSync12(sibling)) return sibling;
18056
+ return join12(process.cwd(), "dist", "plugins");
18057
+ }
18058
+ function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18059
+ if (!existsSync12(bundledDir)) return [];
18060
+ let entries;
18061
+ try {
18062
+ entries = readdirSync3(bundledDir);
18063
+ } catch {
18064
+ return [];
18065
+ }
18066
+ const out = [];
18067
+ for (const name of entries.sort()) {
18068
+ const dir = join12(bundledDir, name);
18069
+ try {
18070
+ if (!statSync4(dir).isDirectory()) continue;
18071
+ const manifest = readManifest(dir);
18072
+ if (manifest !== null) out.push({ dir, manifest });
18073
+ } catch {
18074
+ }
18075
+ }
18076
+ return out;
18077
+ }
18078
+ function readDismissed(userDir = pluginsDir()) {
18079
+ try {
18080
+ const raw2 = JSON.parse(readFileSync11(dismissedPathFor(userDir), "utf-8"));
18081
+ return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
18082
+ } catch {
18083
+ return [];
18084
+ }
17271
18085
  }
17272
- function writeImageBlob(dataDir, fileId, side, bytes) {
17273
- if (bytes.length === 0) return;
17274
- const dir = blobDir(dataDir);
17275
- mkdirSync6(dir, { recursive: true });
17276
- writeFileSync4(join5(dir, blobName(fileId, side)), bytes);
18086
+ function writeDismissed(ids, userDir) {
18087
+ const path = dismissedPathFor(userDir);
18088
+ mkdirSync9(dirname7(path), { recursive: true });
18089
+ writeFileSync10(path, JSON.stringify([...new Set(ids)], null, 2), "utf-8");
17277
18090
  }
17278
- function readImageBlob(dataDir, fileId, side) {
17279
- const path = join5(blobDir(dataDir), blobName(fileId, side));
17280
- if (!existsSync5(path)) return null;
18091
+ function compareVersions(a, b) {
18092
+ const pa = a.split(".").map((p) => parseInt(p, 10) || 0);
18093
+ const pb = b.split(".").map((p) => parseInt(p, 10) || 0);
18094
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
18095
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
18096
+ if (d !== 0) return d > 0 ? 1 : -1;
18097
+ }
18098
+ return 0;
18099
+ }
18100
+ function hashPluginDir(dir) {
18101
+ const h = createHash3("sha1");
18102
+ const walk = (d, rel) => {
18103
+ for (const name of readdirSync3(d).sort()) {
18104
+ const abs = join12(d, name);
18105
+ const r = rel === "" ? name : `${rel}/${name}`;
18106
+ if (statSync4(abs).isDirectory()) walk(abs, r);
18107
+ else {
18108
+ h.update(r);
18109
+ h.update(readFileSync11(abs));
18110
+ }
18111
+ }
18112
+ };
18113
+ walk(dir, "");
18114
+ return h.digest("hex");
18115
+ }
18116
+ function shouldInstall(src, dest, bundledVersion) {
18117
+ if (!existsSync12(dest)) return true;
18118
+ const destManifest = readManifest(dest);
18119
+ const destVersion = destManifest?.version ?? "0";
18120
+ const cmp = compareVersions(bundledVersion, destVersion);
18121
+ if (cmp > 0) return true;
18122
+ if (cmp < 0) return false;
17281
18123
  try {
17282
- return readFileSync4(path);
18124
+ return hashPluginDir(src) !== hashPluginDir(dest);
18125
+ } catch {
18126
+ return false;
18127
+ }
18128
+ }
18129
+ function installBundledPlugins(opts) {
18130
+ const bundledDir = opts?.bundledDir ?? bundledPluginsDir();
18131
+ const userDir = opts?.userDir ?? pluginsDir();
18132
+ if (!existsSync12(bundledDir)) return;
18133
+ const dismissed = new Set(readDismissed(userDir));
18134
+ let entries;
18135
+ try {
18136
+ entries = readdirSync3(bundledDir);
18137
+ } catch {
18138
+ return;
18139
+ }
18140
+ for (const name of entries) {
18141
+ const src = join12(bundledDir, name);
18142
+ try {
18143
+ if (!statSync4(src).isDirectory()) continue;
18144
+ const manifest = readManifest(src);
18145
+ if (manifest === null || dismissed.has(manifest.id)) continue;
18146
+ if (manifest.autoInstall === false) continue;
18147
+ const dest = join12(userDir, manifest.id);
18148
+ if (!shouldInstall(src, dest, manifest.version)) continue;
18149
+ mkdirSync9(userDir, { recursive: true });
18150
+ rmSync6(dest, { recursive: true, force: true });
18151
+ cpSync(src, dest, { recursive: true });
18152
+ } catch {
18153
+ }
18154
+ }
18155
+ }
18156
+ function installPluginFromDisk(sourceDir, opts) {
18157
+ const manifest = readManifest(sourceDir);
18158
+ if (manifest === null) throw new Error("The selected folder is not a Glassbox plugin (no manifest.json).");
18159
+ const userDir = opts?.userDir ?? pluginsDir();
18160
+ mkdirSync9(userDir, { recursive: true });
18161
+ const dest = join12(userDir, manifest.id);
18162
+ rmSync6(dest, { recursive: true, force: true });
18163
+ try {
18164
+ symlinkSync(sourceDir, dest, "dir");
18165
+ } catch {
18166
+ cpSync(sourceDir, dest, { recursive: true });
18167
+ }
18168
+ writeDismissed(readDismissed(userDir).filter((x) => x !== manifest.id), userDir);
18169
+ return { id: manifest.id };
18170
+ }
18171
+ function undismissPlugin(id, userDir = pluginsDir()) {
18172
+ writeDismissed(readDismissed(userDir).filter((x) => x !== id), userDir);
18173
+ }
18174
+ function uninstallPlugin(id, opts) {
18175
+ const userDir = opts?.userDir ?? pluginsDir();
18176
+ rmSync6(join12(userDir, id), { recursive: true, force: true });
18177
+ writeDismissed([...readDismissed(userDir), id], userDir);
18178
+ }
18179
+ var DISMISSED_FILE;
18180
+ var init_install = __esm({
18181
+ "src/plugins/install.ts"() {
18182
+ "use strict";
18183
+ init_loader();
18184
+ DISMISSED_FILE = "dismissed-plugins.json";
18185
+ }
18186
+ });
18187
+
18188
+ // src/plugins/index.ts
18189
+ var plugins_exports = {};
18190
+ __export(plugins_exports, {
18191
+ __resetContentPluginsForTest: () => __resetContentPluginsForTest,
18192
+ __setContentRegistryForTest: () => __setContentRegistryForTest,
18193
+ decodeImageWithPlugin: () => decodeImageWithPlugin,
18194
+ describeInstalledPlugins: () => describeInstalledPlugins,
18195
+ diffContent: () => diffContent,
18196
+ getLoadedPlugins: () => getLoadedPlugins,
18197
+ getPluginManifest: () => getPluginManifest,
18198
+ initContentPlugins: () => initContentPlugins,
18199
+ listPluginUIElements: () => listPluginUIElements,
18200
+ mightHandleFile: () => mightHandleFile,
18201
+ notifyReviewCompleted: () => notifyReviewCompleted,
18202
+ notifyReviewCreated: () => notifyReviewCreated,
18203
+ persistPluginUIState: () => persistPluginUIState,
18204
+ pluginsEnabled: () => pluginsEnabled,
18205
+ reloadContentPlugins: () => reloadContentPlugins,
18206
+ renderContent: () => renderContent,
18207
+ runPluginAction: () => runPluginAction
18208
+ });
18209
+ function pluginsEnabled() {
18210
+ return PLUGINS_ENABLED;
18211
+ }
18212
+ function enablementCheckFor(repoRoot) {
18213
+ const lists = readEnablementLists(repoRoot);
18214
+ return (id) => {
18215
+ const scope = disabledScope(id, lists);
18216
+ return scope === null ? { disabled: false } : { disabled: true, scope };
18217
+ };
18218
+ }
18219
+ async function loadFor(repoRoot) {
18220
+ installBundledPlugins();
18221
+ clearAllPluginUIElements();
18222
+ const res = await loadAllPlugins(void 0, enablementCheckFor(repoRoot), repoRoot);
18223
+ registry2 = res.registry;
18224
+ loadedPlugins = res.loaded;
18225
+ currentRepoRoot = repoRoot;
18226
+ const failed = loadedPlugins.filter((p) => p.status === "error").length;
18227
+ const disabled = loadedPlugins.filter((p) => p.status === "disabled").length;
18228
+ if (loadedPlugins.length > 0) {
18229
+ const parts = [`${loadedPlugins.length - failed - disabled} loaded`];
18230
+ if (disabled > 0) parts.push(`${disabled} disabled`);
18231
+ if (failed > 0) parts.push(`${failed} failed`);
18232
+ console.log(` Content plugins: ${parts.join(", ")}.`);
18233
+ }
18234
+ }
18235
+ async function initContentPlugins(repoRoot = "") {
18236
+ if (initialized) return;
18237
+ initialized = true;
18238
+ if (!PLUGINS_ENABLED) return;
18239
+ try {
18240
+ await loadFor(repoRoot);
18241
+ } catch (e) {
18242
+ console.warn(` Content plugins failed to load: ${e instanceof Error ? e.message : String(e)}`);
18243
+ }
18244
+ }
18245
+ async function reloadContentPlugins(repoRoot = currentRepoRoot) {
18246
+ if (!PLUGINS_ENABLED) return;
18247
+ initialized = true;
18248
+ try {
18249
+ await loadFor(repoRoot);
18250
+ } catch (e) {
18251
+ console.warn(` Content plugins failed to reload: ${e instanceof Error ? e.message : String(e)}`);
18252
+ }
18253
+ }
18254
+ function getLoadedPlugins() {
18255
+ return loadedPlugins;
18256
+ }
18257
+ function resolveConfigLabels(manifest) {
18258
+ const out = {};
18259
+ const walk = (items) => {
18260
+ for (const item of items ?? []) {
18261
+ if (item.type === "label" && item.id !== void 0 && item.id !== "") {
18262
+ const override = getConfigLabelOverride(manifest.id, item.id);
18263
+ out[item.id] = override ?? { text: item.text ?? "", color: item.color };
18264
+ }
18265
+ if (item.type === "group") walk(item.items);
18266
+ }
18267
+ };
18268
+ walk(manifest.configLayout);
18269
+ return out;
18270
+ }
18271
+ function describeInstalledPlugins(repoRoot) {
18272
+ const globalDisabled = new Set(readGlobalDisabled());
18273
+ const projectDisabled = new Set(readProjectDisabled(repoRoot));
18274
+ return loadedPlugins.map((p) => {
18275
+ const extensions = (p.manifest?.contentTypes ?? []).flatMap((ct) => ct.extensions ?? []);
18276
+ const display = p.manifest ? readPluginPreferenceDisplay(p.manifest, repoRoot) : { values: {}, secretConfigured: [] };
18277
+ return {
18278
+ id: p.id,
18279
+ name: p.manifest?.name ?? p.id,
18280
+ version: p.manifest?.version ?? "0",
18281
+ extensions,
18282
+ status: p.status,
18283
+ error: p.error,
18284
+ enabled: p.status === "loaded",
18285
+ disabledScope: p.disabledScope,
18286
+ globalDisabled: globalDisabled.has(p.id),
18287
+ projectDisabled: projectDisabled.has(p.id),
18288
+ preferences: p.manifest?.preferences ?? [],
18289
+ preferenceValues: display.values,
18290
+ secretConfigured: display.secretConfigured,
18291
+ configLayout: p.manifest?.configLayout,
18292
+ configLabels: p.manifest ? resolveConfigLabels(p.manifest) : {}
18293
+ };
18294
+ });
18295
+ }
18296
+ function getPluginManifest(id) {
18297
+ return loadedPlugins.find((p) => p.id === id)?.manifest ?? void 0;
18298
+ }
18299
+ async function runPluginAction(id, actionId, value) {
18300
+ if (!PLUGINS_ENABLED) throw new Error("Plugins are disabled");
18301
+ const loaded = loadedPlugins.find((p) => p.id === id);
18302
+ if (loaded === void 0 || loaded.status !== "loaded" || loaded.instance === void 0 || loaded.context === void 0) {
18303
+ throw new Error("Plugin not active");
18304
+ }
18305
+ if (typeof loaded.instance.onAction !== "function") {
18306
+ throw new Error("Plugin does not handle actions");
18307
+ }
18308
+ return await loaded.instance.onAction(actionId, loaded.context, value);
18309
+ }
18310
+ function statefulKey(e) {
18311
+ return e.type === "toggle" || e.type === "switch" || e.type === "segmented-control" ? e.stateKey : void 0;
18312
+ }
18313
+ function elementAction(e) {
18314
+ return e.type === "link" ? void 0 : e.action;
18315
+ }
18316
+ function persistPluginUIState(id, actionId, value, repoRoot) {
18317
+ const manifest = loadedPlugins.find((p) => p.id === id && p.status === "loaded")?.manifest;
18318
+ if (manifest === null || manifest === void 0) return;
18319
+ const group = getPluginUIElements().find((g) => g.pluginId === id);
18320
+ const el = group?.elements.find((e) => elementAction(e) === actionId && statefulKey(e) !== void 0 && statefulKey(e) !== "");
18321
+ const key = el ? statefulKey(el) : void 0;
18322
+ if (key === void 0 || key === "") return;
18323
+ try {
18324
+ writePluginSetting(manifest, repoRoot, key, value);
18325
+ } catch {
18326
+ }
18327
+ }
18328
+ function toReviewHookInfo(review) {
18329
+ return { id: review.id, repoPath: review.repo_path, repoName: review.repo_name, mode: review.mode, status: review.status };
18330
+ }
18331
+ function toAnnotationHookInfo(a) {
18332
+ return { id: a.id, filePath: a.file_path, lineNumber: a.line_number, side: a.side, category: a.category, content: a.content };
18333
+ }
18334
+ async function notifyReviewCreated(review) {
18335
+ if (!PLUGINS_ENABLED) return;
18336
+ const info = toReviewHookInfo(review);
18337
+ for (const p of loadedPlugins) {
18338
+ const hook = p.registration?.reviewHooks?.onReviewCreated;
18339
+ if (p.status !== "loaded" || hook === void 0 || p.context === void 0) continue;
18340
+ try {
18341
+ await hook(info, p.context);
18342
+ } catch (e) {
18343
+ console.warn(` [plugin:${p.id}] onReviewCreated hook failed: ${e instanceof Error ? e.message : String(e)}`);
18344
+ }
18345
+ }
18346
+ }
18347
+ async function notifyReviewCompleted(review, annotations, exportPath) {
18348
+ if (!PLUGINS_ENABLED) return;
18349
+ const info = toReviewHookInfo(review);
18350
+ const anns = annotations.map(toAnnotationHookInfo);
18351
+ for (const p of loadedPlugins) {
18352
+ const hook = p.registration?.reviewHooks?.onReviewCompleted;
18353
+ if (p.status !== "loaded" || hook === void 0 || p.context === void 0) continue;
18354
+ try {
18355
+ await hook(info, anns, exportPath, p.context);
18356
+ } catch (e) {
18357
+ console.warn(` [plugin:${p.id}] onReviewCompleted hook failed: ${e instanceof Error ? e.message : String(e)}`);
18358
+ }
18359
+ }
18360
+ }
18361
+ function listPluginUIElements(repoRoot = "") {
18362
+ if (!PLUGINS_ENABLED) return [];
18363
+ const loaded = new Map(loadedPlugins.filter((p) => p.status === "loaded").map((p) => [p.id, p]));
18364
+ const out = [];
18365
+ for (const { pluginId, elements } of getPluginUIElements()) {
18366
+ const plugin = loaded.get(pluginId);
18367
+ if (plugin === void 0) continue;
18368
+ for (const el of elements) {
18369
+ const key = statefulKey(el);
18370
+ const value = key !== void 0 && key !== "" && plugin.manifest !== null ? readPluginSetting(plugin.manifest, repoRoot, key) ?? void 0 : void 0;
18371
+ out.push({ ...el, pluginId, value });
18372
+ }
18373
+ }
18374
+ return out;
18375
+ }
18376
+ function mightHandleFile(path, mime) {
18377
+ return PLUGINS_ENABLED && registry2.mightHandleByPath(path, mime);
18378
+ }
18379
+ function usableView(view) {
18380
+ if (view === void 0) return null;
18381
+ const svg = typeof view.svg === "string" && view.svg !== "";
18382
+ const html = typeof view.html === "string" && view.html !== "";
18383
+ return svg || html ? view : null;
18384
+ }
18385
+ async function renderContent(input) {
18386
+ if (!PLUGINS_ENABLED) return null;
18387
+ const renderer = registry2.findRenderer(input);
18388
+ if (renderer === void 0) return null;
18389
+ try {
18390
+ return usableView(await renderer.render(input));
17283
18391
  } catch {
17284
18392
  return null;
17285
18393
  }
17286
18394
  }
17287
- function clearImageBlobs(dataDir) {
18395
+ async function decodeImageWithPlugin(bytes, path, mime) {
18396
+ if (!PLUGINS_ENABLED) return null;
18397
+ const decoder = registry2.findImageDecoder({ bytes, path, mime });
18398
+ if (decoder === void 0) return null;
17288
18399
  try {
17289
- rmSync3(blobDir(dataDir), { recursive: true, force: true });
18400
+ return await decoder.decode({ bytes, path }) ?? null;
17290
18401
  } catch {
18402
+ return null;
17291
18403
  }
17292
18404
  }
17293
- var init_image_blobs = __esm({
17294
- "src/git/image-blobs.ts"() {
18405
+ async function diffContent(input) {
18406
+ if (!PLUGINS_ENABLED) return null;
18407
+ const differ = registry2.findDiffer(input);
18408
+ if (differ === void 0) return null;
18409
+ try {
18410
+ return usableView(await differ.diff(input));
18411
+ } catch {
18412
+ return null;
18413
+ }
18414
+ }
18415
+ function __setContentRegistryForTest(r, loaded = []) {
18416
+ registry2 = r;
18417
+ loadedPlugins = loaded;
18418
+ initialized = true;
18419
+ }
18420
+ function __resetContentPluginsForTest() {
18421
+ registry2 = new ContentPluginRegistry();
18422
+ loadedPlugins = [];
18423
+ initialized = false;
18424
+ }
18425
+ var registry2, loadedPlugins, initialized, currentRepoRoot;
18426
+ var init_plugins = __esm({
18427
+ "src/plugins/index.ts"() {
17295
18428
  "use strict";
18429
+ init_feature_flags();
18430
+ init_enablement();
18431
+ init_install();
18432
+ init_loader();
18433
+ init_registry();
18434
+ init_settings();
18435
+ registry2 = new ContentPluginRegistry();
18436
+ loadedPlugins = [];
18437
+ initialized = false;
18438
+ currentRepoRoot = "";
17296
18439
  }
17297
18440
  });
17298
18441
 
@@ -17449,17 +18592,17 @@ __export(difftool_discovery_exports, {
17449
18592
  tryAcquireStartingLock: () => tryAcquireStartingLock,
17450
18593
  writeDiscovery: () => writeDiscovery
17451
18594
  });
17452
- import { existsSync as existsSync16, mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync14 } from "fs";
18595
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync22, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "fs";
17453
18596
  import { homedir as homedir3 } from "os";
17454
- import { join as join19 } from "path";
18597
+ import { join as join23 } from "path";
17455
18598
  function difftoolHome() {
17456
- return join19(homedir3(), ".glassbox");
18599
+ return join23(homedir3(), ".glassbox");
17457
18600
  }
17458
18601
  function discoveryPath(home = difftoolHome()) {
17459
- return join19(home, "difftool.lock");
18602
+ return join23(home, "difftool.lock");
17460
18603
  }
17461
18604
  function startingLockPath(home = difftoolHome()) {
17462
- return join19(home, "difftool-starting.lock");
18605
+ return join23(home, "difftool-starting.lock");
17463
18606
  }
17464
18607
  function parseDiscovery(raw2) {
17465
18608
  let parsed;
@@ -17473,35 +18616,35 @@ function parseDiscovery(raw2) {
17473
18616
  }
17474
18617
  function readDiscovery(home = difftoolHome()) {
17475
18618
  const path = discoveryPath(home);
17476
- if (!existsSync16(path)) return null;
18619
+ if (!existsSync19(path)) return null;
17477
18620
  try {
17478
- return parseDiscovery(readFileSync20(path, "utf-8"));
18621
+ return parseDiscovery(readFileSync22(path, "utf-8"));
17479
18622
  } catch {
17480
18623
  return null;
17481
18624
  }
17482
18625
  }
17483
18626
  function writeDiscovery(port, home = difftoolHome()) {
17484
- mkdirSync14(home, { recursive: true });
17485
- writeFileSync14(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
18627
+ mkdirSync16(home, { recursive: true });
18628
+ writeFileSync16(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
17486
18629
  }
17487
18630
  function clearDiscovery(home = difftoolHome()) {
17488
18631
  try {
17489
- rmSync6(discoveryPath(home), { force: true });
18632
+ rmSync7(discoveryPath(home), { force: true });
17490
18633
  } catch {
17491
18634
  }
17492
18635
  }
17493
18636
  function tryAcquireStartingLock(home = difftoolHome()) {
17494
- mkdirSync14(home, { recursive: true });
18637
+ mkdirSync16(home, { recursive: true });
17495
18638
  const path = startingLockPath(home);
17496
18639
  try {
17497
- writeFileSync14(path, String(process.pid), { flag: "wx" });
18640
+ writeFileSync16(path, String(process.pid), { flag: "wx" });
17498
18641
  return true;
17499
18642
  } catch {
17500
18643
  try {
17501
- const ageMs = Date.now() - statSync5(path).mtimeMs;
18644
+ const ageMs = Date.now() - statSync7(path).mtimeMs;
17502
18645
  if (ageMs > STARTING_LOCK_STALE_MS) {
17503
- rmSync6(path, { force: true });
17504
- writeFileSync14(path, String(process.pid), { flag: "wx" });
18646
+ rmSync7(path, { force: true });
18647
+ writeFileSync16(path, String(process.pid), { flag: "wx" });
17505
18648
  return true;
17506
18649
  }
17507
18650
  } catch {
@@ -17511,7 +18654,7 @@ function tryAcquireStartingLock(home = difftoolHome()) {
17511
18654
  }
17512
18655
  function releaseStartingLock(home = difftoolHome()) {
17513
18656
  try {
17514
- rmSync6(startingLockPath(home), { force: true });
18657
+ rmSync7(startingLockPath(home), { force: true });
17515
18658
  } catch {
17516
18659
  }
17517
18660
  }
@@ -21966,16 +23109,16 @@ __export(perceptual_diff_exports, {
21966
23109
  decodeImage: () => decodeImage,
21967
23110
  isIdentical: () => isIdentical
21968
23111
  });
21969
- import { readFileSync as readFileSync21 } from "fs";
21970
- import { extname as extname2 } from "path";
23112
+ import { readFileSync as readFileSync23 } from "fs";
23113
+ import { extname as extname3 } from "path";
21971
23114
  function decodeImage(path) {
21972
23115
  let buf;
21973
23116
  try {
21974
- buf = readFileSync21(path);
23117
+ buf = readFileSync23(path);
21975
23118
  } catch {
21976
23119
  return null;
21977
23120
  }
21978
- const ext = extname2(path).toLowerCase();
23121
+ const ext = extname3(path).toLowerCase();
21979
23122
  try {
21980
23123
  if (ext === ".png") {
21981
23124
  const png = import_pngjs.PNG.sync.read(buf);
@@ -21990,9 +23133,24 @@ function decodeImage(path) {
21990
23133
  }
21991
23134
  return null;
21992
23135
  }
21993
- function comparePerceptual(actualPath, expectedPath) {
21994
- const actual = decodeImage(actualPath);
21995
- const expected = decodeImage(expectedPath);
23136
+ async function decodeWithFallback(path, pluginDecode) {
23137
+ const core = decodeImage(path);
23138
+ if (core !== null) return core;
23139
+ let buf;
23140
+ try {
23141
+ buf = readFileSync23(path);
23142
+ } catch {
23143
+ return null;
23144
+ }
23145
+ try {
23146
+ return await pluginDecode(new Uint8Array(buf), path);
23147
+ } catch {
23148
+ return null;
23149
+ }
23150
+ }
23151
+ async function comparePerceptual(actualPath, expectedPath, pluginDecode = decodeImageWithPlugin) {
23152
+ const actual = await decodeWithFallback(actualPath, pluginDecode);
23153
+ const expected = await decodeWithFallback(expectedPath, pluginDecode);
21996
23154
  if (actual === null || expected === null) {
21997
23155
  return { scorable: false, score: null, reason: "undecodable" };
21998
23156
  }
@@ -22023,14 +23181,15 @@ var init_perceptual_diff = __esm({
22023
23181
  import_jpeg_js = __toESM(require_jpeg_js(), 1);
22024
23182
  init_pixelmatch();
22025
23183
  import_pngjs = __toESM(require_png(), 1);
23184
+ init_plugins();
22026
23185
  PERCEPTUAL_THRESHOLD = 0.1;
22027
23186
  }
22028
23187
  });
22029
23188
 
22030
23189
  // src/cli.ts
22031
- import { existsSync as existsSync17, mkdirSync as mkdirSync15, realpathSync, statSync as statSync6 } from "fs";
23190
+ import { existsSync as existsSync20, mkdirSync as mkdirSync17, realpathSync, statSync as statSync8 } from "fs";
22032
23191
  import { tmpdir as tmpdir2 } from "os";
22033
- import { basename as basename5, join as join20, resolve as resolve12 } from "path";
23192
+ import { basename as basename5, join as join24, resolve as resolve12 } from "path";
22034
23193
 
22035
23194
  // src/cli-subcommands.ts
22036
23195
  import { resolve as resolve3 } from "path";
@@ -22095,160 +23254,23 @@ async function handleDifftoolRegistration(action, local, force) {
22095
23254
  } else {
22096
23255
  console.log(`Nothing to unregister at --${scope} scope (current tool: ${status.tool ?? "none"}).`);
22097
23256
  }
22098
- process.exit(0);
22099
- }
22100
-
22101
- // src/cli.ts
22102
- init_connection();
22103
- init_queries();
22104
- init_debug();
22105
-
22106
- // src/ai/config.ts
22107
- init_zod();
22108
-
22109
- // src/global-config.ts
22110
- init_zod();
22111
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
22112
- import { homedir } from "os";
22113
- import { join as join4 } from "path";
22114
- var GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
22115
- function resolveGlobalConfigDir() {
22116
- const override = process.env.GLASSBOX_CONFIG_DIR;
22117
- if (override !== void 0 && override.trim() !== "") return override;
22118
- return join4(homedir(), ".glassbox");
22119
- }
22120
- var GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
22121
- var GLOBAL_CONFIG_PATH = join4(GLOBAL_CONFIG_DIR, "config.json");
22122
- function readGlobalConfig() {
22123
- try {
22124
- if (existsSync4(GLOBAL_CONFIG_PATH)) {
22125
- const raw2 = JSON.parse(readFileSync3(GLOBAL_CONFIG_PATH, "utf-8"));
22126
- const parsed = GlobalConfigSchema.safeParse(raw2);
22127
- if (parsed.success) return parsed.data;
22128
- }
22129
- } catch {
22130
- }
22131
- return {};
22132
- }
22133
- function writeGlobalConfig(config2) {
22134
- mkdirSync5(GLOBAL_CONFIG_DIR, { recursive: true });
22135
- writeFileSync3(GLOBAL_CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
22136
- try {
22137
- chmodSync(GLOBAL_CONFIG_PATH, 384);
22138
- } catch {
22139
- }
22140
- }
22141
- function updateGlobalConfig(mutator) {
22142
- const cfg = readGlobalConfig();
22143
- const result = mutator(cfg);
22144
- writeGlobalConfig(result === void 0 ? cfg : result);
22145
- }
22146
-
22147
- // src/ai/api-keys.ts
22148
- import { spawnSync as spawnSync6 } from "child_process";
22149
-
22150
- // src/ai/keychain.ts
22151
- import { spawnSync as spawnSync5 } from "child_process";
22152
- var WIN_CRED_READ_PS = `
22153
- Add-Type -TypeDefinition @'
22154
- using System;
22155
- using System.Runtime.InteropServices;
22156
- public class CredHelper {
22157
- [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
22158
- static extern bool CredRead(string t, int type, int f, out IntPtr p);
22159
- [DllImport("advapi32")]
22160
- static extern void CredFree(IntPtr p);
22161
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
22162
- struct CRED {
22163
- public int Flags; public int Type; public string TargetName; public string Comment;
22164
- public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob;
22165
- public int Persist; public int AttributeCount; public IntPtr Attributes;
22166
- public string TargetAlias; public string UserName;
22167
- }
22168
- public static string Read(string target) {
22169
- IntPtr ptr;
22170
- if (!CredRead(target, 1, 0, out ptr)) return "";
22171
- CRED c = (CRED)Marshal.PtrToStructure(ptr, typeof(CRED));
22172
- string r = Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
22173
- CredFree(ptr);
22174
- return r;
22175
- }
22176
- }
22177
- '@
22178
- `;
22179
- function winCredTarget(platform) {
22180
- return `glassbox-${platform}-api-key`;
22181
- }
22182
- function getKeyFromKeychain(platform) {
22183
- const os = process.platform;
22184
- const account = `${platform}-api-key`;
22185
- try {
22186
- if (os === "darwin") {
22187
- const r = spawnSync5("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
22188
- const result = r.stdout.trim();
22189
- return r.status === 0 && result !== "" ? result : null;
22190
- }
22191
- if (os === "linux") {
22192
- const r = spawnSync5("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
22193
- const result = r.stdout.trim();
22194
- return r.status === 0 && result !== "" ? result : null;
22195
- }
22196
- if (os === "win32") {
22197
- const target = winCredTarget(platform);
22198
- const list = spawnSync5("cmdkey", ["/list:" + target], { encoding: "utf-8" });
22199
- if (list.status !== 0 || (list.stdout || "").includes("* NONE *")) return null;
22200
- const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
22201
- const r = spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
22202
- const result = r.stdout.trim();
22203
- return r.status === 0 && result !== "" ? result : null;
22204
- }
22205
- } catch {
22206
- return null;
22207
- }
22208
- return null;
22209
- }
22210
- function assertSpawnOk(label, r) {
22211
- if (r.error) throw new Error(`${label} failed: ${r.error.message}`);
22212
- if (r.status !== 0) {
22213
- const detail = (String(r.stderr) || String(r.stdout)).trim();
22214
- throw new Error(`${label} failed (exit ${String(r.status)})${detail ? `: ${detail}` : ""}`);
22215
- }
22216
- }
22217
- function saveKeyToKeychain(platform, key) {
22218
- const os = process.platform;
22219
- const account = `${platform}-api-key`;
22220
- if (os === "darwin") {
22221
- spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
22222
- assertSpawnOk("Keychain write", spawnSync5("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", key], { encoding: "utf-8" }));
22223
- return;
22224
- }
22225
- if (os === "linux") {
22226
- assertSpawnOk("System keyring write", spawnSync5("secret-tool", ["store", "--label=Glassbox API Key", "service", "glassbox", "account", account], { input: key, encoding: "utf-8" }));
22227
- return;
22228
- }
22229
- if (os === "win32") {
22230
- const target = winCredTarget(platform);
22231
- const escapedKey = key.replace(/'/g, "''");
22232
- const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedKey}'`;
22233
- assertSpawnOk("Credential Manager write", spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" }));
22234
- }
22235
- }
22236
- function isKeychainAvailable() {
22237
- const os = process.platform;
22238
- if (os === "darwin" || os === "win32") return true;
22239
- if (os === "linux") {
22240
- return spawnSync5("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
22241
- }
22242
- return false;
22243
- }
22244
- function getKeychainLabel() {
22245
- const os = process.platform;
22246
- if (os === "darwin") return "Keychain";
22247
- if (os === "linux") return "System Keyring";
22248
- if (os === "win32") return "Credential Manager";
22249
- return "System Keychain";
23257
+ process.exit(0);
22250
23258
  }
22251
23259
 
23260
+ // src/cli.ts
23261
+ init_connection();
23262
+ init_queries();
23263
+ init_debug();
23264
+
23265
+ // src/ai/config.ts
23266
+ init_zod();
23267
+ init_global_config();
23268
+
23269
+ // src/ai/api-keys.ts
23270
+ init_global_config();
23271
+ import { spawnSync as spawnSync6 } from "child_process";
23272
+ init_keychain();
23273
+
22252
23274
  // src/ai/models.ts
22253
23275
  init_zod();
22254
23276
  var AIModelSchema = external_exports.object({
@@ -22340,6 +23362,7 @@ function resolveModelId(platform, modelId) {
22340
23362
  }
22341
23363
 
22342
23364
  // src/ai/api-keys.ts
23365
+ init_keychain();
22343
23366
  function getKeyFromEnv(platform) {
22344
23367
  const envName = ENV_KEY_NAMES[platform];
22345
23368
  return process.env[envName] ?? null;
@@ -23551,8 +24574,10 @@ function getFileDiffs(mode, cwd) {
23551
24574
  const diffArgs = getDiffArgs(mode);
23552
24575
  const rawDiff = gitOrEmpty([...diffArgs, "-U3"], repoRoot);
23553
24576
  const diffs = parseDiff(rawDiff);
23554
- if (mode.type === "uncommitted") {
23555
- const untracked = git(["ls-files", "--others", "--exclude-standard"], repoRoot).trim();
24577
+ if (mode.type === "uncommitted" || mode.type === "files") {
24578
+ const lsArgs = ["ls-files", "--others", "--exclude-standard"];
24579
+ if (mode.type === "files") lsArgs.push("--", ...mode.patterns);
24580
+ const untracked = git(lsArgs, repoRoot).trim();
23556
24581
  if (untracked) {
23557
24582
  for (const file2 of untracked.split("\n").filter(Boolean)) {
23558
24583
  if (!diffs.some((d) => d.filePath === file2)) {
@@ -23950,10 +24975,10 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
23950
24975
 
23951
24976
  // src/server.ts
23952
24977
  import { serve } from "@hono/node-server";
23953
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
23954
- import { Hono as Hono20 } from "hono";
23955
- import { dirname as dirname7, join as join16 } from "path";
23956
- import { fileURLToPath as fileURLToPath2 } from "url";
24978
+ import { existsSync as existsSync16, readFileSync as readFileSync19 } from "fs";
24979
+ import { Hono as Hono21 } from "hono";
24980
+ import { dirname as dirname8, join as join20 } from "path";
24981
+ import { fileURLToPath as fileURLToPath3 } from "url";
23957
24982
 
23958
24983
  // src/channel-config.ts
23959
24984
  init_zod();
@@ -24053,6 +25078,11 @@ async function triggerChannel(dataDir, message) {
24053
25078
  }
24054
25079
  }
24055
25080
 
25081
+ // src/server.ts
25082
+ init_queries();
25083
+ init_global_config();
25084
+ init_plugins();
25085
+
24056
25086
  // src/routes/ai-api.ts
24057
25087
  import { Hono as Hono3 } from "hono";
24058
25088
 
@@ -25294,39 +26324,7 @@ __export(ai_exports, {
25294
26324
  startAnalysis: () => startAnalysis
25295
26325
  });
25296
26326
  init_zod();
25297
-
25298
- // src/api/_runner.ts
25299
- init_zod();
25300
- var OkResponseSchema = external_exports.object({ ok: external_exports.literal(true) });
25301
- function currentReviewId() {
25302
- if (typeof document === "undefined") return "";
25303
- return document.body.dataset.reviewId ?? "";
25304
- }
25305
- async function apiCall(responseSchema, path, opts = {}) {
25306
- const separator = path.includes("?") ? "&" : "?";
25307
- const url2 = "/api" + path + separator + "reviewId=" + encodeURIComponent(currentReviewId());
25308
- const res = await fetch(url2, {
25309
- headers: { "Content-Type": "application/json" },
25310
- method: opts.method,
25311
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
25312
- });
25313
- const json2 = await res.json();
25314
- const result = responseSchema.safeParse(json2);
25315
- if (!result.success) {
25316
- const summary = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
25317
- throw new Error(`API response from ${path} failed validation: ${summary}`);
25318
- }
25319
- return result.data;
25320
- }
25321
- function qs(params) {
25322
- const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
25323
- if (entries.length === 0) return "";
25324
- const usp = new URLSearchParams();
25325
- for (const [k, v] of entries) usp.set(k, String(v));
25326
- return "?" + usp.toString();
25327
- }
25328
-
25329
- // src/api/ai.ts
26327
+ init_runner();
25330
26328
  var KeySourceSchema = external_exports.enum(["env", "keychain", "config"]).nullable();
25331
26329
  var KeyStorageSchema = external_exports.enum(["keychain", "config"]);
25332
26330
  var GuidedReviewConfigShapeSchema = external_exports.object({
@@ -25539,6 +26537,7 @@ __export(annotations_exports, {
25539
26537
  });
25540
26538
  init_zod();
25541
26539
  init_schemas3();
26540
+ init_runner();
25542
26541
  var AnnotationCategorySchema = external_exports.enum([
25543
26542
  "bug",
25544
26543
  "fix",
@@ -25644,6 +26643,7 @@ __export(channel_exports, {
25644
26643
  triggerChannel: () => triggerChannel2
25645
26644
  });
25646
26645
  init_zod();
26646
+ init_runner();
25647
26647
  var GetChannelStatusRespSchema = external_exports.object({
25648
26648
  enabled: external_exports.boolean(),
25649
26649
  connected: external_exports.boolean()
@@ -25687,6 +26687,7 @@ __export(context_exports, {
25687
26687
  getContextLines: () => getContextLines
25688
26688
  });
25689
26689
  init_zod();
26690
+ init_runner();
25690
26691
  var ContextLineSchema = external_exports.object({
25691
26692
  num: external_exports.number().int(),
25692
26693
  content: external_exports.string()
@@ -25736,6 +26737,7 @@ __export(files_exports, {
25736
26737
  });
25737
26738
  init_zod();
25738
26739
  init_schemas3();
26740
+ init_runner();
25739
26741
  var FileStatusSchema = external_exports.enum(["pending", "reviewed"]);
25740
26742
  var GroundTruthMetaSchema = external_exports.object({
25741
26743
  label: external_exports.string().optional(),
@@ -25750,6 +26752,10 @@ var GroundTruthMetaSchema = external_exports.object({
25750
26752
  });
25751
26753
  var ListFilesRespSchema = external_exports.object({
25752
26754
  files: external_exports.array(ReviewFileSchema),
26755
+ /** Ids of files a content plugin renders (doc 29, GB-1052) — the client shows
26756
+ * the Code/Rendered toggle and routes the Rendered view through the image
26757
+ * viewer for these, like an SVG file. */
26758
+ pluginRendered: external_exports.array(external_exports.string()).optional(),
25753
26759
  annotationCounts: external_exports.record(external_exports.string(), external_exports.number()),
25754
26760
  staleCounts: external_exports.record(external_exports.string(), external_exports.number()),
25755
26761
  /** Keyed by review-file id; omitted/empty for non-ground-truth reviews. */
@@ -25807,6 +26813,7 @@ __export(image_exports, {
25807
26813
  imageUrl: () => imageUrl
25808
26814
  });
25809
26815
  init_zod();
26816
+ init_runner();
25810
26817
  var ImageSideSchema = external_exports.enum(["old", "new"]);
25811
26818
  var GetImageMetadataReqSchema = external_exports.object({ fileId: external_exports.string() });
25812
26819
  var GetImageMetadataRespSchema = external_exports.object({
@@ -25833,6 +26840,7 @@ __export(outline_exports, {
25833
26840
  getOutline: () => getOutline
25834
26841
  });
25835
26842
  init_zod();
26843
+ init_runner();
25836
26844
  var baseOutlineSymbol = external_exports.object({
25837
26845
  name: external_exports.string(),
25838
26846
  kind: external_exports.enum(["class", "function"]),
@@ -25868,29 +26876,8 @@ async function findSymbolDefinition(req) {
25868
26876
  );
25869
26877
  }
25870
26878
 
25871
- // src/api/project-settings.ts
25872
- var project_settings_exports = {};
25873
- __export(project_settings_exports, {
25874
- GetProjectSettingsRespSchema: () => GetProjectSettingsRespSchema,
25875
- ProjectSettingsSchema: () => ProjectSettingsSchema,
25876
- UpdateProjectSettingsReqSchema: () => UpdateProjectSettingsReqSchema,
25877
- UpdateProjectSettingsRespSchema: () => UpdateProjectSettingsRespSchema,
25878
- getProjectSettings: () => getProjectSettings,
25879
- updateProjectSettings: () => updateProjectSettings
25880
- });
25881
- init_zod();
25882
- var ProjectSettingsSchema = external_exports.object({
25883
- appName: external_exports.string().optional()
25884
- });
25885
- var GetProjectSettingsRespSchema = ProjectSettingsSchema;
25886
- var UpdateProjectSettingsReqSchema = ProjectSettingsSchema.partial();
25887
- var UpdateProjectSettingsRespSchema = ProjectSettingsSchema;
25888
- async function getProjectSettings() {
25889
- return apiCall(GetProjectSettingsRespSchema, "/project-settings");
25890
- }
25891
- async function updateProjectSettings(req) {
25892
- return apiCall(UpdateProjectSettingsRespSchema, "/project-settings", { method: "PATCH", body: req });
25893
- }
26879
+ // src/api/index.ts
26880
+ init_project_settings();
25894
26881
 
25895
26882
  // src/api/reviews.ts
25896
26883
  var reviews_exports = {};
@@ -25916,6 +26903,7 @@ __export(reviews_exports, {
25916
26903
  });
25917
26904
  init_zod();
25918
26905
  init_schemas3();
26906
+ init_runner();
25919
26907
  var ListReviewsRespSchema = external_exports.array(ReviewSchema);
25920
26908
  var GetCurrentReviewRespSchema = ReviewSchema.nullable();
25921
26909
  var OnCompleteHookResultSchema = external_exports.object({
@@ -25979,6 +26967,7 @@ __export(share_prompt_exports, {
25979
26967
  tickSharePrompt: () => tickSharePrompt
25980
26968
  });
25981
26969
  init_zod();
26970
+ init_runner();
25982
26971
  var GetSharePromptStateRespSchema = external_exports.object({
25983
26972
  dismissedAt: external_exports.number().nullable(),
25984
26973
  totalOpenMs: external_exports.number()
@@ -26006,6 +26995,7 @@ __export(system_exports, {
26006
26995
  openExternal: () => openExternal
26007
26996
  });
26008
26997
  init_zod();
26998
+ init_runner();
26009
26999
  var OpenExternalReqSchema = external_exports.object({
26010
27000
  // Restricted to http(s): this hands the value to the OS "open" handler, so
26011
27001
  // we don't want to let it launch arbitrary schemes (file:, custom apps).
@@ -26519,6 +27509,7 @@ function themeToInlineStyle(colors) {
26519
27509
  }
26520
27510
 
26521
27511
  // src/api/themes.ts
27512
+ init_runner();
26522
27513
  function buildThemeColorsSchema() {
26523
27514
  const shape = {};
26524
27515
  for (const key of THEME_VARIABLES) shape[key] = external_exports.string();
@@ -26611,11 +27602,13 @@ async function deleteTheme(req) {
26611
27602
  // src/api/attachments.ts
26612
27603
  init_zod();
26613
27604
  init_schemas3();
27605
+ init_runner();
26614
27606
  var ListAttachmentsRespSchema = external_exports.array(AttachmentSchema);
26615
27607
 
26616
27608
  // src/api/difftool.ts
26617
27609
  init_zod();
26618
27610
  init_schemas3();
27611
+ init_runner();
26619
27612
  var DifftoolStatusRespSchema = external_exports.object({
26620
27613
  tool: external_exports.string().nullable(),
26621
27614
  cmd: external_exports.string().nullable(),
@@ -26650,8 +27643,163 @@ var DifftoolPollRespSchema = external_exports.object({
26650
27643
  });
26651
27644
  var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
26652
27645
 
27646
+ // src/api/plugins.ts
27647
+ init_zod();
27648
+ init_runner();
27649
+ var PluginStatusSchema = external_exports.enum(["loaded", "disabled", "error"]);
27650
+ var PluginPreferenceInfoSchema = external_exports.object({
27651
+ key: external_exports.string(),
27652
+ label: external_exports.string(),
27653
+ type: external_exports.enum(["string", "number", "boolean", "select"]),
27654
+ default: external_exports.string().optional(),
27655
+ description: external_exports.string().optional(),
27656
+ options: external_exports.array(external_exports.string()).optional(),
27657
+ scope: external_exports.enum(["global", "project"]).optional(),
27658
+ secret: external_exports.boolean().optional()
27659
+ });
27660
+ var ConfigLabelColorSchema2 = external_exports.enum(["default", "success", "error", "warning", "transient"]);
27661
+ var ConfigLayoutItemSchema2 = external_exports.lazy(
27662
+ () => external_exports.object({
27663
+ type: external_exports.enum(["preference", "divider", "spacer", "label", "button", "group"]),
27664
+ key: external_exports.string().optional(),
27665
+ id: external_exports.string().optional(),
27666
+ text: external_exports.string().optional(),
27667
+ color: ConfigLabelColorSchema2.optional(),
27668
+ label: external_exports.string().optional(),
27669
+ action: external_exports.string().optional(),
27670
+ style: external_exports.string().optional(),
27671
+ title: external_exports.string().optional(),
27672
+ collapsed: external_exports.boolean().optional(),
27673
+ items: external_exports.array(ConfigLayoutItemSchema2).optional()
27674
+ })
27675
+ );
27676
+ var PluginInfoSchema = external_exports.object({
27677
+ id: external_exports.string(),
27678
+ name: external_exports.string(),
27679
+ version: external_exports.string(),
27680
+ /** Extensions the plugin's content types declare (informational). */
27681
+ extensions: external_exports.array(external_exports.string()),
27682
+ status: PluginStatusSchema,
27683
+ error: external_exports.string().optional(),
27684
+ /** True when active (loaded). False when disabled by either scope. */
27685
+ enabled: external_exports.boolean(),
27686
+ /** Which scope disables it (global wins), when disabled. */
27687
+ disabledScope: external_exports.enum(["global", "project"]).optional(),
27688
+ /** The two independent disable flags, so the UI can render both toggles. */
27689
+ globalDisabled: external_exports.boolean(),
27690
+ projectDisabled: external_exports.boolean(),
27691
+ /** Manifest-declared preferences + their current values (doc 29 FR-29.12).
27692
+ * Secret values are never sent; `secretConfigured` names the secret keys that
27693
+ * have a stored value (GB-1054). */
27694
+ preferences: external_exports.array(PluginPreferenceInfoSchema),
27695
+ preferenceValues: external_exports.record(external_exports.string(), external_exports.string()),
27696
+ secretConfigured: external_exports.array(external_exports.string()),
27697
+ /** Optional manifest arrangement of the preferences (doc 29 FR-29.18). */
27698
+ configLayout: external_exports.array(ConfigLayoutItemSchema2).optional(),
27699
+ /** Effective `label`-item text/color, keyed by label id (doc 29 FR-29.18). */
27700
+ configLabels: external_exports.record(external_exports.string(), external_exports.object({ text: external_exports.string(), color: ConfigLabelColorSchema2.optional() }))
27701
+ });
27702
+ var ListPluginsRespSchema = external_exports.object({
27703
+ plugins: external_exports.array(PluginInfoSchema),
27704
+ /** Set when a mutation (e.g. install-from-disk of a non-plugin folder) failed;
27705
+ * the list is still returned so the UI stays in sync + can show the message. */
27706
+ error: external_exports.string().optional()
27707
+ });
27708
+ var SetPluginDisabledReqSchema = external_exports.object({
27709
+ scope: external_exports.enum(["global", "project"]),
27710
+ disabled: external_exports.boolean()
27711
+ });
27712
+ var InstallPluginReqSchema = external_exports.object({ path: external_exports.string().min(1) });
27713
+ var RequirementStatusSchema = external_exports.object({
27714
+ id: external_exports.string(),
27715
+ label: external_exports.string(),
27716
+ met: external_exports.boolean(),
27717
+ hint: external_exports.string(),
27718
+ docUrl: external_exports.string().optional()
27719
+ });
27720
+ var AvailablePluginSchema = external_exports.object({
27721
+ id: external_exports.string(),
27722
+ name: external_exports.string(),
27723
+ version: external_exports.string(),
27724
+ description: external_exports.string().optional(),
27725
+ extensions: external_exports.array(external_exports.string()),
27726
+ requirements: external_exports.array(RequirementStatusSchema),
27727
+ provisionNotes: external_exports.array(external_exports.string()),
27728
+ selfContained: external_exports.boolean(),
27729
+ cliHint: external_exports.string().optional()
27730
+ });
27731
+ var ListAvailablePluginsRespSchema = external_exports.object({ available: external_exports.array(AvailablePluginSchema) });
27732
+ var ProvisionOutcomeSchema = external_exports.object({
27733
+ step: external_exports.string(),
27734
+ ok: external_exports.boolean(),
27735
+ skipped: external_exports.boolean(),
27736
+ detail: external_exports.string()
27737
+ });
27738
+ var InstallResultSchema = external_exports.object({
27739
+ id: external_exports.string(),
27740
+ installed: external_exports.boolean(),
27741
+ status: external_exports.enum(["ready", "needs-setup", "error"]),
27742
+ requirements: external_exports.array(RequirementStatusSchema),
27743
+ provisioned: external_exports.array(ProvisionOutcomeSchema),
27744
+ instructions: external_exports.array(external_exports.string()),
27745
+ error: external_exports.string().optional()
27746
+ });
27747
+ var InstallBundledPluginRespSchema = external_exports.object({
27748
+ result: InstallResultSchema,
27749
+ plugins: external_exports.array(PluginInfoSchema),
27750
+ available: external_exports.array(AvailablePluginSchema)
27751
+ });
27752
+ var UninstallPluginRespSchema = ListPluginsRespSchema.extend({
27753
+ available: external_exports.array(AvailablePluginSchema).optional()
27754
+ });
27755
+ var SetPluginPreferenceReqSchema = external_exports.object({ key: external_exports.string().min(1), value: external_exports.string() });
27756
+ var RunPluginActionReqSchema = external_exports.object({ actionId: external_exports.string().min(1), value: external_exports.string().optional() });
27757
+ var UIElementFaceSchema = external_exports.object({
27758
+ label: external_exports.string().optional(),
27759
+ icon: external_exports.string().optional(),
27760
+ title: external_exports.string().optional(),
27761
+ style: external_exports.string().optional()
27762
+ });
27763
+ var UISegmentSchema = external_exports.object({
27764
+ id: external_exports.string(),
27765
+ label: external_exports.string().optional(),
27766
+ icon: external_exports.string().optional(),
27767
+ title: external_exports.string().optional()
27768
+ });
27769
+ var PluginUIElementSchema = external_exports.object({
27770
+ pluginId: external_exports.string(),
27771
+ id: external_exports.string(),
27772
+ type: external_exports.string(),
27773
+ location: external_exports.string(),
27774
+ label: external_exports.string().optional(),
27775
+ icon: external_exports.string().optional(),
27776
+ title: external_exports.string().optional(),
27777
+ style: external_exports.string().optional(),
27778
+ action: external_exports.string().optional(),
27779
+ url: external_exports.string().optional(),
27780
+ // Stateful controls (doc 30 FR-30.3): toggle/switch (`on`/`off`), segmented
27781
+ // (`segments`/`selectionMode`); `stateKey` persists, `value` is the resolved
27782
+ // current state the host attaches when listing.
27783
+ on: UIElementFaceSchema.optional(),
27784
+ off: UIElementFaceSchema.optional(),
27785
+ onLabel: external_exports.string().optional(),
27786
+ offLabel: external_exports.string().optional(),
27787
+ segments: external_exports.array(UISegmentSchema).optional(),
27788
+ selectionMode: external_exports.string().optional(),
27789
+ stateKey: external_exports.string().optional(),
27790
+ value: external_exports.string().optional()
27791
+ });
27792
+ var ListPluginUiRespSchema = external_exports.object({ elements: external_exports.array(PluginUIElementSchema) });
27793
+ var RunPluginActionRespSchema = ListPluginsRespSchema.extend({
27794
+ result: external_exports.object({ message: external_exports.string().optional() }).optional()
27795
+ });
27796
+
27797
+ // src/api/index.ts
27798
+ init_project_settings();
27799
+
26653
27800
  // src/api/review-notes.ts
26654
27801
  init_zod();
27802
+ init_runner();
26655
27803
  var DiscardReviewNoteReqSchema = external_exports.object({
26656
27804
  guid: external_exports.string().min(1),
26657
27805
  /** Repo-relative source file the note is on (scopes the shard search). */
@@ -27098,7 +28246,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
27098
28246
  aiApiRoutes.route("/", aiAnalysisRoutes);
27099
28247
 
27100
28248
  // src/routes/api.ts
27101
- import { Hono as Hono15 } from "hono";
28249
+ import { Hono as Hono16 } from "hono";
27102
28250
 
27103
28251
  // src/routes/api/annotations.ts
27104
28252
  import { Hono as Hono4 } from "hono";
@@ -27108,8 +28256,8 @@ init_queries();
27108
28256
  init_attachment_queries();
27109
28257
  init_queries();
27110
28258
  init_schemas3();
27111
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
27112
- import { join as join10 } from "path";
28259
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
28260
+ import { join as join13 } from "path";
27113
28261
  init_image_metadata();
27114
28262
 
27115
28263
  // src/utils/formatReviewMode.ts
@@ -27328,10 +28476,10 @@ function buildRegion(a, gt, dimsFor) {
27328
28476
 
27329
28477
  // src/export/generate.ts
27330
28478
  function deleteReviewExport(reviewId, repoRoot) {
27331
- const exportDir = join10(repoRoot, ".glassbox");
28479
+ const exportDir = join13(repoRoot, ".glassbox");
27332
28480
  for (const ext of ["md", "json"]) {
27333
- const archivePath = join10(exportDir, `review-${reviewId}.${ext}`);
27334
- if (existsSync10(archivePath)) unlinkSync2(archivePath);
28481
+ const archivePath = join13(exportDir, `review-${reviewId}.${ext}`);
28482
+ if (existsSync13(archivePath)) unlinkSync2(archivePath);
27335
28483
  }
27336
28484
  }
27337
28485
  function annotationAnchorLabel(a) {
@@ -27358,8 +28506,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
27358
28506
  if (!review) throw new Error("Review not found");
27359
28507
  const files = await getReviewFiles(reviewId);
27360
28508
  const annotations = await getAnnotationsForReview(reviewId);
27361
- const exportDir = join10(repoRoot, ".glassbox");
27362
- mkdirSync8(exportDir, { recursive: true });
28509
+ const exportDir = join13(repoRoot, ".glassbox");
28510
+ mkdirSync10(exportDir, { recursive: true });
27363
28511
  const byFile = {};
27364
28512
  for (const a of annotations) {
27365
28513
  if (!(a.file_path in byFile)) byFile[a.file_path] = [];
@@ -27452,20 +28600,20 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
27452
28600
  resolveDims: resolveImageDims
27453
28601
  });
27454
28602
  const json2 = JSON.stringify(exportData, null, 2);
27455
- const archivePath = join10(exportDir, `review-${review.id}.md`);
27456
- writeFileSync9(archivePath, content, "utf-8");
27457
- writeFileSync9(join10(exportDir, `review-${review.id}.json`), json2, "utf-8");
28603
+ const archivePath = join13(exportDir, `review-${review.id}.md`);
28604
+ writeFileSync11(archivePath, content, "utf-8");
28605
+ writeFileSync11(join13(exportDir, `review-${review.id}.json`), json2, "utf-8");
27458
28606
  if (isCurrent) {
27459
- const latestPath = join10(exportDir, "latest-review.md");
27460
- writeFileSync9(latestPath, content, "utf-8");
27461
- writeFileSync9(join10(exportDir, "latest-review.json"), json2, "utf-8");
28607
+ const latestPath = join13(exportDir, "latest-review.md");
28608
+ writeFileSync11(latestPath, content, "utf-8");
28609
+ writeFileSync11(join13(exportDir, "latest-review.json"), json2, "utf-8");
27462
28610
  return latestPath;
27463
28611
  }
27464
28612
  return archivePath;
27465
28613
  }
27466
28614
  function resolveImageDims(absPath) {
27467
28615
  try {
27468
- const meta3 = extractMetadata(readFileSync9(absPath), absPath);
28616
+ const meta3 = extractMetadata(readFileSync12(absPath), absPath);
27469
28617
  if (meta3.width !== null && meta3.height !== null) {
27470
28618
  return { width: meta3.width, height: meta3.height };
27471
28619
  }
@@ -27570,7 +28718,7 @@ annotationsRoutes.get("/annotations/all", async (c) => {
27570
28718
  init_store2();
27571
28719
  init_attachment_queries();
27572
28720
  init_queries();
27573
- import { readFileSync as readFileSync10, statSync as statSync3 } from "fs";
28721
+ import { readFileSync as readFileSync13, statSync as statSync5 } from "fs";
27574
28722
  import { Hono as Hono5 } from "hono";
27575
28723
 
27576
28724
  // src/utils/mime.ts
@@ -27684,9 +28832,9 @@ attachmentsRoutes.get("/attachments/:id/raw", async (c) => {
27684
28832
  const attachment = await getAttachment(idParam.data);
27685
28833
  if (attachment === void 0) return c.text("Not found", 404);
27686
28834
  try {
27687
- const stat = statSync3(attachment.stored_path);
28835
+ const stat = statSync5(attachment.stored_path);
27688
28836
  if (!stat.isFile()) return c.text("Not found", 404);
27689
- const bytes = readFileSync10(attachment.stored_path);
28837
+ const bytes = readFileSync13(attachment.stored_path);
27690
28838
  return new Response(new Uint8Array(bytes), {
27691
28839
  headers: {
27692
28840
  "Content-Type": attachment.mime_type,
@@ -27704,7 +28852,7 @@ attachmentsRoutes.post("/attachments/:id/quicklook", async (c) => {
27704
28852
  const attachment = await getAttachment(idParam.data);
27705
28853
  if (attachment === void 0) return errorResponse(c, "Attachment not found", 404);
27706
28854
  try {
27707
- statSync3(attachment.stored_path);
28855
+ statSync5(attachment.stored_path);
27708
28856
  } catch {
27709
28857
  return errorResponse(c, "Attachment file missing", 404);
27710
28858
  }
@@ -27806,6 +28954,19 @@ function groundTruthMetaByFileId(mode, files) {
27806
28954
  return out;
27807
28955
  }
27808
28956
 
28957
+ // src/plugins/fileView.ts
28958
+ init_plugins();
28959
+ function pluginRendersFile(filePath) {
28960
+ return pluginsEnabled() && mightHandleFile(filePath);
28961
+ }
28962
+ async function renderPluginSvgSide(mode, filePath, oldPath, side, cwd) {
28963
+ if (mode === null || !pluginRendersFile(filePath)) return null;
28964
+ const source = getModeFileContent(mode, side === "old" ? oldPath : filePath, side, cwd);
28965
+ if (source.trim() === "") return null;
28966
+ const view = await renderContent({ bytes: new TextEncoder().encode(source), text: source, path: filePath, side });
28967
+ return view?.svg !== void 0 && view.svg !== "" ? view.svg : null;
28968
+ }
28969
+
27809
28970
  // src/routes/api/files.ts
27810
28971
  init_openOS();
27811
28972
  var filesRoutes = new Hono7();
@@ -27818,7 +28979,14 @@ filesRoutes.get("/files", async (c) => {
27818
28979
  getReview(reviewId)
27819
28980
  ]);
27820
28981
  const groundTruth = review ? groundTruthMetaByFileId(parseModeString(review.mode), files) : void 0;
27821
- return c.json({ files, annotationCounts, staleCounts, ...groundTruth ? { groundTruth } : {} });
28982
+ const pluginRendered = files.filter((f) => pluginRendersFile(f.file_path)).map((f) => f.id);
28983
+ return c.json({
28984
+ files,
28985
+ annotationCounts,
28986
+ staleCounts,
28987
+ ...groundTruth ? { groundTruth } : {},
28988
+ ...pluginRendered.length > 0 ? { pluginRendered } : {}
28989
+ });
27822
28990
  });
27823
28991
  filesRoutes.get("/files/:fileId", async (c) => {
27824
28992
  const fileId = requirePathParam(c, "fileId");
@@ -27880,8 +29048,8 @@ import { Hono as Hono8 } from "hono";
27880
29048
 
27881
29049
  // src/git/image.ts
27882
29050
  import { spawnSync as spawnSync8 } from "child_process";
27883
- import { readFileSync as readFileSync11 } from "fs";
27884
- import { join as join11, resolve as resolve8 } from "path";
29051
+ import { readFileSync as readFileSync14 } from "fs";
29052
+ import { join as join14, resolve as resolve8 } from "path";
27885
29053
  init_image_metadata();
27886
29054
  function getOldRef(mode) {
27887
29055
  switch (mode.type) {
@@ -27945,14 +29113,14 @@ function gitShowFile(ref, filePath, repoRoot) {
27945
29113
  }
27946
29114
  function readWorkingFile(filePath, repoRoot) {
27947
29115
  try {
27948
- return readFileSync11(resolve8(repoRoot, filePath));
29116
+ return readFileSync14(resolve8(repoRoot, filePath));
27949
29117
  } catch {
27950
29118
  return null;
27951
29119
  }
27952
29120
  }
27953
29121
  function readDiskImage(absPath) {
27954
29122
  try {
27955
- const data = readFileSync11(absPath);
29123
+ const data = readFileSync14(absPath);
27956
29124
  return { data, size: data.length };
27957
29125
  } catch {
27958
29126
  return null;
@@ -27963,7 +29131,7 @@ function groundTruthEntry(mode, filePath) {
27963
29131
  }
27964
29132
  function getOldImage(mode, filePath, oldPath, repoRoot) {
27965
29133
  if (mode.type === "diff") {
27966
- return readDiskImage(join11(directComparisonRoots(mode).rootA, oldPath ?? filePath));
29134
+ return readDiskImage(join14(directComparisonRoots(mode).rootA, oldPath ?? filePath));
27967
29135
  }
27968
29136
  if (mode.type === "ground-truth") {
27969
29137
  const entry = groundTruthEntry(mode, filePath);
@@ -27983,7 +29151,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
27983
29151
  }
27984
29152
  function getNewImage(mode, filePath, repoRoot) {
27985
29153
  if (mode.type === "diff") {
27986
- return readDiskImage(join11(directComparisonRoots(mode).rootB, filePath));
29154
+ return readDiskImage(join14(directComparisonRoots(mode).rootB, filePath));
27987
29155
  }
27988
29156
  if (mode.type === "ground-truth") {
27989
29157
  const entry = groundTruthEntry(mode, filePath);
@@ -28057,6 +29225,12 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
28057
29225
  const diff = parseDiffData(file2.diff_data);
28058
29226
  const oldPath = diff?.oldPath ?? null;
28059
29227
  const status = diff?.status ?? "modified";
29228
+ if (!isImageFile(file2.file_path) && !(side === "old" && status === "added") && !(side === "new" && status === "deleted")) {
29229
+ const svg = await renderPluginSvgSide(mode, file2.file_path, oldPath ?? file2.file_path, side, repoRoot);
29230
+ if (svg !== null) {
29231
+ return new Response(svg, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" } });
29232
+ }
29233
+ }
28060
29234
  const image = resolveImageSide(
28061
29235
  fileIdParam.data,
28062
29236
  side,
@@ -28079,7 +29253,7 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
28079
29253
  init_queries();
28080
29254
  init_debug();
28081
29255
  import { spawnSync as spawnSync9 } from "child_process";
28082
- import { readFileSync as readFileSync12 } from "fs";
29256
+ import { readFileSync as readFileSync15 } from "fs";
28083
29257
  import { Hono as Hono9 } from "hono";
28084
29258
  import { resolve as resolve9 } from "path";
28085
29259
 
@@ -28457,7 +29631,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
28457
29631
  }
28458
29632
  let content = "";
28459
29633
  try {
28460
- content = readFileSync12(resolve9(repoRoot, filePath), "utf-8");
29634
+ content = readFileSync15(resolve9(repoRoot, filePath), "utf-8");
28461
29635
  } catch {
28462
29636
  continue;
28463
29637
  }
@@ -28492,48 +29666,268 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
28492
29666
  }
28493
29667
  }
28494
29668
 
28495
- // src/routes/api/project-settings.ts
28496
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
29669
+ // src/routes/api/plugins.ts
28497
29670
  import { Hono as Hono10 } from "hono";
28498
- import { join as join12 } from "path";
28499
- var projectSettingsRoutes = new Hono10();
28500
- function readProjectSettings(repoRoot) {
28501
- const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
29671
+
29672
+ // src/plugins/available.ts
29673
+ init_install();
29674
+ init_loader();
29675
+ import { existsSync as existsSync14 } from "fs";
29676
+ import { join as join15 } from "path";
29677
+
29678
+ // src/plugins/readiness.ts
29679
+ import { spawnSync as spawnSync10 } from "child_process";
29680
+ var defaultProbe = (command, args) => {
28502
29681
  try {
28503
- if (existsSync11(settingsPath)) {
28504
- const raw2 = JSON.parse(readFileSync13(settingsPath, "utf-8"));
28505
- const parsed = ProjectSettingsSchema.safeParse(raw2);
28506
- if (parsed.success) return parsed.data;
28507
- }
29682
+ const r = spawnSync10(command, args, { stdio: "ignore", timeout: 1e4 });
29683
+ return r.error === void 0 && r.status === 0;
28508
29684
  } catch {
29685
+ return false;
28509
29686
  }
28510
- return {};
29687
+ };
29688
+ function checkRequirement(req, runProbe = defaultProbe) {
29689
+ const met = runProbe(req.command, req.checkArgs ?? ["--version"]);
29690
+ return { id: req.id, label: req.label, met, hint: req.hint, docUrl: req.docUrl };
28511
29691
  }
28512
- function writeProjectSettings(repoRoot, settings) {
28513
- const dir = join12(repoRoot, ".glassbox");
28514
- mkdirSync9(dir, { recursive: true });
28515
- writeFileSync10(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
29692
+ function checkRequirements(reqs, runProbe = defaultProbe) {
29693
+ return (reqs ?? []).map((r) => checkRequirement(r, runProbe));
28516
29694
  }
28517
- projectSettingsRoutes.get("/project-settings", (c) => {
29695
+ function requirementMet(statuses, id) {
29696
+ return statuses.find((s) => s.id === id)?.met ?? false;
29697
+ }
29698
+
29699
+ // src/plugins/available.ts
29700
+ function manifestExtensions(manifest) {
29701
+ return (manifest.contentTypes ?? []).flatMap((ct) => ct.extensions ?? []);
29702
+ }
29703
+ function provisionNotes(install) {
29704
+ const notes = [];
29705
+ for (const step of install?.provision ?? []) {
29706
+ if (step.kind === "fetch") notes.push(`Downloads ${step.dest}.`);
29707
+ else notes.push(step.note ?? `Installs ${step.packages.join(", ")}.`);
29708
+ }
29709
+ return notes;
29710
+ }
29711
+ function describeAvailablePlugin(manifest, runProbe) {
29712
+ const requirements = checkRequirements(manifest.install?.requirements, runProbe);
29713
+ const notes = provisionNotes(manifest.install);
29714
+ return {
29715
+ id: manifest.id,
29716
+ name: manifest.name,
29717
+ version: manifest.version,
29718
+ description: manifest.description,
29719
+ extensions: manifestExtensions(manifest),
29720
+ requirements,
29721
+ provisionNotes: notes,
29722
+ selfContained: requirements.length === 0 && notes.length === 0,
29723
+ cliHint: manifest.install?.cliHint
29724
+ };
29725
+ }
29726
+ function listAvailablePlugins(opts) {
29727
+ const bundledDir = opts?.bundledDir ?? bundledPluginsDir();
29728
+ const userDir = opts?.userDir ?? pluginsDir();
29729
+ const out = [];
29730
+ for (const { manifest } of discoverBundledPlugins(bundledDir)) {
29731
+ if (manifest.autoInstall !== false) continue;
29732
+ if (existsSync14(join15(userDir, manifest.id))) continue;
29733
+ out.push(describeAvailablePlugin(manifest, opts?.runProbe));
29734
+ }
29735
+ return out.sort((a, b) => a.name.localeCompare(b.name));
29736
+ }
29737
+
29738
+ // src/routes/api/plugins.ts
29739
+ init_enablement();
29740
+ init_plugins();
29741
+ init_install();
29742
+
29743
+ // src/plugins/install-action.ts
29744
+ init_install();
29745
+ init_loader();
29746
+ import { spawnSync as spawnSync11 } from "child_process";
29747
+ import { createHash as createHash4 } from "crypto";
29748
+ import { cpSync as cpSync2, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
29749
+ import { join as join16 } from "path";
29750
+ var defaultFetchBytes = async (url2) => {
29751
+ const res = await fetch(url2);
29752
+ if (!res.ok) throw new Error(`HTTP ${String(res.status)}`);
29753
+ return new Uint8Array(await res.arrayBuffer());
29754
+ };
29755
+ var defaultRunNpmInstall = (dir, packages) => {
29756
+ const r = spawnSync11("npm", ["install", "--no-save", "--prefix", dir, ...packages], { stdio: "ignore", timeout: 6e5 });
29757
+ if (r.error !== void 0) return { ok: false, detail: r.error.message };
29758
+ if (r.status !== 0) return { ok: false, detail: `npm exited ${String(r.status)}` };
29759
+ return { ok: true, detail: "installed" };
29760
+ };
29761
+ async function runProvisionStep(step, installDir, requirements, deps) {
29762
+ if (step.kind === "fetch") {
29763
+ const label2 = `Download ${step.dest}`;
29764
+ try {
29765
+ const bytes = await deps.fetchBytes(step.url);
29766
+ if (step.sha256 !== void 0) {
29767
+ const got = createHash4("sha256").update(bytes).digest("hex");
29768
+ if (got.toLowerCase() !== step.sha256.toLowerCase()) {
29769
+ return { step: label2, ok: false, skipped: false, detail: `checksum mismatch (expected ${step.sha256})` };
29770
+ }
29771
+ }
29772
+ writeFileSync12(join16(installDir, step.dest), bytes);
29773
+ return { step: label2, ok: true, skipped: false, detail: `saved ${step.dest}` };
29774
+ } catch (e) {
29775
+ return { step: label2, ok: false, skipped: false, detail: e instanceof Error ? e.message : "download failed" };
29776
+ }
29777
+ }
29778
+ const label = `Install ${step.packages.join(", ")}`;
29779
+ const reqId = step.requires ?? "npm";
29780
+ if (!requirementMet(requirements, reqId)) {
29781
+ return { step: label, ok: false, skipped: true, detail: `${reqId} is not available` };
29782
+ }
29783
+ const r = deps.runNpmInstall(installDir, step.packages);
29784
+ return { step: label, ok: r.ok, skipped: false, detail: r.detail };
29785
+ }
29786
+ function buildInstructions(manifest, requirements, provisioned) {
29787
+ const out = [];
29788
+ for (const r of requirements) if (!r.met) out.push(`${r.label}: ${r.hint}`);
29789
+ for (const p of provisioned) {
29790
+ if (p.ok) continue;
29791
+ out.push(p.skipped ? `${p.step} was skipped \u2014 ${p.detail}.` : `${p.step} failed \u2014 ${p.detail}.`);
29792
+ }
29793
+ const cliHint = manifest.install?.cliHint;
29794
+ if (out.length > 0 && cliHint !== void 0 && cliHint !== "") out.push(`Or finish from a terminal: ${cliHint}`);
29795
+ return out;
29796
+ }
29797
+ async function installAvailablePlugin(id, deps = {}) {
29798
+ const bundledDir = deps.bundledDir ?? bundledPluginsDir();
29799
+ const userDir = deps.userDir ?? pluginsDir();
29800
+ const fetchBytes = deps.fetchBytes ?? defaultFetchBytes;
29801
+ const runNpmInstall = deps.runNpmInstall ?? defaultRunNpmInstall;
29802
+ const bundled = discoverBundledPlugins(bundledDir).find((b) => b.manifest.id === id);
29803
+ if (bundled === void 0) {
29804
+ return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: "Not a bundled plugin available to install." };
29805
+ }
29806
+ const manifest = bundled.manifest;
29807
+ const dest = join16(userDir, id);
29808
+ try {
29809
+ mkdirSync11(userDir, { recursive: true });
29810
+ cpSync2(bundled.dir, dest, { recursive: true });
29811
+ undismissPlugin(id, userDir);
29812
+ } catch (e) {
29813
+ return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: e instanceof Error ? e.message : "Copy failed." };
29814
+ }
29815
+ const requirements = checkRequirements(manifest.install?.requirements, deps.runProbe);
29816
+ const provisioned = [];
29817
+ for (const step of manifest.install?.provision ?? []) {
29818
+ provisioned.push(await runProvisionStep(step, dest, requirements, { fetchBytes, runNpmInstall }));
29819
+ }
29820
+ const instructions = buildInstructions(manifest, requirements, provisioned);
29821
+ const status = instructions.length === 0 ? "ready" : "needs-setup";
29822
+ return { id, installed: true, status, requirements, provisioned, instructions };
29823
+ }
29824
+
29825
+ // src/routes/api/plugins.ts
29826
+ init_loader();
29827
+ init_settings();
29828
+ var pluginsRoutes = new Hono10();
29829
+ pluginsRoutes.get("/plugins", (c) => {
29830
+ return c.json({ plugins: describeInstalledPlugins(c.get("repoRoot")) });
29831
+ });
29832
+ pluginsRoutes.get("/plugins/ui", (c) => {
29833
+ return c.json({ elements: listPluginUIElements(c.get("repoRoot")) });
29834
+ });
29835
+ pluginsRoutes.get("/plugins/available", (c) => {
29836
+ return c.json({ available: listAvailablePlugins() });
29837
+ });
29838
+ pluginsRoutes.post("/plugins/:id/install-bundled", async (c) => {
29839
+ const id = requirePathParam(c, "id");
29840
+ if (!id.ok) return id.response;
28518
29841
  const repoRoot = c.get("repoRoot");
28519
- return c.json(readProjectSettings(repoRoot));
29842
+ const result = await installAvailablePlugin(id.data);
29843
+ if (result.installed) await reloadContentPlugins(repoRoot);
29844
+ return c.json({ result, plugins: describeInstalledPlugins(repoRoot), available: listAvailablePlugins() });
29845
+ });
29846
+ pluginsRoutes.post("/plugins/:id/disabled", async (c) => {
29847
+ const id = requirePathParam(c, "id");
29848
+ if (!id.ok) return id.response;
29849
+ const parsed = await parseBody(c, SetPluginDisabledReqSchema);
29850
+ if (!parsed.ok) return parsed.response;
29851
+ const repoRoot = c.get("repoRoot");
29852
+ if (parsed.data.scope === "global") setGlobalDisabled(id.data, parsed.data.disabled);
29853
+ else setProjectDisabled(repoRoot, id.data, parsed.data.disabled);
29854
+ await reloadContentPlugins(repoRoot);
29855
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29856
+ });
29857
+ pluginsRoutes.post("/plugins/install", async (c) => {
29858
+ const parsed = await parseBody(c, InstallPluginReqSchema);
29859
+ if (!parsed.ok) return parsed.response;
29860
+ const repoRoot = c.get("repoRoot");
29861
+ try {
29862
+ installPluginFromDisk(parsed.data.path);
29863
+ } catch (e) {
29864
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), error: e instanceof Error ? e.message : "Install failed" }, 400);
29865
+ }
29866
+ await reloadContentPlugins(repoRoot);
29867
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29868
+ });
29869
+ pluginsRoutes.post("/plugins/:id/preferences", async (c) => {
29870
+ const id = requirePathParam(c, "id");
29871
+ if (!id.ok) return id.response;
29872
+ const parsed = await parseBody(c, SetPluginPreferenceReqSchema);
29873
+ if (!parsed.ok) return parsed.response;
29874
+ const manifest = getPluginManifest(id.data);
29875
+ if (manifest === void 0) return c.json({ error: "Plugin not found" }, 404);
29876
+ const repoRoot = c.get("repoRoot");
29877
+ writePluginSetting(manifest, repoRoot, parsed.data.key, parsed.data.value);
29878
+ await reloadContentPlugins(repoRoot);
29879
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29880
+ });
29881
+ pluginsRoutes.post("/plugins/:id/action", async (c) => {
29882
+ const id = requirePathParam(c, "id");
29883
+ if (!id.ok) return id.response;
29884
+ const parsed = await parseBody(c, RunPluginActionReqSchema);
29885
+ if (!parsed.ok) return parsed.response;
29886
+ const repoRoot = c.get("repoRoot");
29887
+ if (parsed.data.value !== void 0) persistPluginUIState(id.data, parsed.data.actionId, parsed.data.value, repoRoot);
29888
+ let result;
29889
+ try {
29890
+ result = await runPluginAction(id.data, parsed.data.actionId, parsed.data.value);
29891
+ } catch (e) {
29892
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), error: e instanceof Error ? e.message : "Action failed" }, 400);
29893
+ }
29894
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), result: result ?? void 0 });
29895
+ });
29896
+ pluginsRoutes.delete("/plugins/:id", async (c) => {
29897
+ const id = requirePathParam(c, "id");
29898
+ if (!id.ok) return id.response;
29899
+ const repoRoot = c.get("repoRoot");
29900
+ uninstallPlugin(id.data);
29901
+ clearConfigLabelOverrides(id.data);
29902
+ clearPluginUIElements(id.data);
29903
+ await reloadContentPlugins(repoRoot);
29904
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), available: listAvailablePlugins() });
29905
+ });
29906
+
29907
+ // src/routes/api/project-settings.ts
29908
+ init_project_settings();
29909
+ init_project_settings_store();
29910
+ import { Hono as Hono11 } from "hono";
29911
+ var projectSettingsRoutes = new Hono11();
29912
+ projectSettingsRoutes.get("/project-settings", (c) => {
29913
+ return c.json(readProjectSettings(c.get("repoRoot")));
28520
29914
  });
28521
29915
  projectSettingsRoutes.patch("/project-settings", async (c) => {
28522
29916
  const repoRoot = c.get("repoRoot");
28523
29917
  const parsed = await parseBody(c, UpdateProjectSettingsReqSchema);
28524
29918
  if (!parsed.ok) return parsed.response;
28525
- const current = readProjectSettings(repoRoot);
28526
- if (parsed.data.appName !== void 0) current.appName = parsed.data.appName || void 0;
28527
- writeProjectSettings(repoRoot, current);
28528
- return c.json(current);
29919
+ updateProjectSettings2(repoRoot, (s) => {
29920
+ if (parsed.data.appName !== void 0) s.appName = parsed.data.appName || void 0;
29921
+ });
29922
+ return c.json(readProjectSettings(repoRoot));
28529
29923
  });
28530
29924
 
28531
29925
  // src/routes/api/review-notes.ts
28532
29926
  init_store();
28533
- import { readFileSync as readFileSync14, statSync as statSync4 } from "fs";
28534
- import { Hono as Hono11 } from "hono";
28535
- import { extname, relative as relative2, resolve as resolve10 } from "path";
28536
- var reviewNotesRoutes = new Hono11();
29927
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "fs";
29928
+ import { Hono as Hono12 } from "hono";
29929
+ import { extname as extname2, relative as relative2, resolve as resolve10 } from "path";
29930
+ var reviewNotesRoutes = new Hono12();
28537
29931
  var ARTIFACT_SERVE_MAX_BYTES = 1e7;
28538
29932
  var IMAGE_CONTENT_TYPES = {
28539
29933
  ".png": "image/png",
@@ -28551,13 +29945,13 @@ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
28551
29945
  const abs = resolve10(repoRoot, file2);
28552
29946
  const rel = relative2(repoRoot, abs);
28553
29947
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
28554
- const ext = extname(abs).toLowerCase();
29948
+ const ext = extname2(abs).toLowerCase();
28555
29949
  if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
28556
29950
  const contentType = IMAGE_CONTENT_TYPES[ext];
28557
29951
  try {
28558
- const stat = statSync4(abs);
29952
+ const stat = statSync6(abs);
28559
29953
  if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
28560
- const body = readFileSync14(abs);
29954
+ const body = readFileSync16(abs);
28561
29955
  return c.body(body, 200, { "Content-Type": contentType });
28562
29956
  } catch {
28563
29957
  return c.text("Not found", 404);
@@ -28574,17 +29968,17 @@ reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
28574
29968
 
28575
29969
  // src/routes/api/reviews.ts
28576
29970
  init_queries();
28577
- import { Hono as Hono12 } from "hono";
29971
+ import { Hono as Hono13 } from "hono";
28578
29972
 
28579
29973
  // src/export/on-complete-hook.ts
28580
29974
  import { spawn as spawn2 } from "child_process";
28581
29975
  import { appendFileSync } from "fs";
28582
- import { join as join13 } from "path";
29976
+ import { join as join17 } from "path";
28583
29977
  function runOnCompleteHook(command, ctx) {
28584
29978
  if (command === null || command.trim() === "") {
28585
29979
  return Promise.resolve({ ran: false, ok: true, exitCode: 0 });
28586
29980
  }
28587
- const logPath = join13(ctx.repoRoot, ".glassbox", "on-complete.log");
29981
+ const logPath = join17(ctx.repoRoot, ".glassbox", "on-complete.log");
28588
29982
  const log = (s) => {
28589
29983
  try {
28590
29984
  appendFileSync(logPath, s);
@@ -28636,7 +30030,8 @@ $ ${command}
28636
30030
  }
28637
30031
 
28638
30032
  // src/routes/api/reviews.ts
28639
- var reviewsRoutes = new Hono12();
30033
+ init_plugins();
30034
+ var reviewsRoutes = new Hono13();
28640
30035
  reviewsRoutes.get("/reviews", async (c) => {
28641
30036
  const repoRoot = c.get("repoRoot");
28642
30037
  const reviews = await listReviews(repoRoot);
@@ -28660,6 +30055,13 @@ reviewsRoutes.post("/review/complete", async (c) => {
28660
30055
  jsonPath: exportPath.replace(/\.md$/, ".json"),
28661
30056
  markdownPath: exportPath
28662
30057
  });
30058
+ try {
30059
+ const completed = await getReview(reviewId);
30060
+ if (completed !== void 0) {
30061
+ await notifyReviewCompleted(completed, await getAnnotationsForReview(reviewId), exportPath);
30062
+ }
30063
+ } catch {
30064
+ }
28663
30065
  return c.json({ status: "completed", exportPath, isCurrent, reviewId, hook });
28664
30066
  });
28665
30067
  reviewsRoutes.post("/review/reopen", async (c) => {
@@ -28721,8 +30123,9 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
28721
30123
 
28722
30124
  // src/routes/api/share-prompt.ts
28723
30125
  init_zod();
28724
- import { Hono as Hono13 } from "hono";
28725
- var sharePromptRoutes = new Hono13();
30126
+ import { Hono as Hono14 } from "hono";
30127
+ init_global_config();
30128
+ var sharePromptRoutes = new Hono14();
28726
30129
  var SharePromptShapeSchema = external_exports.object({
28727
30130
  dismissedAt: external_exports.number().nullable().optional(),
28728
30131
  totalOpenMs: external_exports.number().optional()
@@ -28761,9 +30164,9 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
28761
30164
  });
28762
30165
 
28763
30166
  // src/routes/api/system.ts
28764
- import { Hono as Hono14 } from "hono";
30167
+ import { Hono as Hono15 } from "hono";
28765
30168
  init_openOS();
28766
- var systemRoutes = new Hono14();
30169
+ var systemRoutes = new Hono15();
28767
30170
  systemRoutes.post("/open-external", async (c) => {
28768
30171
  const parsed = await parseBody(c, OpenExternalReqSchema);
28769
30172
  if (!parsed.ok) return parsed.response;
@@ -28775,7 +30178,7 @@ systemRoutes.post("/open-external", async (c) => {
28775
30178
  });
28776
30179
 
28777
30180
  // src/routes/api.ts
28778
- var apiRoutes = new Hono15();
30181
+ var apiRoutes = new Hono16();
28779
30182
  apiRoutes.route("/", reviewsRoutes);
28780
30183
  apiRoutes.route("/", filesRoutes);
28781
30184
  apiRoutes.route("/", annotationsRoutes);
@@ -28783,22 +30186,24 @@ apiRoutes.route("/", attachmentsRoutes);
28783
30186
  apiRoutes.route("/", outlineRoutes);
28784
30187
  apiRoutes.route("/", contextRoutes);
28785
30188
  apiRoutes.route("/", projectSettingsRoutes);
30189
+ apiRoutes.route("/", pluginsRoutes);
28786
30190
  apiRoutes.route("/", imageRoutes);
28787
30191
  apiRoutes.route("/", reviewNotesRoutes);
28788
30192
  apiRoutes.route("/", sharePromptRoutes);
28789
30193
  apiRoutes.route("/", systemRoutes);
28790
30194
 
28791
30195
  // src/routes/channel-api.ts
28792
- import { spawnSync as spawnSync10 } from "child_process";
28793
- import { mkdirSync as mkdirSync10 } from "fs";
28794
- import { Hono as Hono16 } from "hono";
28795
- import { join as join14 } from "path";
28796
- var channelApiRoutes = new Hono16();
30196
+ import { spawnSync as spawnSync12 } from "child_process";
30197
+ import { mkdirSync as mkdirSync12 } from "fs";
30198
+ import { Hono as Hono17 } from "hono";
30199
+ import { join as join18 } from "path";
30200
+ init_global_config();
30201
+ var channelApiRoutes = new Hono17();
28797
30202
  channelApiRoutes.get("/status", async (c) => {
28798
30203
  const config2 = readGlobalConfig();
28799
30204
  const enabled = config2.channelEnabled === true;
28800
30205
  const repoRoot = c.get("repoRoot");
28801
- const dataDir = join14(repoRoot, ".glassbox");
30206
+ const dataDir = join18(repoRoot, ".glassbox");
28802
30207
  const connected = enabled ? await isChannelAlive(dataDir) : false;
28803
30208
  return c.json({ enabled, connected });
28804
30209
  });
@@ -28807,8 +30212,8 @@ channelApiRoutes.post("/enable", (c) => {
28807
30212
  config2.channelEnabled = true;
28808
30213
  });
28809
30214
  const repoRoot = c.get("repoRoot");
28810
- const dataDir = join14(repoRoot, ".glassbox");
28811
- mkdirSync10(dataDir, { recursive: true });
30215
+ const dataDir = join18(repoRoot, ".glassbox");
30216
+ mkdirSync12(dataDir, { recursive: true });
28812
30217
  registerChannel(dataDir);
28813
30218
  return c.json({ ok: true });
28814
30219
  });
@@ -28817,7 +30222,7 @@ channelApiRoutes.post("/disable", (c) => {
28817
30222
  config2.channelEnabled = false;
28818
30223
  });
28819
30224
  const repoRoot = c.get("repoRoot");
28820
- const dataDir = join14(repoRoot, ".glassbox");
30225
+ const dataDir = join18(repoRoot, ".glassbox");
28821
30226
  unregisterChannel(dataDir);
28822
30227
  return c.json({ ok: true });
28823
30228
  });
@@ -28825,7 +30230,7 @@ channelApiRoutes.post("/trigger", async (c) => {
28825
30230
  const parsed = await parseBody(c, TriggerChannelReqSchema);
28826
30231
  if (!parsed.ok) return parsed.response;
28827
30232
  const repoRoot = c.get("repoRoot");
28828
- const dataDir = join14(repoRoot, ".glassbox");
30233
+ const dataDir = join18(repoRoot, ".glassbox");
28829
30234
  const sent = await triggerChannel(dataDir, parsed.data.message);
28830
30235
  if (!sent) {
28831
30236
  return c.json({ error: "Channel not connected" }, 503);
@@ -28834,7 +30239,7 @@ channelApiRoutes.post("/trigger", async (c) => {
28834
30239
  });
28835
30240
  channelApiRoutes.get("/claude-check", (c) => {
28836
30241
  try {
28837
- const result = spawnSync10("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
30242
+ const result = spawnSync12("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
28838
30243
  if (result.status !== 0) {
28839
30244
  return c.json({ installed: false, version: null, meetsMinimum: false });
28840
30245
  }
@@ -28853,13 +30258,13 @@ channelApiRoutes.get("/claude-check", (c) => {
28853
30258
  });
28854
30259
 
28855
30260
  // src/routes/difftool-api.ts
28856
- import { Hono as Hono17 } from "hono";
30261
+ import { Hono as Hono18 } from "hono";
28857
30262
  init_connection();
28858
30263
  init_queries();
28859
30264
  init_session();
28860
30265
  init_difftool();
28861
30266
  init_image_blobs();
28862
- var difftoolApiRoutes = new Hono17();
30267
+ var difftoolApiRoutes = new Hono18();
28863
30268
  difftoolApiRoutes.get("/status", (c) => {
28864
30269
  return c.json(getDifftoolStatus("global"));
28865
30270
  });
@@ -28936,8 +30341,8 @@ difftoolApiRoutes.post("/end", (c) => {
28936
30341
  });
28937
30342
 
28938
30343
  // src/routes/pages.tsx
28939
- import { readFileSync as readFileSync16 } from "fs";
28940
- import { Hono as Hono18 } from "hono";
30344
+ import { readFileSync as readFileSync18 } from "fs";
30345
+ import { Hono as Hono19 } from "hono";
28941
30346
  import { resolve as resolve11 } from "path";
28942
30347
 
28943
30348
  // src/components/diffView.tsx
@@ -29728,7 +31133,28 @@ function ReviewNoteRows({ notes, repliesByNote }) {
29728
31133
  /* @__PURE__ */ jsx4("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
29729
31134
  ] }) : null
29730
31135
  ] }),
29731
- n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.content !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", children: [
31136
+ n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.renderedSvg !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", open: true, children: [
31137
+ /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
31138
+ /* @__PURE__ */ jsx4(IconPaperclip, {}),
31139
+ /* @__PURE__ */ jsx4("span", { children: a.uri })
31140
+ ] }),
31141
+ /* @__PURE__ */ jsx4("div", { className: "ai-note-artifact-imgwrap", children: /* @__PURE__ */ jsx4(
31142
+ "img",
31143
+ {
31144
+ className: "ai-note-artifact-img",
31145
+ loading: "lazy",
31146
+ alt: a.uri,
31147
+ draggable: false,
31148
+ src: `data:image/svg+xml;utf8,${encodeURIComponent(a.renderedSvg)}`
31149
+ }
31150
+ ) })
31151
+ ] }) : a.renderedHtml !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", open: true, children: [
31152
+ /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
31153
+ /* @__PURE__ */ jsx4(IconPaperclip, {}),
31154
+ /* @__PURE__ */ jsx4("span", { children: a.uri })
31155
+ ] }),
31156
+ /* @__PURE__ */ jsx4("div", { className: "ai-note-artifact-rendered", children: raw(a.renderedHtml) })
31157
+ ] }) : a.content !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", children: [
29732
31158
  /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
29733
31159
  /* @__PURE__ */ jsx4(IconPaperclip, {}),
29734
31160
  /* @__PURE__ */ jsx4("span", { children: a.uri })
@@ -29799,9 +31225,10 @@ function AnnotationRows({ annotations }) {
29799
31225
  }
29800
31226
 
29801
31227
  // src/themes/config.ts
29802
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, readdirSync as readdirSync2, readFileSync as readFileSync15, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "fs";
29803
- import { join as join15 } from "path";
29804
- var THEMES_DIR = join15(GLOBAL_CONFIG_DIR, "themes");
31228
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
31229
+ import { join as join19 } from "path";
31230
+ init_global_config();
31231
+ var THEMES_DIR = join19(GLOBAL_CONFIG_DIR, "themes");
29805
31232
  function getActiveThemeId() {
29806
31233
  const config2 = readGlobalConfig();
29807
31234
  const theme = config2.theme;
@@ -29815,13 +31242,13 @@ function setActiveThemeId(id) {
29815
31242
  });
29816
31243
  }
29817
31244
  function loadCustomThemes() {
29818
- if (!existsSync12(THEMES_DIR)) return [];
31245
+ if (!existsSync15(THEMES_DIR)) return [];
29819
31246
  const themes = [];
29820
31247
  try {
29821
- const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
31248
+ const files = readdirSync4(THEMES_DIR).filter((f) => f.endsWith(".json"));
29822
31249
  for (const file2 of files) {
29823
31250
  try {
29824
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync15(join15(THEMES_DIR, file2), "utf-8")));
31251
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync17(join19(THEMES_DIR, file2), "utf-8")));
29825
31252
  if (!parsed.success) continue;
29826
31253
  const d = parsed.data;
29827
31254
  themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
@@ -29833,21 +31260,21 @@ function loadCustomThemes() {
29833
31260
  return themes;
29834
31261
  }
29835
31262
  function saveCustomTheme(theme) {
29836
- mkdirSync11(THEMES_DIR, { recursive: true });
29837
- const filePath = join15(THEMES_DIR, `${theme.id}.json`);
29838
- writeFileSync11(filePath, JSON.stringify(theme, null, 2), "utf-8");
31263
+ mkdirSync13(THEMES_DIR, { recursive: true });
31264
+ const filePath = join19(THEMES_DIR, `${theme.id}.json`);
31265
+ writeFileSync13(filePath, JSON.stringify(theme, null, 2), "utf-8");
29839
31266
  }
29840
31267
  function deleteCustomTheme(id) {
29841
- const filePath = join15(THEMES_DIR, `${id}.json`);
29842
- if (existsSync12(filePath)) {
31268
+ const filePath = join19(THEMES_DIR, `${id}.json`);
31269
+ if (existsSync15(filePath)) {
29843
31270
  unlinkSync3(filePath);
29844
31271
  }
29845
31272
  }
29846
31273
  function getCustomTheme(id) {
29847
- const filePath = join15(THEMES_DIR, `${id}.json`);
29848
- if (!existsSync12(filePath)) return void 0;
31274
+ const filePath = join19(THEMES_DIR, `${id}.json`);
31275
+ if (!existsSync15(filePath)) return void 0;
29849
31276
  try {
29850
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync15(filePath, "utf-8")));
31277
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync17(filePath, "utf-8")));
29851
31278
  if (!parsed.success) return void 0;
29852
31279
  const d = parsed.data;
29853
31280
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -29884,6 +31311,7 @@ function Layout({ title, reviewId, difftool, children }) {
29884
31311
  /* @__PURE__ */ jsx5("meta", { charSet: "utf-8" }),
29885
31312
  /* @__PURE__ */ jsx5("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
29886
31313
  /* @__PURE__ */ jsx5("title", { children: title }),
31314
+ /* @__PURE__ */ jsx5("link", { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }),
29887
31315
  /* @__PURE__ */ jsx5("link", { rel: "stylesheet", href: "/static/styles.css" })
29888
31316
  ] }),
29889
31317
  /* @__PURE__ */ jsxs5("body", { "data-review-id": reviewId, "data-difftool": difftool === true ? "1" : void 0, children: [
@@ -30046,7 +31474,10 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30046
31474
  /* @__PURE__ */ jsx8("div", { className: "file-filter", children: /* @__PURE__ */ jsx8("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
30047
31475
  /* @__PURE__ */ jsx8(FileList, { files, annotationCounts, staleCounts }),
30048
31476
  /* @__PURE__ */ jsx8("div", { className: "sidebar-share", id: "sidebar-share" }),
30049
- /* @__PURE__ */ jsx8("div", { className: "sidebar-footer", children: footer })
31477
+ /* @__PURE__ */ jsxs8("div", { className: "sidebar-footer", children: [
31478
+ footer,
31479
+ /* @__PURE__ */ jsx8("div", { className: "plugin-ui-slot", id: "plugin-ui-sidebar-footer" })
31480
+ ] })
30050
31481
  ] }),
30051
31482
  /* @__PURE__ */ jsx8("div", { className: "sidebar-resize", id: "sidebar-resize" }),
30052
31483
  /* @__PURE__ */ jsxs8("main", { className: "main-content", children: [
@@ -30065,7 +31496,8 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30065
31496
  ] }) }),
30066
31497
  /* @__PURE__ */ jsx8("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx8("path", { d: "m15 18-6-6 6-6" }) }) }),
30067
31498
  /* @__PURE__ */ jsx8("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx8("path", { d: "m9 18 6-6-6-6" }) }) }),
30068
- /* @__PURE__ */ jsx8("span", { className: "nav-file-path", id: "nav-file-path" })
31499
+ /* @__PURE__ */ jsx8("span", { className: "nav-file-path", id: "nav-file-path" }),
31500
+ /* @__PURE__ */ jsx8("span", { className: "plugin-ui-slot", id: "plugin-ui-header" })
30069
31501
  ] }),
30070
31502
  /* @__PURE__ */ jsx8("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
30071
31503
  /* @__PURE__ */ jsxs8("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
@@ -30106,7 +31538,8 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30106
31538
  /* @__PURE__ */ jsx8("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx8(IconActualSize, {}) }),
30107
31539
  /* @__PURE__ */ jsx8("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx8(IconZoomIn, {}) })
30108
31540
  ] })
30109
- ] })
31541
+ ] }),
31542
+ /* @__PURE__ */ jsx8("div", { className: "plugin-ui-slot", id: "plugin-ui-diff-toolbar" })
30110
31543
  ] })
30111
31544
  ] })
30112
31545
  ] })
@@ -30140,6 +31573,25 @@ function svgUsesExternalFonts(svgData) {
30140
31573
  return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
30141
31574
  }
30142
31575
 
31576
+ // src/plugins/artifacts.ts
31577
+ init_plugins();
31578
+ async function renderNoteArtifacts(views) {
31579
+ for (const view of views) {
31580
+ for (const artifact of view.artifacts ?? []) {
31581
+ if (artifact.isImage === true || artifact.content === void 0) continue;
31582
+ const rendered = await renderContent({
31583
+ bytes: new TextEncoder().encode(artifact.content),
31584
+ text: artifact.content,
31585
+ path: artifact.uri
31586
+ });
31587
+ if (rendered === null) continue;
31588
+ if (rendered.svg !== void 0 && rendered.svg !== "") artifact.renderedSvg = rendered.svg;
31589
+ else if (rendered.html !== void 0 && rendered.html !== "") artifact.renderedHtml = rendered.html;
31590
+ }
31591
+ }
31592
+ return views;
31593
+ }
31594
+
30143
31595
  // src/review-notes/reanchor.ts
30144
31596
  var MATCH_RADIUS = 50;
30145
31597
  function reanchorReviewNotes(notes, diff) {
@@ -30173,7 +31625,7 @@ function reanchorReviewNotes(notes, diff) {
30173
31625
  // src/routes/pages.tsx
30174
31626
  init_store();
30175
31627
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "kerfjs/jsx-runtime";
30176
- var pageRoutes = new Hono18();
31628
+ var pageRoutes = new Hono19();
30177
31629
  pageRoutes.get("/", async (c) => {
30178
31630
  const reviewId = c.get("reviewId");
30179
31631
  const review = await getReview(reviewId);
@@ -30202,12 +31654,14 @@ pageRoutes.get("/file/:fileId", async (c) => {
30202
31654
  const reviewMode = review ? parseModeString(review.mode) : null;
30203
31655
  const imageSideLabels = reviewMode ? groundTruthSideLabels(reviewMode) : void 0;
30204
31656
  const stepNav = reviewMode?.type === "ground-truth" ? groundTruthStepNav(reviewMode, file2.id, await getReviewFiles(file2.review_id)) : void 0;
30205
- if (view === "rendered" && isSvgFile(file2.file_path)) {
31657
+ const isSvg = isSvgFile(file2.file_path);
31658
+ const isPluginSvg = !isSvg && !diff.isBinary && pluginRendersFile(file2.file_path);
31659
+ if (view === "rendered" && (isSvg || isPluginSvg)) {
30206
31660
  const repoRoot = c.get("repoRoot");
30207
31661
  let fontWarning = false;
30208
31662
  let svgBaseWidth = 300;
30209
31663
  let svgBaseHeight = 150;
30210
- if (reviewMode) {
31664
+ if (isSvg && reviewMode) {
30211
31665
  const oldImg = diff.status !== "added" ? getOldImage(reviewMode, file2.file_path, diff.oldPath ?? null, repoRoot) : null;
30212
31666
  const newImg = diff.status !== "deleted" ? getNewImage(reviewMode, file2.file_path, repoRoot) : null;
30213
31667
  const svgData = newImg ?? oldImg;
@@ -30219,6 +31673,14 @@ pageRoutes.get("/file/:fileId", async (c) => {
30219
31673
  if (oldImg && svgUsesExternalFonts(oldImg.data) || newImg && svgUsesExternalFonts(newImg.data)) {
30220
31674
  fontWarning = true;
30221
31675
  }
31676
+ } else if (isPluginSvg) {
31677
+ const svg = await renderPluginSvgSide(reviewMode, file2.file_path, diff.oldPath ?? file2.file_path, diff.status !== "deleted" ? "new" : "old", repoRoot);
31678
+ if (svg !== null) {
31679
+ const dims = parseSvgDimensions(svg);
31680
+ svgBaseWidth = dims.width;
31681
+ svgBaseHeight = dims.height;
31682
+ if (svgUsesExternalFonts(Buffer.from(svg))) fontWarning = true;
31683
+ }
30222
31684
  }
30223
31685
  const html2 = /* @__PURE__ */ jsxs9("div", { className: "diff-view", "data-file-id": file2.id, "data-file-path": file2.file_path, "data-is-svg": "true", children: [
30224
31686
  /* @__PURE__ */ jsxs9("div", { className: "diff-header", children: [
@@ -30283,6 +31745,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
30283
31745
  }
30284
31746
  const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
30285
31747
  const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
31748
+ await renderNoteArtifacts(reviewNotes);
30286
31749
  const html = /* @__PURE__ */ jsx9(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes, imageSideLabels, stepNav });
30287
31750
  return c.html(html.toString());
30288
31751
  });
@@ -30292,7 +31755,7 @@ pageRoutes.get("/file-raw", (c) => {
30292
31755
  const repoRoot = c.get("repoRoot");
30293
31756
  let content;
30294
31757
  try {
30295
- content = readFileSync16(resolve11(repoRoot, filePath), "utf-8");
31758
+ content = readFileSync18(resolve11(repoRoot, filePath), "utf-8");
30296
31759
  } catch {
30297
31760
  return c.text("File not found", 404);
30298
31761
  }
@@ -30348,8 +31811,8 @@ pageRoutes.get("/history", async (c) => {
30348
31811
  });
30349
31812
 
30350
31813
  // src/routes/theme-api.ts
30351
- import { Hono as Hono19 } from "hono";
30352
- var themeApiRoutes = new Hono19();
31814
+ import { Hono as Hono20 } from "hono";
31815
+ var themeApiRoutes = new Hono20();
30353
31816
  themeApiRoutes.get("/", (c) => {
30354
31817
  const themes = getAllThemes();
30355
31818
  const activeId = getActiveThemeId();
@@ -30476,7 +31939,7 @@ function tryServe(appFetch, port) {
30476
31939
  });
30477
31940
  }
30478
31941
  async function startServer(port, reviewId, repoRoot, options) {
30479
- const app = new Hono20();
31942
+ const app = new Hono21();
30480
31943
  const onCompleteCommand = options?.onComplete ?? null;
30481
31944
  app.use("*", async (c, next) => {
30482
31945
  c.set("reviewId", reviewId);
@@ -30485,20 +31948,24 @@ async function startServer(port, reviewId, repoRoot, options) {
30485
31948
  c.set("onCompleteCommand", onCompleteCommand);
30486
31949
  await next();
30487
31950
  });
30488
- const selfDir = dirname7(fileURLToPath2(import.meta.url));
30489
- const distDir = existsSync13(join16(selfDir, "client", "styles.css")) ? join16(selfDir, "client") : join16(selfDir, "..", "dist", "client");
31951
+ const selfDir = dirname8(fileURLToPath3(import.meta.url));
31952
+ const distDir = existsSync16(join20(selfDir, "client", "styles.css")) ? join20(selfDir, "client") : join20(selfDir, "..", "dist", "client");
30490
31953
  app.get("/static/styles.css", (c) => {
30491
- const css = readFileSync17(join16(distDir, "styles.css"), "utf-8");
31954
+ const css = readFileSync19(join20(distDir, "styles.css"), "utf-8");
30492
31955
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
30493
31956
  });
30494
31957
  app.get("/static/app.js", (c) => {
30495
- const js = readFileSync17(join16(distDir, "app.global.js"), "utf-8");
31958
+ const js = readFileSync19(join20(distDir, "app.global.js"), "utf-8");
30496
31959
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
30497
31960
  });
30498
31961
  app.get("/static/history.js", (c) => {
30499
- const js = readFileSync17(join16(distDir, "history.global.js"), "utf-8");
31962
+ const js = readFileSync19(join20(distDir, "history.global.js"), "utf-8");
30500
31963
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
30501
31964
  });
31965
+ app.get("/favicon.svg", (c) => {
31966
+ const svg = readFileSync19(join20(distDir, "favicon.svg"), "utf-8");
31967
+ return c.body(svg, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" });
31968
+ });
30502
31969
  app.get("/favicon.ico", (c) => c.body(null, 204));
30503
31970
  app.route("/api", apiRoutes);
30504
31971
  app.route("/api/ai", aiApiRoutes);
@@ -30531,10 +31998,16 @@ async function startServer(port, reviewId, repoRoot, options) {
30531
31998
  console.log(`
30532
31999
  Glassbox running at ${url2}
30533
32000
  `);
32001
+ await initContentPlugins(repoRoot);
32002
+ try {
32003
+ const review = await getReview(reviewId);
32004
+ if (review !== void 0) await notifyReviewCreated(review);
32005
+ } catch {
32006
+ }
30534
32007
  try {
30535
32008
  const globalConfig2 = readGlobalConfig();
30536
32009
  if (globalConfig2.channelEnabled === true) {
30537
- const dataDir = join16(repoRoot, ".glassbox");
32010
+ const dataDir = join20(repoRoot, ".glassbox");
30538
32011
  registerChannel(dataDir);
30539
32012
  }
30540
32013
  } catch {
@@ -30549,8 +32022,8 @@ async function startServer(port, reviewId, repoRoot, options) {
30549
32022
  }
30550
32023
 
30551
32024
  // src/skills.ts
30552
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
30553
- import { join as join17 } from "path";
32025
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
32026
+ import { join as join21 } from "path";
30554
32027
  var SKILL_VERSION = 1;
30555
32028
  function versionHeader() {
30556
32029
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -30561,14 +32034,14 @@ function parseVersionHeader(content) {
30561
32034
  return parseInt(match[1], 10);
30562
32035
  }
30563
32036
  function updateFile(path, content) {
30564
- if (existsSync14(path)) {
30565
- const existing = readFileSync18(path, "utf-8");
32037
+ if (existsSync17(path)) {
32038
+ const existing = readFileSync20(path, "utf-8");
30566
32039
  const version2 = parseVersionHeader(existing);
30567
32040
  if (version2 !== null && version2 >= SKILL_VERSION) {
30568
32041
  return false;
30569
32042
  }
30570
32043
  }
30571
- writeFileSync12(path, content, "utf-8");
32044
+ writeFileSync14(path, content, "utf-8");
30572
32045
  return true;
30573
32046
  }
30574
32047
  function skillBody() {
@@ -30588,8 +32061,8 @@ function skillBody() {
30588
32061
  ].join("\n");
30589
32062
  }
30590
32063
  function ensureClaudeSkills(cwd) {
30591
- const dir = join17(cwd, ".claude", "skills", "glassbox");
30592
- mkdirSync12(dir, { recursive: true });
32064
+ const dir = join21(cwd, ".claude", "skills", "glassbox");
32065
+ mkdirSync14(dir, { recursive: true });
30593
32066
  const content = [
30594
32067
  "---",
30595
32068
  "name: glassbox",
@@ -30601,11 +32074,11 @@ function ensureClaudeSkills(cwd) {
30601
32074
  skillBody(),
30602
32075
  ""
30603
32076
  ].join("\n");
30604
- return updateFile(join17(dir, "SKILL.md"), content);
32077
+ return updateFile(join21(dir, "SKILL.md"), content);
30605
32078
  }
30606
32079
  function ensureCursorRules(cwd) {
30607
- const rulesDir = join17(cwd, ".cursor", "rules");
30608
- mkdirSync12(rulesDir, { recursive: true });
32080
+ const rulesDir = join21(cwd, ".cursor", "rules");
32081
+ mkdirSync14(rulesDir, { recursive: true });
30609
32082
  const content = [
30610
32083
  "---",
30611
32084
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -30616,11 +32089,11 @@ function ensureCursorRules(cwd) {
30616
32089
  skillBody(),
30617
32090
  ""
30618
32091
  ].join("\n");
30619
- return updateFile(join17(rulesDir, "glassbox.mdc"), content);
32092
+ return updateFile(join21(rulesDir, "glassbox.mdc"), content);
30620
32093
  }
30621
32094
  function ensureCopilotPrompts(cwd) {
30622
- const promptsDir = join17(cwd, ".github", "prompts");
30623
- mkdirSync12(promptsDir, { recursive: true });
32095
+ const promptsDir = join21(cwd, ".github", "prompts");
32096
+ mkdirSync14(promptsDir, { recursive: true });
30624
32097
  const content = [
30625
32098
  "---",
30626
32099
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -30630,11 +32103,11 @@ function ensureCopilotPrompts(cwd) {
30630
32103
  skillBody(),
30631
32104
  ""
30632
32105
  ].join("\n");
30633
- return updateFile(join17(promptsDir, "glassbox.prompt.md"), content);
32106
+ return updateFile(join21(promptsDir, "glassbox.prompt.md"), content);
30634
32107
  }
30635
32108
  function ensureWindsurfRules(cwd) {
30636
- const rulesDir = join17(cwd, ".windsurf", "rules");
30637
- mkdirSync12(rulesDir, { recursive: true });
32109
+ const rulesDir = join21(cwd, ".windsurf", "rules");
32110
+ mkdirSync14(rulesDir, { recursive: true });
30638
32111
  const content = [
30639
32112
  "---",
30640
32113
  "trigger: manual",
@@ -30645,21 +32118,21 @@ function ensureWindsurfRules(cwd) {
30645
32118
  skillBody(),
30646
32119
  ""
30647
32120
  ].join("\n");
30648
- return updateFile(join17(rulesDir, "glassbox.md"), content);
32121
+ return updateFile(join21(rulesDir, "glassbox.md"), content);
30649
32122
  }
30650
32123
  function ensureSkills() {
30651
32124
  const cwd = process.cwd();
30652
32125
  const platforms = [];
30653
- if (existsSync14(join17(cwd, ".claude"))) {
32126
+ if (existsSync17(join21(cwd, ".claude"))) {
30654
32127
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
30655
32128
  }
30656
- if (existsSync14(join17(cwd, ".cursor"))) {
32129
+ if (existsSync17(join21(cwd, ".cursor"))) {
30657
32130
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
30658
32131
  }
30659
- if (existsSync14(join17(cwd, ".github", "prompts")) || existsSync14(join17(cwd, ".github", "copilot-instructions.md"))) {
32132
+ if (existsSync17(join21(cwd, ".github", "prompts")) || existsSync17(join21(cwd, ".github", "copilot-instructions.md"))) {
30660
32133
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
30661
32134
  }
30662
- if (existsSync14(join17(cwd, ".windsurf"))) {
32135
+ if (existsSync17(join21(cwd, ".windsurf"))) {
30663
32136
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
30664
32137
  }
30665
32138
  return platforms;
@@ -30667,19 +32140,19 @@ function ensureSkills() {
30667
32140
 
30668
32141
  // src/update-check.ts
30669
32142
  init_zod();
30670
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
32143
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
30671
32144
  import { get } from "https";
30672
32145
  import { homedir as homedir2 } from "os";
30673
- import { dirname as dirname8, join as join18 } from "path";
30674
- import { fileURLToPath as fileURLToPath3 } from "url";
30675
- var DATA_DIR = join18(homedir2(), ".glassbox");
30676
- var CHECK_FILE = join18(DATA_DIR, "last-update-check");
32146
+ import { dirname as dirname9, join as join22 } from "path";
32147
+ import { fileURLToPath as fileURLToPath4 } from "url";
32148
+ var DATA_DIR = join22(homedir2(), ".glassbox");
32149
+ var CHECK_FILE = join22(DATA_DIR, "last-update-check");
30677
32150
  var PACKAGE_NAME = "glassbox";
30678
32151
  var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
30679
32152
  function getCurrentVersion() {
30680
32153
  try {
30681
- const dir = dirname8(fileURLToPath3(import.meta.url));
30682
- const raw2 = JSON.parse(readFileSync19(join18(dir, "..", "package.json"), "utf-8"));
32154
+ const dir = dirname9(fileURLToPath4(import.meta.url));
32155
+ const raw2 = JSON.parse(readFileSync21(join22(dir, "..", "package.json"), "utf-8"));
30683
32156
  return VersionPayloadSchema.parse(raw2).version;
30684
32157
  } catch {
30685
32158
  return "0.0.0";
@@ -30687,16 +32160,16 @@ function getCurrentVersion() {
30687
32160
  }
30688
32161
  function getLastCheckDate() {
30689
32162
  try {
30690
- if (existsSync15(CHECK_FILE)) {
30691
- return readFileSync19(CHECK_FILE, "utf-8").trim();
32163
+ if (existsSync18(CHECK_FILE)) {
32164
+ return readFileSync21(CHECK_FILE, "utf-8").trim();
30692
32165
  }
30693
32166
  } catch {
30694
32167
  }
30695
32168
  return null;
30696
32169
  }
30697
32170
  function saveCheckDate() {
30698
- mkdirSync13(DATA_DIR, { recursive: true });
30699
- writeFileSync13(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
32171
+ mkdirSync15(DATA_DIR, { recursive: true });
32172
+ writeFileSync15(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
30700
32173
  }
30701
32174
  function isFirstUseToday() {
30702
32175
  const last = getLastCheckDate();
@@ -30746,7 +32219,7 @@ function detectUpgradeCommand() {
30746
32219
  }
30747
32220
  return `npm update -g ${PACKAGE_NAME}`;
30748
32221
  }
30749
- function compareVersions(current, latest) {
32222
+ function compareVersions2(current, latest) {
30750
32223
  const a = current.split(".").map(Number);
30751
32224
  const b = latest.split(".").map(Number);
30752
32225
  for (let i = 0; i < 3; i++) {
@@ -30760,7 +32233,7 @@ async function checkForUpdates(force) {
30760
32233
  const current = getCurrentVersion();
30761
32234
  const latest = await fetchLatestVersion();
30762
32235
  saveCheckDate();
30763
- if (latest === null || compareVersions(current, latest) >= 0) return;
32236
+ if (latest === null || compareVersions2(current, latest) >= 0) return;
30764
32237
  const cmd = detectUpgradeCommand();
30765
32238
  const updateLine = `Update available: ${current} \u2192 ${latest}`;
30766
32239
  const cmdLine = `Run: ${cmd}`;
@@ -31001,23 +32474,23 @@ async function main() {
31001
32474
  console.log("AI service test mode enabled \u2014 using mock AI responses");
31002
32475
  }
31003
32476
  if (debug) {
31004
- console.log(`[debug] Build timestamp: ${"2026-07-02T05:52:03.288Z"}`);
32477
+ console.log(`[debug] Build timestamp: ${"2026-07-22T04:50:30.153Z"}`);
31005
32478
  }
31006
32479
  if (projectDir !== null) {
31007
- if (!existsSync17(projectDir) || !statSync6(projectDir).isDirectory()) {
32480
+ if (!existsSync20(projectDir) || !statSync8(projectDir).isDirectory()) {
31008
32481
  console.error(`--project-dir is not a directory: ${projectDir}`);
31009
32482
  process.exit(1);
31010
32483
  }
31011
32484
  process.chdir(projectDir);
31012
32485
  }
31013
32486
  if (dataDir === null) {
31014
- dataDir = join20(process.cwd(), ".glassbox");
32487
+ dataDir = join24(process.cwd(), ".glassbox");
31015
32488
  }
31016
32489
  if (difftoolServe) {
31017
32490
  const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
31018
32491
  const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
31019
32492
  const { clearImageBlobs: clearImageBlobs2 } = await Promise.resolve().then(() => (init_image_blobs(), image_blobs_exports));
31020
- mkdirSync15(dataDir, { recursive: true });
32493
+ mkdirSync17(dataDir, { recursive: true });
31021
32494
  setDataDir(dataDir);
31022
32495
  ensureGlassboxGitignored(dataDir);
31023
32496
  const sessionDataDir = dataDir;
@@ -31053,13 +32526,13 @@ async function main() {
31053
32526
  }
31054
32527
  process.exit(1);
31055
32528
  }
31056
- dataDir = join20(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
32529
+ dataDir = join24(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
31057
32530
  setDemoMode(demo);
31058
32531
  console.log(`
31059
32532
  DEMO MODE: ${scenario.label}
31060
32533
  `);
31061
32534
  }
31062
- mkdirSync15(dataDir, { recursive: true });
32535
+ mkdirSync17(dataDir, { recursive: true });
31063
32536
  if (demo === null) {
31064
32537
  acquireLock(dataDir);
31065
32538
  }
@@ -31084,12 +32557,12 @@ async function main() {
31084
32557
  if (mode.type === "diff") {
31085
32558
  const { pathA, pathB } = mode;
31086
32559
  for (const p of [pathA, pathB]) {
31087
- if (!existsSync17(p)) {
32560
+ if (!existsSync20(p)) {
31088
32561
  console.error(`Error: path does not exist: ${p}`);
31089
32562
  process.exit(1);
31090
32563
  }
31091
32564
  }
31092
- if (statSync6(pathA).isDirectory() !== statSync6(pathB).isDirectory()) {
32565
+ if (statSync8(pathA).isDirectory() !== statSync8(pathB).isDirectory()) {
31093
32566
  console.error("Error: --diff requires two files or two folders, not a mix of both.");
31094
32567
  process.exit(1);
31095
32568
  }
@@ -31106,17 +32579,19 @@ async function main() {
31106
32579
  }
31107
32580
  for (const entry of comparisons) {
31108
32581
  for (const [role, p] of [["actual", entry.actualPath], ["expected", entry.expectedPath]]) {
31109
- if (!existsSync17(p)) {
32582
+ if (!existsSync20(p)) {
31110
32583
  console.error(`Error: ${role} image does not exist: ${p}`);
31111
32584
  process.exit(1);
31112
32585
  }
31113
32586
  }
31114
32587
  }
32588
+ const { initContentPlugins: initContentPlugins2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
32589
+ await initContentPlugins2(cwd);
31115
32590
  const { comparePerceptual: comparePerceptual2 } = await Promise.resolve().then(() => (init_perceptual_diff(), perceptual_diff_exports));
31116
32591
  let identical = 0;
31117
32592
  let undecodable = 0;
31118
32593
  for (const entry of comparisons) {
31119
- const result = comparePerceptual2(entry.actualPath, entry.expectedPath);
32594
+ const result = await comparePerceptual2(entry.actualPath, entry.expectedPath);
31120
32595
  groundTruthScores.set(entry.key, result.score);
31121
32596
  if (result.reason === "undecodable") undecodable++;
31122
32597
  else if (result.score === 0) identical++;