@youtyan/code-viewer 0.9.0 → 0.9.2

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.
@@ -1167,8 +1167,12 @@ function runBytesAsync(args, cwd, options = {}) {
1167
1167
  return new Promise((resolve) => {
1168
1168
  const proc = spawn(args[0], args.slice(1), {
1169
1169
  cwd,
1170
- stdio: ["ignore", "pipe", "pipe"]
1170
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
1171
1171
  });
1172
+ if (options.stdin !== undefined) {
1173
+ proc.stdin?.on("error", () => {});
1174
+ proc.stdin?.end(options.stdin);
1175
+ }
1172
1176
  const stdoutChunks = [];
1173
1177
  const stderrChunks = [];
1174
1178
  let stdoutBytes = 0;
@@ -1448,9 +1452,10 @@ function run(args, cwd) {
1448
1452
  timeout: GIT_COMMAND_TIMEOUT_MS
1449
1453
  });
1450
1454
  }
1451
- function runGitAsync(args, cwd) {
1455
+ function runGitAsync(args, cwd, options = {}) {
1452
1456
  return runAsync(resolveGitArgs(args), cwd, {
1453
- timeout: GIT_COMMAND_TIMEOUT_MS
1457
+ timeout: GIT_COMMAND_TIMEOUT_MS,
1458
+ ...options
1454
1459
  });
1455
1460
  }
1456
1461
  function resolveGitArgs(args) {
@@ -1530,7 +1535,7 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1530
1535
  "status",
1531
1536
  "--porcelain=v1",
1532
1537
  "-z",
1533
- "--untracked-files=all"
1538
+ "--untracked-files=normal"
1534
1539
  ], cwd);
1535
1540
  if (res.code !== 0)
1536
1541
  return map;
@@ -1542,7 +1547,7 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1542
1547
  if (!path)
1543
1548
  continue;
1544
1549
  if (xy === "??") {
1545
- map.set(path, "A");
1550
+ map.set(path, "U");
1546
1551
  continue;
1547
1552
  }
1548
1553
  if (xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C") {
@@ -1557,6 +1562,27 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1557
1562
  setTimedCacheEntry(repoStatusMapCache, cwd, { map }, now);
1558
1563
  return map;
1559
1564
  }
1565
+ function repoStatusForPath(map, path) {
1566
+ const own = map.get(path) ?? map.get(`${path}/`);
1567
+ if (own)
1568
+ return { code: own, inherited: false };
1569
+ for (let slash = path.lastIndexOf("/");slash > 0; slash = path.lastIndexOf("/", slash - 1)) {
1570
+ const ancestor = map.get(`${path.slice(0, slash)}/`);
1571
+ if (ancestor)
1572
+ return { code: ancestor, inherited: true };
1573
+ }
1574
+ return;
1575
+ }
1576
+ async function ignoredPathsAsync(paths, cwd) {
1577
+ if (!paths.length)
1578
+ return new Set;
1579
+ const res = await runGitAsync(["git", "check-ignore", "-z", "--stdin"], cwd, {
1580
+ stdin: paths.join("\x00")
1581
+ });
1582
+ if (res.code !== 0 && res.code !== 1)
1583
+ return new Set;
1584
+ return new Set(res.stdout.split("\x00").filter(Boolean));
1585
+ }
1560
1586
  function showAsync(ref, path, cwd) {
1561
1587
  return runGitAsync(["git", "show", `${ref}:${path}`], cwd);
1562
1588
  }
@@ -9737,245 +9763,716 @@ var init_agent_help = __esm(() => {
9737
9763
  ];
9738
9764
  });
9739
9765
 
9740
- // web-src/server/database/serialize.ts
9741
- function serializeDbValue(value) {
9742
- if (value === null || value === undefined)
9743
- return null;
9744
- if (typeof value === "bigint") {
9745
- return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value.toString();
9746
- }
9747
- if (value instanceof Uint8Array) {
9748
- return `<blob ${value.byteLength} bytes>`;
9749
- }
9750
- if (typeof value === "object") {
9751
- try {
9752
- return JSON.stringify(value);
9753
- } catch {
9754
- return String(value);
9755
- }
9756
- }
9757
- return value;
9766
+ // web-src/server/worktree-watcher.ts
9767
+ import {
9768
+ lstatSync as lstatSync3,
9769
+ readdirSync as nodeReaddirSync,
9770
+ watch as nodeWatch
9771
+ } from "node:fs";
9772
+ import { join as join9, relative as relative4 } from "node:path";
9773
+ function supportsNativeRecursiveWatch(platform) {
9774
+ return platform === "darwin" || platform === "win32";
9758
9775
  }
9759
- function serializeDbRow(row) {
9760
- return row.map(serializeDbValue);
9776
+ function normalizeRelativePath(path) {
9777
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
9761
9778
  }
9762
- function serializeDbRows(rows) {
9763
- return rows.map(serializeDbRow);
9779
+ function isInsideRoot(root, path) {
9780
+ const rel = relative4(root, path).replace(/\\/g, "/");
9781
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
9764
9782
  }
9765
- function coerceDbValue(value, columnType) {
9766
- if (value === null)
9767
- return null;
9768
- const t = (columnType || "").toLowerCase();
9769
- if (/bool/.test(t)) {
9770
- const v = value.trim().toLowerCase();
9771
- if (v === "")
9772
- return null;
9773
- if (v === "true" || v === "t" || v === "1")
9774
- return true;
9775
- if (v === "false" || v === "f" || v === "0")
9783
+ function startWorktreeUpdateWatch(options) {
9784
+ const watch = options.watch || nodeWatch;
9785
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
9786
+ const isDirectory = options.isDirectory || ((path) => {
9787
+ try {
9788
+ return lstatSync3(path).isDirectory();
9789
+ } catch {
9776
9790
  return false;
9777
- return value;
9778
- }
9779
- if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
9780
- const trimmed = value.trim();
9781
- if (trimmed === "")
9791
+ }
9792
+ });
9793
+ const directorySignature = options.directorySignature || ((path) => {
9794
+ try {
9795
+ const stats = lstatSync3(path);
9796
+ if (!stats.isDirectory())
9797
+ return null;
9798
+ return `${stats.dev}:${stats.ino}`;
9799
+ } catch {
9782
9800
  return null;
9783
- const n = Number(trimmed);
9784
- if (Number.isFinite(n) && String(n) === trimmed)
9785
- return n;
9786
- return value;
9787
- }
9788
- return value;
9789
- }
9790
- var MIN_SAFE, MAX_SAFE;
9791
- var init_serialize = __esm(() => {
9792
- MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
9793
- MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
9794
- });
9795
-
9796
- // web-src/server/database/sql-utils.ts
9797
- function sanitizeIdentifier(name, kind = "sqlite") {
9798
- if (kind === "mysql")
9799
- return `\`${name.replace(/`/g, "``")}\``;
9800
- return `"${name.replace(/"/g, '""')}"`;
9801
- }
9802
- function escapeSqlString(value, kind) {
9803
- const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
9804
- return `'${escaped}'`;
9805
- }
9806
- function buildFilterWhere(grouped, kind, exact) {
9807
- const whereParts = [];
9808
- const params = [];
9809
- const useParams = kind === "sqlite";
9810
- const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
9811
- for (const [value, cols] of grouped) {
9812
- const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
9813
- if (cols.length === 1) {
9814
- whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
9815
- if (useParams)
9816
- params.push(`%${value}%`);
9817
- } else {
9818
- const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
9819
- whereParts.push(`(${orParts.join(" OR ")})`);
9820
- if (useParams) {
9821
- for (let i = 0;i < cols.length; i++)
9822
- params.push(`%${value}%`);
9801
+ }
9802
+ });
9803
+ const setTimer = options.setTimeoutFn || setTimeout;
9804
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
9805
+ const debounceMs = options.debounceMs ?? 250;
9806
+ const recursive = options.recursive === true;
9807
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
9808
+ const watchers = new Map;
9809
+ const signatures = new Map;
9810
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
9811
+ const initialScanQueue = [];
9812
+ let initialScanTimer = null;
9813
+ let processingInitialScan = false;
9814
+ const pendingPathInspections = new Map;
9815
+ let pathInspectionTimer = null;
9816
+ let timer = null;
9817
+ const pendingChangedPaths = new Set;
9818
+ let fullUpdatePending = false;
9819
+ let watchLimitReported = false;
9820
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
9821
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative4(options.root, dir));
9822
+ const ignoredDirectory = (dir) => {
9823
+ const rel = directoryRelativePath(dir);
9824
+ return Boolean(rel && ignored(rel));
9825
+ };
9826
+ const scheduleUpdate = (changedPath) => {
9827
+ if (changedPath)
9828
+ pendingChangedPaths.add(changedPath);
9829
+ else
9830
+ fullUpdatePending = true;
9831
+ if (timer)
9832
+ clearTimer(timer);
9833
+ timer = setTimer(() => {
9834
+ timer = null;
9835
+ const paths = !fullUpdatePending && pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
9836
+ pendingChangedPaths.clear();
9837
+ fullUpdatePending = false;
9838
+ options.onUpdate(paths);
9839
+ }, debounceMs);
9840
+ };
9841
+ const reportWatchLimit = () => {
9842
+ if (watchLimitReported)
9843
+ return;
9844
+ watchLimitReported = true;
9845
+ options.onWatchLimit?.(maxWatchedDirectories);
9846
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
9847
+ };
9848
+ const closeSubtree = (dir) => {
9849
+ for (const [watchedDir, watcher] of [...watchers]) {
9850
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
9851
+ continue;
9852
+ try {
9853
+ watcher.close?.();
9854
+ } catch {}
9855
+ watchers.delete(watchedDir);
9856
+ signatures.delete(watchedDir);
9857
+ }
9858
+ };
9859
+ const closeAll = () => {
9860
+ if (initialScanTimer) {
9861
+ clearTimer(initialScanTimer);
9862
+ initialScanTimer = null;
9863
+ }
9864
+ if (pathInspectionTimer) {
9865
+ clearTimer(pathInspectionTimer);
9866
+ pathInspectionTimer = null;
9867
+ }
9868
+ initialScanQueue.length = 0;
9869
+ pendingPathInspections.clear();
9870
+ for (const watcher of [...watchers.values()]) {
9871
+ try {
9872
+ watcher.close?.();
9873
+ } catch {}
9874
+ }
9875
+ watchers.clear();
9876
+ signatures.clear();
9877
+ };
9878
+ const readChildDirectories = (dir) => {
9879
+ let entries;
9880
+ try {
9881
+ entries = readDirs(dir);
9882
+ } catch (error) {
9883
+ options.onError?.(error);
9884
+ return [];
9885
+ }
9886
+ const children = [];
9887
+ for (const entry of entries) {
9888
+ if (!entry.isDirectory())
9889
+ continue;
9890
+ const child = join9(dir, entry.name);
9891
+ if (ignoredDirectory(child))
9892
+ continue;
9893
+ children.push(child);
9894
+ }
9895
+ return children;
9896
+ };
9897
+ const processInitialScanQueue = () => {
9898
+ initialScanTimer = null;
9899
+ if (watchers.size >= maxWatchedDirectories) {
9900
+ reportWatchLimit();
9901
+ initialScanQueue.length = 0;
9902
+ return;
9903
+ }
9904
+ const next = initialScanQueue.shift();
9905
+ if (next) {
9906
+ processingInitialScan = true;
9907
+ try {
9908
+ watchDirectory(next, true);
9909
+ } finally {
9910
+ processingInitialScan = false;
9823
9911
  }
9824
9912
  }
9825
- }
9826
- for (const cond of exact ?? []) {
9827
- const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
9828
- whereParts.push(`${castOf(cond.column)} = ${rhs}`);
9829
- if (useParams)
9830
- params.push(cond.value);
9831
- }
9832
- return { where: whereParts.join(" AND "), params, useParams };
9833
- }
9834
- function filterGroupedColumns(grouped, columnNames) {
9835
- const validColumns = new Set(columnNames);
9836
- const filtered = new Map;
9837
- for (const [value, columns] of grouped) {
9838
- const valid = columns.filter((column) => validColumns.has(column));
9839
- if (valid.length > 0)
9840
- filtered.set(value, valid);
9841
- }
9842
- return filtered;
9843
- }
9844
- function filterExactColumns(exact, columnNames) {
9845
- if (!exact || exact.length === 0)
9846
- return [];
9847
- const validColumns = new Set(columnNames);
9848
- return exact.filter((cond) => validColumns.has(cond.column));
9849
- }
9850
- function filterOrderByColumns(orderBy, columnNames) {
9851
- if (!orderBy)
9852
- return;
9853
- const validColumns = new Set(columnNames);
9854
- const filtered = orderBy.filter((order) => validColumns.has(order.column));
9855
- return filtered.length > 0 ? filtered : undefined;
9856
- }
9857
- function buildOrderClause(orderBy, kind = "sqlite") {
9858
- if (!orderBy?.length)
9859
- return "";
9860
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
9861
- return ` ORDER BY ${parts.join(", ")}`;
9862
- }
9863
- function useParamsFor(kind) {
9864
- return kind === "sqlite";
9913
+ if (watchers.size >= maxWatchedDirectories) {
9914
+ reportWatchLimit();
9915
+ initialScanQueue.length = 0;
9916
+ }
9917
+ if (initialScanQueue.length)
9918
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
9919
+ };
9920
+ const queueInitialChildren = (dir) => {
9921
+ const remaining = maxWatchedDirectories - watchers.size;
9922
+ if (remaining <= 0) {
9923
+ reportWatchLimit();
9924
+ return;
9925
+ }
9926
+ const children = readChildDirectories(dir);
9927
+ if (children.length > remaining)
9928
+ reportWatchLimit();
9929
+ initialScanQueue.push(...children.slice(0, remaining));
9930
+ if (!initialScanTimer && !processingInitialScan)
9931
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
9932
+ };
9933
+ const processChangedPath = (changed, fullChangedPath) => {
9934
+ const known = watchers.has(fullChangedPath);
9935
+ if (isDirectory(fullChangedPath)) {
9936
+ if (known) {
9937
+ const signature = directorySignature(fullChangedPath);
9938
+ if (signature && signature !== signatures.get(fullChangedPath)) {
9939
+ closeSubtree(fullChangedPath);
9940
+ watchDirectory(fullChangedPath, initialScanAsync);
9941
+ }
9942
+ scheduleUpdate(changed);
9943
+ return;
9944
+ }
9945
+ watchDirectory(fullChangedPath, initialScanAsync);
9946
+ } else if (known) {
9947
+ closeSubtree(fullChangedPath);
9948
+ }
9949
+ scheduleUpdate(changed);
9950
+ };
9951
+ const processPathInspections = () => {
9952
+ pathInspectionTimer = null;
9953
+ const entries = [...pendingPathInspections];
9954
+ pendingPathInspections.clear();
9955
+ for (const [changed, fullChangedPath] of entries) {
9956
+ processChangedPath(changed, fullChangedPath);
9957
+ }
9958
+ };
9959
+ const queuePathInspection = (changed, fullChangedPath) => {
9960
+ pendingPathInspections.set(changed, fullChangedPath);
9961
+ if (!pathInspectionTimer)
9962
+ pathInspectionTimer = setTimer(processPathInspections, 25);
9963
+ };
9964
+ const watchDirectory = (dir, initialScan = false) => {
9965
+ if (watchers.has(dir))
9966
+ return;
9967
+ if (watchers.size >= maxWatchedDirectories) {
9968
+ reportWatchLimit();
9969
+ return;
9970
+ }
9971
+ const rel = directoryRelativePath(dir);
9972
+ if (rel && ignored(rel))
9973
+ return;
9974
+ try {
9975
+ const watcher = watch(dir, { persistent: false, recursive }, (_event, filename) => {
9976
+ if (!filename) {
9977
+ scheduleUpdate();
9978
+ return;
9979
+ }
9980
+ const changed = normalizeRelativePath(join9(rel, filename.toString()));
9981
+ if (ignored(changed))
9982
+ return;
9983
+ const fullChangedPath = join9(options.root, changed);
9984
+ if (!isInsideRoot(options.root, fullChangedPath))
9985
+ return;
9986
+ if (recursive) {
9987
+ scheduleUpdate(changed);
9988
+ return;
9989
+ }
9990
+ if (initialScanAsync) {
9991
+ queuePathInspection(changed, fullChangedPath);
9992
+ return;
9993
+ }
9994
+ processChangedPath(changed, fullChangedPath);
9995
+ }) || {};
9996
+ watchers.set(dir, watcher);
9997
+ const signature = directorySignature(dir);
9998
+ if (signature)
9999
+ signatures.set(dir, signature);
10000
+ watcher.on?.("error", () => {
10001
+ if (watchers.get(dir) === watcher) {
10002
+ watchers.delete(dir);
10003
+ signatures.delete(dir);
10004
+ }
10005
+ });
10006
+ watcher.on?.("close", () => {
10007
+ if (watchers.get(dir) === watcher) {
10008
+ watchers.delete(dir);
10009
+ signatures.delete(dir);
10010
+ }
10011
+ });
10012
+ } catch (error) {
10013
+ options.onError?.(error);
10014
+ return;
10015
+ }
10016
+ if (recursive)
10017
+ return;
10018
+ if (initialScanAsync && initialScan) {
10019
+ queueInitialChildren(dir);
10020
+ return;
10021
+ }
10022
+ if (watchers.size >= maxWatchedDirectories) {
10023
+ reportWatchLimit();
10024
+ return;
10025
+ }
10026
+ for (const child of readChildDirectories(dir))
10027
+ watchDirectory(child);
10028
+ };
10029
+ watchDirectory(options.root, true);
10030
+ return { started: watchers.size > 0, close: closeAll };
9865
10031
  }
9866
- function placeValue(coerced, kind, useParams, params) {
9867
- if (useParams) {
9868
- params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
9869
- return "?";
9870
- }
9871
- if (coerced === null)
9872
- return "NULL";
9873
- if (typeof coerced === "number")
9874
- return String(coerced);
9875
- if (typeof coerced === "boolean")
9876
- return coerced ? "TRUE" : "FALSE";
9877
- const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
9878
- return escapeSqlString(text, kind);
10032
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
10033
+ var init_worktree_watcher = __esm(() => {
10034
+ init_search();
10035
+ });
10036
+
10037
+ // web-src/server/watch-child.ts
10038
+ var exports_watch_child = {};
10039
+ __export(exports_watch_child, {
10040
+ runWatchChild: () => runWatchChild,
10041
+ parsePorcelainV2: () => parsePorcelainV2
10042
+ });
10043
+ import { createHash as createHash2 } from "node:crypto";
10044
+ import { createReadStream as createReadStream2, lstatSync as lstatSync4 } from "node:fs";
10045
+ import { join as join10 } from "node:path";
10046
+ function send(message) {
10047
+ process.stdout.write(`${JSON.stringify(message)}
10048
+ `);
9879
10049
  }
9880
- function coerceCell(cell, columnType) {
9881
- return coerceDbValue(cell.value, columnType);
10050
+ function readConfigLine() {
10051
+ return new Promise((resolve2, reject) => {
10052
+ let buffer = "";
10053
+ const stdin = process.stdin;
10054
+ stdin.setEncoding("utf8");
10055
+ const onData = (chunk) => {
10056
+ buffer += chunk;
10057
+ const newline = buffer.indexOf(`
10058
+ `);
10059
+ if (newline === -1)
10060
+ return;
10061
+ stdin.off("data", onData);
10062
+ stdin.off("end", onEnd);
10063
+ resolve2(buffer.slice(0, newline));
10064
+ };
10065
+ const onEnd = () => {
10066
+ stdin.off("data", onData);
10067
+ reject(new Error("watch child: stdin closed before config arrived"));
10068
+ };
10069
+ stdin.on("data", onData);
10070
+ stdin.on("end", onEnd);
10071
+ });
9882
10072
  }
9883
- function formatWriteComparisons(cells, columnTypes, kind, useParams, params, separator) {
9884
- return cells.map((cell) => `${sanitizeIdentifier(cell.column, kind)} = ${placeValue(coerceCell(cell, columnTypes.get(cell.column) ?? "TEXT"), kind, useParams, params)}`).join(separator);
10073
+ function hashFile(path) {
10074
+ return new Promise((resolve2) => {
10075
+ const hash = createHash2("sha256");
10076
+ const stream = createReadStream2(path);
10077
+ stream.on("data", (chunk) => hash.update(chunk));
10078
+ stream.on("end", () => resolve2(hash.digest("hex")));
10079
+ stream.on("error", () => resolve2("unreadable"));
10080
+ });
9885
10081
  }
9886
- function buildInsertSql(table, cells, columnTypes, kind) {
9887
- if (cells.length === 0) {
9888
- throw new Error("insert requires at least one column value");
10082
+ function parsePorcelainV2(raw) {
10083
+ const records = raw.split("\x00");
10084
+ let head = "";
10085
+ const entries = [];
10086
+ for (let i = 0;i < records.length; i++) {
10087
+ const record = records[i];
10088
+ if (!record)
10089
+ continue;
10090
+ if (record.startsWith("# branch.oid ")) {
10091
+ head = record.slice("# branch.oid ".length);
10092
+ continue;
10093
+ }
10094
+ if (record.startsWith("# "))
10095
+ continue;
10096
+ const kind = record[0];
10097
+ if (kind === "1" || kind === "2" || kind === "u") {
10098
+ const metaCount = kind === "1" ? 8 : kind === "2" ? 9 : 10;
10099
+ const status = record.split(" ").slice(0, metaCount).join(" ");
10100
+ entries.push({ path: record.slice(status.length + 1), status });
10101
+ if (kind === "2")
10102
+ i++;
10103
+ continue;
10104
+ }
10105
+ if (kind === "?" || kind === "!") {
10106
+ entries.push({ path: record.slice(2), status: kind });
10107
+ }
9889
10108
  }
9890
- const useParams = useParamsFor(kind);
9891
- const params = [];
9892
- const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
9893
- const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
9894
- const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
9895
- return { sql, params };
10109
+ return { head, entries };
9896
10110
  }
9897
- function buildUpdateSql(table, set, pk, columnTypes, kind) {
9898
- if (set.length === 0) {
9899
- throw new Error("update requires at least one column to set");
9900
- }
9901
- if (pk.length === 0) {
9902
- throw new Error("update requires a primary key condition");
10111
+ async function readWorktreeSnapshot(config) {
10112
+ const result = await runAsync([
10113
+ "git",
10114
+ "--no-optional-locks",
10115
+ "-c",
10116
+ "core.fsmonitor=false",
10117
+ "status",
10118
+ "--porcelain=v2",
10119
+ "-z",
10120
+ "--branch",
10121
+ "--untracked-files=all"
10122
+ ], config.root, { timeout: 60000 });
10123
+ if (result.code !== 0)
10124
+ return null;
10125
+ const parsed = parsePorcelainV2(result.stdout);
10126
+ const entries = new Map;
10127
+ for (const entry of parsed.entries) {
10128
+ if (!entry.path)
10129
+ continue;
10130
+ if (isSkippableSearchPath(entry.path, config.omitDirNames, config.excludeNames))
10131
+ continue;
10132
+ entries.set(entry.path, await pathSignature(config.root, entry));
9903
10133
  }
9904
- const useParams = useParamsFor(kind);
9905
- const params = [];
9906
- const setSql = formatWriteComparisons(set, columnTypes, kind, useParams, params, ", ");
9907
- const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
9908
- const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
9909
- return { sql, params };
10134
+ return { head: parsed.head, entries };
9910
10135
  }
9911
- function buildDeleteSql(table, pk, columnTypes, kind) {
9912
- if (pk.length === 0) {
9913
- throw new Error("delete requires a primary key condition");
10136
+ async function pathSignature(root, entry) {
10137
+ const full = join10(root, entry.path);
10138
+ let stats;
10139
+ try {
10140
+ stats = lstatSync4(full);
10141
+ } catch {
10142
+ return `${entry.status}:absent`;
9914
10143
  }
9915
- const useParams = useParamsFor(kind);
9916
- const params = [];
9917
- const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
9918
- const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
9919
- return { sql, params };
10144
+ const base = `${entry.status}:${stats.size}:${stats.mtimeMs}:${stats.mode}`;
10145
+ if (stats.isFile() && stats.size <= MAX_HASHED_FILE_BYTES) {
10146
+ return `${base}:${await hashFile(full)}`;
10147
+ }
10148
+ return base;
9920
10149
  }
9921
- var init_sql_utils = __esm(() => {
9922
- init_serialize();
9923
- });
9924
-
9925
- // web-src/server/database/mutate.ts
9926
- function assertCells(cells, label) {
9927
- if (!Array.isArray(cells)) {
9928
- throw new Error(`${label} must be an array`);
10150
+ function snapshotDiff(previous, next) {
10151
+ if (previous.head !== next.head)
10152
+ return { full: true, paths: [] };
10153
+ const paths = [];
10154
+ for (const [path, signature] of next.entries) {
10155
+ if (previous.entries.get(path) !== signature)
10156
+ paths.push(path);
9929
10157
  }
9930
- for (const cell of cells) {
9931
- if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
9932
- throw new Error(`${label} contains an invalid cell`);
9933
- }
10158
+ for (const path of previous.entries.keys()) {
10159
+ if (!next.entries.has(path))
10160
+ paths.push(path);
9934
10161
  }
9935
- return cells;
10162
+ return { full: false, paths };
9936
10163
  }
9937
- function buildMutationStatements(table, mutations, columns, kind) {
9938
- if (!Array.isArray(mutations) || mutations.length === 0) {
9939
- throw new Error("no mutations provided");
9940
- }
9941
- if (mutations.length > MAX_MUTATIONS) {
9942
- throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
10164
+ async function runWatchChild() {
10165
+ const config = JSON.parse(await readConfigLine());
10166
+ process.stdin.on("end", () => process.exit(0));
10167
+ process.stdin.on("close", () => process.exit(0));
10168
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
10169
+ process.on(signal, () => process.exit(0));
10170
+ }
10171
+ let watching = false;
10172
+ if (!config.pollOnly) {
10173
+ const recursive = supportsNativeRecursiveWatch(process.platform);
10174
+ const watch = startWorktreeUpdateWatch({
10175
+ root: config.root,
10176
+ omitDirNames: config.omitDirNames,
10177
+ excludeNames: config.excludeNames,
10178
+ recursive,
10179
+ initialScanMode: recursive ? "sync" : "async",
10180
+ maxWatchedDirectories: config.maxWatchedDirectories,
10181
+ debounceMs: config.debounceMs,
10182
+ onUpdate: (paths) => send({ type: "update", paths }),
10183
+ onWatchLimit: (limit) => send({ type: "watch-limit", limit }),
10184
+ onError: (error) => send({
10185
+ type: "warn",
10186
+ message: error instanceof Error ? error.message : String(error)
10187
+ })
10188
+ });
10189
+ watching = watch.started;
9943
10190
  }
9944
- const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
9945
- const columnNames = new Set(columns.map((c) => c.name));
9946
- const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
9947
- const pkNames = new Set(pkColumns);
9948
- const requireKnownColumns = (cells, label) => {
9949
- for (const cell of cells) {
9950
- if (!columnNames.has(cell.column)) {
9951
- throw new Error(`unknown column: ${cell.column}`);
9952
- }
9953
- }
9954
- };
9955
- const requirePrimaryKey = (pk) => {
9956
- if (pkColumns.length === 0) {
9957
- throw new Error("table has no primary key; row update/delete is not supported");
10191
+ send({ type: "ready", watching, pollOnly: config.pollOnly });
10192
+ setInterval(() => send({ type: "heartbeat" }), config.heartbeatIntervalMs).unref?.();
10193
+ setInterval(() => {
10194
+ try {
10195
+ process.kill(config.parentPid, 0);
10196
+ } catch {
10197
+ process.exit(0);
9958
10198
  }
9959
- const provided = new Set(pk.map((c) => c.column));
9960
- for (const name of pkColumns) {
9961
- if (!provided.has(name)) {
9962
- throw new Error(`missing primary key column: ${name}`);
9963
- }
10199
+ }, config.heartbeatIntervalMs);
10200
+ let snapshot = null;
10201
+ let baselineEstablished = false;
10202
+ const poll = async () => {
10203
+ const next = await readWorktreeSnapshot(config);
10204
+ if (!next)
10205
+ return;
10206
+ if (!snapshot) {
10207
+ snapshot = next;
10208
+ if (baselineEstablished)
10209
+ send({ type: "update" });
10210
+ baselineEstablished = true;
10211
+ return;
9964
10212
  }
9965
- for (const cell of pk) {
9966
- if (!pkNames.has(cell.column)) {
9967
- throw new Error(`not a primary key column: ${cell.column}`);
9968
- }
9969
- if (cell.value === null) {
9970
- throw new Error(`primary key column cannot be null: ${cell.column}`);
9971
- }
10213
+ const diff = snapshotDiff(snapshot, next);
10214
+ snapshot = next;
10215
+ if (diff.full) {
10216
+ send({ type: "update" });
10217
+ return;
9972
10218
  }
10219
+ if (diff.paths.length)
10220
+ send({ type: "update", paths: diff.paths });
9973
10221
  };
9974
- const statements = [];
9975
- for (const mutation of mutations) {
9976
- if (!mutation || typeof mutation !== "object") {
9977
- throw new Error("invalid mutation");
9978
- }
10222
+ setInterval(() => {
10223
+ poll().catch((error) => send({
10224
+ type: "warn",
10225
+ message: error instanceof Error ? error.message : String(error)
10226
+ }));
10227
+ }, config.pollIntervalMs);
10228
+ }
10229
+ var MAX_HASHED_FILE_BYTES;
10230
+ var init_watch_child = __esm(() => {
10231
+ init_runtime();
10232
+ init_search();
10233
+ init_worktree_watcher();
10234
+ MAX_HASHED_FILE_BYTES = 4 * 1024 * 1024;
10235
+ });
10236
+
10237
+ // web-src/server/database/serialize.ts
10238
+ function serializeDbValue(value) {
10239
+ if (value === null || value === undefined)
10240
+ return null;
10241
+ if (typeof value === "bigint") {
10242
+ return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value.toString();
10243
+ }
10244
+ if (value instanceof Uint8Array) {
10245
+ return `<blob ${value.byteLength} bytes>`;
10246
+ }
10247
+ if (typeof value === "object") {
10248
+ try {
10249
+ return JSON.stringify(value);
10250
+ } catch {
10251
+ return String(value);
10252
+ }
10253
+ }
10254
+ return value;
10255
+ }
10256
+ function serializeDbRow(row) {
10257
+ return row.map(serializeDbValue);
10258
+ }
10259
+ function serializeDbRows(rows) {
10260
+ return rows.map(serializeDbRow);
10261
+ }
10262
+ function coerceDbValue(value, columnType) {
10263
+ if (value === null)
10264
+ return null;
10265
+ const t = (columnType || "").toLowerCase();
10266
+ if (/bool/.test(t)) {
10267
+ const v = value.trim().toLowerCase();
10268
+ if (v === "")
10269
+ return null;
10270
+ if (v === "true" || v === "t" || v === "1")
10271
+ return true;
10272
+ if (v === "false" || v === "f" || v === "0")
10273
+ return false;
10274
+ return value;
10275
+ }
10276
+ if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
10277
+ const trimmed = value.trim();
10278
+ if (trimmed === "")
10279
+ return null;
10280
+ const n = Number(trimmed);
10281
+ if (Number.isFinite(n) && String(n) === trimmed)
10282
+ return n;
10283
+ return value;
10284
+ }
10285
+ return value;
10286
+ }
10287
+ var MIN_SAFE, MAX_SAFE;
10288
+ var init_serialize = __esm(() => {
10289
+ MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
10290
+ MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
10291
+ });
10292
+
10293
+ // web-src/server/database/sql-utils.ts
10294
+ function sanitizeIdentifier(name, kind = "sqlite") {
10295
+ if (kind === "mysql")
10296
+ return `\`${name.replace(/`/g, "``")}\``;
10297
+ return `"${name.replace(/"/g, '""')}"`;
10298
+ }
10299
+ function escapeSqlString(value, kind) {
10300
+ const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
10301
+ return `'${escaped}'`;
10302
+ }
10303
+ function buildFilterWhere(grouped, kind, exact) {
10304
+ const whereParts = [];
10305
+ const params = [];
10306
+ const useParams = kind === "sqlite";
10307
+ const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
10308
+ for (const [value, cols] of grouped) {
10309
+ const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
10310
+ if (cols.length === 1) {
10311
+ whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
10312
+ if (useParams)
10313
+ params.push(`%${value}%`);
10314
+ } else {
10315
+ const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
10316
+ whereParts.push(`(${orParts.join(" OR ")})`);
10317
+ if (useParams) {
10318
+ for (let i = 0;i < cols.length; i++)
10319
+ params.push(`%${value}%`);
10320
+ }
10321
+ }
10322
+ }
10323
+ for (const cond of exact ?? []) {
10324
+ const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
10325
+ whereParts.push(`${castOf(cond.column)} = ${rhs}`);
10326
+ if (useParams)
10327
+ params.push(cond.value);
10328
+ }
10329
+ return { where: whereParts.join(" AND "), params, useParams };
10330
+ }
10331
+ function filterGroupedColumns(grouped, columnNames) {
10332
+ const validColumns = new Set(columnNames);
10333
+ const filtered = new Map;
10334
+ for (const [value, columns] of grouped) {
10335
+ const valid = columns.filter((column) => validColumns.has(column));
10336
+ if (valid.length > 0)
10337
+ filtered.set(value, valid);
10338
+ }
10339
+ return filtered;
10340
+ }
10341
+ function filterExactColumns(exact, columnNames) {
10342
+ if (!exact || exact.length === 0)
10343
+ return [];
10344
+ const validColumns = new Set(columnNames);
10345
+ return exact.filter((cond) => validColumns.has(cond.column));
10346
+ }
10347
+ function filterOrderByColumns(orderBy, columnNames) {
10348
+ if (!orderBy)
10349
+ return;
10350
+ const validColumns = new Set(columnNames);
10351
+ const filtered = orderBy.filter((order) => validColumns.has(order.column));
10352
+ return filtered.length > 0 ? filtered : undefined;
10353
+ }
10354
+ function buildOrderClause(orderBy, kind = "sqlite") {
10355
+ if (!orderBy?.length)
10356
+ return "";
10357
+ const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
10358
+ return ` ORDER BY ${parts.join(", ")}`;
10359
+ }
10360
+ function useParamsFor(kind) {
10361
+ return kind === "sqlite";
10362
+ }
10363
+ function placeValue(coerced, kind, useParams, params) {
10364
+ if (useParams) {
10365
+ params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
10366
+ return "?";
10367
+ }
10368
+ if (coerced === null)
10369
+ return "NULL";
10370
+ if (typeof coerced === "number")
10371
+ return String(coerced);
10372
+ if (typeof coerced === "boolean")
10373
+ return coerced ? "TRUE" : "FALSE";
10374
+ const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
10375
+ return escapeSqlString(text, kind);
10376
+ }
10377
+ function coerceCell(cell, columnType) {
10378
+ return coerceDbValue(cell.value, columnType);
10379
+ }
10380
+ function formatWriteComparisons(cells, columnTypes, kind, useParams, params, separator) {
10381
+ return cells.map((cell) => `${sanitizeIdentifier(cell.column, kind)} = ${placeValue(coerceCell(cell, columnTypes.get(cell.column) ?? "TEXT"), kind, useParams, params)}`).join(separator);
10382
+ }
10383
+ function buildInsertSql(table, cells, columnTypes, kind) {
10384
+ if (cells.length === 0) {
10385
+ throw new Error("insert requires at least one column value");
10386
+ }
10387
+ const useParams = useParamsFor(kind);
10388
+ const params = [];
10389
+ const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
10390
+ const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
10391
+ const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
10392
+ return { sql, params };
10393
+ }
10394
+ function buildUpdateSql(table, set, pk, columnTypes, kind) {
10395
+ if (set.length === 0) {
10396
+ throw new Error("update requires at least one column to set");
10397
+ }
10398
+ if (pk.length === 0) {
10399
+ throw new Error("update requires a primary key condition");
10400
+ }
10401
+ const useParams = useParamsFor(kind);
10402
+ const params = [];
10403
+ const setSql = formatWriteComparisons(set, columnTypes, kind, useParams, params, ", ");
10404
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10405
+ const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
10406
+ return { sql, params };
10407
+ }
10408
+ function buildDeleteSql(table, pk, columnTypes, kind) {
10409
+ if (pk.length === 0) {
10410
+ throw new Error("delete requires a primary key condition");
10411
+ }
10412
+ const useParams = useParamsFor(kind);
10413
+ const params = [];
10414
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10415
+ const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
10416
+ return { sql, params };
10417
+ }
10418
+ var init_sql_utils = __esm(() => {
10419
+ init_serialize();
10420
+ });
10421
+
10422
+ // web-src/server/database/mutate.ts
10423
+ function assertCells(cells, label) {
10424
+ if (!Array.isArray(cells)) {
10425
+ throw new Error(`${label} must be an array`);
10426
+ }
10427
+ for (const cell of cells) {
10428
+ if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
10429
+ throw new Error(`${label} contains an invalid cell`);
10430
+ }
10431
+ }
10432
+ return cells;
10433
+ }
10434
+ function buildMutationStatements(table, mutations, columns, kind) {
10435
+ if (!Array.isArray(mutations) || mutations.length === 0) {
10436
+ throw new Error("no mutations provided");
10437
+ }
10438
+ if (mutations.length > MAX_MUTATIONS) {
10439
+ throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
10440
+ }
10441
+ const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
10442
+ const columnNames = new Set(columns.map((c) => c.name));
10443
+ const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
10444
+ const pkNames = new Set(pkColumns);
10445
+ const requireKnownColumns = (cells, label) => {
10446
+ for (const cell of cells) {
10447
+ if (!columnNames.has(cell.column)) {
10448
+ throw new Error(`unknown column: ${cell.column}`);
10449
+ }
10450
+ }
10451
+ };
10452
+ const requirePrimaryKey = (pk) => {
10453
+ if (pkColumns.length === 0) {
10454
+ throw new Error("table has no primary key; row update/delete is not supported");
10455
+ }
10456
+ const provided = new Set(pk.map((c) => c.column));
10457
+ for (const name of pkColumns) {
10458
+ if (!provided.has(name)) {
10459
+ throw new Error(`missing primary key column: ${name}`);
10460
+ }
10461
+ }
10462
+ for (const cell of pk) {
10463
+ if (!pkNames.has(cell.column)) {
10464
+ throw new Error(`not a primary key column: ${cell.column}`);
10465
+ }
10466
+ if (cell.value === null) {
10467
+ throw new Error(`primary key column cannot be null: ${cell.column}`);
10468
+ }
10469
+ }
10470
+ };
10471
+ const statements = [];
10472
+ for (const mutation of mutations) {
10473
+ if (!mutation || typeof mutation !== "object") {
10474
+ throw new Error("invalid mutation");
10475
+ }
9979
10476
  if (mutation.kind === "insert") {
9980
10477
  const values = assertCells(mutation.values, "insert values");
9981
10478
  requireKnownColumns(values, "insert values");
@@ -10002,7 +10499,7 @@ var init_mutate = __esm(() => {
10002
10499
  });
10003
10500
 
10004
10501
  // web-src/server/database/sources/sql-snapshot.ts
10005
- import { createHash as createHash2 } from "node:crypto";
10502
+ import { createHash as createHash3 } from "node:crypto";
10006
10503
  function normalizeRawValue(v) {
10007
10504
  if (v === null)
10008
10505
  return "\\N";
@@ -10022,7 +10519,7 @@ function rowToPayloadJson(columns, row) {
10022
10519
  }
10023
10520
  function computeRowHash(columns, row) {
10024
10521
  const parts = columns.map((_, i) => normalizeRawValue(row[i]));
10025
- return createHash2("sha256").update(parts.join("\t")).digest("hex");
10522
+ return createHash3("sha256").update(parts.join("\t")).digest("hex");
10026
10523
  }
10027
10524
  function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
10028
10525
  if (pkColumns.length === 0) {
@@ -12093,7 +12590,7 @@ __export(exports_redis, {
12093
12590
  canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer,
12094
12591
  __setRedisClientFactoryForTest: () => __setRedisClientFactoryForTest
12095
12592
  });
12096
- import { createHash as createHash3 } from "node:crypto";
12593
+ import { createHash as createHash4 } from "node:crypto";
12097
12594
  import { createClient } from "@redis/client";
12098
12595
  function __setRedisClientFactoryForTest(factory) {
12099
12596
  createRedisClientImpl = factory ?? ((options) => createClient(options));
@@ -12470,7 +12967,7 @@ function createRedisAdapter(config) {
12470
12967
  if (type === "none") {
12471
12968
  return {
12472
12969
  payload: { type: "none" },
12473
- fullHash: createHash3("sha256").update("").digest("hex")
12970
+ fullHash: createHash4("sha256").update("").digest("hex")
12474
12971
  };
12475
12972
  }
12476
12973
  if (type === "string")
@@ -12487,7 +12984,7 @@ function createRedisAdapter(config) {
12487
12984
  return snapshotFetchStream(db, hexKey, signal);
12488
12985
  return {
12489
12986
  payload: { type: "none" },
12490
- fullHash: createHash3("sha256").update("").digest("hex")
12987
+ fullHash: createHash4("sha256").update("").digest("hex")
12491
12988
  };
12492
12989
  }
12493
12990
  async function evalHex(db, luaBody, extraArgv, label, signal) {
@@ -12499,7 +12996,7 @@ function createRedisAdapter(config) {
12499
12996
  }
12500
12997
  async function snapshotFetchString(db, hexKey, signal) {
12501
12998
  const fullSize = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('STRLEN', fromhex(ARGV[1]))`, [hexKey], "STRLEN", signal)) || 0;
12502
- const hasher = createHash3("sha256");
12999
+ const hasher = createHash4("sha256");
12503
13000
  let previewBytes = Buffer.alloc(0);
12504
13001
  for (let offset = 0;offset < fullSize; offset += REDIS_STRING_BYTE_LIMIT) {
12505
13002
  if (signal?.aborted)
@@ -12532,7 +13029,7 @@ function createRedisAdapter(config) {
12532
13029
  }
12533
13030
  async function snapshotFetchList(db, hexKey, signal) {
12534
13031
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('LLEN', fromhex(ARGV[1]))`, [hexKey], "LLEN", signal)) || 0;
12535
- const hasher = createHash3("sha256");
13032
+ const hasher = createHash4("sha256");
12536
13033
  let previewItems = [];
12537
13034
  for (let offset = 0;offset < total; offset += REDIS_COLLECTION_LIMIT) {
12538
13035
  if (signal?.aborted)
@@ -12574,7 +13071,7 @@ function createRedisAdapter(config) {
12574
13071
  uniquePairs.push(p);
12575
13072
  }
12576
13073
  uniquePairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
12577
- const hasher = createHash3("sha256");
13074
+ const hasher = createHash4("sha256");
12578
13075
  for (const [f, v] of uniquePairs) {
12579
13076
  hashHexItem(hasher, f);
12580
13077
  hashHexItem(hasher, v);
@@ -12602,7 +13099,7 @@ function createRedisAdapter(config) {
12602
13099
  } while (cursor !== "0");
12603
13100
  const dedup = Array.from(new Set(allMembers));
12604
13101
  dedup.sort();
12605
- const hasher = createHash3("sha256");
13102
+ const hasher = createHash4("sha256");
12606
13103
  for (const m of dedup)
12607
13104
  hashHexItem(hasher, m);
12608
13105
  const previewMembers = dedup.slice(0, REDIS_COLLECTION_LIMIT).map(decodeHexItem);
@@ -12616,7 +13113,7 @@ function createRedisAdapter(config) {
12616
13113
  }
12617
13114
  async function snapshotFetchZset(db, hexKey, signal) {
12618
13115
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('ZCARD', fromhex(ARGV[1]))`, [hexKey], "ZCARD", signal)) || 0;
12619
- const hasher = createHash3("sha256");
13116
+ const hasher = createHash4("sha256");
12620
13117
  let previewMembers = [];
12621
13118
  for (let offset = 0;offset < total; offset += REDIS_COLLECTION_LIMIT) {
12622
13119
  if (signal?.aborted)
@@ -12646,7 +13143,7 @@ function createRedisAdapter(config) {
12646
13143
  }
12647
13144
  async function snapshotFetchStream(db, hexKey, signal) {
12648
13145
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('XLEN', fromhex(ARGV[1]))`, [hexKey], "XLEN", signal)) || 0;
12649
- const hasher = createHash3("sha256");
13146
+ const hasher = createHash4("sha256");
12650
13147
  let previewEntries = [];
12651
13148
  let startId = "-";
12652
13149
  const seenIds = new Set;
@@ -13238,7 +13735,7 @@ var init_raw_file_headers = __esm(() => {
13238
13735
 
13239
13736
  // web-src/server/database/adapters/s3.ts
13240
13737
  import { spawnSync as spawnSync4 } from "node:child_process";
13241
- import { createHash as createHash4, createHmac } from "node:crypto";
13738
+ import { createHash as createHash5, createHmac } from "node:crypto";
13242
13739
  function createS3RequestDeadline() {
13243
13740
  const timeoutMs = s3RequestTimeoutMs;
13244
13741
  return {
@@ -13268,10 +13765,10 @@ function hmac(key, value) {
13268
13765
  return createHmac("sha256", key).update(value, "utf8").digest();
13269
13766
  }
13270
13767
  function sha256(value) {
13271
- return createHash4("sha256").update(value, "utf8").digest("hex");
13768
+ return createHash5("sha256").update(value, "utf8").digest("hex");
13272
13769
  }
13273
13770
  function sha256Bytes(value) {
13274
- return createHash4("sha256").update(value).digest("hex");
13771
+ return createHash5("sha256").update(value).digest("hex");
13275
13772
  }
13276
13773
  function encodeRfc3986(value) {
13277
13774
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
@@ -14278,7 +14775,7 @@ import {
14278
14775
  statSync as statSync5
14279
14776
  } from "node:fs";
14280
14777
  import { lstat, open as open2, readdir, readFile as readFile2, stat as stat2 } from "node:fs/promises";
14281
- import { basename, join as join9, relative as relative4 } from "node:path";
14778
+ import { basename, join as join11, relative as relative5 } from "node:path";
14282
14779
  function isSqliteFile(fullPath) {
14283
14780
  try {
14284
14781
  const stat3 = statSync5(fullPath);
@@ -14349,7 +14846,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14349
14846
  return;
14350
14847
  if (omitSet.has(entry.toLowerCase()))
14351
14848
  continue;
14352
- const full = join9(dir, entry);
14849
+ const full = join11(dir, entry);
14353
14850
  let entryStat;
14354
14851
  try {
14355
14852
  entryStat = await lstat(full);
@@ -14366,7 +14863,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14366
14863
  continue;
14367
14864
  if (!await isSqliteFileAsync(full))
14368
14865
  continue;
14369
- const rel = relative4(cwd, full);
14866
+ const rel = relative5(cwd, full);
14370
14867
  if (rel.startsWith("..") || rel.startsWith("/"))
14371
14868
  continue;
14372
14869
  results.push({
@@ -14393,7 +14890,7 @@ function validateDbPath(cwd, dbPath) {
14393
14890
  const parts = dbPath.split(/[\\/]+/);
14394
14891
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
14395
14892
  return null;
14396
- const full = join9(cwd, dbPath);
14893
+ const full = join11(cwd, dbPath);
14397
14894
  if (!existsSync6(full))
14398
14895
  return null;
14399
14896
  let realCwd;
@@ -14404,7 +14901,7 @@ function validateDbPath(cwd, dbPath) {
14404
14901
  } catch {
14405
14902
  return null;
14406
14903
  }
14407
- const rel = relative4(realCwd, realFull);
14904
+ const rel = relative5(realCwd, realFull);
14408
14905
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
14409
14906
  return null;
14410
14907
  if (!isSqliteFile(realFull))
@@ -14565,7 +15062,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
14565
15062
  }
14566
15063
  async function readDotenvAsync(composeDir) {
14567
15064
  try {
14568
- const content = await readFile2(join9(composeDir, ".env"), "utf-8");
15065
+ const content = await readFile2(join11(composeDir, ".env"), "utf-8");
14569
15066
  return parseDotenvContent(content);
14570
15067
  } catch {
14571
15068
  return {};
@@ -14687,7 +15184,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
14687
15184
  for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
14688
15185
  servicePositions.push({ name: match[1], start: match.index });
14689
15186
  }
14690
- const relDir = relative4(cwd, composeDir);
15187
+ const relDir = relative5(cwd, composeDir);
14691
15188
  const isRoot = relDir === "" || relDir === ".";
14692
15189
  const relDirSlash = relDir.replace(/\\/g, "/");
14693
15190
  const filename = basename(filepath);
@@ -14792,7 +15289,7 @@ async function walkForMarkerFileAsync(dir, depth, omitSet, hasCapacity, visitDir
14792
15289
  return;
14793
15290
  if (omitSet.has(entry.toLowerCase()))
14794
15291
  continue;
14795
- const full = join9(dir, entry);
15292
+ const full = join11(dir, entry);
14796
15293
  let entryStat;
14797
15294
  try {
14798
15295
  entryStat = await lstat(full);
@@ -14819,7 +15316,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
14819
15316
  omitSet.add("node_modules");
14820
15317
  await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_DOCKER_SERVICES, async (dir) => {
14821
15318
  for (const filename of COMPOSE_FILENAMES) {
14822
- const filepath = join9(dir, filename);
15319
+ const filepath = join11(dir, filename);
14823
15320
  if (await pathExistsAsync(filepath)) {
14824
15321
  await parseComposeFileAsync(filepath, dir, cwd, results);
14825
15322
  break;
@@ -14930,410 +15427,153 @@ function isSafeDockerRelDir(value) {
14930
15427
  }
14931
15428
  function isSafeDockerDatabaseName(value) {
14932
15429
  if (value === undefined)
14933
- return true;
14934
- if (value === "")
14935
- return false;
14936
- if (hasControlCharacter(value))
14937
- return false;
14938
- return /^[A-Za-z0-9_$.-]+$/.test(value);
14939
- }
14940
- function cloneSupabaseDiscoveryResult(result) {
14941
- return result.map((entry) => ({ ...entry }));
14942
- }
14943
- function parseSupabaseConfigToml(content) {
14944
- let section = null;
14945
- let projectId = null;
14946
- let dbPort = null;
14947
- for (const rawLine of content.split(`
14948
- `)) {
14949
- const line = rawLine.trim();
14950
- if (!line || line.startsWith("#"))
14951
- continue;
14952
- const sectionMatch = line.match(/^\[([^\]]+)\]$/);
14953
- if (sectionMatch) {
14954
- section = sectionMatch[1];
14955
- continue;
14956
- }
14957
- const kvMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
14958
- if (!kvMatch)
14959
- continue;
14960
- const value = stripScalarSyntax(kvMatch[2]);
14961
- if (section === null && kvMatch[1] === "project_id") {
14962
- projectId = value;
14963
- } else if (section === "db" && kvMatch[1] === "port") {
14964
- dbPort = value;
14965
- }
14966
- }
14967
- if (!projectId || !isSafeDockerServiceName(projectId))
14968
- return null;
14969
- return {
14970
- projectId,
14971
- dbPort: dbPort && /^\d+$/.test(dbPort) ? dbPort : DEFAULT_SUPABASE_DB_PORT
14972
- };
14973
- }
14974
- async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal) {
14975
- const cacheKey = discoveryCacheKey(cwd, omitDirNames);
14976
- const now = Date.now();
14977
- const cached = supabaseDiscoveryCache.get(cacheKey);
14978
- if (cached && cached.expiresAt > now) {
14979
- return cloneSupabaseDiscoveryResult(cached.result);
14980
- }
14981
- const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
14982
- omitSet.add(".git");
14983
- omitSet.add("node_modules");
14984
- const results = [];
14985
- await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
14986
- const configPath = join9(dir, "supabase", "config.toml");
14987
- if (!await pathExistsAsync(configPath))
14988
- return;
14989
- try {
14990
- const content = await readFile2(configPath, "utf-8");
14991
- const parsed = parseSupabaseConfigToml(content);
14992
- if (!parsed)
14993
- return;
14994
- const relDir = relative4(cwd, dir);
14995
- const isRoot = relDir === "" || relDir === ".";
14996
- const relDirSlash = relDir.replace(/\\/g, "/");
14997
- const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
14998
- const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
14999
- results.push({
15000
- id,
15001
- path: isRoot ? "supabase/config.toml" : `${relDirSlash}/supabase/config.toml`,
15002
- name: `${parsed.projectId} (Supabase CLI, postgres@127.0.0.1:${parsed.dbPort}/postgres${labelPath})`,
15003
- sizeBytes: 0,
15004
- kind: "postgresql",
15005
- projectId: parsed.projectId,
15006
- relDirSlash,
15007
- dbPort: parsed.dbPort
15008
- });
15009
- } catch {}
15010
- }, signal);
15011
- if (signal?.aborted)
15012
- return cloneSupabaseDiscoveryResult(results);
15013
- supabaseDiscoveryCache.set(cacheKey, {
15014
- expiresAt: now + SUPABASE_DISCOVERY_TTL_MS,
15015
- result: cloneSupabaseDiscoveryResult(results)
15016
- });
15017
- return cloneSupabaseDiscoveryResult(results);
15018
- }
15019
- function parseSupabaseDbId(dbId) {
15020
- if (!dbId.startsWith("supabase:"))
15021
- return null;
15022
- const rest = dbId.slice("supabase:".length);
15023
- if (!rest)
15024
- return null;
15025
- const atIdx = rest.indexOf("@");
15026
- let projectId;
15027
- let relDir = "";
15028
- if (atIdx >= 0) {
15029
- if (rest.indexOf("@", atIdx + 1) >= 0)
15030
- return null;
15031
- projectId = rest.slice(0, atIdx);
15032
- try {
15033
- relDir = decodeURIComponent(rest.slice(atIdx + 1));
15034
- } catch {
15035
- return null;
15036
- }
15037
- if (!isSafeDockerRelDir(relDir))
15038
- return null;
15039
- } else {
15040
- projectId = rest;
15041
- }
15042
- if (!isSafeDockerServiceName(projectId))
15043
- return null;
15044
- return { projectId, relDir };
15045
- }
15046
- async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal) {
15047
- const parsed = parseSupabaseDbId(dbId);
15048
- if (!parsed)
15049
- return null;
15050
- const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
15051
- return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
15052
- }
15053
- var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache, DB_KIND_VALUES, MAX_SUPABASE_PROJECTS = 30, SUPABASE_DISCOVERY_TTL_MS = 5000, DEFAULT_SUPABASE_DB_PORT = "54322", supabaseDiscoveryCache;
15054
- var init_discovery = __esm(() => {
15055
- SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
15056
- sqliteDiscoveryCache = new Map;
15057
- COMPOSE_FILENAMES = [
15058
- "docker-compose.yml",
15059
- "docker-compose.yaml",
15060
- "compose.yml",
15061
- "compose.yaml"
15062
- ];
15063
- dockerDiscoveryCache = new Map;
15064
- DB_KIND_VALUES = new Set([
15065
- "sqlite",
15066
- "postgresql",
15067
- "mysql",
15068
- "redis",
15069
- "elasticsearch",
15070
- "s3",
15071
- "dynamodb"
15072
- ]);
15073
- supabaseDiscoveryCache = new Map;
15074
- });
15075
-
15076
- // web-src/server/worktree-watcher.ts
15077
- import {
15078
- lstatSync as lstatSync3,
15079
- readdirSync as nodeReaddirSync,
15080
- watch as nodeWatch
15081
- } from "node:fs";
15082
- import { join as join10, relative as relative5 } from "node:path";
15083
- function normalizeRelativePath(path) {
15084
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
15085
- }
15086
- function isInsideRoot(root, path) {
15087
- const rel = relative5(root, path).replace(/\\/g, "/");
15088
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
15089
- }
15090
- function startWorktreeUpdateWatch(options) {
15091
- const watch = options.watch || nodeWatch;
15092
- const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
15093
- const isDirectory = options.isDirectory || ((path) => {
15094
- try {
15095
- return lstatSync3(path).isDirectory();
15096
- } catch {
15097
- return false;
15098
- }
15099
- });
15100
- const directorySignature = options.directorySignature || ((path) => {
15101
- try {
15102
- const stats = lstatSync3(path);
15103
- if (!stats.isDirectory())
15104
- return null;
15105
- return `${stats.dev}:${stats.ino}`;
15106
- } catch {
15107
- return null;
15108
- }
15109
- });
15110
- const setTimer = options.setTimeoutFn || setTimeout;
15111
- const clearTimer = options.clearTimeoutFn || clearTimeout;
15112
- const debounceMs = options.debounceMs ?? 250;
15113
- const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
15114
- const watchers = new Map;
15115
- const signatures = new Map;
15116
- const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
15117
- const initialScanQueue = [];
15118
- let initialScanTimer = null;
15119
- let processingInitialScan = false;
15120
- const pendingPathInspections = new Map;
15121
- let pathInspectionTimer = null;
15122
- let timer = null;
15123
- const pendingChangedPaths = new Set;
15124
- let watchLimitReported = false;
15125
- const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
15126
- const directoryRelativePath = (dir) => normalizeRelativePath(relative5(options.root, dir));
15127
- const ignoredDirectory = (dir) => {
15128
- const rel = directoryRelativePath(dir);
15129
- return Boolean(rel && ignored(rel));
15130
- };
15131
- const scheduleUpdate = (changedPath) => {
15132
- if (changedPath)
15133
- pendingChangedPaths.add(changedPath);
15134
- if (timer)
15135
- clearTimer(timer);
15136
- timer = setTimer(() => {
15137
- timer = null;
15138
- const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
15139
- pendingChangedPaths.clear();
15140
- options.onUpdate(paths);
15141
- }, debounceMs);
15142
- };
15143
- const reportWatchLimit = () => {
15144
- if (watchLimitReported)
15145
- return;
15146
- watchLimitReported = true;
15147
- options.onWatchLimit?.(maxWatchedDirectories);
15148
- options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
15149
- };
15150
- const closeSubtree = (dir) => {
15151
- for (const [watchedDir, watcher] of [...watchers]) {
15152
- if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
15153
- continue;
15154
- try {
15155
- watcher.close?.();
15156
- } catch {}
15157
- watchers.delete(watchedDir);
15158
- signatures.delete(watchedDir);
15159
- }
15160
- };
15161
- const closeAll = () => {
15162
- if (initialScanTimer) {
15163
- clearTimer(initialScanTimer);
15164
- initialScanTimer = null;
15165
- }
15166
- if (pathInspectionTimer) {
15167
- clearTimer(pathInspectionTimer);
15168
- pathInspectionTimer = null;
15169
- }
15170
- initialScanQueue.length = 0;
15171
- pendingPathInspections.clear();
15172
- for (const watcher of [...watchers.values()]) {
15173
- try {
15174
- watcher.close?.();
15175
- } catch {}
15176
- }
15177
- watchers.clear();
15178
- signatures.clear();
15179
- };
15180
- const readChildDirectories = (dir) => {
15181
- let entries;
15182
- try {
15183
- entries = readDirs(dir);
15184
- } catch (error) {
15185
- options.onError?.(error);
15186
- return [];
15187
- }
15188
- const children = [];
15189
- for (const entry of entries) {
15190
- if (!entry.isDirectory())
15191
- continue;
15192
- const child = join10(dir, entry.name);
15193
- if (ignoredDirectory(child))
15194
- continue;
15195
- children.push(child);
15196
- }
15197
- return children;
15198
- };
15199
- const processInitialScanQueue = () => {
15200
- initialScanTimer = null;
15201
- if (watchers.size >= maxWatchedDirectories) {
15202
- reportWatchLimit();
15203
- initialScanQueue.length = 0;
15204
- return;
15205
- }
15206
- const next = initialScanQueue.shift();
15207
- if (next) {
15208
- processingInitialScan = true;
15209
- try {
15210
- watchDirectory(next, true);
15211
- } finally {
15212
- processingInitialScan = false;
15213
- }
15214
- }
15215
- if (watchers.size >= maxWatchedDirectories) {
15216
- reportWatchLimit();
15217
- initialScanQueue.length = 0;
15218
- }
15219
- if (initialScanQueue.length)
15220
- initialScanTimer = setTimer(processInitialScanQueue, 50);
15221
- };
15222
- const queueInitialChildren = (dir) => {
15223
- const remaining = maxWatchedDirectories - watchers.size;
15224
- if (remaining <= 0) {
15225
- reportWatchLimit();
15226
- return;
15227
- }
15228
- const children = readChildDirectories(dir);
15229
- if (children.length > remaining)
15230
- reportWatchLimit();
15231
- initialScanQueue.push(...children.slice(0, remaining));
15232
- if (!initialScanTimer && !processingInitialScan)
15233
- initialScanTimer = setTimer(processInitialScanQueue, 5000);
15234
- };
15235
- const processChangedPath = (changed, fullChangedPath) => {
15236
- const known = watchers.has(fullChangedPath);
15237
- if (isDirectory(fullChangedPath)) {
15238
- if (known) {
15239
- const signature = directorySignature(fullChangedPath);
15240
- if (signature && signature !== signatures.get(fullChangedPath)) {
15241
- closeSubtree(fullChangedPath);
15242
- watchDirectory(fullChangedPath, initialScanAsync);
15243
- }
15244
- scheduleUpdate(changed);
15245
- return;
15246
- }
15247
- watchDirectory(fullChangedPath, initialScanAsync);
15248
- } else if (known) {
15249
- closeSubtree(fullChangedPath);
15430
+ return true;
15431
+ if (value === "")
15432
+ return false;
15433
+ if (hasControlCharacter(value))
15434
+ return false;
15435
+ return /^[A-Za-z0-9_$.-]+$/.test(value);
15436
+ }
15437
+ function cloneSupabaseDiscoveryResult(result) {
15438
+ return result.map((entry) => ({ ...entry }));
15439
+ }
15440
+ function parseSupabaseConfigToml(content) {
15441
+ let section = null;
15442
+ let projectId = null;
15443
+ let dbPort = null;
15444
+ for (const rawLine of content.split(`
15445
+ `)) {
15446
+ const line = rawLine.trim();
15447
+ if (!line || line.startsWith("#"))
15448
+ continue;
15449
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
15450
+ if (sectionMatch) {
15451
+ section = sectionMatch[1];
15452
+ continue;
15250
15453
  }
15251
- scheduleUpdate(changed);
15252
- };
15253
- const processPathInspections = () => {
15254
- pathInspectionTimer = null;
15255
- const entries = [...pendingPathInspections];
15256
- pendingPathInspections.clear();
15257
- for (const [changed, fullChangedPath] of entries) {
15258
- processChangedPath(changed, fullChangedPath);
15454
+ const kvMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
15455
+ if (!kvMatch)
15456
+ continue;
15457
+ const value = stripScalarSyntax(kvMatch[2]);
15458
+ if (section === null && kvMatch[1] === "project_id") {
15459
+ projectId = value;
15460
+ } else if (section === "db" && kvMatch[1] === "port") {
15461
+ dbPort = value;
15259
15462
  }
15463
+ }
15464
+ if (!projectId || !isSafeDockerServiceName(projectId))
15465
+ return null;
15466
+ return {
15467
+ projectId,
15468
+ dbPort: dbPort && /^\d+$/.test(dbPort) ? dbPort : DEFAULT_SUPABASE_DB_PORT
15260
15469
  };
15261
- const queuePathInspection = (changed, fullChangedPath) => {
15262
- pendingPathInspections.set(changed, fullChangedPath);
15263
- if (!pathInspectionTimer)
15264
- pathInspectionTimer = setTimer(processPathInspections, 25);
15265
- };
15266
- const watchDirectory = (dir, initialScan = false) => {
15267
- if (watchers.has(dir))
15268
- return;
15269
- if (watchers.size >= maxWatchedDirectories) {
15270
- reportWatchLimit();
15271
- return;
15272
- }
15273
- const rel = directoryRelativePath(dir);
15274
- if (rel && ignored(rel))
15470
+ }
15471
+ async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal) {
15472
+ const cacheKey = discoveryCacheKey(cwd, omitDirNames);
15473
+ const now = Date.now();
15474
+ const cached = supabaseDiscoveryCache.get(cacheKey);
15475
+ if (cached && cached.expiresAt > now) {
15476
+ return cloneSupabaseDiscoveryResult(cached.result);
15477
+ }
15478
+ const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
15479
+ omitSet.add(".git");
15480
+ omitSet.add("node_modules");
15481
+ const results = [];
15482
+ await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
15483
+ const configPath = join11(dir, "supabase", "config.toml");
15484
+ if (!await pathExistsAsync(configPath))
15275
15485
  return;
15276
15486
  try {
15277
- const watcher = watch(dir, { persistent: false }, (_event, filename) => {
15278
- if (!filename) {
15279
- scheduleUpdate();
15280
- return;
15281
- }
15282
- const changed = normalizeRelativePath(join10(rel, filename.toString()));
15283
- if (ignored(changed))
15284
- return;
15285
- const fullChangedPath = join10(options.root, changed);
15286
- if (!isInsideRoot(options.root, fullChangedPath))
15287
- return;
15288
- if (initialScanAsync) {
15289
- queuePathInspection(changed, fullChangedPath);
15290
- return;
15291
- }
15292
- processChangedPath(changed, fullChangedPath);
15293
- }) || {};
15294
- watchers.set(dir, watcher);
15295
- const signature = directorySignature(dir);
15296
- if (signature)
15297
- signatures.set(dir, signature);
15298
- watcher.on?.("error", () => {
15299
- if (watchers.get(dir) === watcher) {
15300
- watchers.delete(dir);
15301
- signatures.delete(dir);
15302
- }
15303
- });
15304
- watcher.on?.("close", () => {
15305
- if (watchers.get(dir) === watcher) {
15306
- watchers.delete(dir);
15307
- signatures.delete(dir);
15308
- }
15487
+ const content = await readFile2(configPath, "utf-8");
15488
+ const parsed = parseSupabaseConfigToml(content);
15489
+ if (!parsed)
15490
+ return;
15491
+ const relDir = relative5(cwd, dir);
15492
+ const isRoot = relDir === "" || relDir === ".";
15493
+ const relDirSlash = relDir.replace(/\\/g, "/");
15494
+ const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
15495
+ const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
15496
+ results.push({
15497
+ id,
15498
+ path: isRoot ? "supabase/config.toml" : `${relDirSlash}/supabase/config.toml`,
15499
+ name: `${parsed.projectId} (Supabase CLI, postgres@127.0.0.1:${parsed.dbPort}/postgres${labelPath})`,
15500
+ sizeBytes: 0,
15501
+ kind: "postgresql",
15502
+ projectId: parsed.projectId,
15503
+ relDirSlash,
15504
+ dbPort: parsed.dbPort
15309
15505
  });
15310
- } catch (error) {
15311
- options.onError?.(error);
15312
- return;
15313
- }
15314
- if (initialScanAsync && initialScan) {
15315
- queueInitialChildren(dir);
15316
- return;
15317
- }
15318
- if (watchers.size >= maxWatchedDirectories) {
15319
- reportWatchLimit();
15320
- return;
15506
+ } catch {}
15507
+ }, signal);
15508
+ if (signal?.aborted)
15509
+ return cloneSupabaseDiscoveryResult(results);
15510
+ supabaseDiscoveryCache.set(cacheKey, {
15511
+ expiresAt: now + SUPABASE_DISCOVERY_TTL_MS,
15512
+ result: cloneSupabaseDiscoveryResult(results)
15513
+ });
15514
+ return cloneSupabaseDiscoveryResult(results);
15515
+ }
15516
+ function parseSupabaseDbId(dbId) {
15517
+ if (!dbId.startsWith("supabase:"))
15518
+ return null;
15519
+ const rest = dbId.slice("supabase:".length);
15520
+ if (!rest)
15521
+ return null;
15522
+ const atIdx = rest.indexOf("@");
15523
+ let projectId;
15524
+ let relDir = "";
15525
+ if (atIdx >= 0) {
15526
+ if (rest.indexOf("@", atIdx + 1) >= 0)
15527
+ return null;
15528
+ projectId = rest.slice(0, atIdx);
15529
+ try {
15530
+ relDir = decodeURIComponent(rest.slice(atIdx + 1));
15531
+ } catch {
15532
+ return null;
15321
15533
  }
15322
- for (const child of readChildDirectories(dir))
15323
- watchDirectory(child);
15324
- };
15325
- watchDirectory(options.root, true);
15326
- return { started: watchers.size > 0, close: closeAll };
15534
+ if (!isSafeDockerRelDir(relDir))
15535
+ return null;
15536
+ } else {
15537
+ projectId = rest;
15538
+ }
15539
+ if (!isSafeDockerServiceName(projectId))
15540
+ return null;
15541
+ return { projectId, relDir };
15327
15542
  }
15328
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
15329
- var init_worktree_watcher = __esm(() => {
15330
- init_search();
15543
+ async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal) {
15544
+ const parsed = parseSupabaseDbId(dbId);
15545
+ if (!parsed)
15546
+ return null;
15547
+ const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
15548
+ return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
15549
+ }
15550
+ var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache, DB_KIND_VALUES, MAX_SUPABASE_PROJECTS = 30, SUPABASE_DISCOVERY_TTL_MS = 5000, DEFAULT_SUPABASE_DB_PORT = "54322", supabaseDiscoveryCache;
15551
+ var init_discovery = __esm(() => {
15552
+ SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
15553
+ sqliteDiscoveryCache = new Map;
15554
+ COMPOSE_FILENAMES = [
15555
+ "docker-compose.yml",
15556
+ "docker-compose.yaml",
15557
+ "compose.yml",
15558
+ "compose.yaml"
15559
+ ];
15560
+ dockerDiscoveryCache = new Map;
15561
+ DB_KIND_VALUES = new Set([
15562
+ "sqlite",
15563
+ "postgresql",
15564
+ "mysql",
15565
+ "redis",
15566
+ "elasticsearch",
15567
+ "s3",
15568
+ "dynamodb"
15569
+ ]);
15570
+ supabaseDiscoveryCache = new Map;
15331
15571
  });
15332
15572
 
15333
15573
  // web-src/server/state-store.ts
15334
- import { join as join11 } from "node:path";
15574
+ import { join as join12 } from "node:path";
15335
15575
  function codeViewerPath(root, fileName) {
15336
- return join11(root, CODE_VIEWER_DIR2, fileName);
15576
+ return join12(root, CODE_VIEWER_DIR2, fileName);
15337
15577
  }
15338
15578
  function isRecord(value) {
15339
15579
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -16213,7 +16453,7 @@ var init_d1 = __esm(() => {
16213
16453
 
16214
16454
  // web-src/server/database/adapters/dynamodb.ts
16215
16455
  import { spawnSync as spawnSync5 } from "node:child_process";
16216
- import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
16456
+ import { createHash as createHash6, createHmac as createHmac2 } from "node:crypto";
16217
16457
  function createDynamoDbRequestDeadline() {
16218
16458
  const timeoutMs = dynamoDbRequestTimeoutMs;
16219
16459
  return { expiresAt: Date.now() + timeoutMs, timeoutMs };
@@ -16237,7 +16477,7 @@ function hmac2(key, value) {
16237
16477
  return createHmac2("sha256", key).update(value, "utf8").digest();
16238
16478
  }
16239
16479
  function sha2562(value) {
16240
- return createHash5("sha256").update(value, "utf8").digest("hex");
16480
+ return createHash6("sha256").update(value, "utf8").digest("hex");
16241
16481
  }
16242
16482
  function amzDate2(date = new Date) {
16243
16483
  const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
@@ -16882,7 +17122,7 @@ var init_credential_store = __esm(() => {
16882
17122
  // web-src/server/database/connections-store.ts
16883
17123
  import { randomUUID } from "node:crypto";
16884
17124
  import { chmod } from "node:fs/promises";
16885
- import { join as join12 } from "node:path";
17125
+ import { join as join13 } from "node:path";
16886
17126
  function secretKey(cwd, id) {
16887
17127
  return `${cwd}\x00${id}`;
16888
17128
  }
@@ -16933,7 +17173,7 @@ function withRuntimeSecrets(cwd, connection) {
16933
17173
  };
16934
17174
  }
16935
17175
  function connectionsFilePath(root) {
16936
- return join12(root, ".code-viewer", CONNECTIONS_FILE_NAME);
17176
+ return join13(root, ".code-viewer", CONNECTIONS_FILE_NAME);
16937
17177
  }
16938
17178
  function emptyState() {
16939
17179
  return { version: 1, connections: [] };
@@ -18745,9 +18985,9 @@ var init_handle_s3 = __esm(() => {
18745
18985
  });
18746
18986
 
18747
18987
  // web-src/server/database/query-history.ts
18748
- import { join as join13 } from "node:path";
18988
+ import { join as join14 } from "node:path";
18749
18989
  function historyFilePath(root) {
18750
- return join13(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18990
+ return join14(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18751
18991
  }
18752
18992
  function emptyState2() {
18753
18993
  return { version: 1, entries: [] };
@@ -18895,11 +19135,11 @@ var init_query_history = __esm(() => {
18895
19135
  });
18896
19136
 
18897
19137
  // web-src/server/database/snapshot-store.ts
18898
- import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
19138
+ import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
18899
19139
  import { mkdirSync as mkdirSync3 } from "node:fs";
18900
- import { join as join14 } from "node:path";
19140
+ import { join as join15 } from "node:path";
18901
19141
  async function getStoreDb(cwd) {
18902
- const dbPath = join14(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
19142
+ const dbPath = join15(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
18903
19143
  if (storeDb && storeDbPath === dbPath)
18904
19144
  return storeDb;
18905
19145
  if (storeDb) {
@@ -18907,7 +19147,7 @@ async function getStoreDb(cwd) {
18907
19147
  storeDb.close();
18908
19148
  } catch {}
18909
19149
  }
18910
- mkdirSync3(join14(cwd, CODE_VIEWER_DIR4), { recursive: true });
19150
+ mkdirSync3(join15(cwd, CODE_VIEWER_DIR4), { recursive: true });
18911
19151
  const DbClass = await loadSqliteClass();
18912
19152
  storeDb = new DbClass(dbPath);
18913
19153
  storeDbPath = dbPath;
@@ -18926,7 +19166,7 @@ function makeId2(prefix) {
18926
19166
  return `${prefix}-${randomBytes2(8).toString("hex")}`;
18927
19167
  }
18928
19168
  function hashPayload(payloadJson) {
18929
- return createHash6("sha256").update(payloadJson).digest("hex");
19169
+ return createHash7("sha256").update(payloadJson).digest("hex");
18930
19170
  }
18931
19171
  function hashLengthPrefixed(hasher, value) {
18932
19172
  hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
@@ -19001,7 +19241,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
19001
19241
  db.exec("BEGIN");
19002
19242
  try {
19003
19243
  for (const row of rows) {
19004
- const rowKeyHash = createHash6("sha256").update(row.rowKeyJson).digest("hex");
19244
+ const rowKeyHash = createHash7("sha256").update(row.rowKeyJson).digest("hex");
19005
19245
  const payloadHash = hashPayload(row.payloadJson);
19006
19246
  insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
19007
19247
  insertPayload.run(payloadHash, row.payloadJson);
@@ -19015,7 +19255,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
19015
19255
  }
19016
19256
  }
19017
19257
  function computeRevisionTableHash(db, revisionId) {
19018
- const hasher = createHash6("sha256");
19258
+ const hasher = createHash7("sha256");
19019
19259
  hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
19020
19260
  let rowCount = 0;
19021
19261
  let last;
@@ -19513,9 +19753,9 @@ var init_snapshot_runner = __esm(() => {
19513
19753
  });
19514
19754
 
19515
19755
  // web-src/server/database/tabs-store.ts
19516
- import { join as join15 } from "node:path";
19756
+ import { join as join16 } from "node:path";
19517
19757
  function tabsFilePath(root) {
19518
- return join15(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19758
+ return join16(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19519
19759
  }
19520
19760
  function emptyState3() {
19521
19761
  return { version: 1, tabs: [], activeTabId: null };
@@ -21469,7 +21709,7 @@ var init_handle = __esm(() => {
21469
21709
 
21470
21710
  // web-src/server/doctor.ts
21471
21711
  import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync6 } from "node:fs";
21472
- import { dirname as dirname5, join as join16, relative as relative6 } from "node:path";
21712
+ import { dirname as dirname5, join as join17, relative as relative6 } from "node:path";
21473
21713
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21474
21714
  function statusWorse(a, b) {
21475
21715
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -21563,7 +21803,7 @@ function findCodeViewerPackageJson() {
21563
21803
  cursor = dirname5(process.argv[1] || ".");
21564
21804
  }
21565
21805
  for (let depth = 0;depth < 8; depth += 1) {
21566
- const candidate = join16(cursor, "package.json");
21806
+ const candidate = join17(cursor, "package.json");
21567
21807
  try {
21568
21808
  const raw = readFileSync6(candidate, "utf8");
21569
21809
  const pkg = JSON.parse(raw);
@@ -21659,7 +21899,7 @@ async function checkSqlite(cwd) {
21659
21899
  return { id: "sqlite", title: "SQLite driver", rows };
21660
21900
  }
21661
21901
  async function trySnapshotDbOpen(cwd) {
21662
- const dbPath = join16(cwd, SNAPSHOT_DB_REL);
21902
+ const dbPath = join17(cwd, SNAPSHOT_DB_REL);
21663
21903
  try {
21664
21904
  statSync6(dbPath);
21665
21905
  } catch {
@@ -21680,7 +21920,7 @@ async function trySnapshotDbOpen(cwd) {
21680
21920
  }
21681
21921
  }
21682
21922
  function checkSnapshotStore(cwd) {
21683
- const dbPath = join16(cwd, SNAPSHOT_DB_REL);
21923
+ const dbPath = join17(cwd, SNAPSHOT_DB_REL);
21684
21924
  const dir = dirname5(dbPath);
21685
21925
  let dirStatus = "ok";
21686
21926
  let dirDetail = dir;
@@ -22699,12 +22939,12 @@ function startDevAssetReload(options) {
22699
22939
  var init_dev_assets = () => {};
22700
22940
 
22701
22941
  // web-src/server/journal.ts
22702
- import { join as join17 } from "node:path";
22942
+ import { join as join18 } from "node:path";
22703
22943
  function dailyJournalFilePath(root) {
22704
- return join17(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
22944
+ return join18(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
22705
22945
  }
22706
22946
  function journalTasksFilePath(root) {
22707
- return join17(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
22947
+ return join18(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
22708
22948
  }
22709
22949
  function emptyDailyJournalState() {
22710
22950
  return { version: 1, entries: [] };
@@ -23317,7 +23557,7 @@ var init_journal2 = __esm(() => {
23317
23557
  // web-src/server/search-service.ts
23318
23558
  import { existsSync as existsSync7, realpathSync as realpathSync6 } from "node:fs";
23319
23559
  import { lstat as lstat2, readFile as readFile3 } from "node:fs/promises";
23320
- import { join as join18, relative as relative7 } from "node:path";
23560
+ import { join as join19, relative as relative7 } from "node:path";
23321
23561
  async function rgAvailableAsync(cwd) {
23322
23562
  if (rgAvailableCache !== null)
23323
23563
  return rgAvailableCache;
@@ -23347,7 +23587,7 @@ function safeWorktreePath(env, path) {
23347
23587
  return null;
23348
23588
  if (isGitInternalPath(path))
23349
23589
  return null;
23350
- const full = join18(env.cwd, path);
23590
+ const full = join19(env.cwd, path);
23351
23591
  if (!existsSync7(full))
23352
23592
  return null;
23353
23593
  let realCwd;
@@ -23540,7 +23780,7 @@ var init_search_service = __esm(() => {
23540
23780
 
23541
23781
  // web-src/server/mcp.ts
23542
23782
  import { readFileSync as readFileSync7 } from "node:fs";
23543
- import { join as join19 } from "node:path";
23783
+ import { join as join20 } from "node:path";
23544
23784
  function defaultMcpTools(options = {}) {
23545
23785
  return [
23546
23786
  {
@@ -25014,7 +25254,7 @@ var init_mcp = __esm(() => {
25014
25254
  init_search_cli();
25015
25255
  init_search_service();
25016
25256
  init_status_cli();
25017
- PACKAGE_VERSION = JSON.parse(readFileSync7(join19(ROOT, "package.json"), "utf8")).version;
25257
+ PACKAGE_VERSION = JSON.parse(readFileSync7(join20(ROOT, "package.json"), "utf8")).version;
25018
25258
  MCP_SERVER_INFO = {
25019
25259
  name: "code-viewer",
25020
25260
  title: "code-viewer",
@@ -25022,6 +25262,172 @@ var init_mcp = __esm(() => {
25022
25262
  };
25023
25263
  });
25024
25264
 
25265
+ // web-src/server/watch-supervisor.ts
25266
+ import { spawn as spawn3 } from "node:child_process";
25267
+ import { join as join21 } from "node:path";
25268
+ function watchChildCommand() {
25269
+ const entry = process.argv[1] ?? "";
25270
+ const script = entry.endsWith(".ts") ? join21(import.meta.dir, "cli.ts") : entry;
25271
+ return [process.argv[0], script, "watch-child"];
25272
+ }
25273
+ function startWatchSupervisor(options) {
25274
+ const spawnChild = options.spawnFn || spawn3;
25275
+ const now = options.nowFn || Date.now;
25276
+ const setTimer = options.setTimeoutFn || setTimeout;
25277
+ const setRepeating = options.setIntervalFn || setInterval;
25278
+ const clearRepeating = options.clearIntervalFn || clearInterval;
25279
+ const heartbeatMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
25280
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_MS;
25281
+ const command = options.command ?? watchChildCommand();
25282
+ let child = null;
25283
+ let closed = false;
25284
+ let watchFailures = 0;
25285
+ let pollOnly = false;
25286
+ let sawReady = false;
25287
+ let lastSignalAt = now();
25288
+ let watchdog = null;
25289
+ const report = (error) => options.onError?.(error);
25290
+ const abandon = (victim) => {
25291
+ victim.removeAllListeners?.();
25292
+ victim.stdout?.removeAllListeners?.();
25293
+ victim.stderr?.removeAllListeners?.();
25294
+ try {
25295
+ victim.kill("SIGTERM");
25296
+ } catch {}
25297
+ const grace = setTimer(() => {
25298
+ try {
25299
+ victim.kill("SIGKILL");
25300
+ } catch {}
25301
+ }, KILL_GRACE_MS);
25302
+ grace.unref?.();
25303
+ };
25304
+ const handleMessage = (message) => {
25305
+ lastSignalAt = now();
25306
+ if (message.type === "heartbeat")
25307
+ return;
25308
+ if (message.type === "ready") {
25309
+ if (message.pollOnly || message.watching) {
25310
+ sawReady = true;
25311
+ watchFailures = 0;
25312
+ } else {
25313
+ watchFailures++;
25314
+ }
25315
+ return;
25316
+ }
25317
+ if (message.type === "update") {
25318
+ options.onUpdate(message.paths);
25319
+ return;
25320
+ }
25321
+ if (message.type === "watch-limit") {
25322
+ options.onWatchLimit?.(message.limit);
25323
+ return;
25324
+ }
25325
+ if (message.type === "warn")
25326
+ report(new Error(message.message));
25327
+ };
25328
+ const start = () => {
25329
+ if (closed)
25330
+ return;
25331
+ if (!pollOnly && watchFailures >= WATCH_FAILURES_BEFORE_POLL_ONLY) {
25332
+ pollOnly = true;
25333
+ options.onPollOnly?.();
25334
+ }
25335
+ const config = {
25336
+ root: options.root,
25337
+ omitDirNames: options.omitDirNames,
25338
+ excludeNames: options.excludeNames,
25339
+ maxWatchedDirectories: options.maxWatchedDirectories,
25340
+ debounceMs: options.debounceMs ?? 250,
25341
+ pollIntervalMs,
25342
+ heartbeatIntervalMs: heartbeatMs,
25343
+ parentPid: process.pid,
25344
+ pollOnly
25345
+ };
25346
+ let spawned;
25347
+ try {
25348
+ spawned = spawnChild(command[0], command.slice(1), {
25349
+ cwd: options.root,
25350
+ stdio: ["pipe", "pipe", "pipe"]
25351
+ });
25352
+ } catch (error) {
25353
+ report(error);
25354
+ return;
25355
+ }
25356
+ child = spawned;
25357
+ sawReady = false;
25358
+ lastSignalAt = now();
25359
+ try {
25360
+ spawned.stdin?.write(`${JSON.stringify(config)}
25361
+ `);
25362
+ } catch (error) {
25363
+ report(error);
25364
+ }
25365
+ let buffer = "";
25366
+ spawned.stdout?.setEncoding?.("utf8");
25367
+ spawned.stdout?.on("data", (chunk) => {
25368
+ buffer += chunk;
25369
+ let newline = buffer.indexOf(`
25370
+ `);
25371
+ while (newline !== -1) {
25372
+ const line = buffer.slice(0, newline).trim();
25373
+ buffer = buffer.slice(newline + 1);
25374
+ if (line) {
25375
+ try {
25376
+ handleMessage(JSON.parse(line));
25377
+ } catch {}
25378
+ }
25379
+ newline = buffer.indexOf(`
25380
+ `);
25381
+ }
25382
+ });
25383
+ spawned.stderr?.setEncoding?.("utf8");
25384
+ spawned.stderr?.on("data", (chunk) => {
25385
+ const text = String(chunk).trim();
25386
+ if (text)
25387
+ console.warn(`[code-viewer] watch child: ${text}`);
25388
+ });
25389
+ spawned.on("error", (error) => report(error));
25390
+ spawned.on("exit", () => {
25391
+ if (closed || child !== spawned)
25392
+ return;
25393
+ if (!sawReady)
25394
+ watchFailures++;
25395
+ child = null;
25396
+ const retry = setTimer(start, heartbeatMs);
25397
+ retry.unref?.();
25398
+ });
25399
+ };
25400
+ start();
25401
+ watchdog = setRepeating(() => {
25402
+ if (closed || !child)
25403
+ return;
25404
+ if (now() - lastSignalAt < heartbeatMs * HEARTBEAT_MISS_LIMIT)
25405
+ return;
25406
+ const victim = child;
25407
+ child = null;
25408
+ watchFailures++;
25409
+ report(new Error("watch child stopped reporting; restarting"));
25410
+ abandon(victim);
25411
+ start();
25412
+ }, heartbeatMs);
25413
+ watchdog.unref?.();
25414
+ return {
25415
+ close: () => {
25416
+ closed = true;
25417
+ if (watchdog)
25418
+ clearRepeating(watchdog);
25419
+ watchdog = null;
25420
+ const victim = child;
25421
+ child = null;
25422
+ if (victim)
25423
+ abandon(victim);
25424
+ },
25425
+ pollOnly: () => pollOnly
25426
+ };
25427
+ }
25428
+ var DEFAULT_HEARTBEAT_MS = 5000, DEFAULT_POLL_MS = 15000, HEARTBEAT_MISS_LIMIT = 3, KILL_GRACE_MS = 2000, WATCH_FAILURES_BEFORE_POLL_ONLY = 2;
25429
+ var init_watch_supervisor = () => {};
25430
+
25025
25431
  // web-src/server/state-route.ts
25026
25432
  var exports_state_route = {};
25027
25433
  __export(exports_state_route, {
@@ -25098,7 +25504,7 @@ import {
25098
25504
  closeSync as closeSync2,
25099
25505
  constants as constants3,
25100
25506
  existsSync as existsSync8,
25101
- lstatSync as lstatSync4,
25507
+ lstatSync as lstatSync5,
25102
25508
  mkdirSync as mkdirSync4,
25103
25509
  openSync as openSync2,
25104
25510
  readFileSync as readFileSync8,
@@ -25110,7 +25516,7 @@ import {
25110
25516
  writeFileSync as writeFileSync2
25111
25517
  } from "node:fs";
25112
25518
  import { homedir as homedir3 } from "node:os";
25113
- import { basename as basename3, dirname as dirname6, extname as extname2, join as join20, relative as relative8 } from "node:path";
25519
+ import { basename as basename3, dirname as dirname6, extname as extname2, join as join22, relative as relative8 } from "node:path";
25114
25520
  function parseCli() {
25115
25521
  const rest = [];
25116
25522
  for (let i = 2;i < process.argv.length; i++) {
@@ -25232,7 +25638,7 @@ Examples:
25232
25638
  }
25233
25639
  function warnIfLegacyConfigPresent() {
25234
25640
  try {
25235
- if (existsSync8(join20(cwd, ".code-viewer.json"))) {
25641
+ if (existsSync8(join22(cwd, ".code-viewer.json"))) {
25236
25642
  console.warn("[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed.");
25237
25643
  }
25238
25644
  } catch {}
@@ -25339,7 +25745,7 @@ function staticFile(pathname) {
25339
25745
  const spec = map[pathname];
25340
25746
  if (!spec)
25341
25747
  return null;
25342
- const full = join20(WEB_ROOT, spec[0]);
25748
+ const full = join22(WEB_ROOT, spec[0]);
25343
25749
  if (!existsSync8(full))
25344
25750
  return text("not found", 404);
25345
25751
  return new Response(readFileSync8(full), {
@@ -25597,7 +26003,7 @@ function safeWorktreePath2(path) {
25597
26003
  return safeWorktreePath(currentSearchEnv(), path);
25598
26004
  }
25599
26005
  function worktreePath(path) {
25600
- return join20(cwd, path);
26006
+ return join22(cwd, path);
25601
26007
  }
25602
26008
  function safeOpenWorktreePath(path) {
25603
26009
  if (path === "") {
@@ -25750,9 +26156,14 @@ async function handleTree(url) {
25750
26156
  if (tree.error)
25751
26157
  return text(tree.error, tree.status ?? 500);
25752
26158
  const entries = tree.entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
25753
- const statusMap = target === "worktree" || target === "" ? await repoStatusMapAsync(cwd) : null;
26159
+ const worktreeTarget = target === "worktree" || target === "";
26160
+ const [statusMap, ignoredPaths] = worktreeTarget ? await Promise.all([
26161
+ repoStatusMapAsync(cwd),
26162
+ ignoredPathsAsync(entries.map((entry) => entry.path), cwd)
26163
+ ]) : [null, null];
25754
26164
  const withStatus = (entry) => {
25755
- const status = statusMap?.get(entry.path);
26165
+ const found = statusMap && repoStatusForPath(statusMap, entry.path);
26166
+ const status = ignoredPaths?.has(entry.path) && (!found || found.inherited) ? "I" : found?.code;
25756
26167
  return status ? { ...entry, status } : entry;
25757
26168
  };
25758
26169
  const deletedEntries = !recursive && statusMap ? deletedTreeEntriesForPath(statusMap, path) : [];
@@ -25766,7 +26177,7 @@ async function handleTree(url) {
25766
26177
  ...deletedEntries
25767
26178
  ],
25768
26179
  readme: await readReadme(target, path),
25769
- upload_enabled: uploadEnabled && (target === "worktree" || target === "")
26180
+ upload_enabled: uploadEnabled && worktreeTarget
25770
26181
  });
25771
26182
  }
25772
26183
  async function handleSettings() {
@@ -25783,7 +26194,8 @@ async function handleSettings() {
25783
26194
  watch_limit_effective: scopeWatchLimit,
25784
26195
  watch_limit_default: DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT,
25785
26196
  watch_limit_min: MIN_WORKTREE_WATCH_DIRECTORY_LIMIT,
25786
- watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT
26197
+ watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT,
26198
+ watch_recursive: supportsNativeRecursiveWatch(process.platform)
25787
26199
  }
25788
26200
  });
25789
26201
  }
@@ -25926,7 +26338,7 @@ async function handleLog(url) {
25926
26338
  }
25927
26339
  function blamePathKey(p) {
25928
26340
  try {
25929
- const st = statSync7(join20(cwd, p));
26341
+ const st = statSync7(join22(cwd, p));
25930
26342
  return `${st.mtimeMs}:${st.size}`;
25931
26343
  } catch {
25932
26344
  return "missing";
@@ -26465,7 +26877,7 @@ async function handleUploadFiles(req) {
26465
26877
  total += file.size;
26466
26878
  if (total > MAX_UPLOAD_TOTAL_BYTES)
26467
26879
  return text("upload too large", 413);
26468
- const target = join20(realDir, safeName);
26880
+ const target = join22(realDir, safeName);
26469
26881
  if (relative8(realDir, dirname6(target)) !== "")
26470
26882
  return text("invalid filename", 400);
26471
26883
  if (existsSync8(target))
@@ -26587,9 +26999,9 @@ function triggerUpdate(changedPaths) {
26587
26999
  sendSse("update", data);
26588
27000
  }
26589
27001
  function moveMacPathIntoTrash(path) {
26590
- const trashDir = join20(homedir3(), ".Trash");
27002
+ const trashDir = join22(homedir3(), ".Trash");
26591
27003
  const base = basename3(path) || "code-viewer-trash-item";
26592
- const target = join20(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
27004
+ const target = join22(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
26593
27005
  try {
26594
27006
  mkdirSync4(trashDir, { recursive: true });
26595
27007
  renameSync(path, target);
@@ -26599,7 +27011,7 @@ function moveMacPathIntoTrash(path) {
26599
27011
  }
26600
27012
  }
26601
27013
  async function movePathToTrash(path) {
26602
- lstatSync4(path);
27014
+ lstatSync5(path);
26603
27015
  if (process.platform === "darwin") {
26604
27016
  return moveMacPathIntoTrash(path);
26605
27017
  }
@@ -26631,7 +27043,7 @@ async function restoreTrashPath(originalPath, trashPath) {
26631
27043
  if (!existsSync8(trashPath))
26632
27044
  return { ok: false, error: "trash item not found" };
26633
27045
  try {
26634
- const trashRoot = join20(homedir3(), ".Trash");
27046
+ const trashRoot = join22(homedir3(), ".Trash");
26635
27047
  const trashRelative = relative8(trashRoot, trashPath);
26636
27048
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
26637
27049
  return { ok: false, error: "invalid trash handle" };
@@ -26787,7 +27199,7 @@ async function handleCreateDirectory(req) {
26787
27199
  const targetPath = dir ? `${dir}/${name}` : name;
26788
27200
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
26789
27201
  return text("invalid target", 400);
26790
- const target = join20(parent, name);
27202
+ const target = join22(parent, name);
26791
27203
  if (existsSync8(target))
26792
27204
  return text("already exists", 409);
26793
27205
  try {
@@ -27336,18 +27748,19 @@ async function shutdown(exitCode = 0) {
27336
27748
  }
27337
27749
  function startScopedWorktreeWatch() {
27338
27750
  watchLimitReached = null;
27339
- return startWorktreeUpdateWatch({
27751
+ return startWatchSupervisor({
27340
27752
  root: cwd,
27341
27753
  omitDirNames: scopeOmitDirNames,
27342
27754
  excludeNames: scopeExcludeNames,
27343
- watch,
27344
- initialScanMode: "async",
27345
27755
  maxWatchedDirectories: scopeWatchLimit,
27346
27756
  onUpdate: triggerUpdate,
27347
27757
  onWatchLimit: (limit) => {
27348
27758
  watchLimitReached = limit;
27349
27759
  sendSse("watch-limit", String(limit));
27350
27760
  },
27761
+ onPollOnly: () => {
27762
+ console.warn("[code-viewer] file watching is unavailable; updates now come from periodic polling");
27763
+ },
27351
27764
  onError: (error) => {
27352
27765
  const message = error instanceof Error ? error.message : String(error);
27353
27766
  console.warn(`code-viewer worktree watch skipped: ${message}`);
@@ -27390,9 +27803,10 @@ var init_preview = __esm(async () => {
27390
27803
  init_search_service();
27391
27804
  init_server_registry();
27392
27805
  init_state_store();
27806
+ init_watch_supervisor();
27393
27807
  init_worktree_watcher();
27394
- WEB_ROOT = join20(ROOT, "web");
27395
- VERSION = JSON.parse(readFileSync8(join20(ROOT, "package.json"), "utf8")).version;
27808
+ WEB_ROOT = join22(ROOT, "web");
27809
+ VERSION = JSON.parse(readFileSync8(join22(ROOT, "package.json"), "utf8")).version;
27396
27810
  DEFAULT_ARGS = ["HEAD"];
27397
27811
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
27398
27812
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -27688,6 +28102,9 @@ if (process.argv[2] === "agent-help") {
27688
28102
  } else if (process.argv[2] === "skill") {
27689
28103
  const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
27690
28104
  runSkillCli2(process.argv.slice(3));
28105
+ } else if (process.argv[2] === "watch-child") {
28106
+ const { runWatchChild: runWatchChild2 } = await Promise.resolve().then(() => (init_watch_child(), exports_watch_child));
28107
+ await runWatchChild2();
27691
28108
  } else if (process.argv[2] === "doctor") {
27692
28109
  const { runDoctorCli: runDoctorCli2 } = await Promise.resolve().then(() => (init_doctor_cli(), exports_doctor_cli));
27693
28110
  await runDoctorCli2(process.argv.slice(3));