@youtyan/code-viewer 0.2.6 → 0.2.8

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.
Files changed (3) hide show
  1. package/dist/code-viewer.js +819 -251
  2. package/package.json +1 -1
  3. package/web/app.js +460 -225
@@ -17,14 +17,97 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
+ // web-src/server/json-store.ts
21
+ import { randomBytes } from "node:crypto";
22
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
23
+ import { dirname } from "node:path";
24
+ function isEnoent(err) {
25
+ return err?.code === "ENOENT";
26
+ }
27
+ function tmpPath(file) {
28
+ return `${file}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
29
+ }
30
+ async function backupInvalidFile(file, suffix) {
31
+ try {
32
+ await rename(file, `${file}.${suffix}-${Date.now()}`);
33
+ } catch {}
34
+ }
35
+ function createJsonFileStore(options) {
36
+ const queues = new Map;
37
+ const backupSuffix = options.backupSuffix ?? "corrupt";
38
+ const serialize = options.serialize ?? ((state) => `${JSON.stringify(state, null, 2)}
39
+ `);
40
+ async function loadUnqueued(root) {
41
+ const file = options.filePath(root);
42
+ let raw;
43
+ try {
44
+ raw = await readFile(file, "utf8");
45
+ } catch (err) {
46
+ if (isEnoent(err))
47
+ return options.empty();
48
+ throw err;
49
+ }
50
+ try {
51
+ return options.sanitize(JSON.parse(raw));
52
+ } catch {
53
+ await backupInvalidFile(file, backupSuffix);
54
+ return options.empty();
55
+ }
56
+ }
57
+ async function saveUnqueued(root, state) {
58
+ const file = options.filePath(root);
59
+ const normalized = options.sanitize(state);
60
+ const content = serialize(normalized);
61
+ if (options.maxBytes !== undefined && Buffer.byteLength(content, "utf8") > options.maxBytes) {
62
+ throw new Error(options.sizeErrorMessage ?? "JSON state too large");
63
+ }
64
+ await mkdir(dirname(file), { recursive: true });
65
+ const tmp = tmpPath(file);
66
+ await writeFile(tmp, content, "utf8");
67
+ await rename(tmp, file);
68
+ }
69
+ async function load(root) {
70
+ const pendingWrite = queues.get(options.filePath(root));
71
+ if (pendingWrite)
72
+ await pendingWrite.catch(() => {});
73
+ return loadUnqueued(root);
74
+ }
75
+ async function save(root, state) {
76
+ const file = options.filePath(root);
77
+ const previous = queues.get(file) ?? Promise.resolve();
78
+ const run = previous.catch(() => {}).then(() => saveUnqueued(root, state));
79
+ const queued = run.then(() => {}, () => {});
80
+ queues.set(file, queued);
81
+ try {
82
+ await run;
83
+ } finally {
84
+ if (queues.get(file) === queued)
85
+ queues.delete(file);
86
+ }
87
+ }
88
+ async function update(root, updater) {
89
+ const file = options.filePath(root);
90
+ const previous = queues.get(file) ?? Promise.resolve();
91
+ const run = previous.catch(() => {}).then(async () => {
92
+ const current = await loadUnqueued(root);
93
+ const updated = await updater(current);
94
+ await saveUnqueued(root, updated.state);
95
+ return updated.result;
96
+ });
97
+ const queued = run.then(() => {}, () => {});
98
+ queues.set(file, queued);
99
+ try {
100
+ return await run;
101
+ } finally {
102
+ if (queues.get(file) === queued)
103
+ queues.delete(file);
104
+ }
105
+ }
106
+ return { load, save, update };
107
+ }
108
+ var init_json_store = () => {};
109
+
20
110
  // web-src/server/annotations.ts
21
- import {
22
- existsSync,
23
- mkdirSync,
24
- readFileSync,
25
- renameSync,
26
- writeFileSync
27
- } from "node:fs";
28
111
  import { join } from "node:path";
29
112
  function annotationsFilePath(root) {
30
113
  return join(root, CODE_VIEWER_DIR, ANNOTATIONS_FILE_NAME);
@@ -242,24 +325,11 @@ function normalizeAnnotationsState(raw) {
242
325
  sessions: sessions.map(normalizeSession).filter((session) => session !== null)
243
326
  };
244
327
  }
245
- function loadAnnotationsState(root) {
246
- const file = annotationsFilePath(root);
247
- if (!existsSync(file))
248
- return emptyAnnotationsState();
249
- try {
250
- return normalizeAnnotationsState(JSON.parse(readFileSync(file, "utf8")));
251
- } catch {
252
- return emptyAnnotationsState();
253
- }
328
+ async function loadAnnotationsState(root) {
329
+ return annotationsStore.load(root);
254
330
  }
255
- function saveAnnotationsState(root, state) {
256
- const dir = join(root, CODE_VIEWER_DIR);
257
- mkdirSync(dir, { recursive: true });
258
- const file = annotationsFilePath(root);
259
- const tmp = `${file}.tmp-${process.pid}`;
260
- writeFileSync(tmp, `${JSON.stringify(state, null, 2)}
261
- `, "utf8");
262
- renameSync(tmp, file);
331
+ async function saveAnnotationsState(root, state) {
332
+ return annotationsStore.save(root, state);
263
333
  }
264
334
  function startAnnotationSession(state, title, now, id = makeAnnotationId("s")) {
265
335
  const session = {
@@ -484,9 +554,18 @@ function deleteAnnotationById(state, id) {
484
554
  }
485
555
  return { state, removed: null };
486
556
  }
487
- var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300;
557
+ var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300, MAX_ANNOTATIONS_JSON_BYTES = 5000000, annotationsStore;
488
558
  var init_annotations = __esm(() => {
559
+ init_json_store();
489
560
  ANNOTATION_BODY_MAX_BYTES = 64 * 1024;
561
+ annotationsStore = createJsonFileStore({
562
+ filePath: annotationsFilePath,
563
+ empty: emptyAnnotationsState,
564
+ sanitize: normalizeAnnotationsState,
565
+ maxBytes: MAX_ANNOTATIONS_JSON_BYTES,
566
+ backupSuffix: "corrupt",
567
+ sizeErrorMessage: "annotations state too large"
568
+ });
490
569
  });
491
570
 
492
571
  // web-src/server/runtime.ts
@@ -673,10 +752,10 @@ var init_runtime = () => {};
673
752
 
674
753
  // web-src/server/git.ts
675
754
  import {
676
- existsSync as existsSync2,
755
+ existsSync,
677
756
  lstatSync,
678
757
  readdirSync,
679
- readFileSync as readFileSync2,
758
+ readFileSync,
680
759
  statSync
681
760
  } from "node:fs";
682
761
  import { join as join2 } from "node:path";
@@ -1120,7 +1199,7 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
1120
1199
  return omitDirNames.has(name) ? "heavy" : undefined;
1121
1200
  }
1122
1201
  function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
1123
- if (excludeNames.has(name.toLowerCase()) || isToolInternalPath(name))
1202
+ if (excludeNames.has(name.toLowerCase()))
1124
1203
  return {
1125
1204
  name,
1126
1205
  path: "",
@@ -1182,7 +1261,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
1182
1261
  return;
1183
1262
  }
1184
1263
  for (const entry of entries) {
1185
- if (excludeNameSet.has(entry.name.toLowerCase()) || isToolInternalPath(entry.name))
1264
+ if (excludeNameSet.has(entry.name.toLowerCase()))
1186
1265
  continue;
1187
1266
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1188
1267
  const full = join2(dir, entry.name);
@@ -1287,12 +1366,12 @@ function untrackedMeta(cwd) {
1287
1366
  let lines = 0;
1288
1367
  let fileExists = false;
1289
1368
  try {
1290
- fileExists = existsSync2(full) && statSync(full).isFile();
1369
+ fileExists = existsSync(full) && statSync(full).isFile();
1291
1370
  } catch {
1292
1371
  fileExists = false;
1293
1372
  }
1294
1373
  if (fileExists) {
1295
- const data = readFileSync2(full);
1374
+ const data = readFileSync(full);
1296
1375
  const probe = data.subarray(0, 8192);
1297
1376
  binary = probe.includes(0);
1298
1377
  if (!binary)
@@ -1471,11 +1550,11 @@ var init_git = __esm(() => {
1471
1550
  // web-src/server/server-registry.ts
1472
1551
  import { createHash } from "node:crypto";
1473
1552
  import {
1474
- existsSync as existsSync3,
1475
- mkdirSync as mkdirSync2,
1476
- readFileSync as readFileSync3,
1553
+ existsSync as existsSync2,
1554
+ mkdirSync,
1555
+ readFileSync as readFileSync2,
1477
1556
  unlinkSync,
1478
- writeFileSync as writeFileSync2
1557
+ writeFileSync
1479
1558
  } from "node:fs";
1480
1559
  import { homedir } from "node:os";
1481
1560
  import { join as join3 } from "node:path";
@@ -1488,17 +1567,17 @@ function serverRegistryFilePath(root) {
1488
1567
  }
1489
1568
  function writeServerRegistry(entry) {
1490
1569
  try {
1491
- mkdirSync2(registryDir(), { recursive: true });
1492
- writeFileSync2(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
1570
+ mkdirSync(registryDir(), { recursive: true });
1571
+ writeFileSync(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
1493
1572
  `, "utf8");
1494
1573
  } catch {}
1495
1574
  }
1496
1575
  function readServerRegistry(root) {
1497
1576
  const file = serverRegistryFilePath(root);
1498
- if (!existsSync3(file))
1577
+ if (!existsSync2(file))
1499
1578
  return null;
1500
1579
  try {
1501
- const raw = JSON.parse(readFileSync3(file, "utf8"));
1580
+ const raw = JSON.parse(readFileSync2(file, "utf8"));
1502
1581
  if (!raw || typeof raw !== "object")
1503
1582
  return null;
1504
1583
  const entry = raw;
@@ -1532,7 +1611,7 @@ __export(exports_annotate_cli, {
1532
1611
  ANNOTATE_HELP: () => ANNOTATE_HELP,
1533
1612
  ANNOTATE_AGENT_HELP: () => ANNOTATE_AGENT_HELP
1534
1613
  });
1535
- import { readFileSync as readFileSync4, realpathSync } from "node:fs";
1614
+ import { readFileSync as readFileSync3, realpathSync } from "node:fs";
1536
1615
  function takeValue(argv, index, flag) {
1537
1616
  const value = argv[index + 1];
1538
1617
  if (value === undefined)
@@ -1961,7 +2040,7 @@ async function annotationBodyFromCommand(command) {
1961
2040
  let body = command.body;
1962
2041
  if (body === undefined && command.bodyFile !== undefined) {
1963
2042
  try {
1964
- body = readFileSync4(command.bodyFile, "utf8");
2043
+ body = readFileSync3(command.bodyFile, "utf8");
1965
2044
  } catch {
1966
2045
  console.error(`could not read --body-file: ${command.bodyFile}`);
1967
2046
  process.exit(1);
@@ -2029,7 +2108,7 @@ async function runAnnotateCli(argv) {
2029
2108
  let sql = command.sql;
2030
2109
  if (sql === undefined && command.sqlFile !== undefined) {
2031
2110
  try {
2032
- sql = readFileSync4(command.sqlFile, "utf8");
2111
+ sql = readFileSync3(command.sqlFile, "utf8");
2033
2112
  } catch {
2034
2113
  console.error(`could not read --sql-file: ${command.sqlFile}`);
2035
2114
  process.exit(1);
@@ -2098,7 +2177,7 @@ async function runAnnotateCli(argv) {
2098
2177
  if (command.kind === "edit") {
2099
2178
  let bodyText = command.body;
2100
2179
  if (command.bodyFile !== undefined)
2101
- bodyText = readFileSync4(command.bodyFile, "utf8");
2180
+ bodyText = readFileSync3(command.bodyFile, "utf8");
2102
2181
  if (bodyText === undefined) {
2103
2182
  const stdin = await readStdin();
2104
2183
  if (stdin.trim())
@@ -2656,16 +2735,16 @@ var init_query_cli = __esm(() => {
2656
2735
  });
2657
2736
 
2658
2737
  // web-src/server/root.ts
2659
- import { existsSync as existsSync4 } from "node:fs";
2660
- import { dirname, join as join4, normalize } from "node:path";
2738
+ import { existsSync as existsSync3 } from "node:fs";
2739
+ import { dirname as dirname2, join as join4, normalize } from "node:path";
2661
2740
  import { fileURLToPath } from "node:url";
2662
2741
  function findRoot(start) {
2663
2742
  let current = start;
2664
2743
  for (let i = 0;i < 5; i++) {
2665
- if (existsSync4(join4(current, "package.json")) && existsSync4(join4(current, "web"))) {
2744
+ if (existsSync3(join4(current, "package.json")) && existsSync3(join4(current, "web"))) {
2666
2745
  return normalize(current);
2667
2746
  }
2668
- const parent = dirname(current);
2747
+ const parent = dirname2(current);
2669
2748
  if (parent === current)
2670
2749
  break;
2671
2750
  current = parent;
@@ -2674,7 +2753,7 @@ function findRoot(start) {
2674
2753
  }
2675
2754
  var ROOT;
2676
2755
  var init_root = __esm(() => {
2677
- ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
2756
+ ROOT = findRoot(dirname2(fileURLToPath(import.meta.url)));
2678
2757
  });
2679
2758
 
2680
2759
  // web-src/server/skill-cli.ts
@@ -2686,7 +2765,7 @@ __export(exports_skill_cli, {
2686
2765
  SKILL_HELP: () => SKILL_HELP,
2687
2766
  AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
2688
2767
  });
2689
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
2768
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
2690
2769
  import { homedir as homedir2 } from "node:os";
2691
2770
  import { join as join5, resolve } from "node:path";
2692
2771
  function parseAgentList(value) {
@@ -2743,7 +2822,7 @@ function parseSkillArgs(argv) {
2743
2822
  return { ok: true, args: { kind: "install", agents, global, cwd } };
2744
2823
  }
2745
2824
  function installSkill(args, deps) {
2746
- if (!existsSync5(join5(deps.sourceDir, "SKILL.md"))) {
2825
+ if (!existsSync4(join5(deps.sourceDir, "SKILL.md"))) {
2747
2826
  return {
2748
2827
  ok: false,
2749
2828
  error: `bundled skill not found at ${deps.sourceDir}`
@@ -2753,9 +2832,9 @@ function installSkill(args, deps) {
2753
2832
  const results = [];
2754
2833
  for (const agent of args.agents) {
2755
2834
  const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
2756
- const action = existsSync5(target) ? "updated" : "installed";
2835
+ const action = existsSync4(target) ? "updated" : "installed";
2757
2836
  try {
2758
- mkdirSync3(target, { recursive: true });
2837
+ mkdirSync2(target, { recursive: true });
2759
2838
  cpSync(deps.sourceDir, target, { recursive: true });
2760
2839
  } catch (error) {
2761
2840
  return { ok: false, error: String(error) };
@@ -3923,6 +4002,366 @@ function makeId(prefix) {
3923
4002
  return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
3924
4003
  }
3925
4004
 
4005
+ // web-src/server/state-store.ts
4006
+ import { join as join8 } from "node:path";
4007
+ function codeViewerPath(root, fileName) {
4008
+ return join8(root, CODE_VIEWER_DIR2, fileName);
4009
+ }
4010
+ function isRecord(value) {
4011
+ return !!value && typeof value === "object" && !Array.isArray(value);
4012
+ }
4013
+ function optionalString(value, maxLen) {
4014
+ if (typeof value !== "string")
4015
+ return;
4016
+ if (value.length === 0 || value.length > maxLen)
4017
+ return;
4018
+ if (value.includes("\x00"))
4019
+ return;
4020
+ return value;
4021
+ }
4022
+ function optionalBoolean(value) {
4023
+ return typeof value === "boolean" ? value : undefined;
4024
+ }
4025
+ function optionalNumber(value, min, max) {
4026
+ if (typeof value !== "number" || !Number.isFinite(value))
4027
+ return;
4028
+ return Math.max(min, Math.min(max, Math.round(value)));
4029
+ }
4030
+ function optionalFloat(value, min, max) {
4031
+ if (typeof value !== "number" || !Number.isFinite(value))
4032
+ return;
4033
+ return Math.max(min, Math.min(max, value));
4034
+ }
4035
+ function optionalFontSize(value) {
4036
+ return value === "compact" || value === "regular" || value === "large" || value === "xlarge" ? value : undefined;
4037
+ }
4038
+ function normalizeStringList(value, options) {
4039
+ if (!Array.isArray(value))
4040
+ return;
4041
+ const out = [];
4042
+ const seen = new Set;
4043
+ const items = options.keepLast ? [...value].reverse() : value;
4044
+ for (const item of items) {
4045
+ if (out.length >= options.maxItems)
4046
+ break;
4047
+ if (typeof item !== "string")
4048
+ continue;
4049
+ const name = item.trim();
4050
+ if (!name || name.length > options.maxLen || name.includes("\x00"))
4051
+ continue;
4052
+ if (options.pathSafe && (name.includes("/") || name.includes("\\") || name === "." || name === ".." || name === ".git")) {
4053
+ continue;
4054
+ }
4055
+ if (seen.has(name))
4056
+ continue;
4057
+ seen.add(name);
4058
+ out.push(name);
4059
+ }
4060
+ if (options.keepLast)
4061
+ out.reverse();
4062
+ return options.sort === false ? out : out.sort((a, b) => a.localeCompare(b));
4063
+ }
4064
+ function emptySettings() {
4065
+ return { version: 1 };
4066
+ }
4067
+ function emptyViewState() {
4068
+ return { version: 1, collapsedDirs: [], viewedFiles: [] };
4069
+ }
4070
+ function emptyDbUiState() {
4071
+ return { version: 1, columnWidths: {} };
4072
+ }
4073
+ function sanitizeSettings(raw) {
4074
+ if (!isRecord(raw))
4075
+ return emptySettings();
4076
+ const out = { version: 1 };
4077
+ if (raw.layout === "side-by-side" || raw.layout === "line-by-line")
4078
+ out.layout = raw.layout;
4079
+ if (raw.theme === "light" || raw.theme === "dark")
4080
+ out.theme = raw.theme;
4081
+ if (raw.language === "en" || raw.language === "ja")
4082
+ out.language = raw.language;
4083
+ if (raw.sidebarView === "tree" || raw.sidebarView === "flat")
4084
+ out.sidebarView = raw.sidebarView;
4085
+ const sidebarWidth = optionalNumber(raw.sidebarWidth, 180, 900);
4086
+ if (sidebarWidth !== undefined)
4087
+ out.sidebarWidth = sidebarWidth;
4088
+ const historyWidth = optionalNumber(raw.historyWidth, 220, 640);
4089
+ if (historyWidth !== undefined)
4090
+ out.historyWidth = historyWidth;
4091
+ const sidebarHidden = optionalBoolean(raw.sidebarHidden);
4092
+ if (sidebarHidden !== undefined)
4093
+ out.sidebarHidden = sidebarHidden;
4094
+ const sidebarFontSize = optionalFontSize(raw.sidebarFontSize);
4095
+ if (sidebarFontSize)
4096
+ out.sidebarFontSize = sidebarFontSize;
4097
+ const codeFontSize = optionalFontSize(raw.codeFontSize);
4098
+ if (codeFontSize)
4099
+ out.codeFontSize = codeFontSize;
4100
+ const syntaxHighlight = optionalBoolean(raw.syntaxHighlight);
4101
+ if (syntaxHighlight !== undefined)
4102
+ out.syntaxHighlight = syntaxHighlight;
4103
+ const autoUpdate = optionalBoolean(raw.autoUpdate);
4104
+ if (autoUpdate !== undefined)
4105
+ out.autoUpdate = autoUpdate;
4106
+ const queryHistoryPanelWidth = optionalNumber(raw.queryHistoryPanelWidth, 280, 800);
4107
+ if (queryHistoryPanelWidth !== undefined)
4108
+ out.queryHistoryPanelWidth = queryHistoryPanelWidth;
4109
+ const annotationPanelOpen = optionalBoolean(raw.annotationPanelOpen);
4110
+ if (annotationPanelOpen !== undefined)
4111
+ out.annotationPanelOpen = annotationPanelOpen;
4112
+ const annotationFollow = optionalBoolean(raw.annotationFollow);
4113
+ if (annotationFollow !== undefined)
4114
+ out.annotationFollow = annotationFollow;
4115
+ const annotationMuted = optionalBoolean(raw.annotationMuted);
4116
+ if (annotationMuted !== undefined)
4117
+ out.annotationMuted = annotationMuted;
4118
+ const annotationRate = optionalFloat(raw.annotationRate, 0.5, 2);
4119
+ if (annotationRate !== undefined)
4120
+ out.annotationRate = annotationRate;
4121
+ const ignoreWhitespace = optionalBoolean(raw.ignoreWhitespace);
4122
+ if (ignoreWhitespace !== undefined)
4123
+ out.ignoreWhitespace = ignoreWhitespace;
4124
+ const hideTests = optionalBoolean(raw.hideTests);
4125
+ if (hideTests !== undefined)
4126
+ out.hideTests = hideTests;
4127
+ const scopeOmitDirs = normalizeStringList(raw.scopeOmitDirs, {
4128
+ maxItems: 100,
4129
+ maxLen: 64,
4130
+ pathSafe: true
4131
+ });
4132
+ if (scopeOmitDirs)
4133
+ out.scopeOmitDirs = scopeOmitDirs;
4134
+ const scopeExcludeNames = normalizeStringList(raw.scopeExcludeNames, {
4135
+ maxItems: 200,
4136
+ maxLen: 128,
4137
+ pathSafe: true
4138
+ });
4139
+ if (scopeExcludeNames)
4140
+ out.scopeExcludeNames = scopeExcludeNames;
4141
+ if (isRecord(raw.range)) {
4142
+ const from = optionalString(raw.range.from, MAX_REF_LEN);
4143
+ const to = optionalString(raw.range.to, MAX_REF_LEN);
4144
+ if (from && to)
4145
+ out.range = { from, to };
4146
+ }
4147
+ return out;
4148
+ }
4149
+ function mergeSettings(current, patch) {
4150
+ if (!isRecord(patch))
4151
+ return current;
4152
+ const raw = { ...current };
4153
+ for (const [key, value] of Object.entries(patch)) {
4154
+ if (key === "version")
4155
+ continue;
4156
+ if (value === null)
4157
+ delete raw[key];
4158
+ else
4159
+ raw[key] = value;
4160
+ }
4161
+ return sanitizeSettings({ ...raw, version: 1 });
4162
+ }
4163
+ function sanitizeViewState(raw) {
4164
+ if (!isRecord(raw))
4165
+ return emptyViewState();
4166
+ return {
4167
+ version: 1,
4168
+ collapsedDirs: normalizeStringList(raw.collapsedDirs, {
4169
+ maxItems: MAX_VIEW_ITEMS,
4170
+ maxLen: MAX_KEY_LEN,
4171
+ keepLast: true,
4172
+ sort: false
4173
+ }) ?? [],
4174
+ viewedFiles: normalizeStringList(raw.viewedFiles, {
4175
+ maxItems: MAX_VIEW_ITEMS,
4176
+ maxLen: MAX_KEY_LEN,
4177
+ keepLast: true,
4178
+ sort: false
4179
+ }) ?? []
4180
+ };
4181
+ }
4182
+ function mergeViewState(current, patch) {
4183
+ if (!isRecord(patch))
4184
+ return current;
4185
+ const base = sanitizeViewState({ ...current, version: 1 });
4186
+ const collapsedDirs = new Set(base.collapsedDirs);
4187
+ const viewedFiles = new Set(base.viewedFiles);
4188
+ const addedCollapsedDirs = normalizeStringList(patch.addedCollapsedDirs, {
4189
+ maxItems: MAX_VIEW_ITEMS,
4190
+ maxLen: MAX_KEY_LEN,
4191
+ keepLast: true,
4192
+ sort: false
4193
+ });
4194
+ for (const path of addedCollapsedDirs || [])
4195
+ collapsedDirs.add(path);
4196
+ const removedCollapsedDirs = normalizeStringList(patch.removedCollapsedDirs, {
4197
+ maxItems: MAX_VIEW_ITEMS,
4198
+ maxLen: MAX_KEY_LEN,
4199
+ sort: false
4200
+ });
4201
+ for (const path of removedCollapsedDirs || [])
4202
+ collapsedDirs.delete(path);
4203
+ const addedViewedFiles = normalizeStringList(patch.addedViewedFiles, {
4204
+ maxItems: MAX_VIEW_ITEMS,
4205
+ maxLen: MAX_KEY_LEN,
4206
+ keepLast: true,
4207
+ sort: false
4208
+ });
4209
+ for (const path of addedViewedFiles || [])
4210
+ viewedFiles.add(path);
4211
+ const removedViewedFiles = normalizeStringList(patch.removedViewedFiles, {
4212
+ maxItems: MAX_VIEW_ITEMS,
4213
+ maxLen: MAX_KEY_LEN,
4214
+ sort: false
4215
+ });
4216
+ for (const path of removedViewedFiles || [])
4217
+ viewedFiles.delete(path);
4218
+ return sanitizeViewState({
4219
+ version: 1,
4220
+ collapsedDirs: [...collapsedDirs],
4221
+ viewedFiles: [...viewedFiles]
4222
+ });
4223
+ }
4224
+ function safeObjectKey(value) {
4225
+ if (!value || value.length > MAX_KEY_LEN || value.includes("\x00"))
4226
+ return null;
4227
+ return value;
4228
+ }
4229
+ function sanitizeDbUiState(raw) {
4230
+ if (!isRecord(raw) || !isRecord(raw.columnWidths))
4231
+ return emptyDbUiState();
4232
+ const columnWidths = {};
4233
+ let dbCount = 0;
4234
+ for (const [dbIdRaw, tablesRaw] of Object.entries(raw.columnWidths)) {
4235
+ if (dbCount >= MAX_DB_UI_DBS)
4236
+ break;
4237
+ const dbId = safeObjectKey(dbIdRaw);
4238
+ if (!dbId || !isRecord(tablesRaw))
4239
+ continue;
4240
+ const tables = {};
4241
+ let tableCount = 0;
4242
+ for (const [tableRaw, columnsRaw] of Object.entries(tablesRaw)) {
4243
+ if (tableCount >= MAX_DB_UI_TABLES)
4244
+ break;
4245
+ const table = safeObjectKey(tableRaw);
4246
+ if (!table || !isRecord(columnsRaw))
4247
+ continue;
4248
+ const columns = {};
4249
+ let columnCount = 0;
4250
+ for (const [columnRaw, widthRaw] of Object.entries(columnsRaw)) {
4251
+ if (columnCount >= MAX_DB_UI_COLUMNS)
4252
+ break;
4253
+ const column = safeObjectKey(columnRaw);
4254
+ const width = optionalNumber(widthRaw, 60, 1200);
4255
+ if (!column || width === undefined)
4256
+ continue;
4257
+ columns[column] = width;
4258
+ columnCount++;
4259
+ }
4260
+ if (Object.keys(columns).length === 0)
4261
+ continue;
4262
+ tables[table] = columns;
4263
+ tableCount++;
4264
+ }
4265
+ if (Object.keys(tables).length === 0)
4266
+ continue;
4267
+ columnWidths[dbId] = tables;
4268
+ dbCount++;
4269
+ }
4270
+ return { version: 1, columnWidths };
4271
+ }
4272
+ function mergeDbUiState(current, patch) {
4273
+ if (!isRecord(patch))
4274
+ return current;
4275
+ if (!isRecord(patch.columnWidths)) {
4276
+ return sanitizeDbUiState({ ...current, ...patch, version: 1 });
4277
+ }
4278
+ const columnWidths = {
4279
+ ...current.columnWidths
4280
+ };
4281
+ for (const [dbId, tablesRaw] of Object.entries(patch.columnWidths)) {
4282
+ if (tablesRaw === null) {
4283
+ delete columnWidths[dbId];
4284
+ continue;
4285
+ }
4286
+ if (!isRecord(tablesRaw))
4287
+ continue;
4288
+ const tables = { ...columnWidths[dbId] || {} };
4289
+ for (const [table, columnsRaw] of Object.entries(tablesRaw)) {
4290
+ if (columnsRaw === null) {
4291
+ delete tables[table];
4292
+ continue;
4293
+ }
4294
+ if (!isRecord(columnsRaw))
4295
+ continue;
4296
+ const columns = { ...tables[table] || {} };
4297
+ for (const [column, widthRaw] of Object.entries(columnsRaw)) {
4298
+ if (widthRaw === null)
4299
+ delete columns[column];
4300
+ else
4301
+ columns[column] = widthRaw;
4302
+ }
4303
+ tables[table] = columns;
4304
+ }
4305
+ columnWidths[dbId] = tables;
4306
+ }
4307
+ return sanitizeDbUiState({ ...current, ...patch, columnWidths, version: 1 });
4308
+ }
4309
+ async function loadAppSettingsState(root) {
4310
+ return settingsStore.load(root);
4311
+ }
4312
+ async function patchAppSettingsState(root, patch) {
4313
+ return settingsStore.update(root, (state) => {
4314
+ const next = mergeSettings(state, patch);
4315
+ return { state: next, result: next };
4316
+ });
4317
+ }
4318
+ async function loadViewState(root) {
4319
+ return viewStateStore.load(root);
4320
+ }
4321
+ async function patchViewState(root, patch) {
4322
+ return viewStateStore.update(root, (state) => {
4323
+ const next = mergeViewState(state, patch);
4324
+ return { state: next, result: next };
4325
+ });
4326
+ }
4327
+ async function loadDbUiState(root) {
4328
+ return dbUiStore.load(root);
4329
+ }
4330
+ async function patchDbUiState(root, patch) {
4331
+ return dbUiStore.update(root, (state) => {
4332
+ const next = mergeDbUiState(state, patch);
4333
+ return { state: next, result: next };
4334
+ });
4335
+ }
4336
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, settingsStore, viewStateStore, dbUiStore;
4337
+ var init_state_store = __esm(() => {
4338
+ init_json_store();
4339
+ settingsStore = createJsonFileStore({
4340
+ filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
4341
+ empty: emptySettings,
4342
+ sanitize: sanitizeSettings,
4343
+ maxBytes: MAX_SETTINGS_BYTES,
4344
+ backupSuffix: "corrupt",
4345
+ sizeErrorMessage: "settings state too large"
4346
+ });
4347
+ viewStateStore = createJsonFileStore({
4348
+ filePath: (root) => codeViewerPath(root, VIEW_STATE_FILE_NAME),
4349
+ empty: emptyViewState,
4350
+ sanitize: sanitizeViewState,
4351
+ maxBytes: MAX_VIEW_STATE_BYTES,
4352
+ backupSuffix: "corrupt",
4353
+ sizeErrorMessage: "view state too large"
4354
+ });
4355
+ dbUiStore = createJsonFileStore({
4356
+ filePath: (root) => codeViewerPath(root, DB_UI_FILE_NAME),
4357
+ empty: emptyDbUiState,
4358
+ sanitize: sanitizeDbUiState,
4359
+ maxBytes: MAX_DB_UI_BYTES,
4360
+ backupSuffix: "corrupt",
4361
+ sizeErrorMessage: "db UI state too large"
4362
+ });
4363
+ });
4364
+
3926
4365
  // web-src/server/database/adapters/abort.ts
3927
4366
  function abortError(message = "operation aborted") {
3928
4367
  const err = new Error(message);
@@ -4676,6 +5115,10 @@ function createTableMetaCache(now = () => Date.now()) {
4676
5115
  }
4677
5116
  };
4678
5117
  }
5118
+ function observeBackgroundRejection(promise) {
5119
+ promise.catch(() => {});
5120
+ return promise;
5121
+ }
4679
5122
  function createDockerAdapter(config) {
4680
5123
  async function execAsync(sql, signal) {
4681
5124
  const result = await execInContainerAsync(config, sql, 1e4, signal);
@@ -4947,7 +5390,7 @@ function createDockerAdapter(config) {
4947
5390
  const id = tableIdentifier(table);
4948
5391
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}`;
4949
5392
  const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
4950
- const totalRowsPromise = tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql, signal)));
5393
+ const totalRowsPromise = observeBackgroundRejection(tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql, signal))));
4951
5394
  let columns;
4952
5395
  try {
4953
5396
  columns = await columnsPromise;
@@ -5626,14 +6069,14 @@ var init_connection_pool = __esm(() => {
5626
6069
  // web-src/server/database/discovery.ts
5627
6070
  import {
5628
6071
  closeSync,
5629
- existsSync as existsSync6,
6072
+ existsSync as existsSync5,
5630
6073
  openSync,
5631
6074
  readSync,
5632
6075
  realpathSync as realpathSync3,
5633
6076
  statSync as statSync2
5634
6077
  } from "node:fs";
5635
- import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
5636
- import { basename as basename2, join as join8, relative as relative2 } from "node:path";
6078
+ import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
6079
+ import { basename as basename2, join as join9, relative as relative2 } from "node:path";
5637
6080
  function isSqliteFile(fullPath) {
5638
6081
  try {
5639
6082
  const stat2 = statSync2(fullPath);
@@ -5704,7 +6147,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
5704
6147
  return;
5705
6148
  if (omitSet.has(entry.toLowerCase()))
5706
6149
  continue;
5707
- const full = join8(dir, entry);
6150
+ const full = join9(dir, entry);
5708
6151
  let entryStat;
5709
6152
  try {
5710
6153
  entryStat = await lstat(full);
@@ -5748,8 +6191,8 @@ function validateDbPath(cwd, dbPath) {
5748
6191
  const parts = dbPath.split(/[\\/]+/);
5749
6192
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
5750
6193
  return null;
5751
- const full = join8(cwd, dbPath);
5752
- if (!existsSync6(full))
6194
+ const full = join9(cwd, dbPath);
6195
+ if (!existsSync5(full))
5753
6196
  return null;
5754
6197
  let realCwd;
5755
6198
  let realFull;
@@ -5885,7 +6328,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
5885
6328
  }
5886
6329
  async function readDotenvAsync(composeDir) {
5887
6330
  try {
5888
- const content = await readFile(join8(composeDir, ".env"), "utf-8");
6331
+ const content = await readFile2(join9(composeDir, ".env"), "utf-8");
5889
6332
  return parseDotenvContent(content);
5890
6333
  } catch {
5891
6334
  return {};
@@ -6059,7 +6502,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
6059
6502
  async function parseComposeFileAsync(filepath, composeDir, cwd, results) {
6060
6503
  let content;
6061
6504
  try {
6062
- content = await readFile(filepath, "utf-8");
6505
+ content = await readFile2(filepath, "utf-8");
6063
6506
  } catch {
6064
6507
  return;
6065
6508
  }
@@ -6105,7 +6548,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6105
6548
  if (depth > MAX_SCAN_DEPTH)
6106
6549
  return;
6107
6550
  for (const filename of COMPOSE_FILENAMES) {
6108
- const filepath = join8(dir, filename);
6551
+ const filepath = join9(dir, filename);
6109
6552
  if (await pathExistsAsync(filepath)) {
6110
6553
  await parseComposeFileAsync(filepath, dir, cwd, results);
6111
6554
  break;
@@ -6128,7 +6571,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6128
6571
  return;
6129
6572
  if (omitSet.has(entry.toLowerCase()))
6130
6573
  continue;
6131
- const full = join8(dir, entry);
6574
+ const full = join9(dir, entry);
6132
6575
  let entryStat;
6133
6576
  try {
6134
6577
  entryStat = await lstat(full);
@@ -6763,6 +7206,32 @@ function textError(message, status) {
6763
7206
  }
6764
7207
  });
6765
7208
  }
7209
+ async function jsonLoadResponse(load, logPrefix, errorMessage) {
7210
+ try {
7211
+ return json(await load());
7212
+ } catch (err) {
7213
+ console.error(`[code-viewer] ${logPrefix} error:`, err);
7214
+ return textError(errorMessage, 500);
7215
+ }
7216
+ }
7217
+ async function parseBoundedJsonBody(req, maxBytes, tooLargeMessage) {
7218
+ const contentType = req.headers.get("content-type") || "";
7219
+ if (!contentType.toLowerCase().startsWith("application/json")) {
7220
+ return textError("unsupported media type", 415);
7221
+ }
7222
+ const contentLength = Number(req.headers.get("content-length") || "0");
7223
+ if (contentLength > maxBytes)
7224
+ return textError(tooLargeMessage, 413);
7225
+ try {
7226
+ const raw = await req.text();
7227
+ if (Buffer.byteLength(raw, "utf8") > maxBytes) {
7228
+ return textError(tooLargeMessage, 413);
7229
+ }
7230
+ return JSON.parse(raw);
7231
+ } catch {
7232
+ return textError("invalid JSON body", 400);
7233
+ }
7234
+ }
6766
7235
  function waitForCallerAbort(promise, signal, message) {
6767
7236
  if (!signal)
6768
7237
  return promise;
@@ -6794,6 +7263,10 @@ function waitForCallerAbort(promise, signal, message) {
6794
7263
  });
6795
7264
  });
6796
7265
  }
7266
+ function isFilesystemAccessError(err) {
7267
+ const code = err?.code;
7268
+ return code === "EACCES" || code === "EBUSY" || code === "EIO" || code === "EISDIR" || code === "ENOSPC" || code === "ENOTDIR" || code === "EPERM" || code === "EROFS";
7269
+ }
6797
7270
  async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
6798
7271
  if (!dbParam)
6799
7272
  return textError("missing db parameter", 400);
@@ -6860,6 +7333,9 @@ function handleError(prefix, action, err) {
6860
7333
  if (isDockerComposeServiceUnavailableError(err)) {
6861
7334
  return textError(message, err.status);
6862
7335
  }
7336
+ if (isFilesystemAccessError(err)) {
7337
+ return textError(`failed to ${action}`, 500);
7338
+ }
6863
7339
  return textError(`failed to ${action}: ${message}`, 500);
6864
7340
  }
6865
7341
  var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
@@ -8657,13 +9133,9 @@ var init_handle_s3 = __esm(() => {
8657
9133
  });
8658
9134
 
8659
9135
  // web-src/server/database/query-history.ts
8660
- import { mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
8661
- import { join as join9 } from "node:path";
9136
+ import { join as join10 } from "node:path";
8662
9137
  function historyFilePath(root) {
8663
- return join9(root, CODE_VIEWER_DIR2, HISTORY_FILE_NAME);
8664
- }
8665
- function isEnoent(err) {
8666
- return err?.code === "ENOENT";
9138
+ return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
8667
9139
  }
8668
9140
  function emptyState() {
8669
9141
  return { version: 1, entries: [] };
@@ -8682,69 +9154,86 @@ function serializeHistoryState(state) {
8682
9154
  }
8683
9155
  return content;
8684
9156
  }
8685
- async function backupCorruptHistoryFileAsync(file) {
8686
- try {
8687
- await rename(file, `${file}.corrupt-${Date.now()}`);
8688
- } catch {}
9157
+ function optionalString2(value, maxLen) {
9158
+ if (typeof value !== "string")
9159
+ return;
9160
+ if (!value || value.length > maxLen || value.includes("\x00"))
9161
+ return;
9162
+ return value;
8689
9163
  }
8690
- async function loadQueryHistoryAsync(cwd) {
8691
- const pendingWrite = historyWriteQueues.get(cwd);
8692
- if (pendingWrite) {
8693
- await pendingWrite.catch(() => {});
9164
+ function finiteNumber(value, min = 0) {
9165
+ if (typeof value !== "number" || !Number.isFinite(value))
9166
+ return;
9167
+ return Math.max(min, Math.round(value));
9168
+ }
9169
+ function sanitizeDbValue(value) {
9170
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9171
+ return value;
8694
9172
  }
8695
- return loadQueryHistoryAsyncUnqueued(cwd);
9173
+ return null;
8696
9174
  }
8697
- async function loadQueryHistoryAsyncUnqueued(cwd) {
8698
- const file = historyFilePath(cwd);
8699
- let raw;
8700
- try {
8701
- raw = await readFile2(file, "utf8");
8702
- } catch (err) {
8703
- if (isEnoent(err))
8704
- return emptyState();
8705
- await backupCorruptHistoryFileAsync(file);
9175
+ function sanitizeRows(raw) {
9176
+ if (!Array.isArray(raw))
9177
+ return [];
9178
+ return raw.slice(0, MAX_PREVIEW_ROWS).map((row) => {
9179
+ if (!Array.isArray(row))
9180
+ return [];
9181
+ return row.map(sanitizeDbValue);
9182
+ });
9183
+ }
9184
+ function sanitizeEntry(raw) {
9185
+ if (!raw || typeof raw !== "object")
9186
+ return null;
9187
+ const entry = raw;
9188
+ const id = optionalString2(entry.id, MAX_ID_LEN);
9189
+ const dbId = optionalString2(entry.dbId, MAX_DB_ID_LEN);
9190
+ const sql = optionalString2(entry.sql, MAX_SQL_LEN);
9191
+ if (!id || !dbId || !sql)
9192
+ return null;
9193
+ const columns = Array.isArray(entry.columns) ? entry.columns.filter((col) => typeof col === "string" && !!col).map((col) => col.slice(0, MAX_COLUMN_LEN)).slice(0, MAX_COLUMNS) : [];
9194
+ const rowsPreview = sanitizeRows(entry.rowsPreview);
9195
+ const schema = optionalString2(entry.schema, MAX_SCHEMA_LEN);
9196
+ const title = optionalString2(entry.title, MAX_TEXT_LEN);
9197
+ const body = optionalString2(entry.body, MAX_TEXT_LEN);
9198
+ return {
9199
+ id,
9200
+ dbId,
9201
+ ...schema ? { schema } : {},
9202
+ sql,
9203
+ ...title ? { title } : {},
9204
+ ...body ? { body } : {},
9205
+ columns,
9206
+ rowsPreview,
9207
+ rowCount: finiteNumber(entry.rowCount) ?? rowsPreview.length,
9208
+ savedRows: finiteNumber(entry.savedRows) ?? rowsPreview.length,
9209
+ truncated: typeof entry.truncated === "boolean" ? entry.truncated : false,
9210
+ elapsedMs: finiteNumber(entry.elapsedMs) ?? 0,
9211
+ executedAt: optionalString2(entry.executedAt, 64) ?? new Date(0).toISOString(),
9212
+ executedBy: entry.executedBy === "ai" ? "ai" : "user",
9213
+ source: entry.source === "cli" ? "cli" : "browser"
9214
+ };
9215
+ }
9216
+ function sanitizeHistoryState(raw) {
9217
+ if (!raw || typeof raw !== "object")
8706
9218
  return emptyState();
8707
- }
8708
- try {
8709
- const parsed = JSON.parse(raw);
8710
- if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
8711
- await backupCorruptHistoryFileAsync(file);
8712
- return emptyState();
8713
- }
8714
- return parsed;
8715
- } catch (err) {
8716
- if (isEnoent(err))
8717
- return emptyState();
8718
- await backupCorruptHistoryFileAsync(file);
9219
+ const entriesRaw = raw.entries;
9220
+ if (!Array.isArray(entriesRaw))
8719
9221
  return emptyState();
9222
+ const entries = [];
9223
+ for (const entry of entriesRaw) {
9224
+ if (entries.length >= MAX_ENTRIES2)
9225
+ break;
9226
+ const normalized = sanitizeEntry(entry);
9227
+ if (normalized)
9228
+ entries.push(normalized);
8720
9229
  }
9230
+ return { version: 1, entries };
8721
9231
  }
8722
- async function saveQueryHistoryAsync(cwd, state) {
8723
- const dir = join9(cwd, CODE_VIEWER_DIR2);
8724
- await mkdir(dir, { recursive: true });
8725
- const file = historyFilePath(cwd);
8726
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8727
- const content = serializeHistoryState(state);
8728
- await writeFile(tmp, content, "utf8");
8729
- await rename(tmp, file);
9232
+ async function loadQueryHistoryAsync(cwd) {
9233
+ return historyStore.load(cwd);
8730
9234
  }
8731
9235
  async function updateQueryHistoryAsync(cwd, updater) {
8732
- const previous = historyWriteQueues.get(cwd) ?? Promise.resolve();
8733
- const run2 = previous.catch(() => {}).then(async () => {
8734
- const current = await loadQueryHistoryAsyncUnqueued(cwd);
8735
- const updated = await updater(current);
8736
- await saveQueryHistoryAsync(cwd, updated.state);
8737
- return updated.result;
8738
- });
8739
- const queued = run2.then(() => {}, () => {});
8740
- historyWriteQueues.set(cwd, queued);
8741
- try {
8742
- return await run2;
8743
- } finally {
8744
- if (historyWriteQueues.get(cwd) === queued) {
8745
- historyWriteQueues.delete(cwd);
8746
- }
8747
- }
9236
+ return historyStore.update(cwd, updater);
8748
9237
  }
8749
9238
  function clampPreviewRows(rows) {
8750
9239
  return rows.slice(0, MAX_PREVIEW_ROWS);
@@ -8780,15 +9269,23 @@ function clearQueryHistory(state, dbId, schema) {
8780
9269
  })
8781
9270
  };
8782
9271
  }
8783
- var CODE_VIEWER_DIR2 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES = 1e6, historyWriteQueues;
9272
+ var CODE_VIEWER_DIR3 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES = 1e6, MAX_ID_LEN = 128, MAX_DB_ID_LEN = 2048, MAX_SCHEMA_LEN = 512, MAX_SQL_LEN = 64000, MAX_TEXT_LEN = 64000, MAX_COLUMN_LEN = 512, MAX_COLUMNS = 500, historyStore;
8784
9273
  var init_query_history = __esm(() => {
8785
- historyWriteQueues = new Map;
9274
+ init_json_store();
9275
+ historyStore = createJsonFileStore({
9276
+ filePath: historyFilePath,
9277
+ empty: emptyState,
9278
+ sanitize: sanitizeHistoryState,
9279
+ maxBytes: MAX_JSON_BYTES,
9280
+ backupSuffix: "corrupt",
9281
+ serialize: serializeHistoryState
9282
+ });
8786
9283
  });
8787
9284
 
8788
9285
  // web-src/server/database/snapshot-store.ts
8789
- import { createHash as createHash5, randomBytes } from "node:crypto";
8790
- import { mkdirSync as mkdirSync4 } from "node:fs";
8791
- import { join as join10 } from "node:path";
9286
+ import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
9287
+ import { mkdirSync as mkdirSync3 } from "node:fs";
9288
+ import { join as join11 } from "node:path";
8792
9289
  async function getSqliteClass2() {
8793
9290
  if (cachedDbClass2)
8794
9291
  return cachedDbClass2;
@@ -8805,7 +9302,7 @@ async function getSqliteClass2() {
8805
9302
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
8806
9303
  }
8807
9304
  async function getStoreDb(cwd) {
8808
- const dbPath = join10(cwd, CODE_VIEWER_DIR3, SNAPSHOT_DB_NAME);
9305
+ const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
8809
9306
  if (storeDb && storeDbPath === dbPath)
8810
9307
  return storeDb;
8811
9308
  if (storeDb) {
@@ -8813,7 +9310,7 @@ async function getStoreDb(cwd) {
8813
9310
  storeDb.close();
8814
9311
  } catch {}
8815
9312
  }
8816
- mkdirSync4(join10(cwd, CODE_VIEWER_DIR3), { recursive: true });
9313
+ mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
8817
9314
  const DbClass = await getSqliteClass2();
8818
9315
  storeDb = new DbClass(dbPath);
8819
9316
  storeDbPath = dbPath;
@@ -8826,7 +9323,7 @@ async function getStoreDb(cwd) {
8826
9323
  return storeDb;
8827
9324
  }
8828
9325
  function makeId2(prefix) {
8829
- return `${prefix}-${randomBytes(8).toString("hex")}`;
9326
+ return `${prefix}-${randomBytes2(8).toString("hex")}`;
8830
9327
  }
8831
9328
  function hashPayload(payloadJson) {
8832
9329
  return createHash5("sha256").update(payloadJson).digest("hex");
@@ -9073,7 +9570,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
9073
9570
  });
9074
9571
  return { rows, total };
9075
9572
  }
9076
- var CODE_VIEWER_DIR3 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
9573
+ var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
9077
9574
  CREATE TABLE IF NOT EXISTS snapshots (
9078
9575
  id TEXT PRIMARY KEY,
9079
9576
  db_id TEXT NOT NULL,
@@ -9170,13 +9667,9 @@ var init_snapshot_runner = __esm(() => {
9170
9667
  });
9171
9668
 
9172
9669
  // web-src/server/database/tabs-store.ts
9173
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
9174
- import { join as join11 } from "node:path";
9670
+ import { join as join12 } from "node:path";
9175
9671
  function tabsFilePath(root) {
9176
- return join11(root, CODE_VIEWER_DIR4, TABS_FILE_NAME);
9177
- }
9178
- function isEnoent2(err) {
9179
- return err?.code === "ENOENT";
9672
+ return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
9180
9673
  }
9181
9674
  function emptyState2() {
9182
9675
  return { version: 1, tabs: [], activeTabId: null };
@@ -9244,6 +9737,31 @@ function sanitizeEs(v) {
9244
9737
  return;
9245
9738
  return out;
9246
9739
  }
9740
+ function sanitizeS3(v) {
9741
+ if (!v || typeof v !== "object")
9742
+ return;
9743
+ const r = v;
9744
+ const out = {};
9745
+ const bucket = sanitizeOptionalString(r.bucket, MAX_S3_BUCKET_LEN);
9746
+ if (bucket !== undefined)
9747
+ out.bucket = bucket;
9748
+ const prefix = sanitizeOptionalString(r.prefix, MAX_S3_KEY_LEN);
9749
+ if (prefix !== undefined)
9750
+ out.prefix = prefix;
9751
+ const query = sanitizeOptionalString(r.query, MAX_S3_QUERY_LEN);
9752
+ if (query !== undefined)
9753
+ out.query = query;
9754
+ if (r.mode === "prefix" || r.mode === "contains")
9755
+ out.mode = r.mode;
9756
+ if (r.sort === "key-asc" || r.sort === "updated-desc")
9757
+ out.sort = r.sort;
9758
+ const key = sanitizeOptionalString(r.key, MAX_S3_KEY_LEN);
9759
+ if (key !== undefined)
9760
+ out.key = key;
9761
+ if (out.bucket === undefined && out.prefix === undefined && out.query === undefined && out.mode === undefined && out.sort === undefined && out.key === undefined)
9762
+ return;
9763
+ return out;
9764
+ }
9247
9765
  function sanitize(input) {
9248
9766
  if (!input || typeof input !== "object")
9249
9767
  return emptyState2();
@@ -9265,12 +9783,15 @@ function sanitize(input) {
9265
9783
  if (seenIds.has(id))
9266
9784
  continue;
9267
9785
  seenIds.add(id);
9268
- const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN) ?? null;
9786
+ const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN2) ?? null;
9269
9787
  if (isToolInternalDbId(dbId))
9270
9788
  continue;
9789
+ const schema = sanitizeOptionalString(tab.schema, MAX_SCHEMA_NAME_LEN);
9271
9790
  const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
9272
9791
  const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
9273
9792
  const out = { id, dbId, table, view };
9793
+ if (schema !== undefined)
9794
+ out.schema = schema;
9274
9795
  const sqlDraft = sanitizeOptionalString(tab.sqlDraft, MAX_SQL_DRAFT_LEN);
9275
9796
  if (sqlDraft !== undefined)
9276
9797
  out.sqlDraft = sqlDraft;
@@ -9288,6 +9809,9 @@ function sanitize(input) {
9288
9809
  const es = sanitizeEs(tab.es);
9289
9810
  if (es !== undefined)
9290
9811
  out.es = es;
9812
+ const s3 = sanitizeS3(tab.s3);
9813
+ if (s3 !== undefined)
9814
+ out.s3 = s3;
9291
9815
  tabs.push(out);
9292
9816
  }
9293
9817
  let activeTabId = sanitizeOptionalString(obj.activeTabId, MAX_TAB_ID_LEN) ?? null;
@@ -9297,69 +9821,14 @@ function sanitize(input) {
9297
9821
  return { version: 1, tabs, activeTabId };
9298
9822
  }
9299
9823
  async function loadTabsAsync(cwd) {
9300
- const pendingWrite = tabsWriteQueues.get(cwd);
9301
- if (pendingWrite) {
9302
- await pendingWrite.catch(() => {});
9303
- }
9304
- const file = tabsFilePath(cwd);
9305
- let raw;
9306
- try {
9307
- raw = await readFile3(file, "utf8");
9308
- } catch (err) {
9309
- if (isEnoent2(err))
9310
- return emptyState2();
9311
- await backupInvalidTabsFileAsync(file);
9312
- return emptyState2();
9313
- }
9314
- try {
9315
- const parsed = JSON.parse(raw);
9316
- if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
9317
- await backupInvalidTabsFileAsync(file);
9318
- return emptyState2();
9319
- }
9320
- return sanitize(parsed);
9321
- } catch (err) {
9322
- if (isEnoent2(err))
9323
- return emptyState2();
9324
- await backupInvalidTabsFileAsync(file);
9325
- return emptyState2();
9326
- }
9327
- }
9328
- async function backupInvalidTabsFileAsync(file) {
9329
- try {
9330
- await rename2(file, `${file}.bak-${Date.now()}`);
9331
- } catch {}
9332
- }
9333
- async function saveTabsAsyncUnqueued(cwd, state) {
9334
- const normalized = sanitize(state);
9335
- const dir = join11(cwd, CODE_VIEWER_DIR4);
9336
- await mkdir2(dir, { recursive: true });
9337
- const file = tabsFilePath(cwd);
9338
- const tmp = `${file}.${makeId(`tmp-${process.pid}`)}`;
9339
- const content = `${JSON.stringify(normalized, null, 2)}
9340
- `;
9341
- if (Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES2) {
9342
- throw new Error("tabs state too large");
9343
- }
9344
- await writeFile2(tmp, content, "utf8");
9345
- await rename2(tmp, file);
9824
+ return tabsStore.load(cwd);
9346
9825
  }
9347
9826
  async function saveTabsAsync(cwd, state) {
9348
- const previous = tabsWriteQueues.get(cwd) ?? Promise.resolve();
9349
- const run2 = previous.catch(() => {}).then(() => saveTabsAsyncUnqueued(cwd, state));
9350
- const queued = run2.then(() => {}, () => {});
9351
- tabsWriteQueues.set(cwd, queued);
9352
- try {
9353
- await run2;
9354
- } finally {
9355
- if (tabsWriteQueues.get(cwd) === queued) {
9356
- tabsWriteQueues.delete(cwd);
9357
- }
9358
- }
9827
+ return tabsStore.save(cwd, state);
9359
9828
  }
9360
- var CODE_VIEWER_DIR4 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN = 2048, MAX_TABLE_NAME_LEN = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, tabsWriteQueues, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS;
9829
+ var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
9361
9830
  var init_tabs_store = __esm(() => {
9362
- tabsWriteQueues = new Map;
9831
+ init_json_store();
9363
9832
  VALID_VIEWS = new Set([
9364
9833
  "data",
9365
9834
  "query",
@@ -9368,6 +9837,14 @@ var init_tabs_store = __esm(() => {
9368
9837
  "search",
9369
9838
  "snapshot"
9370
9839
  ]);
9840
+ tabsStore = createJsonFileStore({
9841
+ filePath: tabsFilePath,
9842
+ empty: emptyState2,
9843
+ sanitize,
9844
+ maxBytes: MAX_JSON_BYTES2,
9845
+ backupSuffix: "bak",
9846
+ sizeErrorMessage: "tabs state too large"
9847
+ });
9371
9848
  });
9372
9849
 
9373
9850
  // web-src/server/database/handle.ts
@@ -9396,7 +9873,7 @@ function sanitizeFilename(name) {
9396
9873
  function normalizeSchemaParam(value) {
9397
9874
  if (value === undefined || value === null || value === "")
9398
9875
  return;
9399
- if (value.length > MAX_SCHEMA_NAME_LEN) {
9876
+ if (value.length > MAX_SCHEMA_NAME_LEN2) {
9400
9877
  return textError("invalid schema parameter", 400);
9401
9878
  }
9402
9879
  if (hasControlCharacter(value)) {
@@ -10355,6 +10832,23 @@ async function handleTabsPut(cwd, req) {
10355
10832
  return textError(`failed to save tabs: ${message}`, 500);
10356
10833
  }
10357
10834
  }
10835
+ async function handleDbUiGet(cwd) {
10836
+ return jsonLoadResponse(() => loadDbUiState(cwd), "db UI", "failed to load db UI state");
10837
+ }
10838
+ async function handleDbUiPatch(cwd, req) {
10839
+ const body = await parseBoundedJsonBody(req, MAX_DB_UI_BODY_BYTES, "db UI body too large");
10840
+ if (body instanceof Response)
10841
+ return body;
10842
+ try {
10843
+ return json(await patchDbUiState(cwd, body));
10844
+ } catch (err) {
10845
+ const message = err instanceof Error ? err.message : String(err);
10846
+ if (message === "db UI state too large")
10847
+ return textError(message, 413);
10848
+ console.error("[code-viewer] db UI error:", err);
10849
+ return textError("failed to save db UI state", 500);
10850
+ }
10851
+ }
10358
10852
  async function handleClose(cwd, req, omitDirNames) {
10359
10853
  const body = await parsePostJsonBody(req);
10360
10854
  if (body instanceof Response)
@@ -10517,11 +11011,17 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
10517
11011
  methods: ["GET", "PUT", "POST"],
10518
11012
  sideEffect: (m) => m !== "GET",
10519
11013
  handler: () => method === "GET" ? handleTabsGet(cwd) : handleTabsPut(cwd, req)
11014
+ },
11015
+ "/_db/ui": {
11016
+ methods: ["GET", "PATCH"],
11017
+ sideEffect: (m) => m !== "GET",
11018
+ handler: () => method === "GET" ? handleDbUiGet(cwd) : handleDbUiPatch(cwd, req)
10520
11019
  }
10521
11020
  }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
10522
11021
  }
10523
- var initialized = false, dockerAdapterCache, MAX_SCHEMA_NAME_LEN = 1024, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
11022
+ var initialized = false, dockerAdapterCache, MAX_SCHEMA_NAME_LEN2 = 1024, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_DB_UI_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
10524
11023
  var init_handle = __esm(() => {
11024
+ init_state_store();
10525
11025
  init_docker();
10526
11026
  init_docker_utils();
10527
11027
  init_sqlite();
@@ -10575,25 +11075,87 @@ var init_handle = __esm(() => {
10575
11075
  };
10576
11076
  });
10577
11077
 
11078
+ // web-src/server/state-route.ts
11079
+ var exports_state_route = {};
11080
+ __export(exports_state_route, {
11081
+ handleStateRoute: () => handleStateRoute
11082
+ });
11083
+ async function parseJsonBody(req) {
11084
+ return parseBoundedJsonBody(req, MAX_STATE_PATCH_BODY_BYTES, "state body too large");
11085
+ }
11086
+ async function handleSettingsGet(cwd) {
11087
+ return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
11088
+ }
11089
+ async function handleSettingsPatch(cwd, req) {
11090
+ const body = await parseJsonBody(req);
11091
+ if (body instanceof Response)
11092
+ return body;
11093
+ try {
11094
+ return json(await patchAppSettingsState(cwd, body));
11095
+ } catch (err) {
11096
+ const message = err instanceof Error ? err.message : String(err);
11097
+ if (message === "settings state too large")
11098
+ return textError(message, 413);
11099
+ console.error("[code-viewer] state error:", err);
11100
+ return textError("failed to save settings state", 500);
11101
+ }
11102
+ }
11103
+ async function handleViewGet(cwd) {
11104
+ return jsonLoadResponse(() => loadViewState(cwd), "state", "failed to load view state");
11105
+ }
11106
+ async function handleViewPatch(cwd, req) {
11107
+ const body = await parseJsonBody(req);
11108
+ if (body instanceof Response)
11109
+ return body;
11110
+ try {
11111
+ return json(await patchViewState(cwd, body));
11112
+ } catch (err) {
11113
+ const message = err instanceof Error ? err.message : String(err);
11114
+ if (message === "view state too large")
11115
+ return textError(message, 413);
11116
+ console.error("[code-viewer] state error:", err);
11117
+ return textError("failed to save view state", 500);
11118
+ }
11119
+ }
11120
+ async function handleStateRoute(req, url, cwd, sideEffectAllowed) {
11121
+ return dispatchRoutes(req, url, {
11122
+ "/_state/settings": {
11123
+ methods: ["GET", "PATCH"],
11124
+ sideEffect: (method) => method !== "GET",
11125
+ handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req)
11126
+ },
11127
+ "/_state/view": {
11128
+ methods: ["GET", "PATCH"],
11129
+ sideEffect: (method) => method !== "GET",
11130
+ handler: () => req.method === "GET" ? handleViewGet(cwd) : handleViewPatch(cwd, req)
11131
+ }
11132
+ }, sideEffectAllowed, (res) => res, (err) => handleError("state", "handle state request", err));
11133
+ }
11134
+ var MAX_STATE_PATCH_BODY_BYTES = 1e6;
11135
+ var init_state_route = __esm(() => {
11136
+ init_handle_shared();
11137
+ init_state_store();
11138
+ });
11139
+
10578
11140
  // web-src/server/preview.ts
10579
11141
  var exports_preview = {};
10580
11142
  import {
10581
11143
  closeSync as closeSync2,
10582
11144
  constants,
10583
- existsSync as existsSync7,
11145
+ existsSync as existsSync6,
10584
11146
  lstatSync as lstatSync4,
10585
- mkdirSync as mkdirSync5,
11147
+ mkdirSync as mkdirSync4,
10586
11148
  openSync as openSync2,
10587
- readFileSync as readFileSync5,
11149
+ readFileSync as readFileSync4,
10588
11150
  realpathSync as realpathSync4,
10589
- renameSync as renameSync2,
11151
+ renameSync,
10590
11152
  statSync as statSync3,
10591
11153
  unlinkSync as unlinkSync2,
10592
11154
  watch,
10593
- writeFileSync as writeFileSync3
11155
+ writeFileSync as writeFileSync2
10594
11156
  } from "node:fs";
10595
11157
  import { homedir as homedir3 } from "node:os";
10596
- import { basename as basename3, dirname as dirname2, extname as extname2, join as join12, relative as relative3 } from "node:path";
11158
+ import { basename as basename3, dirname as dirname3, extname as extname2, join as join13, relative as relative3 } from "node:path";
10597
11159
  function parseCli() {
10598
11160
  const rest = [];
10599
11161
  for (let i = 2;i < process.argv.length; i++) {
@@ -10734,10 +11296,10 @@ function staticFile(pathname) {
10734
11296
  const spec = map[pathname];
10735
11297
  if (!spec)
10736
11298
  return null;
10737
- const full = join12(WEB_ROOT, spec[0]);
10738
- if (!existsSync7(full))
11299
+ const full = join13(WEB_ROOT, spec[0]);
11300
+ if (!existsSync6(full))
10739
11301
  return text("not found", 404);
10740
- return new Response(readFileSync5(full), {
11302
+ return new Response(readFileSync4(full), {
10741
11303
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
10742
11304
  });
10743
11305
  }
@@ -10968,8 +11530,8 @@ function parseScopeExcludeNamesQuery(value) {
10968
11530
  return normalizeScopeExcludeNames(names);
10969
11531
  }
10970
11532
  function loadProjectConfig() {
10971
- const full = join12(cwd, ".code-viewer.json");
10972
- if (!existsSync7(full))
11533
+ const full = join13(cwd, ".code-viewer.json");
11534
+ if (!existsSync6(full))
10973
11535
  return null;
10974
11536
  let realCwd;
10975
11537
  let realConfig;
@@ -10979,10 +11541,10 @@ function loadProjectConfig() {
10979
11541
  } catch {
10980
11542
  return null;
10981
11543
  }
10982
- if (dirname2(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
11544
+ if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
10983
11545
  return null;
10984
11546
  try {
10985
- const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
11547
+ const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
10986
11548
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
10987
11549
  return null;
10988
11550
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -11033,8 +11595,8 @@ function safeWorktreePath(path) {
11033
11595
  return null;
11034
11596
  if (isGitInternalPath(path))
11035
11597
  return null;
11036
- const full = join12(cwd, path);
11037
- if (!existsSync7(full))
11598
+ const full = join13(cwd, path);
11599
+ if (!existsSync6(full))
11038
11600
  return null;
11039
11601
  let realCwd;
11040
11602
  let realFull;
@@ -11052,7 +11614,7 @@ function safeWorktreePath(path) {
11052
11614
  return realFull;
11053
11615
  }
11054
11616
  function worktreePath(path) {
11055
- return join12(cwd, path);
11617
+ return join13(cwd, path);
11056
11618
  }
11057
11619
  function safeOpenWorktreePath(path) {
11058
11620
  if (path === "") {
@@ -11068,7 +11630,7 @@ function safeOpenWorktreePath(path) {
11068
11630
  return safeWorktreePath(path);
11069
11631
  }
11070
11632
  function parentRepoPath(path) {
11071
- const parent = dirname2(path);
11633
+ const parent = dirname3(path);
11072
11634
  return parent === "." ? "" : parent;
11073
11635
  }
11074
11636
  function isoDate(ms) {
@@ -11135,7 +11697,7 @@ function readReadme(target, dirPath) {
11135
11697
  if (!full)
11136
11698
  continue;
11137
11699
  try {
11138
- return { path, text: readFileSync5(full, "utf8") };
11700
+ return { path, text: readFileSync4(full, "utf8") };
11139
11701
  } catch {
11140
11702
  continue;
11141
11703
  }
@@ -11256,7 +11818,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
11256
11818
  continue;
11257
11819
  let data;
11258
11820
  try {
11259
- data = readFileSync5(full);
11821
+ data = readFileSync4(full);
11260
11822
  } catch {
11261
11823
  continue;
11262
11824
  }
@@ -11792,10 +12354,10 @@ async function handleUploadFiles(req) {
11792
12354
  total += file.size;
11793
12355
  if (total > MAX_UPLOAD_TOTAL_BYTES)
11794
12356
  return text("upload too large", 413);
11795
- const target = join12(realDir, safeName);
11796
- if (relative3(realDir, dirname2(target)) !== "")
12357
+ const target = join13(realDir, safeName);
12358
+ if (relative3(realDir, dirname3(target)) !== "")
11797
12359
  return text("invalid filename", 400);
11798
- if (existsSync7(target))
12360
+ if (existsSync6(target))
11799
12361
  return text("file exists", 409);
11800
12362
  uploads.push({ file, name: safeName, target });
11801
12363
  }
@@ -11804,7 +12366,7 @@ async function handleUploadFiles(req) {
11804
12366
  for (const upload of uploads) {
11805
12367
  const fd = openSync2(upload.target, uploadOpenFlags(), 420);
11806
12368
  try {
11807
- writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
12369
+ writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
11808
12370
  } finally {
11809
12371
  closeSync2(fd);
11810
12372
  }
@@ -11914,12 +12476,12 @@ function triggerUpdate(changedPaths) {
11914
12476
  sendSse("update", data);
11915
12477
  }
11916
12478
  function moveMacPathIntoTrash(path) {
11917
- const trashDir = join12(homedir3(), ".Trash");
12479
+ const trashDir = join13(homedir3(), ".Trash");
11918
12480
  const base = basename3(path) || "code-viewer-trash-item";
11919
- const target = join12(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
12481
+ const target = join13(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
11920
12482
  try {
11921
- mkdirSync5(trashDir, { recursive: true });
11922
- renameSync2(path, target);
12483
+ mkdirSync4(trashDir, { recursive: true });
12484
+ renameSync(path, target);
11923
12485
  return { ok: true, trashPath: target };
11924
12486
  } catch (error) {
11925
12487
  return { ok: false, error: String(error) };
@@ -11950,20 +12512,20 @@ function restoreTrashPath(originalPath, trashPath) {
11950
12512
  if (!parentFullPath)
11951
12513
  return { ok: false, error: "invalid restore target" };
11952
12514
  const original = worktreePath(originalPath);
11953
- if (existsSync7(original))
12515
+ if (existsSync6(original))
11954
12516
  return { ok: false, error: "restore target exists" };
11955
12517
  if (trashPath) {
11956
12518
  if (process.platform !== "darwin")
11957
12519
  return { ok: false, error: "invalid trash handle" };
11958
- if (!existsSync7(trashPath))
12520
+ if (!existsSync6(trashPath))
11959
12521
  return { ok: false, error: "trash item not found" };
11960
12522
  try {
11961
- const trashRoot = join12(homedir3(), ".Trash");
12523
+ const trashRoot = join13(homedir3(), ".Trash");
11962
12524
  const trashRelative = relative3(trashRoot, trashPath);
11963
12525
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
11964
12526
  return { ok: false, error: "invalid trash handle" };
11965
- mkdirSync5(dirname2(original), { recursive: true });
11966
- renameSync2(trashPath, original);
12527
+ mkdirSync4(dirname3(original), { recursive: true });
12528
+ renameSync(trashPath, original);
11967
12529
  return { ok: true };
11968
12530
  } catch (error) {
11969
12531
  return { ok: false, error: String(error) };
@@ -12108,11 +12670,11 @@ async function handleCreateDirectory(req) {
12108
12670
  const targetPath = dir ? `${dir}/${name}` : name;
12109
12671
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
12110
12672
  return text("invalid target", 400);
12111
- const target = join12(parent, name);
12112
- if (existsSync7(target))
12673
+ const target = join13(parent, name);
12674
+ if (existsSync6(target))
12113
12675
  return text("already exists", 409);
12114
12676
  try {
12115
- mkdirSync5(target, { recursive: false });
12677
+ mkdirSync4(target, { recursive: false });
12116
12678
  } catch (error) {
12117
12679
  if (error.code === "EEXIST")
12118
12680
  return text("already exists", 409);
@@ -12158,7 +12720,7 @@ function annotationSse(kind, sessionId, entryId) {
12158
12720
  }
12159
12721
  async function handleAnnotations(req) {
12160
12722
  if (req.method === "GET")
12161
- return json2(loadAnnotationsState(cwd));
12723
+ return json2(await loadAnnotationsState(cwd));
12162
12724
  if (req.method !== "POST")
12163
12725
  return text("method not allowed", 405);
12164
12726
  if (!sideEffectRequestAllowed(req))
@@ -12182,8 +12744,8 @@ async function handleAnnotations(req) {
12182
12744
  const action = body.action;
12183
12745
  if (action === "start") {
12184
12746
  const title = typeof body.title === "string" ? body.title : "";
12185
- const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
12186
- saveAnnotationsState(cwd, started.state);
12747
+ const started = startAnnotationSession(await loadAnnotationsState(cwd), title, new Date().toISOString());
12748
+ await saveAnnotationsState(cwd, started.state);
12187
12749
  annotationSse("start", started.session.id);
12188
12750
  return json2({ ok: true, session: started.session });
12189
12751
  }
@@ -12201,7 +12763,7 @@ async function handleAnnotations(req) {
12201
12763
  if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
12202
12764
  return text("forbidden", 403);
12203
12765
  }
12204
- const result = addAnnotationEntry(loadAnnotationsState(cwd), {
12766
+ const result = addAnnotationEntry(await loadAnnotationsState(cwd), {
12205
12767
  session_id: typeof body.session_id === "string" ? body.session_id : undefined,
12206
12768
  session_title: typeof body.session_title === "string" ? body.session_title : undefined,
12207
12769
  path,
@@ -12216,7 +12778,7 @@ async function handleAnnotations(req) {
12216
12778
  }, new Date().toISOString());
12217
12779
  if (result.ok === false)
12218
12780
  return text(result.error, 400);
12219
- saveAnnotationsState(cwd, result.state);
12781
+ await saveAnnotationsState(cwd, result.state);
12220
12782
  annotationSse("add", result.session.id, result.entry.id);
12221
12783
  return json2({
12222
12784
  ok: true,
@@ -12230,14 +12792,14 @@ async function handleAnnotations(req) {
12230
12792
  const id = typeof body.id === "string" ? body.id : "";
12231
12793
  if (!id)
12232
12794
  return text("invalid id", 400);
12233
- const result = moveAnnotationEntry(loadAnnotationsState(cwd), id, {
12795
+ const result = moveAnnotationEntry(await loadAnnotationsState(cwd), id, {
12234
12796
  before_id: typeof body.before_id === "string" ? body.before_id : undefined,
12235
12797
  after_id: typeof body.after_id === "string" ? body.after_id : undefined,
12236
12798
  position: typeof body.position === "number" ? body.position : undefined
12237
12799
  });
12238
12800
  if (result.ok === false)
12239
12801
  return text(result.error, 400);
12240
- saveAnnotationsState(cwd, result.state);
12802
+ await saveAnnotationsState(cwd, result.state);
12241
12803
  annotationSse("update", result.session.id, result.entry.id);
12242
12804
  return json2({
12243
12805
  ok: true,
@@ -12249,9 +12811,9 @@ async function handleAnnotations(req) {
12249
12811
  const id = typeof body.id === "string" ? body.id : "";
12250
12812
  if (!id)
12251
12813
  return text("invalid id", 400);
12252
- const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
12814
+ const result = deleteAnnotationById(await loadAnnotationsState(cwd), id);
12253
12815
  if (result.removed) {
12254
- saveAnnotationsState(cwd, result.state);
12816
+ await saveAnnotationsState(cwd, result.state);
12255
12817
  annotationSse("delete");
12256
12818
  }
12257
12819
  return json2({ ok: true, removed: result.removed });
@@ -12261,10 +12823,10 @@ async function handleAnnotations(req) {
12261
12823
  const title = typeof body.title === "string" ? body.title : "";
12262
12824
  if (!id)
12263
12825
  return text("invalid id", 400);
12264
- const result = renameAnnotationSession(loadAnnotationsState(cwd), id, title);
12826
+ const result = renameAnnotationSession(await loadAnnotationsState(cwd), id, title);
12265
12827
  if (!result.renamed)
12266
12828
  return text("session not found", 404);
12267
- saveAnnotationsState(cwd, result.state);
12829
+ await saveAnnotationsState(cwd, result.state);
12268
12830
  annotationSse("update", id);
12269
12831
  return json2({ ok: true });
12270
12832
  }
@@ -12272,18 +12834,18 @@ async function handleAnnotations(req) {
12272
12834
  const id = typeof body.id === "string" ? body.id : "";
12273
12835
  if (!id)
12274
12836
  return text("invalid id", 400);
12275
- const result = updateAnnotationEntry(loadAnnotationsState(cwd), id, {
12837
+ const result = updateAnnotationEntry(await loadAnnotationsState(cwd), id, {
12276
12838
  title: typeof body.title === "string" ? body.title : undefined,
12277
12839
  body: typeof body.body === "string" ? body.body : undefined
12278
12840
  });
12279
12841
  if (result.ok === false)
12280
12842
  return text(result.error, 400);
12281
- saveAnnotationsState(cwd, result.state);
12843
+ await saveAnnotationsState(cwd, result.state);
12282
12844
  annotationSse("update", undefined, id);
12283
12845
  return json2({ ok: true, entry: result.entry });
12284
12846
  }
12285
12847
  if (action === "clear") {
12286
- saveAnnotationsState(cwd, emptyAnnotationsState());
12848
+ await saveAnnotationsState(cwd, emptyAnnotationsState());
12287
12849
  annotationSse("clear");
12288
12850
  return json2({ ok: true });
12289
12851
  }
@@ -12352,8 +12914,8 @@ var init_preview = __esm(async () => {
12352
12914
  init_search();
12353
12915
  init_server_registry();
12354
12916
  init_worktree_watcher();
12355
- WEB_ROOT = join12(ROOT, "web");
12356
- VERSION = JSON.parse(readFileSync5(join12(ROOT, "package.json"), "utf8")).version;
12917
+ WEB_ROOT = join13(ROOT, "web");
12918
+ VERSION = JSON.parse(readFileSync4(join13(ROOT, "package.json"), "utf8")).version;
12357
12919
  DEFAULT_ARGS = ["HEAD"];
12358
12920
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
12359
12921
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -12457,6 +13019,12 @@ var init_preview = __esm(async () => {
12457
13019
  if (dbResponse)
12458
13020
  return dbResponse;
12459
13021
  }
13022
+ if (url.pathname.startsWith("/_state/")) {
13023
+ const { handleStateRoute: handleStateRoute2 } = await Promise.resolve().then(() => (init_state_route(), exports_state_route));
13024
+ const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed);
13025
+ if (stateResponse)
13026
+ return stateResponse;
13027
+ }
12460
13028
  if (url.pathname === "/_annotations")
12461
13029
  return handleAnnotations(req);
12462
13030
  if (url.pathname === "/_refs")