@youtyan/code-viewer 0.2.6 → 0.2.7

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 +814 -250
  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);
@@ -5626,14 +6065,14 @@ var init_connection_pool = __esm(() => {
5626
6065
  // web-src/server/database/discovery.ts
5627
6066
  import {
5628
6067
  closeSync,
5629
- existsSync as existsSync6,
6068
+ existsSync as existsSync5,
5630
6069
  openSync,
5631
6070
  readSync,
5632
6071
  realpathSync as realpathSync3,
5633
6072
  statSync as statSync2
5634
6073
  } 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";
6074
+ import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
6075
+ import { basename as basename2, join as join9, relative as relative2 } from "node:path";
5637
6076
  function isSqliteFile(fullPath) {
5638
6077
  try {
5639
6078
  const stat2 = statSync2(fullPath);
@@ -5704,7 +6143,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
5704
6143
  return;
5705
6144
  if (omitSet.has(entry.toLowerCase()))
5706
6145
  continue;
5707
- const full = join8(dir, entry);
6146
+ const full = join9(dir, entry);
5708
6147
  let entryStat;
5709
6148
  try {
5710
6149
  entryStat = await lstat(full);
@@ -5748,8 +6187,8 @@ function validateDbPath(cwd, dbPath) {
5748
6187
  const parts = dbPath.split(/[\\/]+/);
5749
6188
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
5750
6189
  return null;
5751
- const full = join8(cwd, dbPath);
5752
- if (!existsSync6(full))
6190
+ const full = join9(cwd, dbPath);
6191
+ if (!existsSync5(full))
5753
6192
  return null;
5754
6193
  let realCwd;
5755
6194
  let realFull;
@@ -5885,7 +6324,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
5885
6324
  }
5886
6325
  async function readDotenvAsync(composeDir) {
5887
6326
  try {
5888
- const content = await readFile(join8(composeDir, ".env"), "utf-8");
6327
+ const content = await readFile2(join9(composeDir, ".env"), "utf-8");
5889
6328
  return parseDotenvContent(content);
5890
6329
  } catch {
5891
6330
  return {};
@@ -6059,7 +6498,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
6059
6498
  async function parseComposeFileAsync(filepath, composeDir, cwd, results) {
6060
6499
  let content;
6061
6500
  try {
6062
- content = await readFile(filepath, "utf-8");
6501
+ content = await readFile2(filepath, "utf-8");
6063
6502
  } catch {
6064
6503
  return;
6065
6504
  }
@@ -6105,7 +6544,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6105
6544
  if (depth > MAX_SCAN_DEPTH)
6106
6545
  return;
6107
6546
  for (const filename of COMPOSE_FILENAMES) {
6108
- const filepath = join8(dir, filename);
6547
+ const filepath = join9(dir, filename);
6109
6548
  if (await pathExistsAsync(filepath)) {
6110
6549
  await parseComposeFileAsync(filepath, dir, cwd, results);
6111
6550
  break;
@@ -6128,7 +6567,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6128
6567
  return;
6129
6568
  if (omitSet.has(entry.toLowerCase()))
6130
6569
  continue;
6131
- const full = join8(dir, entry);
6570
+ const full = join9(dir, entry);
6132
6571
  let entryStat;
6133
6572
  try {
6134
6573
  entryStat = await lstat(full);
@@ -6763,6 +7202,32 @@ function textError(message, status) {
6763
7202
  }
6764
7203
  });
6765
7204
  }
7205
+ async function jsonLoadResponse(load, logPrefix, errorMessage) {
7206
+ try {
7207
+ return json(await load());
7208
+ } catch (err) {
7209
+ console.error(`[code-viewer] ${logPrefix} error:`, err);
7210
+ return textError(errorMessage, 500);
7211
+ }
7212
+ }
7213
+ async function parseBoundedJsonBody(req, maxBytes, tooLargeMessage) {
7214
+ const contentType = req.headers.get("content-type") || "";
7215
+ if (!contentType.toLowerCase().startsWith("application/json")) {
7216
+ return textError("unsupported media type", 415);
7217
+ }
7218
+ const contentLength = Number(req.headers.get("content-length") || "0");
7219
+ if (contentLength > maxBytes)
7220
+ return textError(tooLargeMessage, 413);
7221
+ try {
7222
+ const raw = await req.text();
7223
+ if (Buffer.byteLength(raw, "utf8") > maxBytes) {
7224
+ return textError(tooLargeMessage, 413);
7225
+ }
7226
+ return JSON.parse(raw);
7227
+ } catch {
7228
+ return textError("invalid JSON body", 400);
7229
+ }
7230
+ }
6766
7231
  function waitForCallerAbort(promise, signal, message) {
6767
7232
  if (!signal)
6768
7233
  return promise;
@@ -6794,6 +7259,10 @@ function waitForCallerAbort(promise, signal, message) {
6794
7259
  });
6795
7260
  });
6796
7261
  }
7262
+ function isFilesystemAccessError(err) {
7263
+ const code = err?.code;
7264
+ return code === "EACCES" || code === "EBUSY" || code === "EIO" || code === "EISDIR" || code === "ENOSPC" || code === "ENOTDIR" || code === "EPERM" || code === "EROFS";
7265
+ }
6797
7266
  async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
6798
7267
  if (!dbParam)
6799
7268
  return textError("missing db parameter", 400);
@@ -6860,6 +7329,9 @@ function handleError(prefix, action, err) {
6860
7329
  if (isDockerComposeServiceUnavailableError(err)) {
6861
7330
  return textError(message, err.status);
6862
7331
  }
7332
+ if (isFilesystemAccessError(err)) {
7333
+ return textError(`failed to ${action}`, 500);
7334
+ }
6863
7335
  return textError(`failed to ${action}: ${message}`, 500);
6864
7336
  }
6865
7337
  var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
@@ -8657,13 +9129,9 @@ var init_handle_s3 = __esm(() => {
8657
9129
  });
8658
9130
 
8659
9131
  // 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";
9132
+ import { join as join10 } from "node:path";
8662
9133
  function historyFilePath(root) {
8663
- return join9(root, CODE_VIEWER_DIR2, HISTORY_FILE_NAME);
8664
- }
8665
- function isEnoent(err) {
8666
- return err?.code === "ENOENT";
9134
+ return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
8667
9135
  }
8668
9136
  function emptyState() {
8669
9137
  return { version: 1, entries: [] };
@@ -8682,69 +9150,86 @@ function serializeHistoryState(state) {
8682
9150
  }
8683
9151
  return content;
8684
9152
  }
8685
- async function backupCorruptHistoryFileAsync(file) {
8686
- try {
8687
- await rename(file, `${file}.corrupt-${Date.now()}`);
8688
- } catch {}
9153
+ function optionalString2(value, maxLen) {
9154
+ if (typeof value !== "string")
9155
+ return;
9156
+ if (!value || value.length > maxLen || value.includes("\x00"))
9157
+ return;
9158
+ return value;
8689
9159
  }
8690
- async function loadQueryHistoryAsync(cwd) {
8691
- const pendingWrite = historyWriteQueues.get(cwd);
8692
- if (pendingWrite) {
8693
- await pendingWrite.catch(() => {});
9160
+ function finiteNumber(value, min = 0) {
9161
+ if (typeof value !== "number" || !Number.isFinite(value))
9162
+ return;
9163
+ return Math.max(min, Math.round(value));
9164
+ }
9165
+ function sanitizeDbValue(value) {
9166
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9167
+ return value;
8694
9168
  }
8695
- return loadQueryHistoryAsyncUnqueued(cwd);
9169
+ return null;
8696
9170
  }
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);
9171
+ function sanitizeRows(raw) {
9172
+ if (!Array.isArray(raw))
9173
+ return [];
9174
+ return raw.slice(0, MAX_PREVIEW_ROWS).map((row) => {
9175
+ if (!Array.isArray(row))
9176
+ return [];
9177
+ return row.map(sanitizeDbValue);
9178
+ });
9179
+ }
9180
+ function sanitizeEntry(raw) {
9181
+ if (!raw || typeof raw !== "object")
9182
+ return null;
9183
+ const entry = raw;
9184
+ const id = optionalString2(entry.id, MAX_ID_LEN);
9185
+ const dbId = optionalString2(entry.dbId, MAX_DB_ID_LEN);
9186
+ const sql = optionalString2(entry.sql, MAX_SQL_LEN);
9187
+ if (!id || !dbId || !sql)
9188
+ return null;
9189
+ 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) : [];
9190
+ const rowsPreview = sanitizeRows(entry.rowsPreview);
9191
+ const schema = optionalString2(entry.schema, MAX_SCHEMA_LEN);
9192
+ const title = optionalString2(entry.title, MAX_TEXT_LEN);
9193
+ const body = optionalString2(entry.body, MAX_TEXT_LEN);
9194
+ return {
9195
+ id,
9196
+ dbId,
9197
+ ...schema ? { schema } : {},
9198
+ sql,
9199
+ ...title ? { title } : {},
9200
+ ...body ? { body } : {},
9201
+ columns,
9202
+ rowsPreview,
9203
+ rowCount: finiteNumber(entry.rowCount) ?? rowsPreview.length,
9204
+ savedRows: finiteNumber(entry.savedRows) ?? rowsPreview.length,
9205
+ truncated: typeof entry.truncated === "boolean" ? entry.truncated : false,
9206
+ elapsedMs: finiteNumber(entry.elapsedMs) ?? 0,
9207
+ executedAt: optionalString2(entry.executedAt, 64) ?? new Date(0).toISOString(),
9208
+ executedBy: entry.executedBy === "ai" ? "ai" : "user",
9209
+ source: entry.source === "cli" ? "cli" : "browser"
9210
+ };
9211
+ }
9212
+ function sanitizeHistoryState(raw) {
9213
+ if (!raw || typeof raw !== "object")
8706
9214
  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);
9215
+ const entriesRaw = raw.entries;
9216
+ if (!Array.isArray(entriesRaw))
8719
9217
  return emptyState();
9218
+ const entries = [];
9219
+ for (const entry of entriesRaw) {
9220
+ if (entries.length >= MAX_ENTRIES2)
9221
+ break;
9222
+ const normalized = sanitizeEntry(entry);
9223
+ if (normalized)
9224
+ entries.push(normalized);
8720
9225
  }
9226
+ return { version: 1, entries };
8721
9227
  }
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);
9228
+ async function loadQueryHistoryAsync(cwd) {
9229
+ return historyStore.load(cwd);
8730
9230
  }
8731
9231
  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
- }
9232
+ return historyStore.update(cwd, updater);
8748
9233
  }
8749
9234
  function clampPreviewRows(rows) {
8750
9235
  return rows.slice(0, MAX_PREVIEW_ROWS);
@@ -8780,15 +9265,23 @@ function clearQueryHistory(state, dbId, schema) {
8780
9265
  })
8781
9266
  };
8782
9267
  }
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;
9268
+ 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
9269
  var init_query_history = __esm(() => {
8785
- historyWriteQueues = new Map;
9270
+ init_json_store();
9271
+ historyStore = createJsonFileStore({
9272
+ filePath: historyFilePath,
9273
+ empty: emptyState,
9274
+ sanitize: sanitizeHistoryState,
9275
+ maxBytes: MAX_JSON_BYTES,
9276
+ backupSuffix: "corrupt",
9277
+ serialize: serializeHistoryState
9278
+ });
8786
9279
  });
8787
9280
 
8788
9281
  // 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";
9282
+ import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
9283
+ import { mkdirSync as mkdirSync3 } from "node:fs";
9284
+ import { join as join11 } from "node:path";
8792
9285
  async function getSqliteClass2() {
8793
9286
  if (cachedDbClass2)
8794
9287
  return cachedDbClass2;
@@ -8805,7 +9298,7 @@ async function getSqliteClass2() {
8805
9298
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
8806
9299
  }
8807
9300
  async function getStoreDb(cwd) {
8808
- const dbPath = join10(cwd, CODE_VIEWER_DIR3, SNAPSHOT_DB_NAME);
9301
+ const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
8809
9302
  if (storeDb && storeDbPath === dbPath)
8810
9303
  return storeDb;
8811
9304
  if (storeDb) {
@@ -8813,7 +9306,7 @@ async function getStoreDb(cwd) {
8813
9306
  storeDb.close();
8814
9307
  } catch {}
8815
9308
  }
8816
- mkdirSync4(join10(cwd, CODE_VIEWER_DIR3), { recursive: true });
9309
+ mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
8817
9310
  const DbClass = await getSqliteClass2();
8818
9311
  storeDb = new DbClass(dbPath);
8819
9312
  storeDbPath = dbPath;
@@ -8826,7 +9319,7 @@ async function getStoreDb(cwd) {
8826
9319
  return storeDb;
8827
9320
  }
8828
9321
  function makeId2(prefix) {
8829
- return `${prefix}-${randomBytes(8).toString("hex")}`;
9322
+ return `${prefix}-${randomBytes2(8).toString("hex")}`;
8830
9323
  }
8831
9324
  function hashPayload(payloadJson) {
8832
9325
  return createHash5("sha256").update(payloadJson).digest("hex");
@@ -9073,7 +9566,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
9073
9566
  });
9074
9567
  return { rows, total };
9075
9568
  }
9076
- var CODE_VIEWER_DIR3 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
9569
+ var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
9077
9570
  CREATE TABLE IF NOT EXISTS snapshots (
9078
9571
  id TEXT PRIMARY KEY,
9079
9572
  db_id TEXT NOT NULL,
@@ -9170,13 +9663,9 @@ var init_snapshot_runner = __esm(() => {
9170
9663
  });
9171
9664
 
9172
9665
  // 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";
9666
+ import { join as join12 } from "node:path";
9175
9667
  function tabsFilePath(root) {
9176
- return join11(root, CODE_VIEWER_DIR4, TABS_FILE_NAME);
9177
- }
9178
- function isEnoent2(err) {
9179
- return err?.code === "ENOENT";
9668
+ return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
9180
9669
  }
9181
9670
  function emptyState2() {
9182
9671
  return { version: 1, tabs: [], activeTabId: null };
@@ -9244,6 +9733,31 @@ function sanitizeEs(v) {
9244
9733
  return;
9245
9734
  return out;
9246
9735
  }
9736
+ function sanitizeS3(v) {
9737
+ if (!v || typeof v !== "object")
9738
+ return;
9739
+ const r = v;
9740
+ const out = {};
9741
+ const bucket = sanitizeOptionalString(r.bucket, MAX_S3_BUCKET_LEN);
9742
+ if (bucket !== undefined)
9743
+ out.bucket = bucket;
9744
+ const prefix = sanitizeOptionalString(r.prefix, MAX_S3_KEY_LEN);
9745
+ if (prefix !== undefined)
9746
+ out.prefix = prefix;
9747
+ const query = sanitizeOptionalString(r.query, MAX_S3_QUERY_LEN);
9748
+ if (query !== undefined)
9749
+ out.query = query;
9750
+ if (r.mode === "prefix" || r.mode === "contains")
9751
+ out.mode = r.mode;
9752
+ if (r.sort === "key-asc" || r.sort === "updated-desc")
9753
+ out.sort = r.sort;
9754
+ const key = sanitizeOptionalString(r.key, MAX_S3_KEY_LEN);
9755
+ if (key !== undefined)
9756
+ out.key = key;
9757
+ if (out.bucket === undefined && out.prefix === undefined && out.query === undefined && out.mode === undefined && out.sort === undefined && out.key === undefined)
9758
+ return;
9759
+ return out;
9760
+ }
9247
9761
  function sanitize(input) {
9248
9762
  if (!input || typeof input !== "object")
9249
9763
  return emptyState2();
@@ -9265,12 +9779,15 @@ function sanitize(input) {
9265
9779
  if (seenIds.has(id))
9266
9780
  continue;
9267
9781
  seenIds.add(id);
9268
- const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN) ?? null;
9782
+ const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN2) ?? null;
9269
9783
  if (isToolInternalDbId(dbId))
9270
9784
  continue;
9785
+ const schema = sanitizeOptionalString(tab.schema, MAX_SCHEMA_NAME_LEN);
9271
9786
  const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
9272
9787
  const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
9273
9788
  const out = { id, dbId, table, view };
9789
+ if (schema !== undefined)
9790
+ out.schema = schema;
9274
9791
  const sqlDraft = sanitizeOptionalString(tab.sqlDraft, MAX_SQL_DRAFT_LEN);
9275
9792
  if (sqlDraft !== undefined)
9276
9793
  out.sqlDraft = sqlDraft;
@@ -9288,6 +9805,9 @@ function sanitize(input) {
9288
9805
  const es = sanitizeEs(tab.es);
9289
9806
  if (es !== undefined)
9290
9807
  out.es = es;
9808
+ const s3 = sanitizeS3(tab.s3);
9809
+ if (s3 !== undefined)
9810
+ out.s3 = s3;
9291
9811
  tabs.push(out);
9292
9812
  }
9293
9813
  let activeTabId = sanitizeOptionalString(obj.activeTabId, MAX_TAB_ID_LEN) ?? null;
@@ -9297,69 +9817,14 @@ function sanitize(input) {
9297
9817
  return { version: 1, tabs, activeTabId };
9298
9818
  }
9299
9819
  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);
9820
+ return tabsStore.load(cwd);
9346
9821
  }
9347
9822
  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
- }
9823
+ return tabsStore.save(cwd, state);
9359
9824
  }
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;
9825
+ 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
9826
  var init_tabs_store = __esm(() => {
9362
- tabsWriteQueues = new Map;
9827
+ init_json_store();
9363
9828
  VALID_VIEWS = new Set([
9364
9829
  "data",
9365
9830
  "query",
@@ -9368,6 +9833,14 @@ var init_tabs_store = __esm(() => {
9368
9833
  "search",
9369
9834
  "snapshot"
9370
9835
  ]);
9836
+ tabsStore = createJsonFileStore({
9837
+ filePath: tabsFilePath,
9838
+ empty: emptyState2,
9839
+ sanitize,
9840
+ maxBytes: MAX_JSON_BYTES2,
9841
+ backupSuffix: "bak",
9842
+ sizeErrorMessage: "tabs state too large"
9843
+ });
9371
9844
  });
9372
9845
 
9373
9846
  // web-src/server/database/handle.ts
@@ -9396,7 +9869,7 @@ function sanitizeFilename(name) {
9396
9869
  function normalizeSchemaParam(value) {
9397
9870
  if (value === undefined || value === null || value === "")
9398
9871
  return;
9399
- if (value.length > MAX_SCHEMA_NAME_LEN) {
9872
+ if (value.length > MAX_SCHEMA_NAME_LEN2) {
9400
9873
  return textError("invalid schema parameter", 400);
9401
9874
  }
9402
9875
  if (hasControlCharacter(value)) {
@@ -10355,6 +10828,23 @@ async function handleTabsPut(cwd, req) {
10355
10828
  return textError(`failed to save tabs: ${message}`, 500);
10356
10829
  }
10357
10830
  }
10831
+ async function handleDbUiGet(cwd) {
10832
+ return jsonLoadResponse(() => loadDbUiState(cwd), "db UI", "failed to load db UI state");
10833
+ }
10834
+ async function handleDbUiPatch(cwd, req) {
10835
+ const body = await parseBoundedJsonBody(req, MAX_DB_UI_BODY_BYTES, "db UI body too large");
10836
+ if (body instanceof Response)
10837
+ return body;
10838
+ try {
10839
+ return json(await patchDbUiState(cwd, body));
10840
+ } catch (err) {
10841
+ const message = err instanceof Error ? err.message : String(err);
10842
+ if (message === "db UI state too large")
10843
+ return textError(message, 413);
10844
+ console.error("[code-viewer] db UI error:", err);
10845
+ return textError("failed to save db UI state", 500);
10846
+ }
10847
+ }
10358
10848
  async function handleClose(cwd, req, omitDirNames) {
10359
10849
  const body = await parsePostJsonBody(req);
10360
10850
  if (body instanceof Response)
@@ -10517,11 +11007,17 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
10517
11007
  methods: ["GET", "PUT", "POST"],
10518
11008
  sideEffect: (m) => m !== "GET",
10519
11009
  handler: () => method === "GET" ? handleTabsGet(cwd) : handleTabsPut(cwd, req)
11010
+ },
11011
+ "/_db/ui": {
11012
+ methods: ["GET", "PATCH"],
11013
+ sideEffect: (m) => m !== "GET",
11014
+ handler: () => method === "GET" ? handleDbUiGet(cwd) : handleDbUiPatch(cwd, req)
10520
11015
  }
10521
11016
  }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
10522
11017
  }
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;
11018
+ 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
11019
  var init_handle = __esm(() => {
11020
+ init_state_store();
10525
11021
  init_docker();
10526
11022
  init_docker_utils();
10527
11023
  init_sqlite();
@@ -10575,25 +11071,87 @@ var init_handle = __esm(() => {
10575
11071
  };
10576
11072
  });
10577
11073
 
11074
+ // web-src/server/state-route.ts
11075
+ var exports_state_route = {};
11076
+ __export(exports_state_route, {
11077
+ handleStateRoute: () => handleStateRoute
11078
+ });
11079
+ async function parseJsonBody(req) {
11080
+ return parseBoundedJsonBody(req, MAX_STATE_PATCH_BODY_BYTES, "state body too large");
11081
+ }
11082
+ async function handleSettingsGet(cwd) {
11083
+ return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
11084
+ }
11085
+ async function handleSettingsPatch(cwd, req) {
11086
+ const body = await parseJsonBody(req);
11087
+ if (body instanceof Response)
11088
+ return body;
11089
+ try {
11090
+ return json(await patchAppSettingsState(cwd, body));
11091
+ } catch (err) {
11092
+ const message = err instanceof Error ? err.message : String(err);
11093
+ if (message === "settings state too large")
11094
+ return textError(message, 413);
11095
+ console.error("[code-viewer] state error:", err);
11096
+ return textError("failed to save settings state", 500);
11097
+ }
11098
+ }
11099
+ async function handleViewGet(cwd) {
11100
+ return jsonLoadResponse(() => loadViewState(cwd), "state", "failed to load view state");
11101
+ }
11102
+ async function handleViewPatch(cwd, req) {
11103
+ const body = await parseJsonBody(req);
11104
+ if (body instanceof Response)
11105
+ return body;
11106
+ try {
11107
+ return json(await patchViewState(cwd, body));
11108
+ } catch (err) {
11109
+ const message = err instanceof Error ? err.message : String(err);
11110
+ if (message === "view state too large")
11111
+ return textError(message, 413);
11112
+ console.error("[code-viewer] state error:", err);
11113
+ return textError("failed to save view state", 500);
11114
+ }
11115
+ }
11116
+ async function handleStateRoute(req, url, cwd, sideEffectAllowed) {
11117
+ return dispatchRoutes(req, url, {
11118
+ "/_state/settings": {
11119
+ methods: ["GET", "PATCH"],
11120
+ sideEffect: (method) => method !== "GET",
11121
+ handler: () => req.method === "GET" ? handleSettingsGet(cwd) : handleSettingsPatch(cwd, req)
11122
+ },
11123
+ "/_state/view": {
11124
+ methods: ["GET", "PATCH"],
11125
+ sideEffect: (method) => method !== "GET",
11126
+ handler: () => req.method === "GET" ? handleViewGet(cwd) : handleViewPatch(cwd, req)
11127
+ }
11128
+ }, sideEffectAllowed, (res) => res, (err) => handleError("state", "handle state request", err));
11129
+ }
11130
+ var MAX_STATE_PATCH_BODY_BYTES = 1e6;
11131
+ var init_state_route = __esm(() => {
11132
+ init_handle_shared();
11133
+ init_state_store();
11134
+ });
11135
+
10578
11136
  // web-src/server/preview.ts
10579
11137
  var exports_preview = {};
10580
11138
  import {
10581
11139
  closeSync as closeSync2,
10582
11140
  constants,
10583
- existsSync as existsSync7,
11141
+ existsSync as existsSync6,
10584
11142
  lstatSync as lstatSync4,
10585
- mkdirSync as mkdirSync5,
11143
+ mkdirSync as mkdirSync4,
10586
11144
  openSync as openSync2,
10587
- readFileSync as readFileSync5,
11145
+ readFileSync as readFileSync4,
10588
11146
  realpathSync as realpathSync4,
10589
- renameSync as renameSync2,
11147
+ renameSync,
10590
11148
  statSync as statSync3,
10591
11149
  unlinkSync as unlinkSync2,
10592
11150
  watch,
10593
- writeFileSync as writeFileSync3
11151
+ writeFileSync as writeFileSync2
10594
11152
  } from "node:fs";
10595
11153
  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";
11154
+ import { basename as basename3, dirname as dirname3, extname as extname2, join as join13, relative as relative3 } from "node:path";
10597
11155
  function parseCli() {
10598
11156
  const rest = [];
10599
11157
  for (let i = 2;i < process.argv.length; i++) {
@@ -10734,10 +11292,10 @@ function staticFile(pathname) {
10734
11292
  const spec = map[pathname];
10735
11293
  if (!spec)
10736
11294
  return null;
10737
- const full = join12(WEB_ROOT, spec[0]);
10738
- if (!existsSync7(full))
11295
+ const full = join13(WEB_ROOT, spec[0]);
11296
+ if (!existsSync6(full))
10739
11297
  return text("not found", 404);
10740
- return new Response(readFileSync5(full), {
11298
+ return new Response(readFileSync4(full), {
10741
11299
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
10742
11300
  });
10743
11301
  }
@@ -10968,8 +11526,8 @@ function parseScopeExcludeNamesQuery(value) {
10968
11526
  return normalizeScopeExcludeNames(names);
10969
11527
  }
10970
11528
  function loadProjectConfig() {
10971
- const full = join12(cwd, ".code-viewer.json");
10972
- if (!existsSync7(full))
11529
+ const full = join13(cwd, ".code-viewer.json");
11530
+ if (!existsSync6(full))
10973
11531
  return null;
10974
11532
  let realCwd;
10975
11533
  let realConfig;
@@ -10979,10 +11537,10 @@ function loadProjectConfig() {
10979
11537
  } catch {
10980
11538
  return null;
10981
11539
  }
10982
- if (dirname2(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
11540
+ if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
10983
11541
  return null;
10984
11542
  try {
10985
- const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
11543
+ const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
10986
11544
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
10987
11545
  return null;
10988
11546
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -11033,8 +11591,8 @@ function safeWorktreePath(path) {
11033
11591
  return null;
11034
11592
  if (isGitInternalPath(path))
11035
11593
  return null;
11036
- const full = join12(cwd, path);
11037
- if (!existsSync7(full))
11594
+ const full = join13(cwd, path);
11595
+ if (!existsSync6(full))
11038
11596
  return null;
11039
11597
  let realCwd;
11040
11598
  let realFull;
@@ -11052,7 +11610,7 @@ function safeWorktreePath(path) {
11052
11610
  return realFull;
11053
11611
  }
11054
11612
  function worktreePath(path) {
11055
- return join12(cwd, path);
11613
+ return join13(cwd, path);
11056
11614
  }
11057
11615
  function safeOpenWorktreePath(path) {
11058
11616
  if (path === "") {
@@ -11068,7 +11626,7 @@ function safeOpenWorktreePath(path) {
11068
11626
  return safeWorktreePath(path);
11069
11627
  }
11070
11628
  function parentRepoPath(path) {
11071
- const parent = dirname2(path);
11629
+ const parent = dirname3(path);
11072
11630
  return parent === "." ? "" : parent;
11073
11631
  }
11074
11632
  function isoDate(ms) {
@@ -11135,7 +11693,7 @@ function readReadme(target, dirPath) {
11135
11693
  if (!full)
11136
11694
  continue;
11137
11695
  try {
11138
- return { path, text: readFileSync5(full, "utf8") };
11696
+ return { path, text: readFileSync4(full, "utf8") };
11139
11697
  } catch {
11140
11698
  continue;
11141
11699
  }
@@ -11256,7 +11814,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
11256
11814
  continue;
11257
11815
  let data;
11258
11816
  try {
11259
- data = readFileSync5(full);
11817
+ data = readFileSync4(full);
11260
11818
  } catch {
11261
11819
  continue;
11262
11820
  }
@@ -11792,10 +12350,10 @@ async function handleUploadFiles(req) {
11792
12350
  total += file.size;
11793
12351
  if (total > MAX_UPLOAD_TOTAL_BYTES)
11794
12352
  return text("upload too large", 413);
11795
- const target = join12(realDir, safeName);
11796
- if (relative3(realDir, dirname2(target)) !== "")
12353
+ const target = join13(realDir, safeName);
12354
+ if (relative3(realDir, dirname3(target)) !== "")
11797
12355
  return text("invalid filename", 400);
11798
- if (existsSync7(target))
12356
+ if (existsSync6(target))
11799
12357
  return text("file exists", 409);
11800
12358
  uploads.push({ file, name: safeName, target });
11801
12359
  }
@@ -11804,7 +12362,7 @@ async function handleUploadFiles(req) {
11804
12362
  for (const upload of uploads) {
11805
12363
  const fd = openSync2(upload.target, uploadOpenFlags(), 420);
11806
12364
  try {
11807
- writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
12365
+ writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
11808
12366
  } finally {
11809
12367
  closeSync2(fd);
11810
12368
  }
@@ -11914,12 +12472,12 @@ function triggerUpdate(changedPaths) {
11914
12472
  sendSse("update", data);
11915
12473
  }
11916
12474
  function moveMacPathIntoTrash(path) {
11917
- const trashDir = join12(homedir3(), ".Trash");
12475
+ const trashDir = join13(homedir3(), ".Trash");
11918
12476
  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)}`);
12477
+ const target = join13(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
11920
12478
  try {
11921
- mkdirSync5(trashDir, { recursive: true });
11922
- renameSync2(path, target);
12479
+ mkdirSync4(trashDir, { recursive: true });
12480
+ renameSync(path, target);
11923
12481
  return { ok: true, trashPath: target };
11924
12482
  } catch (error) {
11925
12483
  return { ok: false, error: String(error) };
@@ -11950,20 +12508,20 @@ function restoreTrashPath(originalPath, trashPath) {
11950
12508
  if (!parentFullPath)
11951
12509
  return { ok: false, error: "invalid restore target" };
11952
12510
  const original = worktreePath(originalPath);
11953
- if (existsSync7(original))
12511
+ if (existsSync6(original))
11954
12512
  return { ok: false, error: "restore target exists" };
11955
12513
  if (trashPath) {
11956
12514
  if (process.platform !== "darwin")
11957
12515
  return { ok: false, error: "invalid trash handle" };
11958
- if (!existsSync7(trashPath))
12516
+ if (!existsSync6(trashPath))
11959
12517
  return { ok: false, error: "trash item not found" };
11960
12518
  try {
11961
- const trashRoot = join12(homedir3(), ".Trash");
12519
+ const trashRoot = join13(homedir3(), ".Trash");
11962
12520
  const trashRelative = relative3(trashRoot, trashPath);
11963
12521
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
11964
12522
  return { ok: false, error: "invalid trash handle" };
11965
- mkdirSync5(dirname2(original), { recursive: true });
11966
- renameSync2(trashPath, original);
12523
+ mkdirSync4(dirname3(original), { recursive: true });
12524
+ renameSync(trashPath, original);
11967
12525
  return { ok: true };
11968
12526
  } catch (error) {
11969
12527
  return { ok: false, error: String(error) };
@@ -12108,11 +12666,11 @@ async function handleCreateDirectory(req) {
12108
12666
  const targetPath = dir ? `${dir}/${name}` : name;
12109
12667
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
12110
12668
  return text("invalid target", 400);
12111
- const target = join12(parent, name);
12112
- if (existsSync7(target))
12669
+ const target = join13(parent, name);
12670
+ if (existsSync6(target))
12113
12671
  return text("already exists", 409);
12114
12672
  try {
12115
- mkdirSync5(target, { recursive: false });
12673
+ mkdirSync4(target, { recursive: false });
12116
12674
  } catch (error) {
12117
12675
  if (error.code === "EEXIST")
12118
12676
  return text("already exists", 409);
@@ -12158,7 +12716,7 @@ function annotationSse(kind, sessionId, entryId) {
12158
12716
  }
12159
12717
  async function handleAnnotations(req) {
12160
12718
  if (req.method === "GET")
12161
- return json2(loadAnnotationsState(cwd));
12719
+ return json2(await loadAnnotationsState(cwd));
12162
12720
  if (req.method !== "POST")
12163
12721
  return text("method not allowed", 405);
12164
12722
  if (!sideEffectRequestAllowed(req))
@@ -12182,8 +12740,8 @@ async function handleAnnotations(req) {
12182
12740
  const action = body.action;
12183
12741
  if (action === "start") {
12184
12742
  const title = typeof body.title === "string" ? body.title : "";
12185
- const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
12186
- saveAnnotationsState(cwd, started.state);
12743
+ const started = startAnnotationSession(await loadAnnotationsState(cwd), title, new Date().toISOString());
12744
+ await saveAnnotationsState(cwd, started.state);
12187
12745
  annotationSse("start", started.session.id);
12188
12746
  return json2({ ok: true, session: started.session });
12189
12747
  }
@@ -12201,7 +12759,7 @@ async function handleAnnotations(req) {
12201
12759
  if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
12202
12760
  return text("forbidden", 403);
12203
12761
  }
12204
- const result = addAnnotationEntry(loadAnnotationsState(cwd), {
12762
+ const result = addAnnotationEntry(await loadAnnotationsState(cwd), {
12205
12763
  session_id: typeof body.session_id === "string" ? body.session_id : undefined,
12206
12764
  session_title: typeof body.session_title === "string" ? body.session_title : undefined,
12207
12765
  path,
@@ -12216,7 +12774,7 @@ async function handleAnnotations(req) {
12216
12774
  }, new Date().toISOString());
12217
12775
  if (result.ok === false)
12218
12776
  return text(result.error, 400);
12219
- saveAnnotationsState(cwd, result.state);
12777
+ await saveAnnotationsState(cwd, result.state);
12220
12778
  annotationSse("add", result.session.id, result.entry.id);
12221
12779
  return json2({
12222
12780
  ok: true,
@@ -12230,14 +12788,14 @@ async function handleAnnotations(req) {
12230
12788
  const id = typeof body.id === "string" ? body.id : "";
12231
12789
  if (!id)
12232
12790
  return text("invalid id", 400);
12233
- const result = moveAnnotationEntry(loadAnnotationsState(cwd), id, {
12791
+ const result = moveAnnotationEntry(await loadAnnotationsState(cwd), id, {
12234
12792
  before_id: typeof body.before_id === "string" ? body.before_id : undefined,
12235
12793
  after_id: typeof body.after_id === "string" ? body.after_id : undefined,
12236
12794
  position: typeof body.position === "number" ? body.position : undefined
12237
12795
  });
12238
12796
  if (result.ok === false)
12239
12797
  return text(result.error, 400);
12240
- saveAnnotationsState(cwd, result.state);
12798
+ await saveAnnotationsState(cwd, result.state);
12241
12799
  annotationSse("update", result.session.id, result.entry.id);
12242
12800
  return json2({
12243
12801
  ok: true,
@@ -12249,9 +12807,9 @@ async function handleAnnotations(req) {
12249
12807
  const id = typeof body.id === "string" ? body.id : "";
12250
12808
  if (!id)
12251
12809
  return text("invalid id", 400);
12252
- const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
12810
+ const result = deleteAnnotationById(await loadAnnotationsState(cwd), id);
12253
12811
  if (result.removed) {
12254
- saveAnnotationsState(cwd, result.state);
12812
+ await saveAnnotationsState(cwd, result.state);
12255
12813
  annotationSse("delete");
12256
12814
  }
12257
12815
  return json2({ ok: true, removed: result.removed });
@@ -12261,10 +12819,10 @@ async function handleAnnotations(req) {
12261
12819
  const title = typeof body.title === "string" ? body.title : "";
12262
12820
  if (!id)
12263
12821
  return text("invalid id", 400);
12264
- const result = renameAnnotationSession(loadAnnotationsState(cwd), id, title);
12822
+ const result = renameAnnotationSession(await loadAnnotationsState(cwd), id, title);
12265
12823
  if (!result.renamed)
12266
12824
  return text("session not found", 404);
12267
- saveAnnotationsState(cwd, result.state);
12825
+ await saveAnnotationsState(cwd, result.state);
12268
12826
  annotationSse("update", id);
12269
12827
  return json2({ ok: true });
12270
12828
  }
@@ -12272,18 +12830,18 @@ async function handleAnnotations(req) {
12272
12830
  const id = typeof body.id === "string" ? body.id : "";
12273
12831
  if (!id)
12274
12832
  return text("invalid id", 400);
12275
- const result = updateAnnotationEntry(loadAnnotationsState(cwd), id, {
12833
+ const result = updateAnnotationEntry(await loadAnnotationsState(cwd), id, {
12276
12834
  title: typeof body.title === "string" ? body.title : undefined,
12277
12835
  body: typeof body.body === "string" ? body.body : undefined
12278
12836
  });
12279
12837
  if (result.ok === false)
12280
12838
  return text(result.error, 400);
12281
- saveAnnotationsState(cwd, result.state);
12839
+ await saveAnnotationsState(cwd, result.state);
12282
12840
  annotationSse("update", undefined, id);
12283
12841
  return json2({ ok: true, entry: result.entry });
12284
12842
  }
12285
12843
  if (action === "clear") {
12286
- saveAnnotationsState(cwd, emptyAnnotationsState());
12844
+ await saveAnnotationsState(cwd, emptyAnnotationsState());
12287
12845
  annotationSse("clear");
12288
12846
  return json2({ ok: true });
12289
12847
  }
@@ -12352,8 +12910,8 @@ var init_preview = __esm(async () => {
12352
12910
  init_search();
12353
12911
  init_server_registry();
12354
12912
  init_worktree_watcher();
12355
- WEB_ROOT = join12(ROOT, "web");
12356
- VERSION = JSON.parse(readFileSync5(join12(ROOT, "package.json"), "utf8")).version;
12913
+ WEB_ROOT = join13(ROOT, "web");
12914
+ VERSION = JSON.parse(readFileSync4(join13(ROOT, "package.json"), "utf8")).version;
12357
12915
  DEFAULT_ARGS = ["HEAD"];
12358
12916
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
12359
12917
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -12457,6 +13015,12 @@ var init_preview = __esm(async () => {
12457
13015
  if (dbResponse)
12458
13016
  return dbResponse;
12459
13017
  }
13018
+ if (url.pathname.startsWith("/_state/")) {
13019
+ const { handleStateRoute: handleStateRoute2 } = await Promise.resolve().then(() => (init_state_route(), exports_state_route));
13020
+ const stateResponse = await handleStateRoute2(req, url, cwd, sideEffectRequestAllowed);
13021
+ if (stateResponse)
13022
+ return stateResponse;
13023
+ }
12460
13024
  if (url.pathname === "/_annotations")
12461
13025
  return handleAnnotations(req);
12462
13026
  if (url.pathname === "/_refs")