@youtyan/code-viewer 0.2.5 → 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 +1218 -345
  2. package/package.json +1 -1
  3. package/web/app.js +3313 -3031
@@ -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)
@@ -1458,6 +1537,9 @@ var init_git = __esm(() => {
1458
1537
  "vendor",
1459
1538
  ".cache",
1460
1539
  "coverage",
1540
+ "tmp",
1541
+ "log",
1542
+ "storage",
1461
1543
  "DerivedData",
1462
1544
  "Pods",
1463
1545
  "bin",
@@ -1468,11 +1550,11 @@ var init_git = __esm(() => {
1468
1550
  // web-src/server/server-registry.ts
1469
1551
  import { createHash } from "node:crypto";
1470
1552
  import {
1471
- existsSync as existsSync3,
1472
- mkdirSync as mkdirSync2,
1473
- readFileSync as readFileSync3,
1553
+ existsSync as existsSync2,
1554
+ mkdirSync,
1555
+ readFileSync as readFileSync2,
1474
1556
  unlinkSync,
1475
- writeFileSync as writeFileSync2
1557
+ writeFileSync
1476
1558
  } from "node:fs";
1477
1559
  import { homedir } from "node:os";
1478
1560
  import { join as join3 } from "node:path";
@@ -1485,17 +1567,17 @@ function serverRegistryFilePath(root) {
1485
1567
  }
1486
1568
  function writeServerRegistry(entry) {
1487
1569
  try {
1488
- mkdirSync2(registryDir(), { recursive: true });
1489
- writeFileSync2(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
1570
+ mkdirSync(registryDir(), { recursive: true });
1571
+ writeFileSync(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
1490
1572
  `, "utf8");
1491
1573
  } catch {}
1492
1574
  }
1493
1575
  function readServerRegistry(root) {
1494
1576
  const file = serverRegistryFilePath(root);
1495
- if (!existsSync3(file))
1577
+ if (!existsSync2(file))
1496
1578
  return null;
1497
1579
  try {
1498
- const raw = JSON.parse(readFileSync3(file, "utf8"));
1580
+ const raw = JSON.parse(readFileSync2(file, "utf8"));
1499
1581
  if (!raw || typeof raw !== "object")
1500
1582
  return null;
1501
1583
  const entry = raw;
@@ -1529,7 +1611,7 @@ __export(exports_annotate_cli, {
1529
1611
  ANNOTATE_HELP: () => ANNOTATE_HELP,
1530
1612
  ANNOTATE_AGENT_HELP: () => ANNOTATE_AGENT_HELP
1531
1613
  });
1532
- import { readFileSync as readFileSync4, realpathSync } from "node:fs";
1614
+ import { readFileSync as readFileSync3, realpathSync } from "node:fs";
1533
1615
  function takeValue(argv, index, flag) {
1534
1616
  const value = argv[index + 1];
1535
1617
  if (value === undefined)
@@ -1958,7 +2040,7 @@ async function annotationBodyFromCommand(command) {
1958
2040
  let body = command.body;
1959
2041
  if (body === undefined && command.bodyFile !== undefined) {
1960
2042
  try {
1961
- body = readFileSync4(command.bodyFile, "utf8");
2043
+ body = readFileSync3(command.bodyFile, "utf8");
1962
2044
  } catch {
1963
2045
  console.error(`could not read --body-file: ${command.bodyFile}`);
1964
2046
  process.exit(1);
@@ -2026,7 +2108,7 @@ async function runAnnotateCli(argv) {
2026
2108
  let sql = command.sql;
2027
2109
  if (sql === undefined && command.sqlFile !== undefined) {
2028
2110
  try {
2029
- sql = readFileSync4(command.sqlFile, "utf8");
2111
+ sql = readFileSync3(command.sqlFile, "utf8");
2030
2112
  } catch {
2031
2113
  console.error(`could not read --sql-file: ${command.sqlFile}`);
2032
2114
  process.exit(1);
@@ -2095,7 +2177,7 @@ async function runAnnotateCli(argv) {
2095
2177
  if (command.kind === "edit") {
2096
2178
  let bodyText = command.body;
2097
2179
  if (command.bodyFile !== undefined)
2098
- bodyText = readFileSync4(command.bodyFile, "utf8");
2180
+ bodyText = readFileSync3(command.bodyFile, "utf8");
2099
2181
  if (bodyText === undefined) {
2100
2182
  const stdin = await readStdin();
2101
2183
  if (stdin.trim())
@@ -2653,16 +2735,16 @@ var init_query_cli = __esm(() => {
2653
2735
  });
2654
2736
 
2655
2737
  // web-src/server/root.ts
2656
- import { existsSync as existsSync4 } from "node:fs";
2657
- 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";
2658
2740
  import { fileURLToPath } from "node:url";
2659
2741
  function findRoot(start) {
2660
2742
  let current = start;
2661
2743
  for (let i = 0;i < 5; i++) {
2662
- if (existsSync4(join4(current, "package.json")) && existsSync4(join4(current, "web"))) {
2744
+ if (existsSync3(join4(current, "package.json")) && existsSync3(join4(current, "web"))) {
2663
2745
  return normalize(current);
2664
2746
  }
2665
- const parent = dirname(current);
2747
+ const parent = dirname2(current);
2666
2748
  if (parent === current)
2667
2749
  break;
2668
2750
  current = parent;
@@ -2671,7 +2753,7 @@ function findRoot(start) {
2671
2753
  }
2672
2754
  var ROOT;
2673
2755
  var init_root = __esm(() => {
2674
- ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
2756
+ ROOT = findRoot(dirname2(fileURLToPath(import.meta.url)));
2675
2757
  });
2676
2758
 
2677
2759
  // web-src/server/skill-cli.ts
@@ -2683,7 +2765,7 @@ __export(exports_skill_cli, {
2683
2765
  SKILL_HELP: () => SKILL_HELP,
2684
2766
  AGENT_SKILL_DIRS: () => AGENT_SKILL_DIRS
2685
2767
  });
2686
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "node:fs";
2768
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
2687
2769
  import { homedir as homedir2 } from "node:os";
2688
2770
  import { join as join5, resolve } from "node:path";
2689
2771
  function parseAgentList(value) {
@@ -2740,7 +2822,7 @@ function parseSkillArgs(argv) {
2740
2822
  return { ok: true, args: { kind: "install", agents, global, cwd } };
2741
2823
  }
2742
2824
  function installSkill(args, deps) {
2743
- if (!existsSync5(join5(deps.sourceDir, "SKILL.md"))) {
2825
+ if (!existsSync4(join5(deps.sourceDir, "SKILL.md"))) {
2744
2826
  return {
2745
2827
  ok: false,
2746
2828
  error: `bundled skill not found at ${deps.sourceDir}`
@@ -2750,9 +2832,9 @@ function installSkill(args, deps) {
2750
2832
  const results = [];
2751
2833
  for (const agent of args.agents) {
2752
2834
  const target = join5(base, AGENT_SKILL_DIRS[agent], "skills", SKILL_NAME);
2753
- const action = existsSync5(target) ? "updated" : "installed";
2835
+ const action = existsSync4(target) ? "updated" : "installed";
2754
2836
  try {
2755
- mkdirSync3(target, { recursive: true });
2837
+ mkdirSync2(target, { recursive: true });
2756
2838
  cpSync(deps.sourceDir, target, { recursive: true });
2757
2839
  } catch (error) {
2758
2840
  return { ok: false, error: String(error) };
@@ -3681,14 +3763,23 @@ function startWorktreeUpdateWatch(options) {
3681
3763
  const setTimer = options.setTimeoutFn || setTimeout;
3682
3764
  const clearTimer = options.clearTimeoutFn || clearTimeout;
3683
3765
  const debounceMs = options.debounceMs ?? 250;
3766
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
3684
3767
  const watchers = new Map;
3685
3768
  const signatures = new Map;
3686
3769
  const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
3687
3770
  const initialScanQueue = [];
3688
3771
  let initialScanTimer = null;
3772
+ const pendingPathInspections = new Map;
3773
+ let pathInspectionTimer = null;
3689
3774
  let timer = null;
3690
3775
  const pendingChangedPaths = new Set;
3776
+ let watchLimitReported = false;
3691
3777
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
3778
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
3779
+ const ignoredDirectory = (dir) => {
3780
+ const rel = directoryRelativePath(dir);
3781
+ return Boolean(rel && ignored(rel));
3782
+ };
3692
3783
  const scheduleUpdate = (changedPath) => {
3693
3784
  if (changedPath)
3694
3785
  pendingChangedPaths.add(changedPath);
@@ -3701,6 +3792,12 @@ function startWorktreeUpdateWatch(options) {
3701
3792
  options.onUpdate(paths);
3702
3793
  }, debounceMs);
3703
3794
  };
3795
+ const reportWatchLimit = () => {
3796
+ if (watchLimitReported)
3797
+ return;
3798
+ watchLimitReported = true;
3799
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
3800
+ };
3704
3801
  const closeSubtree = (dir) => {
3705
3802
  for (const [watchedDir, watcher] of [...watchers]) {
3706
3803
  if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
@@ -3717,7 +3814,12 @@ function startWorktreeUpdateWatch(options) {
3717
3814
  clearTimer(initialScanTimer);
3718
3815
  initialScanTimer = null;
3719
3816
  }
3817
+ if (pathInspectionTimer) {
3818
+ clearTimer(pathInspectionTimer);
3819
+ pathInspectionTimer = null;
3820
+ }
3720
3821
  initialScanQueue.length = 0;
3822
+ pendingPathInspections.clear();
3721
3823
  for (const watcher of [...watchers.values()]) {
3722
3824
  try {
3723
3825
  watcher.close?.();
@@ -3738,27 +3840,82 @@ function startWorktreeUpdateWatch(options) {
3738
3840
  for (const entry of entries) {
3739
3841
  if (!entry.isDirectory())
3740
3842
  continue;
3741
- children.push(join7(dir, entry.name));
3843
+ const child = join7(dir, entry.name);
3844
+ if (ignoredDirectory(child))
3845
+ continue;
3846
+ children.push(child);
3742
3847
  }
3743
3848
  return children;
3744
3849
  };
3745
3850
  const processInitialScanQueue = () => {
3746
3851
  initialScanTimer = null;
3852
+ if (watchers.size >= maxWatchedDirectories) {
3853
+ reportWatchLimit();
3854
+ initialScanQueue.length = 0;
3855
+ return;
3856
+ }
3747
3857
  const next = initialScanQueue.shift();
3748
3858
  if (next)
3749
3859
  watchDirectory(next, true);
3860
+ if (watchers.size >= maxWatchedDirectories) {
3861
+ reportWatchLimit();
3862
+ initialScanQueue.length = 0;
3863
+ }
3750
3864
  if (initialScanQueue.length)
3751
3865
  initialScanTimer = setTimer(processInitialScanQueue, 50);
3752
3866
  };
3753
3867
  const queueInitialChildren = (dir) => {
3754
- initialScanQueue.push(...readChildDirectories(dir));
3868
+ const remaining = maxWatchedDirectories - watchers.size;
3869
+ if (remaining <= 0) {
3870
+ reportWatchLimit();
3871
+ return;
3872
+ }
3873
+ const children = readChildDirectories(dir);
3874
+ if (children.length > remaining)
3875
+ reportWatchLimit();
3876
+ initialScanQueue.push(...children.slice(0, remaining));
3755
3877
  if (!initialScanTimer)
3756
3878
  initialScanTimer = setTimer(processInitialScanQueue, 5000);
3757
3879
  };
3880
+ const processChangedPath = (changed, fullChangedPath) => {
3881
+ const known = watchers.has(fullChangedPath);
3882
+ if (isDirectory(fullChangedPath)) {
3883
+ if (known) {
3884
+ const signature = directorySignature(fullChangedPath);
3885
+ if (signature && signature !== signatures.get(fullChangedPath)) {
3886
+ closeSubtree(fullChangedPath);
3887
+ watchDirectory(fullChangedPath, initialScanAsync);
3888
+ }
3889
+ scheduleUpdate(changed);
3890
+ return;
3891
+ }
3892
+ watchDirectory(fullChangedPath, initialScanAsync);
3893
+ } else if (known) {
3894
+ closeSubtree(fullChangedPath);
3895
+ }
3896
+ scheduleUpdate(changed);
3897
+ };
3898
+ const processPathInspections = () => {
3899
+ pathInspectionTimer = null;
3900
+ const entries = [...pendingPathInspections];
3901
+ pendingPathInspections.clear();
3902
+ for (const [changed, fullChangedPath] of entries) {
3903
+ processChangedPath(changed, fullChangedPath);
3904
+ }
3905
+ };
3906
+ const queuePathInspection = (changed, fullChangedPath) => {
3907
+ pendingPathInspections.set(changed, fullChangedPath);
3908
+ if (!pathInspectionTimer)
3909
+ pathInspectionTimer = setTimer(processPathInspections, 25);
3910
+ };
3758
3911
  const watchDirectory = (dir, initialScan = false) => {
3759
3912
  if (watchers.has(dir))
3760
3913
  return;
3761
- const rel = normalizeRelativePath(relative(options.root, dir));
3914
+ if (watchers.size >= maxWatchedDirectories) {
3915
+ reportWatchLimit();
3916
+ return;
3917
+ }
3918
+ const rel = directoryRelativePath(dir);
3762
3919
  if (rel && ignored(rel))
3763
3920
  return;
3764
3921
  try {
@@ -3773,22 +3930,11 @@ function startWorktreeUpdateWatch(options) {
3773
3930
  const fullChangedPath = join7(options.root, changed);
3774
3931
  if (!isInsideRoot(options.root, fullChangedPath))
3775
3932
  return;
3776
- const known = watchers.has(fullChangedPath);
3777
- if (isDirectory(fullChangedPath)) {
3778
- if (known) {
3779
- const signature2 = directorySignature(fullChangedPath);
3780
- if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
3781
- closeSubtree(fullChangedPath);
3782
- watchDirectory(fullChangedPath);
3783
- }
3784
- scheduleUpdate(changed);
3785
- return;
3786
- }
3787
- watchDirectory(fullChangedPath);
3788
- } else if (known) {
3789
- closeSubtree(fullChangedPath);
3933
+ if (initialScanAsync) {
3934
+ queuePathInspection(changed, fullChangedPath);
3935
+ return;
3790
3936
  }
3791
- scheduleUpdate(changed);
3937
+ processChangedPath(changed, fullChangedPath);
3792
3938
  }) || {};
3793
3939
  watchers.set(dir, watcher);
3794
3940
  const signature = directorySignature(dir);
@@ -3814,12 +3960,17 @@ function startWorktreeUpdateWatch(options) {
3814
3960
  queueInitialChildren(dir);
3815
3961
  return;
3816
3962
  }
3963
+ if (watchers.size >= maxWatchedDirectories) {
3964
+ reportWatchLimit();
3965
+ return;
3966
+ }
3817
3967
  for (const child of readChildDirectories(dir))
3818
3968
  watchDirectory(child);
3819
3969
  };
3820
3970
  watchDirectory(options.root, true);
3821
3971
  return { started: watchers.size > 0, close: closeAll };
3822
3972
  }
3973
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
3823
3974
  var init_worktree_watcher = __esm(() => {
3824
3975
  init_search();
3825
3976
  });
@@ -3851,6 +4002,366 @@ function makeId(prefix) {
3851
4002
  return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
3852
4003
  }
3853
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
+
3854
4365
  // web-src/server/database/adapters/abort.ts
3855
4366
  function abortError(message = "operation aborted") {
3856
4367
  const err = new Error(message);
@@ -5554,14 +6065,14 @@ var init_connection_pool = __esm(() => {
5554
6065
  // web-src/server/database/discovery.ts
5555
6066
  import {
5556
6067
  closeSync,
5557
- existsSync as existsSync6,
6068
+ existsSync as existsSync5,
5558
6069
  openSync,
5559
6070
  readSync,
5560
6071
  realpathSync as realpathSync3,
5561
6072
  statSync as statSync2
5562
6073
  } from "node:fs";
5563
- import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
5564
- 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";
5565
6076
  function isSqliteFile(fullPath) {
5566
6077
  try {
5567
6078
  const stat2 = statSync2(fullPath);
@@ -5632,7 +6143,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
5632
6143
  return;
5633
6144
  if (omitSet.has(entry.toLowerCase()))
5634
6145
  continue;
5635
- const full = join8(dir, entry);
6146
+ const full = join9(dir, entry);
5636
6147
  let entryStat;
5637
6148
  try {
5638
6149
  entryStat = await lstat(full);
@@ -5676,8 +6187,8 @@ function validateDbPath(cwd, dbPath) {
5676
6187
  const parts = dbPath.split(/[\\/]+/);
5677
6188
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
5678
6189
  return null;
5679
- const full = join8(cwd, dbPath);
5680
- if (!existsSync6(full))
6190
+ const full = join9(cwd, dbPath);
6191
+ if (!existsSync5(full))
5681
6192
  return null;
5682
6193
  let realCwd;
5683
6194
  let realFull;
@@ -5813,7 +6324,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
5813
6324
  }
5814
6325
  async function readDotenvAsync(composeDir) {
5815
6326
  try {
5816
- const content = await readFile(join8(composeDir, ".env"), "utf-8");
6327
+ const content = await readFile2(join9(composeDir, ".env"), "utf-8");
5817
6328
  return parseDotenvContent(content);
5818
6329
  } catch {
5819
6330
  return {};
@@ -5855,7 +6366,7 @@ function parseComposeEnv(serviceBlock, composeDirEnv = {}) {
5855
6366
  }
5856
6367
  return env;
5857
6368
  }
5858
- function parseComposePortMappings(serviceBlock) {
6369
+ function parseComposePortMappings(serviceBlock, composeDirEnv = {}) {
5859
6370
  const portsMatch = serviceBlock.match(/^[ \t]+ports:\s*\n((?:[ \t]+- [^\n]+\n?)*)/m);
5860
6371
  if (!portsMatch)
5861
6372
  return [];
@@ -5865,26 +6376,45 @@ function parseComposePortMappings(serviceBlock) {
5865
6376
  const trimmed = line.trim();
5866
6377
  if (!trimmed.startsWith("-"))
5867
6378
  continue;
5868
- const value = trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim();
6379
+ const value = resolveEnvValue(trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim(), composeDirEnv);
5869
6380
  if (!value || value.includes("target:"))
5870
6381
  continue;
5871
6382
  const parts = value.split(":");
5872
6383
  const container = parts.pop()?.trim() || "";
5873
6384
  const host = parts.pop()?.trim() || "";
5874
- if (!/^\d+$/.test(container))
6385
+ const containerPorts = expandPortRange(container);
6386
+ const hostPorts = host ? expandPortRange(host) : [];
6387
+ if (!containerPorts)
5875
6388
  continue;
5876
- if (host && !/^\d+$/.test(host))
6389
+ if (host && (!hostPorts || hostPorts.length !== containerPorts.length)) {
5877
6390
  continue;
5878
- mappings.push({ host, container });
6391
+ }
6392
+ for (let i = 0;i < containerPorts.length; i++) {
6393
+ mappings.push({
6394
+ host: hostPorts ? (hostPorts[i] ?? "").toString() : "",
6395
+ container: containerPorts[i].toString()
6396
+ });
6397
+ }
5879
6398
  }
5880
6399
  return mappings;
5881
6400
  }
5882
- function parseComposePorts(serviceBlock) {
5883
- const first = parseComposePortMappings(serviceBlock).find((m) => m.host);
6401
+ function expandPortRange(value) {
6402
+ const match = value.match(/^(\d+)(?:-(\d+))?$/);
6403
+ if (!match)
6404
+ return null;
6405
+ const start = Number(match[1]);
6406
+ const end = Number(match[2] || match[1]);
6407
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end > 65535 || end < start) {
6408
+ return null;
6409
+ }
6410
+ return Array.from({ length: end - start + 1 }, (_, idx) => start + idx);
6411
+ }
6412
+ function parseComposePorts(serviceBlock, composeDirEnv = {}) {
6413
+ const first = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.host);
5884
6414
  return first?.host || null;
5885
6415
  }
5886
- function parseComposeHostPortForContainer(serviceBlock, containerPort) {
5887
- const found = parseComposePortMappings(serviceBlock).find((m) => m.container === containerPort && m.host);
6416
+ function parseComposeHostPortForContainer(serviceBlock, containerPort, composeDirEnv = {}) {
6417
+ const found = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.container === containerPort && m.host);
5888
6418
  return found?.host || null;
5889
6419
  }
5890
6420
  function parseComposeContainerPort(serviceBlock) {
@@ -5935,7 +6465,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
5935
6465
  continue;
5936
6466
  const defaultPort = defaultPortFor(kind, image, env);
5937
6467
  const serviceContainerPort = kind === "s3" ? defaultPort : containerPort || defaultPort;
5938
- const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort) || parseComposeHostPortForContainer(svcBlock, defaultPort) || parseComposePorts(svcBlock);
6468
+ const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
5939
6469
  const hostPort = kind === "s3" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
5940
6470
  const imageLabel = image ?? `build:${kind}`;
5941
6471
  const id = isRoot ? `docker:${svc.name}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}`;
@@ -5968,7 +6498,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
5968
6498
  async function parseComposeFileAsync(filepath, composeDir, cwd, results) {
5969
6499
  let content;
5970
6500
  try {
5971
- content = await readFile(filepath, "utf-8");
6501
+ content = await readFile2(filepath, "utf-8");
5972
6502
  } catch {
5973
6503
  return;
5974
6504
  }
@@ -6014,7 +6544,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6014
6544
  if (depth > MAX_SCAN_DEPTH)
6015
6545
  return;
6016
6546
  for (const filename of COMPOSE_FILENAMES) {
6017
- const filepath = join8(dir, filename);
6547
+ const filepath = join9(dir, filename);
6018
6548
  if (await pathExistsAsync(filepath)) {
6019
6549
  await parseComposeFileAsync(filepath, dir, cwd, results);
6020
6550
  break;
@@ -6037,7 +6567,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
6037
6567
  return;
6038
6568
  if (omitSet.has(entry.toLowerCase()))
6039
6569
  continue;
6040
- const full = join8(dir, entry);
6570
+ const full = join9(dir, entry);
6041
6571
  let entryStat;
6042
6572
  try {
6043
6573
  entryStat = await lstat(full);
@@ -6672,6 +7202,67 @@ function textError(message, status) {
6672
7202
  }
6673
7203
  });
6674
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
+ }
7231
+ function waitForCallerAbort(promise, signal, message) {
7232
+ if (!signal)
7233
+ return promise;
7234
+ if (signal.aborted)
7235
+ return Promise.reject(abortError(message));
7236
+ return new Promise((resolve2, reject) => {
7237
+ let settled = false;
7238
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
7239
+ const onAbort = () => {
7240
+ if (settled)
7241
+ return;
7242
+ settled = true;
7243
+ cleanup();
7244
+ reject(abortError(message));
7245
+ };
7246
+ signal.addEventListener("abort", onAbort, { once: true });
7247
+ promise.then((value) => {
7248
+ if (settled)
7249
+ return;
7250
+ settled = true;
7251
+ cleanup();
7252
+ resolve2(value);
7253
+ }, (err) => {
7254
+ if (settled)
7255
+ return;
7256
+ settled = true;
7257
+ cleanup();
7258
+ reject(err);
7259
+ });
7260
+ });
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
+ }
6675
7266
  async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
6676
7267
  if (!dbParam)
6677
7268
  return textError("missing db parameter", 400);
@@ -6681,10 +7272,26 @@ async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omi
6681
7272
  const parsed = parseDockerDbId(dbParam);
6682
7273
  if (!parsed)
6683
7274
  return textError("invalid docker db id", 400);
6684
- const info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
7275
+ let info;
7276
+ try {
7277
+ info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
7278
+ } catch (err) {
7279
+ if (isAbortLikeError(err, signal)) {
7280
+ return textError(`${kind} lookup aborted`, 503);
7281
+ }
7282
+ throw err;
7283
+ }
6685
7284
  if (!info)
6686
7285
  return textError(`${kind} service not found`, 404);
6687
- const explorer = await cache.getOrOpenAsync(dbParam, () => openFn(info));
7286
+ let explorer;
7287
+ try {
7288
+ explorer = await waitForCallerAbort(cache.getOrOpenAsync(dbParam, () => openFn(info)), signal, `${kind} open aborted`);
7289
+ } catch (err) {
7290
+ if (isAbortLikeError(err, signal)) {
7291
+ return textError(`${kind} open aborted`, 503);
7292
+ }
7293
+ throw err;
7294
+ }
6688
7295
  return { dbId: dbParam, explorer };
6689
7296
  }
6690
7297
  async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
@@ -6722,6 +7329,9 @@ function handleError(prefix, action, err) {
6722
7329
  if (isDockerComposeServiceUnavailableError(err)) {
6723
7330
  return textError(message, err.status);
6724
7331
  }
7332
+ if (isFilesystemAccessError(err)) {
7333
+ return textError(`failed to ${action}`, 500);
7334
+ }
6725
7335
  return textError(`failed to ${action}: ${message}`, 500);
6726
7336
  }
6727
7337
  var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
@@ -7591,6 +8201,31 @@ function s3ObjectName(key) {
7591
8201
  // web-src/server/database/adapters/s3.ts
7592
8202
  import { spawnSync as spawnSync4 } from "node:child_process";
7593
8203
  import { createHash as createHash4, createHmac } from "node:crypto";
8204
+ function createS3RequestDeadline() {
8205
+ const timeoutMs = s3RequestTimeoutMs;
8206
+ return {
8207
+ expiresAt: Date.now() + timeoutMs,
8208
+ timeoutMs
8209
+ };
8210
+ }
8211
+ function createS3DockerCurlDeadline() {
8212
+ const timeoutMs = s3DockerCurlTimeoutMs;
8213
+ return {
8214
+ expiresAt: Date.now() + timeoutMs,
8215
+ timeoutMs
8216
+ };
8217
+ }
8218
+ function createS3TransportDeadline(config) {
8219
+ return config.dockerContainerName ? createS3DockerCurlDeadline() : createS3RequestDeadline();
8220
+ }
8221
+ function s3TimeoutError(deadline) {
8222
+ return new S3HttpError(503, `S3 request timed out after ${deadline?.timeoutMs ?? s3RequestTimeoutMs}ms`);
8223
+ }
8224
+ function remainingS3TimeoutMs(deadline) {
8225
+ if (!deadline)
8226
+ return s3RequestTimeoutMs;
8227
+ return Math.max(0, deadline.expiresAt - Date.now());
8228
+ }
7594
8229
  function hmac(key, value) {
7595
8230
  return createHmac("sha256", key).update(value, "utf8").digest();
7596
8231
  }
@@ -7725,13 +8360,139 @@ function dockerCurlCommand(opts) {
7725
8360
  input: Buffer.from(curlHeaderConfig(opts.headers), "utf8")
7726
8361
  };
7727
8362
  }
8363
+ function guardedS3Transport(signal, operation, deadline) {
8364
+ if (signal?.aborted) {
8365
+ return Promise.reject(new S3HttpError(503, "S3 HTTP transport aborted"));
8366
+ }
8367
+ const timeoutMs = remainingS3TimeoutMs(deadline);
8368
+ if (timeoutMs <= 0) {
8369
+ return Promise.reject(s3TimeoutError(deadline));
8370
+ }
8371
+ const controller = new AbortController;
8372
+ let timedOut = false;
8373
+ let settled = false;
8374
+ let timer;
8375
+ let cleanupParent = () => {};
8376
+ const abort = (err, reject) => {
8377
+ if (settled)
8378
+ return;
8379
+ settled = true;
8380
+ controller.abort(err);
8381
+ reject(err);
8382
+ };
8383
+ const guarded = new Promise((resolve2, reject) => {
8384
+ const onParentAbort = () => abort(new S3HttpError(503, "S3 HTTP transport aborted"), reject);
8385
+ if (signal) {
8386
+ signal.addEventListener("abort", onParentAbort, { once: true });
8387
+ cleanupParent = () => signal.removeEventListener("abort", onParentAbort);
8388
+ }
8389
+ timer = setTimeout(() => {
8390
+ timedOut = true;
8391
+ abort(s3TimeoutError(deadline), reject);
8392
+ }, timeoutMs);
8393
+ operation(controller.signal).then((value) => {
8394
+ if (settled)
8395
+ return;
8396
+ settled = true;
8397
+ resolve2(value);
8398
+ }, (err) => {
8399
+ if (settled)
8400
+ return;
8401
+ settled = true;
8402
+ if (timedOut) {
8403
+ reject(s3TimeoutError(deadline));
8404
+ } else if (signal?.aborted) {
8405
+ reject(new S3HttpError(503, "S3 HTTP transport aborted"));
8406
+ } else {
8407
+ reject(err);
8408
+ }
8409
+ });
8410
+ });
8411
+ return guarded.finally(() => {
8412
+ if (timer)
8413
+ clearTimeout(timer);
8414
+ cleanupParent();
8415
+ });
8416
+ }
8417
+ async function readStreamChunkWithTimeout(reader, signal, deadline) {
8418
+ return guardedS3Transport(signal, (transportSignal) => {
8419
+ const cancelRead = () => {
8420
+ reader.cancel(transportSignal.reason).catch(() => {});
8421
+ };
8422
+ if (transportSignal.aborted) {
8423
+ cancelRead();
8424
+ } else {
8425
+ transportSignal.addEventListener("abort", cancelRead, { once: true });
8426
+ }
8427
+ return reader.read().finally(() => {
8428
+ transportSignal.removeEventListener("abort", cancelRead);
8429
+ });
8430
+ }, deadline);
8431
+ }
8432
+ async function readResponseBytesWithTimeout(res, signal, deadline) {
8433
+ if (!res.body)
8434
+ return new Uint8Array;
8435
+ const reader = res.body.getReader();
8436
+ const chunks = [];
8437
+ let total = 0;
8438
+ try {
8439
+ for (;; ) {
8440
+ const { done, value } = await readStreamChunkWithTimeout(reader, signal, deadline);
8441
+ if (done)
8442
+ break;
8443
+ if (!value?.byteLength)
8444
+ continue;
8445
+ chunks.push(value);
8446
+ total += value.byteLength;
8447
+ }
8448
+ } finally {
8449
+ try {
8450
+ reader.releaseLock();
8451
+ } catch {}
8452
+ }
8453
+ if (chunks.length === 1)
8454
+ return chunks[0];
8455
+ const bytes = new Uint8Array(total);
8456
+ let offset = 0;
8457
+ for (const chunk of chunks) {
8458
+ bytes.set(chunk, offset);
8459
+ offset += chunk.byteLength;
8460
+ }
8461
+ return bytes;
8462
+ }
8463
+ async function readResponseTextWithTimeout(res, signal, deadline) {
8464
+ return new TextDecoder("utf-8", { fatal: false }).decode(await readResponseBytesWithTimeout(res, signal, deadline));
8465
+ }
8466
+ function timeoutReadableStream(body, signal) {
8467
+ if (!body)
8468
+ return null;
8469
+ const reader = body.getReader();
8470
+ return new ReadableStream({
8471
+ async pull(controller) {
8472
+ try {
8473
+ const { done, value } = await readStreamChunkWithTimeout(reader, signal);
8474
+ if (done) {
8475
+ controller.close();
8476
+ return;
8477
+ }
8478
+ if (value)
8479
+ controller.enqueue(value);
8480
+ } catch (error) {
8481
+ controller.error(error);
8482
+ }
8483
+ },
8484
+ cancel(reason) {
8485
+ return reader.cancel(reason);
8486
+ }
8487
+ });
8488
+ }
7728
8489
  async function dockerCurlFetch(opts) {
7729
8490
  const { args, input } = dockerCurlCommand(opts);
7730
8491
  if (spawnSyncImplIsTestOverride) {
7731
8492
  const proc2 = spawnSyncImpl3("docker", args, {
7732
8493
  encoding: "buffer",
7733
8494
  input,
7734
- timeout: 30000,
8495
+ timeout: s3DockerCurlTimeoutMs,
7735
8496
  stdio: ["pipe", "pipe", "pipe"]
7736
8497
  });
7737
8498
  if ((proc2.status ?? 1) !== 0) {
@@ -7747,10 +8508,10 @@ async function dockerCurlFetch(opts) {
7747
8508
  command: "docker",
7748
8509
  args,
7749
8510
  input,
7750
- timeoutMs: 30000,
8511
+ timeoutMs: s3DockerCurlTimeoutMs,
7751
8512
  signal: opts.signal,
7752
8513
  abortMessage: "S3 HTTP transport aborted",
7753
- timeoutMessage: "docker exec curl timed out"
8514
+ timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
7754
8515
  });
7755
8516
  if (proc.code !== 0) {
7756
8517
  const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
@@ -7824,73 +8585,77 @@ function parseObjects(xml) {
7824
8585
  };
7825
8586
  }
7826
8587
  function createS3Adapter(config) {
7827
- async function signedFetch(opts) {
7828
- const endpoint = new URL(config.endpoint);
7829
- const { dateStamp, amzDate: requestDate } = amzDate();
7830
- const path = buildPath(opts.bucket, opts.key);
7831
- const query = canonicalQuery(opts.query);
7832
- const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
7833
- const headers = {
7834
- host: endpoint.host,
7835
- "x-amz-content-sha256": EMPTY_SHA256,
7836
- "x-amz-date": requestDate,
7837
- ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
7838
- ...opts.headers || {}
7839
- };
7840
- const signedNames = signedHeadersString(headers);
7841
- const canonicalRequest = [
7842
- opts.method,
7843
- path,
7844
- query,
7845
- canonicalHeaders(headers),
7846
- signedNames,
7847
- EMPTY_SHA256
7848
- ].join(`
8588
+ async function signedFetch(opts, deadline = createS3TransportDeadline(config)) {
8589
+ return guardedS3Transport(opts.signal, (transportSignal) => {
8590
+ const endpoint = new URL(config.endpoint);
8591
+ const { dateStamp, amzDate: requestDate } = amzDate();
8592
+ const path = buildPath(opts.bucket, opts.key);
8593
+ const query = canonicalQuery(opts.query);
8594
+ const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
8595
+ const headers = {
8596
+ host: endpoint.host,
8597
+ "x-amz-content-sha256": EMPTY_SHA256,
8598
+ "x-amz-date": requestDate,
8599
+ ...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
8600
+ ...opts.headers || {}
8601
+ };
8602
+ const signedNames = signedHeadersString(headers);
8603
+ const canonicalRequest = [
8604
+ opts.method,
8605
+ path,
8606
+ query,
8607
+ canonicalHeaders(headers),
8608
+ signedNames,
8609
+ EMPTY_SHA256
8610
+ ].join(`
7849
8611
  `);
7850
- const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
7851
- const stringToSign = [
7852
- "AWS4-HMAC-SHA256",
7853
- requestDate,
7854
- scope,
7855
- sha256(canonicalRequest)
7856
- ].join(`
8612
+ const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
8613
+ const stringToSign = [
8614
+ "AWS4-HMAC-SHA256",
8615
+ requestDate,
8616
+ scope,
8617
+ sha256(canonicalRequest)
8618
+ ].join(`
7857
8619
  `);
7858
- const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
7859
- const requestHeaders = new Headers;
7860
- for (const [key, value] of Object.entries(headers)) {
7861
- if (key !== "host")
7862
- requestHeaders.set(key, value);
7863
- }
7864
- requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
7865
- if (config.dockerContainerName) {
7866
- if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
7867
- throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8620
+ const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
8621
+ const requestHeaders = new Headers;
8622
+ for (const [key, value] of Object.entries(headers)) {
8623
+ if (key !== "host")
8624
+ requestHeaders.set(key, value);
7868
8625
  }
7869
- return dockerCurlFetch({
7870
- containerName: config.dockerContainerName,
8626
+ requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
8627
+ if (config.dockerContainerName) {
8628
+ if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
8629
+ throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
8630
+ }
8631
+ return dockerCurlFetch({
8632
+ containerName: config.dockerContainerName,
8633
+ method: opts.method,
8634
+ url,
8635
+ headers: requestHeaders,
8636
+ signal: transportSignal
8637
+ });
8638
+ }
8639
+ return fetch(url, {
7871
8640
  method: opts.method,
7872
- url,
7873
8641
  headers: requestHeaders,
7874
- signal: opts.signal
8642
+ signal: transportSignal
7875
8643
  });
7876
- }
7877
- return fetch(url, {
7878
- method: opts.method,
7879
- headers: requestHeaders,
7880
- signal: opts.signal
7881
- });
8644
+ }, deadline);
7882
8645
  }
7883
- async function textOrThrow(res) {
7884
- const text = await res.text();
8646
+ async function textOrThrow(res, signal, deadline) {
8647
+ const text = await readResponseTextWithTimeout(res, signal, deadline);
7885
8648
  if (!res.ok)
7886
8649
  throw sanitizeS3Error(res.status, text);
7887
8650
  return text;
7888
8651
  }
7889
8652
  async function listBuckets(signal) {
7890
- const xml = await textOrThrow(await signedFetch({ method: "GET", signal }));
8653
+ const deadline = createS3TransportDeadline(config);
8654
+ const xml = await textOrThrow(await signedFetch({ method: "GET", signal }, deadline), signal, deadline);
7891
8655
  return parseBuckets(xml);
7892
8656
  }
7893
8657
  async function listObjects(opts) {
8658
+ const deadline = createS3TransportDeadline(config);
7894
8659
  const xml = await textOrThrow(await signedFetch({
7895
8660
  method: "GET",
7896
8661
  bucket: opts.bucket,
@@ -7901,32 +8666,34 @@ function createS3Adapter(config) {
7901
8666
  ...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
7902
8667
  },
7903
8668
  signal: opts.signal
7904
- }));
8669
+ }, deadline), opts.signal, deadline);
7905
8670
  return parseObjects(xml);
7906
8671
  }
7907
8672
  async function headObject(opts) {
8673
+ const deadline = createS3TransportDeadline(config);
7908
8674
  const res = await signedFetch({
7909
8675
  method: "HEAD",
7910
8676
  bucket: opts.bucket,
7911
8677
  key: opts.key,
7912
8678
  signal: opts.signal
7913
- });
8679
+ }, deadline);
7914
8680
  if (!res.ok)
7915
- throw sanitizeS3Error(res.status, await res.text());
8681
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
7916
8682
  return headFromObjectResponse(opts.bucket, opts.key, res);
7917
8683
  }
7918
8684
  async function getObjectText(opts) {
7919
8685
  const maxBytes = Math.min(1024 * 1024, Math.max(1, opts.maxBytes ?? 512 * 1024));
8686
+ const deadline = createS3TransportDeadline(config);
7920
8687
  const res = await signedFetch({
7921
8688
  method: "GET",
7922
8689
  bucket: opts.bucket,
7923
8690
  key: opts.key,
7924
8691
  headers: { range: `bytes=0-${maxBytes - 1}` },
7925
8692
  signal: opts.signal
7926
- });
8693
+ }, deadline);
7927
8694
  if (!res.ok && res.status !== 206)
7928
- throw sanitizeS3Error(res.status, await res.text());
7929
- const bytes = new Uint8Array(await res.arrayBuffer());
8695
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
8696
+ const bytes = await readResponseBytesWithTimeout(res, opts.signal, deadline);
7930
8697
  const head = headFromObjectResponse(opts.bucket, opts.key, res);
7931
8698
  const fullSize = head.sizeBytes;
7932
8699
  return {
@@ -7936,17 +8703,18 @@ function createS3Adapter(config) {
7936
8703
  };
7937
8704
  }
7938
8705
  async function getObjectResponse(opts) {
8706
+ const deadline = createS3TransportDeadline(config);
7939
8707
  const res = await signedFetch({
7940
8708
  method: opts.method,
7941
8709
  bucket: opts.bucket,
7942
8710
  key: opts.key,
7943
8711
  headers: opts.range ? { range: opts.range } : undefined,
7944
8712
  signal: opts.signal
7945
- });
8713
+ }, deadline);
7946
8714
  if (!res.ok && res.status !== 206) {
7947
- throw sanitizeS3Error(res.status, await res.text());
8715
+ throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
7948
8716
  }
7949
- return new Response(opts.method === "HEAD" ? null : res.body, {
8717
+ return new Response(opts.method === "HEAD" ? null : timeoutReadableStream(res.body, opts.signal), {
7950
8718
  status: res.status,
7951
8719
  headers: rawObjectHeaders(opts.key, res)
7952
8720
  });
@@ -7966,6 +8734,9 @@ async function s3ConfigFromDockerInfoAsync(info, signal) {
7966
8734
  const image = info.image?.toLowerCase() || "";
7967
8735
  const minioDefault = image.includes("minio");
7968
8736
  const env = info.env;
8737
+ if (!info.hostPort && minioDefault) {
8738
+ throw new S3HttpError(503, 'MinIO S3 browsing requires a published host port. Add a compose port mapping like "9000:9000" for the MinIO API.');
8739
+ }
7969
8740
  const dockerContainerName = info.hostPort ? undefined : await resolveRunningComposeContainerNameOrThrowAsync(info.serviceName, info.composeDir, signal);
7970
8741
  return {
7971
8742
  endpoint: info.hostPort ? `http://localhost:${info.hostPort}` : `http://127.0.0.1:${info.containerPort}`,
@@ -7982,7 +8753,7 @@ async function openS3ExplorerAsync(info, signal) {
7982
8753
  function isS3HttpError(err) {
7983
8754
  return err instanceof S3HttpError;
7984
8755
  }
7985
- var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
8756
+ var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", DEFAULT_S3_REQUEST_TIMEOUT_MS = 5000, DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS = 30000, s3RequestTimeoutMs, s3DockerCurlTimeoutMs;
7986
8757
  var init_s3 = __esm(() => {
7987
8758
  init_raw_file_headers();
7988
8759
  init_docker_utils();
@@ -7995,6 +8766,8 @@ var init_s3 = __esm(() => {
7995
8766
  this.status = status;
7996
8767
  }
7997
8768
  };
8769
+ s3RequestTimeoutMs = DEFAULT_S3_REQUEST_TIMEOUT_MS;
8770
+ s3DockerCurlTimeoutMs = DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS;
7998
8771
  });
7999
8772
 
8000
8773
  // web-src/server/database/handle-s3.ts
@@ -8356,13 +9129,9 @@ var init_handle_s3 = __esm(() => {
8356
9129
  });
8357
9130
 
8358
9131
  // web-src/server/database/query-history.ts
8359
- import { mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
8360
- import { join as join9 } from "node:path";
9132
+ import { join as join10 } from "node:path";
8361
9133
  function historyFilePath(root) {
8362
- return join9(root, CODE_VIEWER_DIR2, HISTORY_FILE_NAME);
8363
- }
8364
- function isEnoent(err) {
8365
- return err?.code === "ENOENT";
9134
+ return join10(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
8366
9135
  }
8367
9136
  function emptyState() {
8368
9137
  return { version: 1, entries: [] };
@@ -8381,69 +9150,86 @@ function serializeHistoryState(state) {
8381
9150
  }
8382
9151
  return content;
8383
9152
  }
8384
- async function backupCorruptHistoryFileAsync(file) {
8385
- try {
8386
- await rename(file, `${file}.corrupt-${Date.now()}`);
8387
- } 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;
8388
9159
  }
8389
- async function loadQueryHistoryAsync(cwd) {
8390
- const pendingWrite = historyWriteQueues.get(cwd);
8391
- if (pendingWrite) {
8392
- 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;
8393
9168
  }
8394
- return loadQueryHistoryAsyncUnqueued(cwd);
9169
+ return null;
8395
9170
  }
8396
- async function loadQueryHistoryAsyncUnqueued(cwd) {
8397
- const file = historyFilePath(cwd);
8398
- let raw;
8399
- try {
8400
- raw = await readFile2(file, "utf8");
8401
- } catch (err) {
8402
- if (isEnoent(err))
8403
- return emptyState();
8404
- 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")
8405
9214
  return emptyState();
8406
- }
8407
- try {
8408
- const parsed = JSON.parse(raw);
8409
- if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
8410
- await backupCorruptHistoryFileAsync(file);
8411
- return emptyState();
8412
- }
8413
- return parsed;
8414
- } catch (err) {
8415
- if (isEnoent(err))
8416
- return emptyState();
8417
- await backupCorruptHistoryFileAsync(file);
9215
+ const entriesRaw = raw.entries;
9216
+ if (!Array.isArray(entriesRaw))
8418
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);
8419
9225
  }
9226
+ return { version: 1, entries };
8420
9227
  }
8421
- async function saveQueryHistoryAsync(cwd, state) {
8422
- const dir = join9(cwd, CODE_VIEWER_DIR2);
8423
- await mkdir(dir, { recursive: true });
8424
- const file = historyFilePath(cwd);
8425
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8426
- const content = serializeHistoryState(state);
8427
- await writeFile(tmp, content, "utf8");
8428
- await rename(tmp, file);
9228
+ async function loadQueryHistoryAsync(cwd) {
9229
+ return historyStore.load(cwd);
8429
9230
  }
8430
9231
  async function updateQueryHistoryAsync(cwd, updater) {
8431
- const previous = historyWriteQueues.get(cwd) ?? Promise.resolve();
8432
- const run2 = previous.catch(() => {}).then(async () => {
8433
- const current = await loadQueryHistoryAsyncUnqueued(cwd);
8434
- const updated = await updater(current);
8435
- await saveQueryHistoryAsync(cwd, updated.state);
8436
- return updated.result;
8437
- });
8438
- const queued = run2.then(() => {}, () => {});
8439
- historyWriteQueues.set(cwd, queued);
8440
- try {
8441
- return await run2;
8442
- } finally {
8443
- if (historyWriteQueues.get(cwd) === queued) {
8444
- historyWriteQueues.delete(cwd);
8445
- }
8446
- }
9232
+ return historyStore.update(cwd, updater);
8447
9233
  }
8448
9234
  function clampPreviewRows(rows) {
8449
9235
  return rows.slice(0, MAX_PREVIEW_ROWS);
@@ -8479,15 +9265,23 @@ function clearQueryHistory(state, dbId, schema) {
8479
9265
  })
8480
9266
  };
8481
9267
  }
8482
- 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;
8483
9269
  var init_query_history = __esm(() => {
8484
- 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
+ });
8485
9279
  });
8486
9280
 
8487
9281
  // web-src/server/database/snapshot-store.ts
8488
- import { createHash as createHash5, randomBytes } from "node:crypto";
8489
- import { mkdirSync as mkdirSync4 } from "node:fs";
8490
- 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";
8491
9285
  async function getSqliteClass2() {
8492
9286
  if (cachedDbClass2)
8493
9287
  return cachedDbClass2;
@@ -8504,7 +9298,7 @@ async function getSqliteClass2() {
8504
9298
  throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
8505
9299
  }
8506
9300
  async function getStoreDb(cwd) {
8507
- const dbPath = join10(cwd, CODE_VIEWER_DIR3, SNAPSHOT_DB_NAME);
9301
+ const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
8508
9302
  if (storeDb && storeDbPath === dbPath)
8509
9303
  return storeDb;
8510
9304
  if (storeDb) {
@@ -8512,7 +9306,7 @@ async function getStoreDb(cwd) {
8512
9306
  storeDb.close();
8513
9307
  } catch {}
8514
9308
  }
8515
- mkdirSync4(join10(cwd, CODE_VIEWER_DIR3), { recursive: true });
9309
+ mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
8516
9310
  const DbClass = await getSqliteClass2();
8517
9311
  storeDb = new DbClass(dbPath);
8518
9312
  storeDbPath = dbPath;
@@ -8525,7 +9319,7 @@ async function getStoreDb(cwd) {
8525
9319
  return storeDb;
8526
9320
  }
8527
9321
  function makeId2(prefix) {
8528
- return `${prefix}-${randomBytes(8).toString("hex")}`;
9322
+ return `${prefix}-${randomBytes2(8).toString("hex")}`;
8529
9323
  }
8530
9324
  function hashPayload(payloadJson) {
8531
9325
  return createHash5("sha256").update(payloadJson).digest("hex");
@@ -8772,7 +9566,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
8772
9566
  });
8773
9567
  return { rows, total };
8774
9568
  }
8775
- 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 = `
8776
9570
  CREATE TABLE IF NOT EXISTS snapshots (
8777
9571
  id TEXT PRIMARY KEY,
8778
9572
  db_id TEXT NOT NULL,
@@ -8869,13 +9663,9 @@ var init_snapshot_runner = __esm(() => {
8869
9663
  });
8870
9664
 
8871
9665
  // web-src/server/database/tabs-store.ts
8872
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
8873
- import { join as join11 } from "node:path";
9666
+ import { join as join12 } from "node:path";
8874
9667
  function tabsFilePath(root) {
8875
- return join11(root, CODE_VIEWER_DIR4, TABS_FILE_NAME);
8876
- }
8877
- function isEnoent2(err) {
8878
- return err?.code === "ENOENT";
9668
+ return join12(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
8879
9669
  }
8880
9670
  function emptyState2() {
8881
9671
  return { version: 1, tabs: [], activeTabId: null };
@@ -8943,6 +9733,31 @@ function sanitizeEs(v) {
8943
9733
  return;
8944
9734
  return out;
8945
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
+ }
8946
9761
  function sanitize(input) {
8947
9762
  if (!input || typeof input !== "object")
8948
9763
  return emptyState2();
@@ -8964,12 +9779,15 @@ function sanitize(input) {
8964
9779
  if (seenIds.has(id))
8965
9780
  continue;
8966
9781
  seenIds.add(id);
8967
- const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN) ?? null;
9782
+ const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN2) ?? null;
8968
9783
  if (isToolInternalDbId(dbId))
8969
9784
  continue;
9785
+ const schema = sanitizeOptionalString(tab.schema, MAX_SCHEMA_NAME_LEN);
8970
9786
  const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
8971
9787
  const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
8972
9788
  const out = { id, dbId, table, view };
9789
+ if (schema !== undefined)
9790
+ out.schema = schema;
8973
9791
  const sqlDraft = sanitizeOptionalString(tab.sqlDraft, MAX_SQL_DRAFT_LEN);
8974
9792
  if (sqlDraft !== undefined)
8975
9793
  out.sqlDraft = sqlDraft;
@@ -8987,6 +9805,9 @@ function sanitize(input) {
8987
9805
  const es = sanitizeEs(tab.es);
8988
9806
  if (es !== undefined)
8989
9807
  out.es = es;
9808
+ const s3 = sanitizeS3(tab.s3);
9809
+ if (s3 !== undefined)
9810
+ out.s3 = s3;
8990
9811
  tabs.push(out);
8991
9812
  }
8992
9813
  let activeTabId = sanitizeOptionalString(obj.activeTabId, MAX_TAB_ID_LEN) ?? null;
@@ -8996,69 +9817,14 @@ function sanitize(input) {
8996
9817
  return { version: 1, tabs, activeTabId };
8997
9818
  }
8998
9819
  async function loadTabsAsync(cwd) {
8999
- const pendingWrite = tabsWriteQueues.get(cwd);
9000
- if (pendingWrite) {
9001
- await pendingWrite.catch(() => {});
9002
- }
9003
- const file = tabsFilePath(cwd);
9004
- let raw;
9005
- try {
9006
- raw = await readFile3(file, "utf8");
9007
- } catch (err) {
9008
- if (isEnoent2(err))
9009
- return emptyState2();
9010
- await backupInvalidTabsFileAsync(file);
9011
- return emptyState2();
9012
- }
9013
- try {
9014
- const parsed = JSON.parse(raw);
9015
- if (!parsed || typeof parsed !== "object" || parsed.version !== 1) {
9016
- await backupInvalidTabsFileAsync(file);
9017
- return emptyState2();
9018
- }
9019
- return sanitize(parsed);
9020
- } catch (err) {
9021
- if (isEnoent2(err))
9022
- return emptyState2();
9023
- await backupInvalidTabsFileAsync(file);
9024
- return emptyState2();
9025
- }
9026
- }
9027
- async function backupInvalidTabsFileAsync(file) {
9028
- try {
9029
- await rename2(file, `${file}.bak-${Date.now()}`);
9030
- } catch {}
9031
- }
9032
- async function saveTabsAsyncUnqueued(cwd, state) {
9033
- const normalized = sanitize(state);
9034
- const dir = join11(cwd, CODE_VIEWER_DIR4);
9035
- await mkdir2(dir, { recursive: true });
9036
- const file = tabsFilePath(cwd);
9037
- const tmp = `${file}.${makeId(`tmp-${process.pid}`)}`;
9038
- const content = `${JSON.stringify(normalized, null, 2)}
9039
- `;
9040
- if (Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES2) {
9041
- throw new Error("tabs state too large");
9042
- }
9043
- await writeFile2(tmp, content, "utf8");
9044
- await rename2(tmp, file);
9820
+ return tabsStore.load(cwd);
9045
9821
  }
9046
9822
  async function saveTabsAsync(cwd, state) {
9047
- const previous = tabsWriteQueues.get(cwd) ?? Promise.resolve();
9048
- const run2 = previous.catch(() => {}).then(() => saveTabsAsyncUnqueued(cwd, state));
9049
- const queued = run2.then(() => {}, () => {});
9050
- tabsWriteQueues.set(cwd, queued);
9051
- try {
9052
- await run2;
9053
- } finally {
9054
- if (tabsWriteQueues.get(cwd) === queued) {
9055
- tabsWriteQueues.delete(cwd);
9056
- }
9057
- }
9823
+ return tabsStore.save(cwd, state);
9058
9824
  }
9059
- 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;
9060
9826
  var init_tabs_store = __esm(() => {
9061
- tabsWriteQueues = new Map;
9827
+ init_json_store();
9062
9828
  VALID_VIEWS = new Set([
9063
9829
  "data",
9064
9830
  "query",
@@ -9067,6 +9833,14 @@ var init_tabs_store = __esm(() => {
9067
9833
  "search",
9068
9834
  "snapshot"
9069
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
+ });
9070
9844
  });
9071
9845
 
9072
9846
  // web-src/server/database/handle.ts
@@ -9095,7 +9869,7 @@ function sanitizeFilename(name) {
9095
9869
  function normalizeSchemaParam(value) {
9096
9870
  if (value === undefined || value === null || value === "")
9097
9871
  return;
9098
- if (value.length > MAX_SCHEMA_NAME_LEN) {
9872
+ if (value.length > MAX_SCHEMA_NAME_LEN2) {
9099
9873
  return textError("invalid schema parameter", 400);
9100
9874
  }
9101
9875
  if (hasControlCharacter(value)) {
@@ -10054,6 +10828,23 @@ async function handleTabsPut(cwd, req) {
10054
10828
  return textError(`failed to save tabs: ${message}`, 500);
10055
10829
  }
10056
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
+ }
10057
10848
  async function handleClose(cwd, req, omitDirNames) {
10058
10849
  const body = await parsePostJsonBody(req);
10059
10850
  if (body instanceof Response)
@@ -10216,11 +11007,17 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
10216
11007
  methods: ["GET", "PUT", "POST"],
10217
11008
  sideEffect: (m) => m !== "GET",
10218
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)
10219
11015
  }
10220
11016
  }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
10221
11017
  }
10222
- 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;
10223
11019
  var init_handle = __esm(() => {
11020
+ init_state_store();
10224
11021
  init_docker();
10225
11022
  init_docker_utils();
10226
11023
  init_sqlite();
@@ -10274,25 +11071,87 @@ var init_handle = __esm(() => {
10274
11071
  };
10275
11072
  });
10276
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
+
10277
11136
  // web-src/server/preview.ts
10278
11137
  var exports_preview = {};
10279
11138
  import {
10280
11139
  closeSync as closeSync2,
10281
11140
  constants,
10282
- existsSync as existsSync7,
11141
+ existsSync as existsSync6,
10283
11142
  lstatSync as lstatSync4,
10284
- mkdirSync as mkdirSync5,
11143
+ mkdirSync as mkdirSync4,
10285
11144
  openSync as openSync2,
10286
- readFileSync as readFileSync5,
11145
+ readFileSync as readFileSync4,
10287
11146
  realpathSync as realpathSync4,
10288
- renameSync as renameSync2,
11147
+ renameSync,
10289
11148
  statSync as statSync3,
10290
11149
  unlinkSync as unlinkSync2,
10291
11150
  watch,
10292
- writeFileSync as writeFileSync3
11151
+ writeFileSync as writeFileSync2
10293
11152
  } from "node:fs";
10294
11153
  import { homedir as homedir3 } from "node:os";
10295
- 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";
10296
11155
  function parseCli() {
10297
11156
  const rest = [];
10298
11157
  for (let i = 2;i < process.argv.length; i++) {
@@ -10433,10 +11292,10 @@ function staticFile(pathname) {
10433
11292
  const spec = map[pathname];
10434
11293
  if (!spec)
10435
11294
  return null;
10436
- const full = join12(WEB_ROOT, spec[0]);
10437
- if (!existsSync7(full))
11295
+ const full = join13(WEB_ROOT, spec[0]);
11296
+ if (!existsSync6(full))
10438
11297
  return text("not found", 404);
10439
- return new Response(readFileSync5(full), {
11298
+ return new Response(readFileSync4(full), {
10440
11299
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
10441
11300
  });
10442
11301
  }
@@ -10667,8 +11526,8 @@ function parseScopeExcludeNamesQuery(value) {
10667
11526
  return normalizeScopeExcludeNames(names);
10668
11527
  }
10669
11528
  function loadProjectConfig() {
10670
- const full = join12(cwd, ".code-viewer.json");
10671
- if (!existsSync7(full))
11529
+ const full = join13(cwd, ".code-viewer.json");
11530
+ if (!existsSync6(full))
10672
11531
  return null;
10673
11532
  let realCwd;
10674
11533
  let realConfig;
@@ -10678,10 +11537,10 @@ function loadProjectConfig() {
10678
11537
  } catch {
10679
11538
  return null;
10680
11539
  }
10681
- if (dirname2(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
11540
+ if (dirname3(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
10682
11541
  return null;
10683
11542
  try {
10684
- const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
11543
+ const parsed = JSON.parse(readFileSync4(realConfig, "utf8"));
10685
11544
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
10686
11545
  return null;
10687
11546
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -10732,8 +11591,8 @@ function safeWorktreePath(path) {
10732
11591
  return null;
10733
11592
  if (isGitInternalPath(path))
10734
11593
  return null;
10735
- const full = join12(cwd, path);
10736
- if (!existsSync7(full))
11594
+ const full = join13(cwd, path);
11595
+ if (!existsSync6(full))
10737
11596
  return null;
10738
11597
  let realCwd;
10739
11598
  let realFull;
@@ -10751,7 +11610,7 @@ function safeWorktreePath(path) {
10751
11610
  return realFull;
10752
11611
  }
10753
11612
  function worktreePath(path) {
10754
- return join12(cwd, path);
11613
+ return join13(cwd, path);
10755
11614
  }
10756
11615
  function safeOpenWorktreePath(path) {
10757
11616
  if (path === "") {
@@ -10767,7 +11626,7 @@ function safeOpenWorktreePath(path) {
10767
11626
  return safeWorktreePath(path);
10768
11627
  }
10769
11628
  function parentRepoPath(path) {
10770
- const parent = dirname2(path);
11629
+ const parent = dirname3(path);
10771
11630
  return parent === "." ? "" : parent;
10772
11631
  }
10773
11632
  function isoDate(ms) {
@@ -10834,7 +11693,7 @@ function readReadme(target, dirPath) {
10834
11693
  if (!full)
10835
11694
  continue;
10836
11695
  try {
10837
- return { path, text: readFileSync5(full, "utf8") };
11696
+ return { path, text: readFileSync4(full, "utf8") };
10838
11697
  } catch {
10839
11698
  continue;
10840
11699
  }
@@ -10889,6 +11748,13 @@ function handleSettings() {
10889
11748
  }
10890
11749
  });
10891
11750
  }
11751
+ function worktreeWatchDirectoryLimitFromEnv() {
11752
+ const raw = process.env.CODE_VIEWER_WORKTREE_WATCH_LIMIT;
11753
+ if (!raw)
11754
+ return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11755
+ const parsed = Number(raw);
11756
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
11757
+ }
10892
11758
  function handleFiles2(url) {
10893
11759
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
10894
11760
  if (target !== "worktree" && !verifyTreeRef(target, cwd))
@@ -10948,7 +11814,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
10948
11814
  continue;
10949
11815
  let data;
10950
11816
  try {
10951
- data = readFileSync5(full);
11817
+ data = readFileSync4(full);
10952
11818
  } catch {
10953
11819
  continue;
10954
11820
  }
@@ -11484,10 +12350,10 @@ async function handleUploadFiles(req) {
11484
12350
  total += file.size;
11485
12351
  if (total > MAX_UPLOAD_TOTAL_BYTES)
11486
12352
  return text("upload too large", 413);
11487
- const target = join12(realDir, safeName);
11488
- if (relative3(realDir, dirname2(target)) !== "")
12353
+ const target = join13(realDir, safeName);
12354
+ if (relative3(realDir, dirname3(target)) !== "")
11489
12355
  return text("invalid filename", 400);
11490
- if (existsSync7(target))
12356
+ if (existsSync6(target))
11491
12357
  return text("file exists", 409);
11492
12358
  uploads.push({ file, name: safeName, target });
11493
12359
  }
@@ -11496,7 +12362,7 @@ async function handleUploadFiles(req) {
11496
12362
  for (const upload of uploads) {
11497
12363
  const fd = openSync2(upload.target, uploadOpenFlags(), 420);
11498
12364
  try {
11499
- writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
12365
+ writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
11500
12366
  } finally {
11501
12367
  closeSync2(fd);
11502
12368
  }
@@ -11606,12 +12472,12 @@ function triggerUpdate(changedPaths) {
11606
12472
  sendSse("update", data);
11607
12473
  }
11608
12474
  function moveMacPathIntoTrash(path) {
11609
- const trashDir = join12(homedir3(), ".Trash");
12475
+ const trashDir = join13(homedir3(), ".Trash");
11610
12476
  const base = basename3(path) || "code-viewer-trash-item";
11611
- 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)}`);
11612
12478
  try {
11613
- mkdirSync5(trashDir, { recursive: true });
11614
- renameSync2(path, target);
12479
+ mkdirSync4(trashDir, { recursive: true });
12480
+ renameSync(path, target);
11615
12481
  return { ok: true, trashPath: target };
11616
12482
  } catch (error) {
11617
12483
  return { ok: false, error: String(error) };
@@ -11642,20 +12508,20 @@ function restoreTrashPath(originalPath, trashPath) {
11642
12508
  if (!parentFullPath)
11643
12509
  return { ok: false, error: "invalid restore target" };
11644
12510
  const original = worktreePath(originalPath);
11645
- if (existsSync7(original))
12511
+ if (existsSync6(original))
11646
12512
  return { ok: false, error: "restore target exists" };
11647
12513
  if (trashPath) {
11648
12514
  if (process.platform !== "darwin")
11649
12515
  return { ok: false, error: "invalid trash handle" };
11650
- if (!existsSync7(trashPath))
12516
+ if (!existsSync6(trashPath))
11651
12517
  return { ok: false, error: "trash item not found" };
11652
12518
  try {
11653
- const trashRoot = join12(homedir3(), ".Trash");
12519
+ const trashRoot = join13(homedir3(), ".Trash");
11654
12520
  const trashRelative = relative3(trashRoot, trashPath);
11655
12521
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
11656
12522
  return { ok: false, error: "invalid trash handle" };
11657
- mkdirSync5(dirname2(original), { recursive: true });
11658
- renameSync2(trashPath, original);
12523
+ mkdirSync4(dirname3(original), { recursive: true });
12524
+ renameSync(trashPath, original);
11659
12525
  return { ok: true };
11660
12526
  } catch (error) {
11661
12527
  return { ok: false, error: String(error) };
@@ -11800,11 +12666,11 @@ async function handleCreateDirectory(req) {
11800
12666
  const targetPath = dir ? `${dir}/${name}` : name;
11801
12667
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
11802
12668
  return text("invalid target", 400);
11803
- const target = join12(parent, name);
11804
- if (existsSync7(target))
12669
+ const target = join13(parent, name);
12670
+ if (existsSync6(target))
11805
12671
  return text("already exists", 409);
11806
12672
  try {
11807
- mkdirSync5(target, { recursive: false });
12673
+ mkdirSync4(target, { recursive: false });
11808
12674
  } catch (error) {
11809
12675
  if (error.code === "EEXIST")
11810
12676
  return text("already exists", 409);
@@ -11850,7 +12716,7 @@ function annotationSse(kind, sessionId, entryId) {
11850
12716
  }
11851
12717
  async function handleAnnotations(req) {
11852
12718
  if (req.method === "GET")
11853
- return json2(loadAnnotationsState(cwd));
12719
+ return json2(await loadAnnotationsState(cwd));
11854
12720
  if (req.method !== "POST")
11855
12721
  return text("method not allowed", 405);
11856
12722
  if (!sideEffectRequestAllowed(req))
@@ -11874,8 +12740,8 @@ async function handleAnnotations(req) {
11874
12740
  const action = body.action;
11875
12741
  if (action === "start") {
11876
12742
  const title = typeof body.title === "string" ? body.title : "";
11877
- const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
11878
- saveAnnotationsState(cwd, started.state);
12743
+ const started = startAnnotationSession(await loadAnnotationsState(cwd), title, new Date().toISOString());
12744
+ await saveAnnotationsState(cwd, started.state);
11879
12745
  annotationSse("start", started.session.id);
11880
12746
  return json2({ ok: true, session: started.session });
11881
12747
  }
@@ -11893,7 +12759,7 @@ async function handleAnnotations(req) {
11893
12759
  if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
11894
12760
  return text("forbidden", 403);
11895
12761
  }
11896
- const result = addAnnotationEntry(loadAnnotationsState(cwd), {
12762
+ const result = addAnnotationEntry(await loadAnnotationsState(cwd), {
11897
12763
  session_id: typeof body.session_id === "string" ? body.session_id : undefined,
11898
12764
  session_title: typeof body.session_title === "string" ? body.session_title : undefined,
11899
12765
  path,
@@ -11908,7 +12774,7 @@ async function handleAnnotations(req) {
11908
12774
  }, new Date().toISOString());
11909
12775
  if (result.ok === false)
11910
12776
  return text(result.error, 400);
11911
- saveAnnotationsState(cwd, result.state);
12777
+ await saveAnnotationsState(cwd, result.state);
11912
12778
  annotationSse("add", result.session.id, result.entry.id);
11913
12779
  return json2({
11914
12780
  ok: true,
@@ -11922,14 +12788,14 @@ async function handleAnnotations(req) {
11922
12788
  const id = typeof body.id === "string" ? body.id : "";
11923
12789
  if (!id)
11924
12790
  return text("invalid id", 400);
11925
- const result = moveAnnotationEntry(loadAnnotationsState(cwd), id, {
12791
+ const result = moveAnnotationEntry(await loadAnnotationsState(cwd), id, {
11926
12792
  before_id: typeof body.before_id === "string" ? body.before_id : undefined,
11927
12793
  after_id: typeof body.after_id === "string" ? body.after_id : undefined,
11928
12794
  position: typeof body.position === "number" ? body.position : undefined
11929
12795
  });
11930
12796
  if (result.ok === false)
11931
12797
  return text(result.error, 400);
11932
- saveAnnotationsState(cwd, result.state);
12798
+ await saveAnnotationsState(cwd, result.state);
11933
12799
  annotationSse("update", result.session.id, result.entry.id);
11934
12800
  return json2({
11935
12801
  ok: true,
@@ -11941,9 +12807,9 @@ async function handleAnnotations(req) {
11941
12807
  const id = typeof body.id === "string" ? body.id : "";
11942
12808
  if (!id)
11943
12809
  return text("invalid id", 400);
11944
- const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
12810
+ const result = deleteAnnotationById(await loadAnnotationsState(cwd), id);
11945
12811
  if (result.removed) {
11946
- saveAnnotationsState(cwd, result.state);
12812
+ await saveAnnotationsState(cwd, result.state);
11947
12813
  annotationSse("delete");
11948
12814
  }
11949
12815
  return json2({ ok: true, removed: result.removed });
@@ -11953,10 +12819,10 @@ async function handleAnnotations(req) {
11953
12819
  const title = typeof body.title === "string" ? body.title : "";
11954
12820
  if (!id)
11955
12821
  return text("invalid id", 400);
11956
- const result = renameAnnotationSession(loadAnnotationsState(cwd), id, title);
12822
+ const result = renameAnnotationSession(await loadAnnotationsState(cwd), id, title);
11957
12823
  if (!result.renamed)
11958
12824
  return text("session not found", 404);
11959
- saveAnnotationsState(cwd, result.state);
12825
+ await saveAnnotationsState(cwd, result.state);
11960
12826
  annotationSse("update", id);
11961
12827
  return json2({ ok: true });
11962
12828
  }
@@ -11964,18 +12830,18 @@ async function handleAnnotations(req) {
11964
12830
  const id = typeof body.id === "string" ? body.id : "";
11965
12831
  if (!id)
11966
12832
  return text("invalid id", 400);
11967
- const result = updateAnnotationEntry(loadAnnotationsState(cwd), id, {
12833
+ const result = updateAnnotationEntry(await loadAnnotationsState(cwd), id, {
11968
12834
  title: typeof body.title === "string" ? body.title : undefined,
11969
12835
  body: typeof body.body === "string" ? body.body : undefined
11970
12836
  });
11971
12837
  if (result.ok === false)
11972
12838
  return text(result.error, 400);
11973
- saveAnnotationsState(cwd, result.state);
12839
+ await saveAnnotationsState(cwd, result.state);
11974
12840
  annotationSse("update", undefined, id);
11975
12841
  return json2({ ok: true, entry: result.entry });
11976
12842
  }
11977
12843
  if (action === "clear") {
11978
- saveAnnotationsState(cwd, emptyAnnotationsState());
12844
+ await saveAnnotationsState(cwd, emptyAnnotationsState());
11979
12845
  annotationSse("clear");
11980
12846
  return json2({ ok: true });
11981
12847
  }
@@ -12044,8 +12910,8 @@ var init_preview = __esm(async () => {
12044
12910
  init_search();
12045
12911
  init_server_registry();
12046
12912
  init_worktree_watcher();
12047
- WEB_ROOT = join12(ROOT, "web");
12048
- 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;
12049
12915
  DEFAULT_ARGS = ["HEAD"];
12050
12916
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
12051
12917
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -12149,6 +13015,12 @@ var init_preview = __esm(async () => {
12149
13015
  if (dbResponse)
12150
13016
  return dbResponse;
12151
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
+ }
12152
13024
  if (url.pathname === "/_annotations")
12153
13025
  return handleAnnotations(req);
12154
13026
  if (url.pathname === "/_refs")
@@ -12237,6 +13109,7 @@ data: ok
12237
13109
  excludeNames: scopeExcludeNames,
12238
13110
  watch,
12239
13111
  initialScanMode: "async",
13112
+ maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
12240
13113
  onUpdate: triggerUpdate,
12241
13114
  onError: (error) => {
12242
13115
  const message = error instanceof Error ? error.message : String(error);