@youtyan/code-viewer 0.9.1 → 0.10.0

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.
@@ -129,6 +129,19 @@ function makeTimedId(prefix) {
129
129
  }
130
130
  var BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
131
131
 
132
+ // web-src/core/tools.ts
133
+ function isToolId(value) {
134
+ return TOOL_IDS.includes(value);
135
+ }
136
+ var TOOL_IDS, MIN_TOOLS_SHEET_WIDTH = 480, MAX_TOOLS_SHEET_WIDTH = 4000;
137
+ var init_tools = __esm(() => {
138
+ TOOL_IDS = [
139
+ "markdown",
140
+ "mermaid",
141
+ "json"
142
+ ];
143
+ });
144
+
132
145
  // web-src/core/routes.ts
133
146
  function assertNever(value) {
134
147
  throw new Error(`unhandled route: ${JSON.stringify(value)}`);
@@ -232,6 +245,7 @@ function buildRoute(route) {
232
245
  }
233
246
  var SPA_PATHS, APP_ENTRY_PATHS;
234
247
  var init_routes = __esm(() => {
248
+ init_tools();
235
249
  SPA_PATHS = [
236
250
  "/todif",
237
251
  "/todiff",
@@ -9763,249 +9777,720 @@ var init_agent_help = __esm(() => {
9763
9777
  ];
9764
9778
  });
9765
9779
 
9766
- // web-src/server/database/serialize.ts
9767
- function serializeDbValue(value) {
9768
- if (value === null || value === undefined)
9769
- return null;
9770
- if (typeof value === "bigint") {
9771
- return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value.toString();
9772
- }
9773
- if (value instanceof Uint8Array) {
9774
- return `<blob ${value.byteLength} bytes>`;
9775
- }
9776
- if (typeof value === "object") {
9777
- try {
9778
- return JSON.stringify(value);
9779
- } catch {
9780
- return String(value);
9781
- }
9782
- }
9783
- return value;
9780
+ // web-src/server/worktree-watcher.ts
9781
+ import {
9782
+ lstatSync as lstatSync3,
9783
+ readdirSync as nodeReaddirSync,
9784
+ watch as nodeWatch
9785
+ } from "node:fs";
9786
+ import { join as join9, relative as relative4 } from "node:path";
9787
+ function supportsNativeRecursiveWatch(platform) {
9788
+ return platform === "darwin" || platform === "win32";
9784
9789
  }
9785
- function serializeDbRow(row) {
9786
- return row.map(serializeDbValue);
9790
+ function normalizeRelativePath(path) {
9791
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
9787
9792
  }
9788
- function serializeDbRows(rows) {
9789
- return rows.map(serializeDbRow);
9793
+ function isInsideRoot(root, path) {
9794
+ const rel = relative4(root, path).replace(/\\/g, "/");
9795
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
9790
9796
  }
9791
- function coerceDbValue(value, columnType) {
9792
- if (value === null)
9793
- return null;
9794
- const t = (columnType || "").toLowerCase();
9795
- if (/bool/.test(t)) {
9796
- const v = value.trim().toLowerCase();
9797
- if (v === "")
9798
- return null;
9799
- if (v === "true" || v === "t" || v === "1")
9800
- return true;
9801
- if (v === "false" || v === "f" || v === "0")
9797
+ function startWorktreeUpdateWatch(options) {
9798
+ const watch = options.watch || nodeWatch;
9799
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
9800
+ const isDirectory = options.isDirectory || ((path) => {
9801
+ try {
9802
+ return lstatSync3(path).isDirectory();
9803
+ } catch {
9802
9804
  return false;
9803
- return value;
9804
- }
9805
- if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
9806
- const trimmed = value.trim();
9807
- if (trimmed === "")
9805
+ }
9806
+ });
9807
+ const directorySignature = options.directorySignature || ((path) => {
9808
+ try {
9809
+ const stats = lstatSync3(path);
9810
+ if (!stats.isDirectory())
9811
+ return null;
9812
+ return `${stats.dev}:${stats.ino}`;
9813
+ } catch {
9808
9814
  return null;
9809
- const n = Number(trimmed);
9810
- if (Number.isFinite(n) && String(n) === trimmed)
9811
- return n;
9812
- return value;
9813
- }
9814
- return value;
9815
- }
9816
- var MIN_SAFE, MAX_SAFE;
9817
- var init_serialize = __esm(() => {
9818
- MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
9819
- MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
9820
- });
9821
-
9822
- // web-src/server/database/sql-utils.ts
9823
- function sanitizeIdentifier(name, kind = "sqlite") {
9824
- if (kind === "mysql")
9825
- return `\`${name.replace(/`/g, "``")}\``;
9826
- return `"${name.replace(/"/g, '""')}"`;
9827
- }
9828
- function escapeSqlString(value, kind) {
9829
- const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
9830
- return `'${escaped}'`;
9831
- }
9832
- function buildFilterWhere(grouped, kind, exact) {
9833
- const whereParts = [];
9834
- const params = [];
9835
- const useParams = kind === "sqlite";
9836
- const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
9837
- for (const [value, cols] of grouped) {
9838
- const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
9839
- if (cols.length === 1) {
9840
- whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
9841
- if (useParams)
9842
- params.push(`%${value}%`);
9843
- } else {
9844
- const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
9845
- whereParts.push(`(${orParts.join(" OR ")})`);
9846
- if (useParams) {
9847
- for (let i = 0;i < cols.length; i++)
9848
- params.push(`%${value}%`);
9815
+ }
9816
+ });
9817
+ const setTimer = options.setTimeoutFn || setTimeout;
9818
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
9819
+ const debounceMs = options.debounceMs ?? 250;
9820
+ const recursive = options.recursive === true;
9821
+ const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
9822
+ const watchers = new Map;
9823
+ const signatures = new Map;
9824
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
9825
+ const initialScanQueue = [];
9826
+ let initialScanTimer = null;
9827
+ let processingInitialScan = false;
9828
+ const pendingPathInspections = new Map;
9829
+ let pathInspectionTimer = null;
9830
+ let timer = null;
9831
+ const pendingChangedPaths = new Set;
9832
+ let fullUpdatePending = false;
9833
+ let watchLimitReported = false;
9834
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
9835
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative4(options.root, dir));
9836
+ const ignoredDirectory = (dir) => {
9837
+ const rel = directoryRelativePath(dir);
9838
+ return Boolean(rel && ignored(rel));
9839
+ };
9840
+ const scheduleUpdate = (changedPath) => {
9841
+ if (changedPath)
9842
+ pendingChangedPaths.add(changedPath);
9843
+ else
9844
+ fullUpdatePending = true;
9845
+ if (timer)
9846
+ clearTimer(timer);
9847
+ timer = setTimer(() => {
9848
+ timer = null;
9849
+ const paths = !fullUpdatePending && pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
9850
+ pendingChangedPaths.clear();
9851
+ fullUpdatePending = false;
9852
+ options.onUpdate(paths);
9853
+ }, debounceMs);
9854
+ };
9855
+ const reportWatchLimit = () => {
9856
+ if (watchLimitReported)
9857
+ return;
9858
+ watchLimitReported = true;
9859
+ options.onWatchLimit?.(maxWatchedDirectories);
9860
+ options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
9861
+ };
9862
+ const closeSubtree = (dir) => {
9863
+ for (const [watchedDir, watcher] of [...watchers]) {
9864
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
9865
+ continue;
9866
+ try {
9867
+ watcher.close?.();
9868
+ } catch {}
9869
+ watchers.delete(watchedDir);
9870
+ signatures.delete(watchedDir);
9871
+ }
9872
+ };
9873
+ const closeAll = () => {
9874
+ if (initialScanTimer) {
9875
+ clearTimer(initialScanTimer);
9876
+ initialScanTimer = null;
9877
+ }
9878
+ if (pathInspectionTimer) {
9879
+ clearTimer(pathInspectionTimer);
9880
+ pathInspectionTimer = null;
9881
+ }
9882
+ initialScanQueue.length = 0;
9883
+ pendingPathInspections.clear();
9884
+ for (const watcher of [...watchers.values()]) {
9885
+ try {
9886
+ watcher.close?.();
9887
+ } catch {}
9888
+ }
9889
+ watchers.clear();
9890
+ signatures.clear();
9891
+ };
9892
+ const readChildDirectories = (dir) => {
9893
+ let entries;
9894
+ try {
9895
+ entries = readDirs(dir);
9896
+ } catch (error) {
9897
+ options.onError?.(error);
9898
+ return [];
9899
+ }
9900
+ const children = [];
9901
+ for (const entry of entries) {
9902
+ if (!entry.isDirectory())
9903
+ continue;
9904
+ const child = join9(dir, entry.name);
9905
+ if (ignoredDirectory(child))
9906
+ continue;
9907
+ children.push(child);
9908
+ }
9909
+ return children;
9910
+ };
9911
+ const processInitialScanQueue = () => {
9912
+ initialScanTimer = null;
9913
+ if (watchers.size >= maxWatchedDirectories) {
9914
+ reportWatchLimit();
9915
+ initialScanQueue.length = 0;
9916
+ return;
9917
+ }
9918
+ const next = initialScanQueue.shift();
9919
+ if (next) {
9920
+ processingInitialScan = true;
9921
+ try {
9922
+ watchDirectory(next, true);
9923
+ } finally {
9924
+ processingInitialScan = false;
9849
9925
  }
9850
9926
  }
9851
- }
9852
- for (const cond of exact ?? []) {
9853
- const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
9854
- whereParts.push(`${castOf(cond.column)} = ${rhs}`);
9855
- if (useParams)
9856
- params.push(cond.value);
9857
- }
9858
- return { where: whereParts.join(" AND "), params, useParams };
9859
- }
9860
- function filterGroupedColumns(grouped, columnNames) {
9861
- const validColumns = new Set(columnNames);
9862
- const filtered = new Map;
9863
- for (const [value, columns] of grouped) {
9864
- const valid = columns.filter((column) => validColumns.has(column));
9865
- if (valid.length > 0)
9866
- filtered.set(value, valid);
9867
- }
9868
- return filtered;
9869
- }
9870
- function filterExactColumns(exact, columnNames) {
9871
- if (!exact || exact.length === 0)
9872
- return [];
9873
- const validColumns = new Set(columnNames);
9874
- return exact.filter((cond) => validColumns.has(cond.column));
9875
- }
9876
- function filterOrderByColumns(orderBy, columnNames) {
9877
- if (!orderBy)
9878
- return;
9879
- const validColumns = new Set(columnNames);
9880
- const filtered = orderBy.filter((order) => validColumns.has(order.column));
9881
- return filtered.length > 0 ? filtered : undefined;
9882
- }
9883
- function buildOrderClause(orderBy, kind = "sqlite") {
9884
- if (!orderBy?.length)
9885
- return "";
9886
- const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
9887
- return ` ORDER BY ${parts.join(", ")}`;
9888
- }
9889
- function useParamsFor(kind) {
9890
- return kind === "sqlite";
9891
- }
9892
- function placeValue(coerced, kind, useParams, params) {
9893
- if (useParams) {
9894
- params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
9895
- return "?";
9896
- }
9897
- if (coerced === null)
9898
- return "NULL";
9899
- if (typeof coerced === "number")
9900
- return String(coerced);
9901
- if (typeof coerced === "boolean")
9902
- return coerced ? "TRUE" : "FALSE";
9903
- const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
9904
- return escapeSqlString(text, kind);
9927
+ if (watchers.size >= maxWatchedDirectories) {
9928
+ reportWatchLimit();
9929
+ initialScanQueue.length = 0;
9930
+ }
9931
+ if (initialScanQueue.length)
9932
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
9933
+ };
9934
+ const queueInitialChildren = (dir) => {
9935
+ const remaining = maxWatchedDirectories - watchers.size;
9936
+ if (remaining <= 0) {
9937
+ reportWatchLimit();
9938
+ return;
9939
+ }
9940
+ const children = readChildDirectories(dir);
9941
+ if (children.length > remaining)
9942
+ reportWatchLimit();
9943
+ initialScanQueue.push(...children.slice(0, remaining));
9944
+ if (!initialScanTimer && !processingInitialScan)
9945
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
9946
+ };
9947
+ const processChangedPath = (changed, fullChangedPath) => {
9948
+ const known = watchers.has(fullChangedPath);
9949
+ if (isDirectory(fullChangedPath)) {
9950
+ if (known) {
9951
+ const signature = directorySignature(fullChangedPath);
9952
+ if (signature && signature !== signatures.get(fullChangedPath)) {
9953
+ closeSubtree(fullChangedPath);
9954
+ watchDirectory(fullChangedPath, initialScanAsync);
9955
+ }
9956
+ scheduleUpdate(changed);
9957
+ return;
9958
+ }
9959
+ watchDirectory(fullChangedPath, initialScanAsync);
9960
+ } else if (known) {
9961
+ closeSubtree(fullChangedPath);
9962
+ }
9963
+ scheduleUpdate(changed);
9964
+ };
9965
+ const processPathInspections = () => {
9966
+ pathInspectionTimer = null;
9967
+ const entries = [...pendingPathInspections];
9968
+ pendingPathInspections.clear();
9969
+ for (const [changed, fullChangedPath] of entries) {
9970
+ processChangedPath(changed, fullChangedPath);
9971
+ }
9972
+ };
9973
+ const queuePathInspection = (changed, fullChangedPath) => {
9974
+ pendingPathInspections.set(changed, fullChangedPath);
9975
+ if (!pathInspectionTimer)
9976
+ pathInspectionTimer = setTimer(processPathInspections, 25);
9977
+ };
9978
+ const watchDirectory = (dir, initialScan = false) => {
9979
+ if (watchers.has(dir))
9980
+ return;
9981
+ if (watchers.size >= maxWatchedDirectories) {
9982
+ reportWatchLimit();
9983
+ return;
9984
+ }
9985
+ const rel = directoryRelativePath(dir);
9986
+ if (rel && ignored(rel))
9987
+ return;
9988
+ try {
9989
+ const watcher = watch(dir, { persistent: false, recursive }, (_event, filename) => {
9990
+ if (!filename) {
9991
+ scheduleUpdate();
9992
+ return;
9993
+ }
9994
+ const changed = normalizeRelativePath(join9(rel, filename.toString()));
9995
+ if (ignored(changed))
9996
+ return;
9997
+ const fullChangedPath = join9(options.root, changed);
9998
+ if (!isInsideRoot(options.root, fullChangedPath))
9999
+ return;
10000
+ if (recursive) {
10001
+ scheduleUpdate(changed);
10002
+ return;
10003
+ }
10004
+ if (initialScanAsync) {
10005
+ queuePathInspection(changed, fullChangedPath);
10006
+ return;
10007
+ }
10008
+ processChangedPath(changed, fullChangedPath);
10009
+ }) || {};
10010
+ watchers.set(dir, watcher);
10011
+ const signature = directorySignature(dir);
10012
+ if (signature)
10013
+ signatures.set(dir, signature);
10014
+ watcher.on?.("error", () => {
10015
+ if (watchers.get(dir) === watcher) {
10016
+ watchers.delete(dir);
10017
+ signatures.delete(dir);
10018
+ }
10019
+ });
10020
+ watcher.on?.("close", () => {
10021
+ if (watchers.get(dir) === watcher) {
10022
+ watchers.delete(dir);
10023
+ signatures.delete(dir);
10024
+ }
10025
+ });
10026
+ } catch (error) {
10027
+ options.onError?.(error);
10028
+ return;
10029
+ }
10030
+ if (recursive)
10031
+ return;
10032
+ if (initialScanAsync && initialScan) {
10033
+ queueInitialChildren(dir);
10034
+ return;
10035
+ }
10036
+ if (watchers.size >= maxWatchedDirectories) {
10037
+ reportWatchLimit();
10038
+ return;
10039
+ }
10040
+ for (const child of readChildDirectories(dir))
10041
+ watchDirectory(child);
10042
+ };
10043
+ watchDirectory(options.root, true);
10044
+ return { started: watchers.size > 0, close: closeAll };
9905
10045
  }
9906
- function coerceCell(cell, columnType) {
9907
- return coerceDbValue(cell.value, columnType);
10046
+ var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
10047
+ var init_worktree_watcher = __esm(() => {
10048
+ init_search();
10049
+ });
10050
+
10051
+ // web-src/server/watch-child.ts
10052
+ var exports_watch_child = {};
10053
+ __export(exports_watch_child, {
10054
+ runWatchChild: () => runWatchChild,
10055
+ parsePorcelainV2: () => parsePorcelainV2
10056
+ });
10057
+ import { createHash as createHash2 } from "node:crypto";
10058
+ import { createReadStream as createReadStream2, lstatSync as lstatSync4 } from "node:fs";
10059
+ import { join as join10 } from "node:path";
10060
+ function send(message) {
10061
+ process.stdout.write(`${JSON.stringify(message)}
10062
+ `);
9908
10063
  }
9909
- function formatWriteComparisons(cells, columnTypes, kind, useParams, params, separator) {
9910
- return cells.map((cell) => `${sanitizeIdentifier(cell.column, kind)} = ${placeValue(coerceCell(cell, columnTypes.get(cell.column) ?? "TEXT"), kind, useParams, params)}`).join(separator);
10064
+ function readConfigLine() {
10065
+ return new Promise((resolve2, reject) => {
10066
+ let buffer = "";
10067
+ const stdin = process.stdin;
10068
+ stdin.setEncoding("utf8");
10069
+ const onData = (chunk) => {
10070
+ buffer += chunk;
10071
+ const newline = buffer.indexOf(`
10072
+ `);
10073
+ if (newline === -1)
10074
+ return;
10075
+ stdin.off("data", onData);
10076
+ stdin.off("end", onEnd);
10077
+ resolve2(buffer.slice(0, newline));
10078
+ };
10079
+ const onEnd = () => {
10080
+ stdin.off("data", onData);
10081
+ reject(new Error("watch child: stdin closed before config arrived"));
10082
+ };
10083
+ stdin.on("data", onData);
10084
+ stdin.on("end", onEnd);
10085
+ });
9911
10086
  }
9912
- function buildInsertSql(table, cells, columnTypes, kind) {
9913
- if (cells.length === 0) {
9914
- throw new Error("insert requires at least one column value");
9915
- }
9916
- const useParams = useParamsFor(kind);
9917
- const params = [];
9918
- const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
9919
- const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
9920
- const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
9921
- return { sql, params };
10087
+ function hashFile(path) {
10088
+ return new Promise((resolve2) => {
10089
+ const hash = createHash2("sha256");
10090
+ const stream = createReadStream2(path);
10091
+ stream.on("data", (chunk) => hash.update(chunk));
10092
+ stream.on("end", () => resolve2(hash.digest("hex")));
10093
+ stream.on("error", () => resolve2("unreadable"));
10094
+ });
9922
10095
  }
9923
- function buildUpdateSql(table, set, pk, columnTypes, kind) {
9924
- if (set.length === 0) {
9925
- throw new Error("update requires at least one column to set");
9926
- }
9927
- if (pk.length === 0) {
9928
- throw new Error("update requires a primary key condition");
10096
+ function parsePorcelainV2(raw) {
10097
+ const records = raw.split("\x00");
10098
+ let head = "";
10099
+ const entries = [];
10100
+ for (let i = 0;i < records.length; i++) {
10101
+ const record = records[i];
10102
+ if (!record)
10103
+ continue;
10104
+ if (record.startsWith("# branch.oid ")) {
10105
+ head = record.slice("# branch.oid ".length);
10106
+ continue;
10107
+ }
10108
+ if (record.startsWith("# "))
10109
+ continue;
10110
+ const kind = record[0];
10111
+ if (kind === "1" || kind === "2" || kind === "u") {
10112
+ const metaCount = kind === "1" ? 8 : kind === "2" ? 9 : 10;
10113
+ const status = record.split(" ").slice(0, metaCount).join(" ");
10114
+ entries.push({ path: record.slice(status.length + 1), status });
10115
+ if (kind === "2")
10116
+ i++;
10117
+ continue;
10118
+ }
10119
+ if (kind === "?" || kind === "!") {
10120
+ entries.push({ path: record.slice(2), status: kind });
10121
+ }
9929
10122
  }
9930
- const useParams = useParamsFor(kind);
9931
- const params = [];
9932
- const setSql = formatWriteComparisons(set, columnTypes, kind, useParams, params, ", ");
9933
- const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
9934
- const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
9935
- return { sql, params };
10123
+ return { head, entries };
9936
10124
  }
9937
- function buildDeleteSql(table, pk, columnTypes, kind) {
9938
- if (pk.length === 0) {
9939
- throw new Error("delete requires a primary key condition");
10125
+ async function readWorktreeSnapshot(config) {
10126
+ const result = await runAsync([
10127
+ "git",
10128
+ "--no-optional-locks",
10129
+ "-c",
10130
+ "core.fsmonitor=false",
10131
+ "status",
10132
+ "--porcelain=v2",
10133
+ "-z",
10134
+ "--branch",
10135
+ "--untracked-files=all"
10136
+ ], config.root, { timeout: 60000 });
10137
+ if (result.code !== 0)
10138
+ return null;
10139
+ const parsed = parsePorcelainV2(result.stdout);
10140
+ const entries = new Map;
10141
+ for (const entry of parsed.entries) {
10142
+ if (!entry.path)
10143
+ continue;
10144
+ if (isSkippableSearchPath(entry.path, config.omitDirNames, config.excludeNames))
10145
+ continue;
10146
+ entries.set(entry.path, await pathSignature(config.root, entry));
9940
10147
  }
9941
- const useParams = useParamsFor(kind);
9942
- const params = [];
9943
- const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
9944
- const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
9945
- return { sql, params };
10148
+ return { head: parsed.head, entries };
9946
10149
  }
9947
- var init_sql_utils = __esm(() => {
9948
- init_serialize();
9949
- });
9950
-
9951
- // web-src/server/database/mutate.ts
9952
- function assertCells(cells, label) {
9953
- if (!Array.isArray(cells)) {
9954
- throw new Error(`${label} must be an array`);
10150
+ async function pathSignature(root, entry) {
10151
+ const full = join10(root, entry.path);
10152
+ let stats;
10153
+ try {
10154
+ stats = lstatSync4(full);
10155
+ } catch {
10156
+ return `${entry.status}:absent`;
9955
10157
  }
9956
- for (const cell of cells) {
9957
- if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
9958
- throw new Error(`${label} contains an invalid cell`);
9959
- }
10158
+ const base = `${entry.status}:${stats.size}:${stats.mtimeMs}:${stats.mode}`;
10159
+ if (stats.isFile() && stats.size <= MAX_HASHED_FILE_BYTES) {
10160
+ return `${base}:${await hashFile(full)}`;
9960
10161
  }
9961
- return cells;
10162
+ return base;
9962
10163
  }
9963
- function buildMutationStatements(table, mutations, columns, kind) {
9964
- if (!Array.isArray(mutations) || mutations.length === 0) {
9965
- throw new Error("no mutations provided");
10164
+ function snapshotDiff(previous, next) {
10165
+ if (previous.head !== next.head)
10166
+ return { full: true, paths: [] };
10167
+ const paths = [];
10168
+ for (const [path, signature] of next.entries) {
10169
+ if (previous.entries.get(path) !== signature)
10170
+ paths.push(path);
9966
10171
  }
9967
- if (mutations.length > MAX_MUTATIONS) {
9968
- throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
10172
+ for (const path of previous.entries.keys()) {
10173
+ if (!next.entries.has(path))
10174
+ paths.push(path);
9969
10175
  }
9970
- const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
9971
- const columnNames = new Set(columns.map((c) => c.name));
9972
- const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
9973
- const pkNames = new Set(pkColumns);
9974
- const requireKnownColumns = (cells, label) => {
9975
- for (const cell of cells) {
9976
- if (!columnNames.has(cell.column)) {
9977
- throw new Error(`unknown column: ${cell.column}`);
9978
- }
9979
- }
9980
- };
9981
- const requirePrimaryKey = (pk) => {
9982
- if (pkColumns.length === 0) {
9983
- throw new Error("table has no primary key; row update/delete is not supported");
10176
+ return { full: false, paths };
10177
+ }
10178
+ async function runWatchChild() {
10179
+ const config = JSON.parse(await readConfigLine());
10180
+ process.stdin.on("end", () => process.exit(0));
10181
+ process.stdin.on("close", () => process.exit(0));
10182
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
10183
+ process.on(signal, () => process.exit(0));
10184
+ }
10185
+ let watching = false;
10186
+ if (!config.pollOnly) {
10187
+ const recursive = supportsNativeRecursiveWatch(process.platform);
10188
+ const watch = startWorktreeUpdateWatch({
10189
+ root: config.root,
10190
+ omitDirNames: config.omitDirNames,
10191
+ excludeNames: config.excludeNames,
10192
+ recursive,
10193
+ initialScanMode: recursive ? "sync" : "async",
10194
+ maxWatchedDirectories: config.maxWatchedDirectories,
10195
+ debounceMs: config.debounceMs,
10196
+ onUpdate: (paths) => send({ type: "update", paths }),
10197
+ onWatchLimit: (limit) => send({ type: "watch-limit", limit }),
10198
+ onError: (error) => send({
10199
+ type: "warn",
10200
+ message: error instanceof Error ? error.message : String(error)
10201
+ })
10202
+ });
10203
+ watching = watch.started;
10204
+ }
10205
+ send({ type: "ready", watching, pollOnly: config.pollOnly });
10206
+ setInterval(() => send({ type: "heartbeat" }), config.heartbeatIntervalMs).unref?.();
10207
+ setInterval(() => {
10208
+ try {
10209
+ process.kill(config.parentPid, 0);
10210
+ } catch {
10211
+ process.exit(0);
9984
10212
  }
9985
- const provided = new Set(pk.map((c) => c.column));
9986
- for (const name of pkColumns) {
9987
- if (!provided.has(name)) {
9988
- throw new Error(`missing primary key column: ${name}`);
9989
- }
10213
+ }, config.heartbeatIntervalMs);
10214
+ let snapshot = null;
10215
+ let baselineEstablished = false;
10216
+ const poll = async () => {
10217
+ const next = await readWorktreeSnapshot(config);
10218
+ if (!next)
10219
+ return;
10220
+ if (!snapshot) {
10221
+ snapshot = next;
10222
+ if (baselineEstablished)
10223
+ send({ type: "update" });
10224
+ baselineEstablished = true;
10225
+ return;
9990
10226
  }
9991
- for (const cell of pk) {
9992
- if (!pkNames.has(cell.column)) {
9993
- throw new Error(`not a primary key column: ${cell.column}`);
9994
- }
9995
- if (cell.value === null) {
9996
- throw new Error(`primary key column cannot be null: ${cell.column}`);
9997
- }
10227
+ const diff = snapshotDiff(snapshot, next);
10228
+ snapshot = next;
10229
+ if (diff.full) {
10230
+ send({ type: "update" });
10231
+ return;
9998
10232
  }
10233
+ if (diff.paths.length)
10234
+ send({ type: "update", paths: diff.paths });
9999
10235
  };
10000
- const statements = [];
10001
- for (const mutation of mutations) {
10002
- if (!mutation || typeof mutation !== "object") {
10003
- throw new Error("invalid mutation");
10004
- }
10005
- if (mutation.kind === "insert") {
10006
- const values = assertCells(mutation.values, "insert values");
10007
- requireKnownColumns(values, "insert values");
10008
- statements.push(buildInsertSql(table, values, columnTypes, kind));
10236
+ setInterval(() => {
10237
+ poll().catch((error) => send({
10238
+ type: "warn",
10239
+ message: error instanceof Error ? error.message : String(error)
10240
+ }));
10241
+ }, config.pollIntervalMs);
10242
+ }
10243
+ var MAX_HASHED_FILE_BYTES;
10244
+ var init_watch_child = __esm(() => {
10245
+ init_runtime();
10246
+ init_search();
10247
+ init_worktree_watcher();
10248
+ MAX_HASHED_FILE_BYTES = 4 * 1024 * 1024;
10249
+ });
10250
+
10251
+ // web-src/server/database/serialize.ts
10252
+ function serializeDbValue(value) {
10253
+ if (value === null || value === undefined)
10254
+ return null;
10255
+ if (typeof value === "bigint") {
10256
+ return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value.toString();
10257
+ }
10258
+ if (value instanceof Uint8Array) {
10259
+ return `<blob ${value.byteLength} bytes>`;
10260
+ }
10261
+ if (typeof value === "object") {
10262
+ try {
10263
+ return JSON.stringify(value);
10264
+ } catch {
10265
+ return String(value);
10266
+ }
10267
+ }
10268
+ return value;
10269
+ }
10270
+ function serializeDbRow(row) {
10271
+ return row.map(serializeDbValue);
10272
+ }
10273
+ function serializeDbRows(rows) {
10274
+ return rows.map(serializeDbRow);
10275
+ }
10276
+ function coerceDbValue(value, columnType) {
10277
+ if (value === null)
10278
+ return null;
10279
+ const t = (columnType || "").toLowerCase();
10280
+ if (/bool/.test(t)) {
10281
+ const v = value.trim().toLowerCase();
10282
+ if (v === "")
10283
+ return null;
10284
+ if (v === "true" || v === "t" || v === "1")
10285
+ return true;
10286
+ if (v === "false" || v === "f" || v === "0")
10287
+ return false;
10288
+ return value;
10289
+ }
10290
+ if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
10291
+ const trimmed = value.trim();
10292
+ if (trimmed === "")
10293
+ return null;
10294
+ const n = Number(trimmed);
10295
+ if (Number.isFinite(n) && String(n) === trimmed)
10296
+ return n;
10297
+ return value;
10298
+ }
10299
+ return value;
10300
+ }
10301
+ var MIN_SAFE, MAX_SAFE;
10302
+ var init_serialize = __esm(() => {
10303
+ MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
10304
+ MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
10305
+ });
10306
+
10307
+ // web-src/server/database/sql-utils.ts
10308
+ function sanitizeIdentifier(name, kind = "sqlite") {
10309
+ if (kind === "mysql")
10310
+ return `\`${name.replace(/`/g, "``")}\``;
10311
+ return `"${name.replace(/"/g, '""')}"`;
10312
+ }
10313
+ function escapeSqlString(value, kind) {
10314
+ const escaped = kind === "mysql" ? value.replace(/\\/g, "\\\\").replace(/'/g, "''") : value.replace(/'/g, "''");
10315
+ return `'${escaped}'`;
10316
+ }
10317
+ function buildFilterWhere(grouped, kind, exact) {
10318
+ const whereParts = [];
10319
+ const params = [];
10320
+ const useParams = kind === "sqlite";
10321
+ const castOf = (column) => kind === "mysql" ? `CAST(${sanitizeIdentifier(column, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier(column, kind)} AS TEXT)`;
10322
+ for (const [value, cols] of grouped) {
10323
+ const likeVal = useParams ? "?" : escapeSqlString(`%${value}%`, kind);
10324
+ if (cols.length === 1) {
10325
+ whereParts.push(`${castOf(cols[0])} LIKE ${likeVal}`);
10326
+ if (useParams)
10327
+ params.push(`%${value}%`);
10328
+ } else {
10329
+ const orParts = cols.map((column) => `${castOf(column)} LIKE ${likeVal}`);
10330
+ whereParts.push(`(${orParts.join(" OR ")})`);
10331
+ if (useParams) {
10332
+ for (let i = 0;i < cols.length; i++)
10333
+ params.push(`%${value}%`);
10334
+ }
10335
+ }
10336
+ }
10337
+ for (const cond of exact ?? []) {
10338
+ const rhs = useParams ? "?" : escapeSqlString(cond.value, kind);
10339
+ whereParts.push(`${castOf(cond.column)} = ${rhs}`);
10340
+ if (useParams)
10341
+ params.push(cond.value);
10342
+ }
10343
+ return { where: whereParts.join(" AND "), params, useParams };
10344
+ }
10345
+ function filterGroupedColumns(grouped, columnNames) {
10346
+ const validColumns = new Set(columnNames);
10347
+ const filtered = new Map;
10348
+ for (const [value, columns] of grouped) {
10349
+ const valid = columns.filter((column) => validColumns.has(column));
10350
+ if (valid.length > 0)
10351
+ filtered.set(value, valid);
10352
+ }
10353
+ return filtered;
10354
+ }
10355
+ function filterExactColumns(exact, columnNames) {
10356
+ if (!exact || exact.length === 0)
10357
+ return [];
10358
+ const validColumns = new Set(columnNames);
10359
+ return exact.filter((cond) => validColumns.has(cond.column));
10360
+ }
10361
+ function filterOrderByColumns(orderBy, columnNames) {
10362
+ if (!orderBy)
10363
+ return;
10364
+ const validColumns = new Set(columnNames);
10365
+ const filtered = orderBy.filter((order) => validColumns.has(order.column));
10366
+ return filtered.length > 0 ? filtered : undefined;
10367
+ }
10368
+ function buildOrderClause(orderBy, kind = "sqlite") {
10369
+ if (!orderBy?.length)
10370
+ return "";
10371
+ const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
10372
+ return ` ORDER BY ${parts.join(", ")}`;
10373
+ }
10374
+ function useParamsFor(kind) {
10375
+ return kind === "sqlite";
10376
+ }
10377
+ function placeValue(coerced, kind, useParams, params) {
10378
+ if (useParams) {
10379
+ params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
10380
+ return "?";
10381
+ }
10382
+ if (coerced === null)
10383
+ return "NULL";
10384
+ if (typeof coerced === "number")
10385
+ return String(coerced);
10386
+ if (typeof coerced === "boolean")
10387
+ return coerced ? "TRUE" : "FALSE";
10388
+ const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
10389
+ return escapeSqlString(text, kind);
10390
+ }
10391
+ function coerceCell(cell, columnType) {
10392
+ return coerceDbValue(cell.value, columnType);
10393
+ }
10394
+ function formatWriteComparisons(cells, columnTypes, kind, useParams, params, separator) {
10395
+ return cells.map((cell) => `${sanitizeIdentifier(cell.column, kind)} = ${placeValue(coerceCell(cell, columnTypes.get(cell.column) ?? "TEXT"), kind, useParams, params)}`).join(separator);
10396
+ }
10397
+ function buildInsertSql(table, cells, columnTypes, kind) {
10398
+ if (cells.length === 0) {
10399
+ throw new Error("insert requires at least one column value");
10400
+ }
10401
+ const useParams = useParamsFor(kind);
10402
+ const params = [];
10403
+ const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
10404
+ const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
10405
+ const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
10406
+ return { sql, params };
10407
+ }
10408
+ function buildUpdateSql(table, set, pk, columnTypes, kind) {
10409
+ if (set.length === 0) {
10410
+ throw new Error("update requires at least one column to set");
10411
+ }
10412
+ if (pk.length === 0) {
10413
+ throw new Error("update requires a primary key condition");
10414
+ }
10415
+ const useParams = useParamsFor(kind);
10416
+ const params = [];
10417
+ const setSql = formatWriteComparisons(set, columnTypes, kind, useParams, params, ", ");
10418
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10419
+ const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
10420
+ return { sql, params };
10421
+ }
10422
+ function buildDeleteSql(table, pk, columnTypes, kind) {
10423
+ if (pk.length === 0) {
10424
+ throw new Error("delete requires a primary key condition");
10425
+ }
10426
+ const useParams = useParamsFor(kind);
10427
+ const params = [];
10428
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10429
+ const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
10430
+ return { sql, params };
10431
+ }
10432
+ var init_sql_utils = __esm(() => {
10433
+ init_serialize();
10434
+ });
10435
+
10436
+ // web-src/server/database/mutate.ts
10437
+ function assertCells(cells, label) {
10438
+ if (!Array.isArray(cells)) {
10439
+ throw new Error(`${label} must be an array`);
10440
+ }
10441
+ for (const cell of cells) {
10442
+ if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
10443
+ throw new Error(`${label} contains an invalid cell`);
10444
+ }
10445
+ }
10446
+ return cells;
10447
+ }
10448
+ function buildMutationStatements(table, mutations, columns, kind) {
10449
+ if (!Array.isArray(mutations) || mutations.length === 0) {
10450
+ throw new Error("no mutations provided");
10451
+ }
10452
+ if (mutations.length > MAX_MUTATIONS) {
10453
+ throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
10454
+ }
10455
+ const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
10456
+ const columnNames = new Set(columns.map((c) => c.name));
10457
+ const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
10458
+ const pkNames = new Set(pkColumns);
10459
+ const requireKnownColumns = (cells, label) => {
10460
+ for (const cell of cells) {
10461
+ if (!columnNames.has(cell.column)) {
10462
+ throw new Error(`unknown column: ${cell.column}`);
10463
+ }
10464
+ }
10465
+ };
10466
+ const requirePrimaryKey = (pk) => {
10467
+ if (pkColumns.length === 0) {
10468
+ throw new Error("table has no primary key; row update/delete is not supported");
10469
+ }
10470
+ const provided = new Set(pk.map((c) => c.column));
10471
+ for (const name of pkColumns) {
10472
+ if (!provided.has(name)) {
10473
+ throw new Error(`missing primary key column: ${name}`);
10474
+ }
10475
+ }
10476
+ for (const cell of pk) {
10477
+ if (!pkNames.has(cell.column)) {
10478
+ throw new Error(`not a primary key column: ${cell.column}`);
10479
+ }
10480
+ if (cell.value === null) {
10481
+ throw new Error(`primary key column cannot be null: ${cell.column}`);
10482
+ }
10483
+ }
10484
+ };
10485
+ const statements = [];
10486
+ for (const mutation of mutations) {
10487
+ if (!mutation || typeof mutation !== "object") {
10488
+ throw new Error("invalid mutation");
10489
+ }
10490
+ if (mutation.kind === "insert") {
10491
+ const values = assertCells(mutation.values, "insert values");
10492
+ requireKnownColumns(values, "insert values");
10493
+ statements.push(buildInsertSql(table, values, columnTypes, kind));
10009
10494
  } else if (mutation.kind === "update") {
10010
10495
  const pk = assertCells(mutation.pk, "update pk");
10011
10496
  const values = assertCells(mutation.values, "update values");
@@ -10028,7 +10513,7 @@ var init_mutate = __esm(() => {
10028
10513
  });
10029
10514
 
10030
10515
  // web-src/server/database/sources/sql-snapshot.ts
10031
- import { createHash as createHash2 } from "node:crypto";
10516
+ import { createHash as createHash3 } from "node:crypto";
10032
10517
  function normalizeRawValue(v) {
10033
10518
  if (v === null)
10034
10519
  return "\\N";
@@ -10048,7 +10533,7 @@ function rowToPayloadJson(columns, row) {
10048
10533
  }
10049
10534
  function computeRowHash(columns, row) {
10050
10535
  const parts = columns.map((_, i) => normalizeRawValue(row[i]));
10051
- return createHash2("sha256").update(parts.join("\t")).digest("hex");
10536
+ return createHash3("sha256").update(parts.join("\t")).digest("hex");
10052
10537
  }
10053
10538
  function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
10054
10539
  if (pkColumns.length === 0) {
@@ -12119,7 +12604,7 @@ __export(exports_redis, {
12119
12604
  canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer,
12120
12605
  __setRedisClientFactoryForTest: () => __setRedisClientFactoryForTest
12121
12606
  });
12122
- import { createHash as createHash3 } from "node:crypto";
12607
+ import { createHash as createHash4 } from "node:crypto";
12123
12608
  import { createClient } from "@redis/client";
12124
12609
  function __setRedisClientFactoryForTest(factory) {
12125
12610
  createRedisClientImpl = factory ?? ((options) => createClient(options));
@@ -12496,7 +12981,7 @@ function createRedisAdapter(config) {
12496
12981
  if (type === "none") {
12497
12982
  return {
12498
12983
  payload: { type: "none" },
12499
- fullHash: createHash3("sha256").update("").digest("hex")
12984
+ fullHash: createHash4("sha256").update("").digest("hex")
12500
12985
  };
12501
12986
  }
12502
12987
  if (type === "string")
@@ -12513,7 +12998,7 @@ function createRedisAdapter(config) {
12513
12998
  return snapshotFetchStream(db, hexKey, signal);
12514
12999
  return {
12515
13000
  payload: { type: "none" },
12516
- fullHash: createHash3("sha256").update("").digest("hex")
13001
+ fullHash: createHash4("sha256").update("").digest("hex")
12517
13002
  };
12518
13003
  }
12519
13004
  async function evalHex(db, luaBody, extraArgv, label, signal) {
@@ -12525,7 +13010,7 @@ function createRedisAdapter(config) {
12525
13010
  }
12526
13011
  async function snapshotFetchString(db, hexKey, signal) {
12527
13012
  const fullSize = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('STRLEN', fromhex(ARGV[1]))`, [hexKey], "STRLEN", signal)) || 0;
12528
- const hasher = createHash3("sha256");
13013
+ const hasher = createHash4("sha256");
12529
13014
  let previewBytes = Buffer.alloc(0);
12530
13015
  for (let offset = 0;offset < fullSize; offset += REDIS_STRING_BYTE_LIMIT) {
12531
13016
  if (signal?.aborted)
@@ -12558,7 +13043,7 @@ function createRedisAdapter(config) {
12558
13043
  }
12559
13044
  async function snapshotFetchList(db, hexKey, signal) {
12560
13045
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('LLEN', fromhex(ARGV[1]))`, [hexKey], "LLEN", signal)) || 0;
12561
- const hasher = createHash3("sha256");
13046
+ const hasher = createHash4("sha256");
12562
13047
  let previewItems = [];
12563
13048
  for (let offset = 0;offset < total; offset += REDIS_COLLECTION_LIMIT) {
12564
13049
  if (signal?.aborted)
@@ -12600,7 +13085,7 @@ function createRedisAdapter(config) {
12600
13085
  uniquePairs.push(p);
12601
13086
  }
12602
13087
  uniquePairs.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
12603
- const hasher = createHash3("sha256");
13088
+ const hasher = createHash4("sha256");
12604
13089
  for (const [f, v] of uniquePairs) {
12605
13090
  hashHexItem(hasher, f);
12606
13091
  hashHexItem(hasher, v);
@@ -12628,7 +13113,7 @@ function createRedisAdapter(config) {
12628
13113
  } while (cursor !== "0");
12629
13114
  const dedup = Array.from(new Set(allMembers));
12630
13115
  dedup.sort();
12631
- const hasher = createHash3("sha256");
13116
+ const hasher = createHash4("sha256");
12632
13117
  for (const m of dedup)
12633
13118
  hashHexItem(hasher, m);
12634
13119
  const previewMembers = dedup.slice(0, REDIS_COLLECTION_LIMIT).map(decodeHexItem);
@@ -12642,7 +13127,7 @@ function createRedisAdapter(config) {
12642
13127
  }
12643
13128
  async function snapshotFetchZset(db, hexKey, signal) {
12644
13129
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('ZCARD', fromhex(ARGV[1]))`, [hexKey], "ZCARD", signal)) || 0;
12645
- const hasher = createHash3("sha256");
13130
+ const hasher = createHash4("sha256");
12646
13131
  let previewMembers = [];
12647
13132
  for (let offset = 0;offset < total; offset += REDIS_COLLECTION_LIMIT) {
12648
13133
  if (signal?.aborted)
@@ -12672,7 +13157,7 @@ function createRedisAdapter(config) {
12672
13157
  }
12673
13158
  async function snapshotFetchStream(db, hexKey, signal) {
12674
13159
  const total = Number(await evalHex(db, `${LUA_HEX_KEY_PRELUDE} return redis.call('XLEN', fromhex(ARGV[1]))`, [hexKey], "XLEN", signal)) || 0;
12675
- const hasher = createHash3("sha256");
13160
+ const hasher = createHash4("sha256");
12676
13161
  let previewEntries = [];
12677
13162
  let startId = "-";
12678
13163
  const seenIds = new Set;
@@ -13264,7 +13749,7 @@ var init_raw_file_headers = __esm(() => {
13264
13749
 
13265
13750
  // web-src/server/database/adapters/s3.ts
13266
13751
  import { spawnSync as spawnSync4 } from "node:child_process";
13267
- import { createHash as createHash4, createHmac } from "node:crypto";
13752
+ import { createHash as createHash5, createHmac } from "node:crypto";
13268
13753
  function createS3RequestDeadline() {
13269
13754
  const timeoutMs = s3RequestTimeoutMs;
13270
13755
  return {
@@ -13294,10 +13779,10 @@ function hmac(key, value) {
13294
13779
  return createHmac("sha256", key).update(value, "utf8").digest();
13295
13780
  }
13296
13781
  function sha256(value) {
13297
- return createHash4("sha256").update(value, "utf8").digest("hex");
13782
+ return createHash5("sha256").update(value, "utf8").digest("hex");
13298
13783
  }
13299
13784
  function sha256Bytes(value) {
13300
- return createHash4("sha256").update(value).digest("hex");
13785
+ return createHash5("sha256").update(value).digest("hex");
13301
13786
  }
13302
13787
  function encodeRfc3986(value) {
13303
13788
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
@@ -14304,7 +14789,7 @@ import {
14304
14789
  statSync as statSync5
14305
14790
  } from "node:fs";
14306
14791
  import { lstat, open as open2, readdir, readFile as readFile2, stat as stat2 } from "node:fs/promises";
14307
- import { basename, join as join9, relative as relative4 } from "node:path";
14792
+ import { basename, join as join11, relative as relative5 } from "node:path";
14308
14793
  function isSqliteFile(fullPath) {
14309
14794
  try {
14310
14795
  const stat3 = statSync5(fullPath);
@@ -14375,7 +14860,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14375
14860
  return;
14376
14861
  if (omitSet.has(entry.toLowerCase()))
14377
14862
  continue;
14378
- const full = join9(dir, entry);
14863
+ const full = join11(dir, entry);
14379
14864
  let entryStat;
14380
14865
  try {
14381
14866
  entryStat = await lstat(full);
@@ -14392,7 +14877,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14392
14877
  continue;
14393
14878
  if (!await isSqliteFileAsync(full))
14394
14879
  continue;
14395
- const rel = relative4(cwd, full);
14880
+ const rel = relative5(cwd, full);
14396
14881
  if (rel.startsWith("..") || rel.startsWith("/"))
14397
14882
  continue;
14398
14883
  results.push({
@@ -14419,7 +14904,7 @@ function validateDbPath(cwd, dbPath) {
14419
14904
  const parts = dbPath.split(/[\\/]+/);
14420
14905
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
14421
14906
  return null;
14422
- const full = join9(cwd, dbPath);
14907
+ const full = join11(cwd, dbPath);
14423
14908
  if (!existsSync6(full))
14424
14909
  return null;
14425
14910
  let realCwd;
@@ -14430,7 +14915,7 @@ function validateDbPath(cwd, dbPath) {
14430
14915
  } catch {
14431
14916
  return null;
14432
14917
  }
14433
- const rel = relative4(realCwd, realFull);
14918
+ const rel = relative5(realCwd, realFull);
14434
14919
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
14435
14920
  return null;
14436
14921
  if (!isSqliteFile(realFull))
@@ -14591,7 +15076,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
14591
15076
  }
14592
15077
  async function readDotenvAsync(composeDir) {
14593
15078
  try {
14594
- const content = await readFile2(join9(composeDir, ".env"), "utf-8");
15079
+ const content = await readFile2(join11(composeDir, ".env"), "utf-8");
14595
15080
  return parseDotenvContent(content);
14596
15081
  } catch {
14597
15082
  return {};
@@ -14713,7 +15198,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
14713
15198
  for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
14714
15199
  servicePositions.push({ name: match[1], start: match.index });
14715
15200
  }
14716
- const relDir = relative4(cwd, composeDir);
15201
+ const relDir = relative5(cwd, composeDir);
14717
15202
  const isRoot = relDir === "" || relDir === ".";
14718
15203
  const relDirSlash = relDir.replace(/\\/g, "/");
14719
15204
  const filename = basename(filepath);
@@ -14818,7 +15303,7 @@ async function walkForMarkerFileAsync(dir, depth, omitSet, hasCapacity, visitDir
14818
15303
  return;
14819
15304
  if (omitSet.has(entry.toLowerCase()))
14820
15305
  continue;
14821
- const full = join9(dir, entry);
15306
+ const full = join11(dir, entry);
14822
15307
  let entryStat;
14823
15308
  try {
14824
15309
  entryStat = await lstat(full);
@@ -14845,7 +15330,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
14845
15330
  omitSet.add("node_modules");
14846
15331
  await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_DOCKER_SERVICES, async (dir) => {
14847
15332
  for (const filename of COMPOSE_FILENAMES) {
14848
- const filepath = join9(dir, filename);
15333
+ const filepath = join11(dir, filename);
14849
15334
  if (await pathExistsAsync(filepath)) {
14850
15335
  await parseComposeFileAsync(filepath, dir, cwd, results);
14851
15336
  break;
@@ -14956,410 +15441,153 @@ function isSafeDockerRelDir(value) {
14956
15441
  }
14957
15442
  function isSafeDockerDatabaseName(value) {
14958
15443
  if (value === undefined)
14959
- return true;
14960
- if (value === "")
14961
- return false;
14962
- if (hasControlCharacter(value))
14963
- return false;
14964
- return /^[A-Za-z0-9_$.-]+$/.test(value);
14965
- }
14966
- function cloneSupabaseDiscoveryResult(result) {
14967
- return result.map((entry) => ({ ...entry }));
14968
- }
14969
- function parseSupabaseConfigToml(content) {
14970
- let section = null;
14971
- let projectId = null;
14972
- let dbPort = null;
14973
- for (const rawLine of content.split(`
14974
- `)) {
14975
- const line = rawLine.trim();
14976
- if (!line || line.startsWith("#"))
14977
- continue;
14978
- const sectionMatch = line.match(/^\[([^\]]+)\]$/);
14979
- if (sectionMatch) {
14980
- section = sectionMatch[1];
14981
- continue;
14982
- }
14983
- const kvMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
14984
- if (!kvMatch)
14985
- continue;
14986
- const value = stripScalarSyntax(kvMatch[2]);
14987
- if (section === null && kvMatch[1] === "project_id") {
14988
- projectId = value;
14989
- } else if (section === "db" && kvMatch[1] === "port") {
14990
- dbPort = value;
14991
- }
14992
- }
14993
- if (!projectId || !isSafeDockerServiceName(projectId))
14994
- return null;
14995
- return {
14996
- projectId,
14997
- dbPort: dbPort && /^\d+$/.test(dbPort) ? dbPort : DEFAULT_SUPABASE_DB_PORT
14998
- };
14999
- }
15000
- async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal) {
15001
- const cacheKey = discoveryCacheKey(cwd, omitDirNames);
15002
- const now = Date.now();
15003
- const cached = supabaseDiscoveryCache.get(cacheKey);
15004
- if (cached && cached.expiresAt > now) {
15005
- return cloneSupabaseDiscoveryResult(cached.result);
15006
- }
15007
- const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
15008
- omitSet.add(".git");
15009
- omitSet.add("node_modules");
15010
- const results = [];
15011
- await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
15012
- const configPath = join9(dir, "supabase", "config.toml");
15013
- if (!await pathExistsAsync(configPath))
15014
- return;
15015
- try {
15016
- const content = await readFile2(configPath, "utf-8");
15017
- const parsed = parseSupabaseConfigToml(content);
15018
- if (!parsed)
15019
- return;
15020
- const relDir = relative4(cwd, dir);
15021
- const isRoot = relDir === "" || relDir === ".";
15022
- const relDirSlash = relDir.replace(/\\/g, "/");
15023
- const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
15024
- const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
15025
- results.push({
15026
- id,
15027
- path: isRoot ? "supabase/config.toml" : `${relDirSlash}/supabase/config.toml`,
15028
- name: `${parsed.projectId} (Supabase CLI, postgres@127.0.0.1:${parsed.dbPort}/postgres${labelPath})`,
15029
- sizeBytes: 0,
15030
- kind: "postgresql",
15031
- projectId: parsed.projectId,
15032
- relDirSlash,
15033
- dbPort: parsed.dbPort
15034
- });
15035
- } catch {}
15036
- }, signal);
15037
- if (signal?.aborted)
15038
- return cloneSupabaseDiscoveryResult(results);
15039
- supabaseDiscoveryCache.set(cacheKey, {
15040
- expiresAt: now + SUPABASE_DISCOVERY_TTL_MS,
15041
- result: cloneSupabaseDiscoveryResult(results)
15042
- });
15043
- return cloneSupabaseDiscoveryResult(results);
15044
- }
15045
- function parseSupabaseDbId(dbId) {
15046
- if (!dbId.startsWith("supabase:"))
15047
- return null;
15048
- const rest = dbId.slice("supabase:".length);
15049
- if (!rest)
15050
- return null;
15051
- const atIdx = rest.indexOf("@");
15052
- let projectId;
15053
- let relDir = "";
15054
- if (atIdx >= 0) {
15055
- if (rest.indexOf("@", atIdx + 1) >= 0)
15056
- return null;
15057
- projectId = rest.slice(0, atIdx);
15058
- try {
15059
- relDir = decodeURIComponent(rest.slice(atIdx + 1));
15060
- } catch {
15061
- return null;
15062
- }
15063
- if (!isSafeDockerRelDir(relDir))
15064
- return null;
15065
- } else {
15066
- projectId = rest;
15067
- }
15068
- if (!isSafeDockerServiceName(projectId))
15069
- return null;
15070
- return { projectId, relDir };
15071
- }
15072
- async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal) {
15073
- const parsed = parseSupabaseDbId(dbId);
15074
- if (!parsed)
15075
- return null;
15076
- const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
15077
- return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
15078
- }
15079
- 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;
15080
- var init_discovery = __esm(() => {
15081
- SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
15082
- sqliteDiscoveryCache = new Map;
15083
- COMPOSE_FILENAMES = [
15084
- "docker-compose.yml",
15085
- "docker-compose.yaml",
15086
- "compose.yml",
15087
- "compose.yaml"
15088
- ];
15089
- dockerDiscoveryCache = new Map;
15090
- DB_KIND_VALUES = new Set([
15091
- "sqlite",
15092
- "postgresql",
15093
- "mysql",
15094
- "redis",
15095
- "elasticsearch",
15096
- "s3",
15097
- "dynamodb"
15098
- ]);
15099
- supabaseDiscoveryCache = new Map;
15100
- });
15101
-
15102
- // web-src/server/worktree-watcher.ts
15103
- import {
15104
- lstatSync as lstatSync3,
15105
- readdirSync as nodeReaddirSync,
15106
- watch as nodeWatch
15107
- } from "node:fs";
15108
- import { join as join10, relative as relative5 } from "node:path";
15109
- function normalizeRelativePath(path) {
15110
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
15111
- }
15112
- function isInsideRoot(root, path) {
15113
- const rel = relative5(root, path).replace(/\\/g, "/");
15114
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
15115
- }
15116
- function startWorktreeUpdateWatch(options) {
15117
- const watch = options.watch || nodeWatch;
15118
- const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
15119
- const isDirectory = options.isDirectory || ((path) => {
15120
- try {
15121
- return lstatSync3(path).isDirectory();
15122
- } catch {
15123
- return false;
15124
- }
15125
- });
15126
- const directorySignature = options.directorySignature || ((path) => {
15127
- try {
15128
- const stats = lstatSync3(path);
15129
- if (!stats.isDirectory())
15130
- return null;
15131
- return `${stats.dev}:${stats.ino}`;
15132
- } catch {
15133
- return null;
15134
- }
15135
- });
15136
- const setTimer = options.setTimeoutFn || setTimeout;
15137
- const clearTimer = options.clearTimeoutFn || clearTimeout;
15138
- const debounceMs = options.debounceMs ?? 250;
15139
- const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
15140
- const watchers = new Map;
15141
- const signatures = new Map;
15142
- const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
15143
- const initialScanQueue = [];
15144
- let initialScanTimer = null;
15145
- let processingInitialScan = false;
15146
- const pendingPathInspections = new Map;
15147
- let pathInspectionTimer = null;
15148
- let timer = null;
15149
- const pendingChangedPaths = new Set;
15150
- let watchLimitReported = false;
15151
- const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
15152
- const directoryRelativePath = (dir) => normalizeRelativePath(relative5(options.root, dir));
15153
- const ignoredDirectory = (dir) => {
15154
- const rel = directoryRelativePath(dir);
15155
- return Boolean(rel && ignored(rel));
15156
- };
15157
- const scheduleUpdate = (changedPath) => {
15158
- if (changedPath)
15159
- pendingChangedPaths.add(changedPath);
15160
- if (timer)
15161
- clearTimer(timer);
15162
- timer = setTimer(() => {
15163
- timer = null;
15164
- const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
15165
- pendingChangedPaths.clear();
15166
- options.onUpdate(paths);
15167
- }, debounceMs);
15168
- };
15169
- const reportWatchLimit = () => {
15170
- if (watchLimitReported)
15171
- return;
15172
- watchLimitReported = true;
15173
- options.onWatchLimit?.(maxWatchedDirectories);
15174
- options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
15175
- };
15176
- const closeSubtree = (dir) => {
15177
- for (const [watchedDir, watcher] of [...watchers]) {
15178
- if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
15179
- continue;
15180
- try {
15181
- watcher.close?.();
15182
- } catch {}
15183
- watchers.delete(watchedDir);
15184
- signatures.delete(watchedDir);
15185
- }
15186
- };
15187
- const closeAll = () => {
15188
- if (initialScanTimer) {
15189
- clearTimer(initialScanTimer);
15190
- initialScanTimer = null;
15191
- }
15192
- if (pathInspectionTimer) {
15193
- clearTimer(pathInspectionTimer);
15194
- pathInspectionTimer = null;
15195
- }
15196
- initialScanQueue.length = 0;
15197
- pendingPathInspections.clear();
15198
- for (const watcher of [...watchers.values()]) {
15199
- try {
15200
- watcher.close?.();
15201
- } catch {}
15202
- }
15203
- watchers.clear();
15204
- signatures.clear();
15205
- };
15206
- const readChildDirectories = (dir) => {
15207
- let entries;
15208
- try {
15209
- entries = readDirs(dir);
15210
- } catch (error) {
15211
- options.onError?.(error);
15212
- return [];
15213
- }
15214
- const children = [];
15215
- for (const entry of entries) {
15216
- if (!entry.isDirectory())
15217
- continue;
15218
- const child = join10(dir, entry.name);
15219
- if (ignoredDirectory(child))
15220
- continue;
15221
- children.push(child);
15222
- }
15223
- return children;
15224
- };
15225
- const processInitialScanQueue = () => {
15226
- initialScanTimer = null;
15227
- if (watchers.size >= maxWatchedDirectories) {
15228
- reportWatchLimit();
15229
- initialScanQueue.length = 0;
15230
- return;
15231
- }
15232
- const next = initialScanQueue.shift();
15233
- if (next) {
15234
- processingInitialScan = true;
15235
- try {
15236
- watchDirectory(next, true);
15237
- } finally {
15238
- processingInitialScan = false;
15239
- }
15240
- }
15241
- if (watchers.size >= maxWatchedDirectories) {
15242
- reportWatchLimit();
15243
- initialScanQueue.length = 0;
15244
- }
15245
- if (initialScanQueue.length)
15246
- initialScanTimer = setTimer(processInitialScanQueue, 50);
15247
- };
15248
- const queueInitialChildren = (dir) => {
15249
- const remaining = maxWatchedDirectories - watchers.size;
15250
- if (remaining <= 0) {
15251
- reportWatchLimit();
15252
- return;
15253
- }
15254
- const children = readChildDirectories(dir);
15255
- if (children.length > remaining)
15256
- reportWatchLimit();
15257
- initialScanQueue.push(...children.slice(0, remaining));
15258
- if (!initialScanTimer && !processingInitialScan)
15259
- initialScanTimer = setTimer(processInitialScanQueue, 5000);
15260
- };
15261
- const processChangedPath = (changed, fullChangedPath) => {
15262
- const known = watchers.has(fullChangedPath);
15263
- if (isDirectory(fullChangedPath)) {
15264
- if (known) {
15265
- const signature = directorySignature(fullChangedPath);
15266
- if (signature && signature !== signatures.get(fullChangedPath)) {
15267
- closeSubtree(fullChangedPath);
15268
- watchDirectory(fullChangedPath, initialScanAsync);
15269
- }
15270
- scheduleUpdate(changed);
15271
- return;
15272
- }
15273
- watchDirectory(fullChangedPath, initialScanAsync);
15274
- } else if (known) {
15275
- closeSubtree(fullChangedPath);
15276
- }
15277
- scheduleUpdate(changed);
15278
- };
15279
- const processPathInspections = () => {
15280
- pathInspectionTimer = null;
15281
- const entries = [...pendingPathInspections];
15282
- pendingPathInspections.clear();
15283
- for (const [changed, fullChangedPath] of entries) {
15284
- processChangedPath(changed, fullChangedPath);
15285
- }
15286
- };
15287
- const queuePathInspection = (changed, fullChangedPath) => {
15288
- pendingPathInspections.set(changed, fullChangedPath);
15289
- if (!pathInspectionTimer)
15290
- pathInspectionTimer = setTimer(processPathInspections, 25);
15291
- };
15292
- const watchDirectory = (dir, initialScan = false) => {
15293
- if (watchers.has(dir))
15294
- return;
15295
- if (watchers.size >= maxWatchedDirectories) {
15296
- reportWatchLimit();
15297
- return;
15298
- }
15299
- const rel = directoryRelativePath(dir);
15300
- if (rel && ignored(rel))
15301
- return;
15302
- try {
15303
- const watcher = watch(dir, { persistent: false }, (_event, filename) => {
15304
- if (!filename) {
15305
- scheduleUpdate();
15306
- return;
15307
- }
15308
- const changed = normalizeRelativePath(join10(rel, filename.toString()));
15309
- if (ignored(changed))
15310
- return;
15311
- const fullChangedPath = join10(options.root, changed);
15312
- if (!isInsideRoot(options.root, fullChangedPath))
15313
- return;
15314
- if (initialScanAsync) {
15315
- queuePathInspection(changed, fullChangedPath);
15316
- return;
15317
- }
15318
- processChangedPath(changed, fullChangedPath);
15319
- }) || {};
15320
- watchers.set(dir, watcher);
15321
- const signature = directorySignature(dir);
15322
- if (signature)
15323
- signatures.set(dir, signature);
15324
- watcher.on?.("error", () => {
15325
- if (watchers.get(dir) === watcher) {
15326
- watchers.delete(dir);
15327
- signatures.delete(dir);
15328
- }
15329
- });
15330
- watcher.on?.("close", () => {
15331
- if (watchers.get(dir) === watcher) {
15332
- watchers.delete(dir);
15333
- signatures.delete(dir);
15334
- }
15335
- });
15336
- } catch (error) {
15337
- options.onError?.(error);
15338
- return;
15444
+ return true;
15445
+ if (value === "")
15446
+ return false;
15447
+ if (hasControlCharacter(value))
15448
+ return false;
15449
+ return /^[A-Za-z0-9_$.-]+$/.test(value);
15450
+ }
15451
+ function cloneSupabaseDiscoveryResult(result) {
15452
+ return result.map((entry) => ({ ...entry }));
15453
+ }
15454
+ function parseSupabaseConfigToml(content) {
15455
+ let section = null;
15456
+ let projectId = null;
15457
+ let dbPort = null;
15458
+ for (const rawLine of content.split(`
15459
+ `)) {
15460
+ const line = rawLine.trim();
15461
+ if (!line || line.startsWith("#"))
15462
+ continue;
15463
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
15464
+ if (sectionMatch) {
15465
+ section = sectionMatch[1];
15466
+ continue;
15339
15467
  }
15340
- if (initialScanAsync && initialScan) {
15341
- queueInitialChildren(dir);
15342
- return;
15468
+ const kvMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
15469
+ if (!kvMatch)
15470
+ continue;
15471
+ const value = stripScalarSyntax(kvMatch[2]);
15472
+ if (section === null && kvMatch[1] === "project_id") {
15473
+ projectId = value;
15474
+ } else if (section === "db" && kvMatch[1] === "port") {
15475
+ dbPort = value;
15343
15476
  }
15344
- if (watchers.size >= maxWatchedDirectories) {
15345
- reportWatchLimit();
15477
+ }
15478
+ if (!projectId || !isSafeDockerServiceName(projectId))
15479
+ return null;
15480
+ return {
15481
+ projectId,
15482
+ dbPort: dbPort && /^\d+$/.test(dbPort) ? dbPort : DEFAULT_SUPABASE_DB_PORT
15483
+ };
15484
+ }
15485
+ async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal) {
15486
+ const cacheKey = discoveryCacheKey(cwd, omitDirNames);
15487
+ const now = Date.now();
15488
+ const cached = supabaseDiscoveryCache.get(cacheKey);
15489
+ if (cached && cached.expiresAt > now) {
15490
+ return cloneSupabaseDiscoveryResult(cached.result);
15491
+ }
15492
+ const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
15493
+ omitSet.add(".git");
15494
+ omitSet.add("node_modules");
15495
+ const results = [];
15496
+ await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
15497
+ const configPath = join11(dir, "supabase", "config.toml");
15498
+ if (!await pathExistsAsync(configPath))
15346
15499
  return;
15500
+ try {
15501
+ const content = await readFile2(configPath, "utf-8");
15502
+ const parsed = parseSupabaseConfigToml(content);
15503
+ if (!parsed)
15504
+ return;
15505
+ const relDir = relative5(cwd, dir);
15506
+ const isRoot = relDir === "" || relDir === ".";
15507
+ const relDirSlash = relDir.replace(/\\/g, "/");
15508
+ const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
15509
+ const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
15510
+ results.push({
15511
+ id,
15512
+ path: isRoot ? "supabase/config.toml" : `${relDirSlash}/supabase/config.toml`,
15513
+ name: `${parsed.projectId} (Supabase CLI, postgres@127.0.0.1:${parsed.dbPort}/postgres${labelPath})`,
15514
+ sizeBytes: 0,
15515
+ kind: "postgresql",
15516
+ projectId: parsed.projectId,
15517
+ relDirSlash,
15518
+ dbPort: parsed.dbPort
15519
+ });
15520
+ } catch {}
15521
+ }, signal);
15522
+ if (signal?.aborted)
15523
+ return cloneSupabaseDiscoveryResult(results);
15524
+ supabaseDiscoveryCache.set(cacheKey, {
15525
+ expiresAt: now + SUPABASE_DISCOVERY_TTL_MS,
15526
+ result: cloneSupabaseDiscoveryResult(results)
15527
+ });
15528
+ return cloneSupabaseDiscoveryResult(results);
15529
+ }
15530
+ function parseSupabaseDbId(dbId) {
15531
+ if (!dbId.startsWith("supabase:"))
15532
+ return null;
15533
+ const rest = dbId.slice("supabase:".length);
15534
+ if (!rest)
15535
+ return null;
15536
+ const atIdx = rest.indexOf("@");
15537
+ let projectId;
15538
+ let relDir = "";
15539
+ if (atIdx >= 0) {
15540
+ if (rest.indexOf("@", atIdx + 1) >= 0)
15541
+ return null;
15542
+ projectId = rest.slice(0, atIdx);
15543
+ try {
15544
+ relDir = decodeURIComponent(rest.slice(atIdx + 1));
15545
+ } catch {
15546
+ return null;
15347
15547
  }
15348
- for (const child of readChildDirectories(dir))
15349
- watchDirectory(child);
15350
- };
15351
- watchDirectory(options.root, true);
15352
- return { started: watchers.size > 0, close: closeAll };
15548
+ if (!isSafeDockerRelDir(relDir))
15549
+ return null;
15550
+ } else {
15551
+ projectId = rest;
15552
+ }
15553
+ if (!isSafeDockerServiceName(projectId))
15554
+ return null;
15555
+ return { projectId, relDir };
15353
15556
  }
15354
- var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
15355
- var init_worktree_watcher = __esm(() => {
15356
- init_search();
15557
+ async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal) {
15558
+ const parsed = parseSupabaseDbId(dbId);
15559
+ if (!parsed)
15560
+ return null;
15561
+ const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
15562
+ return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
15563
+ }
15564
+ 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;
15565
+ var init_discovery = __esm(() => {
15566
+ SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
15567
+ sqliteDiscoveryCache = new Map;
15568
+ COMPOSE_FILENAMES = [
15569
+ "docker-compose.yml",
15570
+ "docker-compose.yaml",
15571
+ "compose.yml",
15572
+ "compose.yaml"
15573
+ ];
15574
+ dockerDiscoveryCache = new Map;
15575
+ DB_KIND_VALUES = new Set([
15576
+ "sqlite",
15577
+ "postgresql",
15578
+ "mysql",
15579
+ "redis",
15580
+ "elasticsearch",
15581
+ "s3",
15582
+ "dynamodb"
15583
+ ]);
15584
+ supabaseDiscoveryCache = new Map;
15357
15585
  });
15358
15586
 
15359
15587
  // web-src/server/state-store.ts
15360
- import { join as join11 } from "node:path";
15588
+ import { join as join12 } from "node:path";
15361
15589
  function codeViewerPath(root, fileName) {
15362
- return join11(root, CODE_VIEWER_DIR2, fileName);
15590
+ return join12(root, CODE_VIEWER_DIR2, fileName);
15363
15591
  }
15364
15592
  function isRecord(value) {
15365
15593
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -15807,35 +16035,105 @@ function mergeDbUiState(current, patch) {
15807
16035
  version: 1
15808
16036
  });
15809
16037
  }
16038
+ function emptyToolsState() {
16039
+ return { version: 1 };
16040
+ }
16041
+ function sanitizeToolsState(raw) {
16042
+ if (!isRecord(raw))
16043
+ return emptyToolsState();
16044
+ const out = { version: 1 };
16045
+ if (isToolId(raw.activeTool))
16046
+ out.activeTool = raw.activeTool;
16047
+ const width = optionalNumber(raw.width, MIN_TOOLS_SHEET_WIDTH, MAX_TOOLS_SHEET_WIDTH);
16048
+ if (width !== undefined)
16049
+ out.width = width;
16050
+ if (isRecord(raw.drafts)) {
16051
+ const drafts = {};
16052
+ for (const id of TOOL_IDS) {
16053
+ const draft = raw.drafts[id];
16054
+ if (typeof draft !== "string")
16055
+ continue;
16056
+ if (draft.length === 0 || draft.length > MAX_TOOL_DRAFT_LEN)
16057
+ continue;
16058
+ drafts[id] = draft;
16059
+ }
16060
+ if (Object.keys(drafts).length > 0)
16061
+ out.drafts = drafts;
16062
+ }
16063
+ return out;
16064
+ }
16065
+ function mergeToolsState(current, patch) {
16066
+ if (!isRecord(patch))
16067
+ return current;
16068
+ const next = { ...current, version: 1 };
16069
+ if ("activeTool" in patch) {
16070
+ if (patch.activeTool === null)
16071
+ delete next.activeTool;
16072
+ else if (isToolId(patch.activeTool))
16073
+ next.activeTool = patch.activeTool;
16074
+ }
16075
+ if ("width" in patch) {
16076
+ if (patch.width === null)
16077
+ delete next.width;
16078
+ else {
16079
+ const width = optionalNumber(patch.width, MIN_TOOLS_SHEET_WIDTH, MAX_TOOLS_SHEET_WIDTH);
16080
+ if (width !== undefined)
16081
+ next.width = width;
16082
+ }
16083
+ }
16084
+ if (isRecord(patch.drafts)) {
16085
+ const drafts = { ...next.drafts ?? {} };
16086
+ for (const id of TOOL_IDS) {
16087
+ if (!(id in patch.drafts))
16088
+ continue;
16089
+ const value = patch.drafts[id];
16090
+ if (value === null || value === "") {
16091
+ delete drafts[id];
16092
+ continue;
16093
+ }
16094
+ if (typeof value !== "string")
16095
+ continue;
16096
+ if (value.length > MAX_TOOL_DRAFT_LEN)
16097
+ throw new Error("tools state too large");
16098
+ drafts[id] = value;
16099
+ }
16100
+ next.drafts = drafts;
16101
+ }
16102
+ return sanitizeToolsState(next);
16103
+ }
16104
+ function patchJsonState(store, root, patch, merge) {
16105
+ return store.update(root, (state) => {
16106
+ const next = merge(state, patch);
16107
+ return { state: next, result: next };
16108
+ });
16109
+ }
15810
16110
  async function loadAppSettingsState(root) {
15811
16111
  return settingsStore.load(root);
15812
16112
  }
15813
16113
  async function patchAppSettingsState(root, patch) {
15814
- return settingsStore.update(root, (state) => {
15815
- const next = mergeSettings(state, patch);
15816
- return { state: next, result: next };
15817
- });
16114
+ return patchJsonState(settingsStore, root, patch, mergeSettings);
15818
16115
  }
15819
16116
  async function loadViewState(root) {
15820
16117
  return viewStateStore.load(root);
15821
16118
  }
15822
16119
  async function patchViewState(root, patch) {
15823
- return viewStateStore.update(root, (state) => {
15824
- const next = mergeViewState(state, patch);
15825
- return { state: next, result: next };
15826
- });
16120
+ return patchJsonState(viewStateStore, root, patch, mergeViewState);
15827
16121
  }
15828
16122
  async function loadDbUiState(root) {
15829
16123
  return dbUiStore.load(root);
15830
16124
  }
15831
16125
  async function patchDbUiState(root, patch) {
15832
- return dbUiStore.update(root, (state) => {
15833
- const next = mergeDbUiState(state, patch);
15834
- return { state: next, result: next };
15835
- });
16126
+ return patchJsonState(dbUiStore, root, patch, mergeDbUiState);
15836
16127
  }
15837
- var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
16128
+ async function loadToolsState(root) {
16129
+ return toolsStore.load(root);
16130
+ }
16131
+ async function patchToolsState(root, patch) {
16132
+ return patchJsonState(toolsStore, root, patch, mergeToolsState);
16133
+ }
16134
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", TOOLS_FILE_NAME = "tools.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_TOOL_DRAFT_LEN = 200000, MAX_TOOLS_BYTES = 4000000, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore, toolsStore;
15838
16135
  var init_state_store = __esm(() => {
16136
+ init_tools();
15839
16137
  init_json_store();
15840
16138
  init_worktree_watcher();
15841
16139
  DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
@@ -15863,6 +16161,14 @@ var init_state_store = __esm(() => {
15863
16161
  backupSuffix: "corrupt",
15864
16162
  sizeErrorMessage: "db UI state too large"
15865
16163
  });
16164
+ toolsStore = createJsonFileStore({
16165
+ filePath: (root) => codeViewerPath(root, TOOLS_FILE_NAME),
16166
+ empty: emptyToolsState,
16167
+ sanitize: sanitizeToolsState,
16168
+ maxBytes: MAX_TOOLS_BYTES,
16169
+ backupSuffix: "corrupt",
16170
+ sizeErrorMessage: "tools state too large"
16171
+ });
15866
16172
  });
15867
16173
 
15868
16174
  // web-src/server/database/adapters/async-facade.ts
@@ -16239,7 +16545,7 @@ var init_d1 = __esm(() => {
16239
16545
 
16240
16546
  // web-src/server/database/adapters/dynamodb.ts
16241
16547
  import { spawnSync as spawnSync5 } from "node:child_process";
16242
- import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
16548
+ import { createHash as createHash6, createHmac as createHmac2 } from "node:crypto";
16243
16549
  function createDynamoDbRequestDeadline() {
16244
16550
  const timeoutMs = dynamoDbRequestTimeoutMs;
16245
16551
  return { expiresAt: Date.now() + timeoutMs, timeoutMs };
@@ -16263,7 +16569,7 @@ function hmac2(key, value) {
16263
16569
  return createHmac2("sha256", key).update(value, "utf8").digest();
16264
16570
  }
16265
16571
  function sha2562(value) {
16266
- return createHash5("sha256").update(value, "utf8").digest("hex");
16572
+ return createHash6("sha256").update(value, "utf8").digest("hex");
16267
16573
  }
16268
16574
  function amzDate2(date = new Date) {
16269
16575
  const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
@@ -16908,7 +17214,7 @@ var init_credential_store = __esm(() => {
16908
17214
  // web-src/server/database/connections-store.ts
16909
17215
  import { randomUUID } from "node:crypto";
16910
17216
  import { chmod } from "node:fs/promises";
16911
- import { join as join12 } from "node:path";
17217
+ import { join as join13 } from "node:path";
16912
17218
  function secretKey(cwd, id) {
16913
17219
  return `${cwd}\x00${id}`;
16914
17220
  }
@@ -16959,7 +17265,7 @@ function withRuntimeSecrets(cwd, connection) {
16959
17265
  };
16960
17266
  }
16961
17267
  function connectionsFilePath(root) {
16962
- return join12(root, ".code-viewer", CONNECTIONS_FILE_NAME);
17268
+ return join13(root, ".code-viewer", CONNECTIONS_FILE_NAME);
16963
17269
  }
16964
17270
  function emptyState() {
16965
17271
  return { version: 1, connections: [] };
@@ -18771,9 +19077,9 @@ var init_handle_s3 = __esm(() => {
18771
19077
  });
18772
19078
 
18773
19079
  // web-src/server/database/query-history.ts
18774
- import { join as join13 } from "node:path";
19080
+ import { join as join14 } from "node:path";
18775
19081
  function historyFilePath(root) {
18776
- return join13(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
19082
+ return join14(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18777
19083
  }
18778
19084
  function emptyState2() {
18779
19085
  return { version: 1, entries: [] };
@@ -18921,11 +19227,11 @@ var init_query_history = __esm(() => {
18921
19227
  });
18922
19228
 
18923
19229
  // web-src/server/database/snapshot-store.ts
18924
- import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
19230
+ import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
18925
19231
  import { mkdirSync as mkdirSync3 } from "node:fs";
18926
- import { join as join14 } from "node:path";
19232
+ import { join as join15 } from "node:path";
18927
19233
  async function getStoreDb(cwd) {
18928
- const dbPath = join14(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
19234
+ const dbPath = join15(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
18929
19235
  if (storeDb && storeDbPath === dbPath)
18930
19236
  return storeDb;
18931
19237
  if (storeDb) {
@@ -18933,7 +19239,7 @@ async function getStoreDb(cwd) {
18933
19239
  storeDb.close();
18934
19240
  } catch {}
18935
19241
  }
18936
- mkdirSync3(join14(cwd, CODE_VIEWER_DIR4), { recursive: true });
19242
+ mkdirSync3(join15(cwd, CODE_VIEWER_DIR4), { recursive: true });
18937
19243
  const DbClass = await loadSqliteClass();
18938
19244
  storeDb = new DbClass(dbPath);
18939
19245
  storeDbPath = dbPath;
@@ -18952,7 +19258,7 @@ function makeId2(prefix) {
18952
19258
  return `${prefix}-${randomBytes2(8).toString("hex")}`;
18953
19259
  }
18954
19260
  function hashPayload(payloadJson) {
18955
- return createHash6("sha256").update(payloadJson).digest("hex");
19261
+ return createHash7("sha256").update(payloadJson).digest("hex");
18956
19262
  }
18957
19263
  function hashLengthPrefixed(hasher, value) {
18958
19264
  hasher.update(`${Buffer.byteLength(value, "utf8")}:`);
@@ -19027,7 +19333,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
19027
19333
  db.exec("BEGIN");
19028
19334
  try {
19029
19335
  for (const row of rows) {
19030
- const rowKeyHash = createHash6("sha256").update(row.rowKeyJson).digest("hex");
19336
+ const rowKeyHash = createHash7("sha256").update(row.rowKeyJson).digest("hex");
19031
19337
  const payloadHash = hashPayload(row.payloadJson);
19032
19338
  insertRow.run(revisionId, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
19033
19339
  insertPayload.run(payloadHash, row.payloadJson);
@@ -19041,7 +19347,7 @@ async function addSnapshotTableRows(cwd, revisionId, rows) {
19041
19347
  }
19042
19348
  }
19043
19349
  function computeRevisionTableHash(db, revisionId) {
19044
- const hasher = createHash6("sha256");
19350
+ const hasher = createHash7("sha256");
19045
19351
  hashLengthPrefixed(hasher, `snapshot-table-v${SNAPSHOT_TABLE_HASH_VERSION}`);
19046
19352
  let rowCount = 0;
19047
19353
  let last;
@@ -19539,9 +19845,9 @@ var init_snapshot_runner = __esm(() => {
19539
19845
  });
19540
19846
 
19541
19847
  // web-src/server/database/tabs-store.ts
19542
- import { join as join15 } from "node:path";
19848
+ import { join as join16 } from "node:path";
19543
19849
  function tabsFilePath(root) {
19544
- return join15(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19850
+ return join16(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19545
19851
  }
19546
19852
  function emptyState3() {
19547
19853
  return { version: 1, tabs: [], activeTabId: null };
@@ -21495,7 +21801,7 @@ var init_handle = __esm(() => {
21495
21801
 
21496
21802
  // web-src/server/doctor.ts
21497
21803
  import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync6 } from "node:fs";
21498
- import { dirname as dirname5, join as join16, relative as relative6 } from "node:path";
21804
+ import { dirname as dirname5, join as join17, relative as relative6 } from "node:path";
21499
21805
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21500
21806
  function statusWorse(a, b) {
21501
21807
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -21589,7 +21895,7 @@ function findCodeViewerPackageJson() {
21589
21895
  cursor = dirname5(process.argv[1] || ".");
21590
21896
  }
21591
21897
  for (let depth = 0;depth < 8; depth += 1) {
21592
- const candidate = join16(cursor, "package.json");
21898
+ const candidate = join17(cursor, "package.json");
21593
21899
  try {
21594
21900
  const raw = readFileSync6(candidate, "utf8");
21595
21901
  const pkg = JSON.parse(raw);
@@ -21685,7 +21991,7 @@ async function checkSqlite(cwd) {
21685
21991
  return { id: "sqlite", title: "SQLite driver", rows };
21686
21992
  }
21687
21993
  async function trySnapshotDbOpen(cwd) {
21688
- const dbPath = join16(cwd, SNAPSHOT_DB_REL);
21994
+ const dbPath = join17(cwd, SNAPSHOT_DB_REL);
21689
21995
  try {
21690
21996
  statSync6(dbPath);
21691
21997
  } catch {
@@ -21706,7 +22012,7 @@ async function trySnapshotDbOpen(cwd) {
21706
22012
  }
21707
22013
  }
21708
22014
  function checkSnapshotStore(cwd) {
21709
- const dbPath = join16(cwd, SNAPSHOT_DB_REL);
22015
+ const dbPath = join17(cwd, SNAPSHOT_DB_REL);
21710
22016
  const dir = dirname5(dbPath);
21711
22017
  let dirStatus = "ok";
21712
22018
  let dirDetail = dir;
@@ -22725,12 +23031,12 @@ function startDevAssetReload(options) {
22725
23031
  var init_dev_assets = () => {};
22726
23032
 
22727
23033
  // web-src/server/journal.ts
22728
- import { join as join17 } from "node:path";
23034
+ import { join as join18 } from "node:path";
22729
23035
  function dailyJournalFilePath(root) {
22730
- return join17(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
23036
+ return join18(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
22731
23037
  }
22732
23038
  function journalTasksFilePath(root) {
22733
- return join17(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
23039
+ return join18(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
22734
23040
  }
22735
23041
  function emptyDailyJournalState() {
22736
23042
  return { version: 1, entries: [] };
@@ -23343,7 +23649,7 @@ var init_journal2 = __esm(() => {
23343
23649
  // web-src/server/search-service.ts
23344
23650
  import { existsSync as existsSync7, realpathSync as realpathSync6 } from "node:fs";
23345
23651
  import { lstat as lstat2, readFile as readFile3 } from "node:fs/promises";
23346
- import { join as join18, relative as relative7 } from "node:path";
23652
+ import { join as join19, relative as relative7 } from "node:path";
23347
23653
  async function rgAvailableAsync(cwd) {
23348
23654
  if (rgAvailableCache !== null)
23349
23655
  return rgAvailableCache;
@@ -23373,7 +23679,7 @@ function safeWorktreePath(env, path) {
23373
23679
  return null;
23374
23680
  if (isGitInternalPath(path))
23375
23681
  return null;
23376
- const full = join18(env.cwd, path);
23682
+ const full = join19(env.cwd, path);
23377
23683
  if (!existsSync7(full))
23378
23684
  return null;
23379
23685
  let realCwd;
@@ -23566,7 +23872,7 @@ var init_search_service = __esm(() => {
23566
23872
 
23567
23873
  // web-src/server/mcp.ts
23568
23874
  import { readFileSync as readFileSync7 } from "node:fs";
23569
- import { join as join19 } from "node:path";
23875
+ import { join as join20 } from "node:path";
23570
23876
  function defaultMcpTools(options = {}) {
23571
23877
  return [
23572
23878
  {
@@ -25040,7 +25346,7 @@ var init_mcp = __esm(() => {
25040
25346
  init_search_cli();
25041
25347
  init_search_service();
25042
25348
  init_status_cli();
25043
- PACKAGE_VERSION = JSON.parse(readFileSync7(join19(ROOT, "package.json"), "utf8")).version;
25349
+ PACKAGE_VERSION = JSON.parse(readFileSync7(join20(ROOT, "package.json"), "utf8")).version;
25044
25350
  MCP_SERVER_INFO = {
25045
25351
  name: "code-viewer",
25046
25352
  title: "code-viewer",
@@ -25048,55 +25354,219 @@ var init_mcp = __esm(() => {
25048
25354
  };
25049
25355
  });
25050
25356
 
25357
+ // web-src/server/watch-supervisor.ts
25358
+ import { spawn as spawn3 } from "node:child_process";
25359
+ import { join as join21 } from "node:path";
25360
+ function watchChildCommand() {
25361
+ const entry = process.argv[1] ?? "";
25362
+ const script = entry.endsWith(".ts") ? join21(import.meta.dir, "cli.ts") : entry;
25363
+ return [process.argv[0], script, "watch-child"];
25364
+ }
25365
+ function startWatchSupervisor(options) {
25366
+ const spawnChild = options.spawnFn || spawn3;
25367
+ const now = options.nowFn || Date.now;
25368
+ const setTimer = options.setTimeoutFn || setTimeout;
25369
+ const setRepeating = options.setIntervalFn || setInterval;
25370
+ const clearRepeating = options.clearIntervalFn || clearInterval;
25371
+ const heartbeatMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
25372
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_MS;
25373
+ const command = options.command ?? watchChildCommand();
25374
+ let child = null;
25375
+ let closed = false;
25376
+ let watchFailures = 0;
25377
+ let pollOnly = false;
25378
+ let sawReady = false;
25379
+ let lastSignalAt = now();
25380
+ let watchdog = null;
25381
+ const report = (error) => options.onError?.(error);
25382
+ const abandon = (victim) => {
25383
+ victim.removeAllListeners?.();
25384
+ victim.stdout?.removeAllListeners?.();
25385
+ victim.stderr?.removeAllListeners?.();
25386
+ try {
25387
+ victim.kill("SIGTERM");
25388
+ } catch {}
25389
+ const grace = setTimer(() => {
25390
+ try {
25391
+ victim.kill("SIGKILL");
25392
+ } catch {}
25393
+ }, KILL_GRACE_MS);
25394
+ grace.unref?.();
25395
+ };
25396
+ const handleMessage = (message) => {
25397
+ lastSignalAt = now();
25398
+ if (message.type === "heartbeat")
25399
+ return;
25400
+ if (message.type === "ready") {
25401
+ if (message.pollOnly || message.watching) {
25402
+ sawReady = true;
25403
+ watchFailures = 0;
25404
+ } else {
25405
+ watchFailures++;
25406
+ }
25407
+ return;
25408
+ }
25409
+ if (message.type === "update") {
25410
+ options.onUpdate(message.paths);
25411
+ return;
25412
+ }
25413
+ if (message.type === "watch-limit") {
25414
+ options.onWatchLimit?.(message.limit);
25415
+ return;
25416
+ }
25417
+ if (message.type === "warn")
25418
+ report(new Error(message.message));
25419
+ };
25420
+ const start = () => {
25421
+ if (closed)
25422
+ return;
25423
+ if (!pollOnly && watchFailures >= WATCH_FAILURES_BEFORE_POLL_ONLY) {
25424
+ pollOnly = true;
25425
+ options.onPollOnly?.();
25426
+ }
25427
+ const config = {
25428
+ root: options.root,
25429
+ omitDirNames: options.omitDirNames,
25430
+ excludeNames: options.excludeNames,
25431
+ maxWatchedDirectories: options.maxWatchedDirectories,
25432
+ debounceMs: options.debounceMs ?? 250,
25433
+ pollIntervalMs,
25434
+ heartbeatIntervalMs: heartbeatMs,
25435
+ parentPid: process.pid,
25436
+ pollOnly
25437
+ };
25438
+ let spawned;
25439
+ try {
25440
+ spawned = spawnChild(command[0], command.slice(1), {
25441
+ cwd: options.root,
25442
+ stdio: ["pipe", "pipe", "pipe"]
25443
+ });
25444
+ } catch (error) {
25445
+ report(error);
25446
+ return;
25447
+ }
25448
+ child = spawned;
25449
+ sawReady = false;
25450
+ lastSignalAt = now();
25451
+ try {
25452
+ spawned.stdin?.write(`${JSON.stringify(config)}
25453
+ `);
25454
+ } catch (error) {
25455
+ report(error);
25456
+ }
25457
+ let buffer = "";
25458
+ spawned.stdout?.setEncoding?.("utf8");
25459
+ spawned.stdout?.on("data", (chunk) => {
25460
+ buffer += chunk;
25461
+ let newline = buffer.indexOf(`
25462
+ `);
25463
+ while (newline !== -1) {
25464
+ const line = buffer.slice(0, newline).trim();
25465
+ buffer = buffer.slice(newline + 1);
25466
+ if (line) {
25467
+ try {
25468
+ handleMessage(JSON.parse(line));
25469
+ } catch {}
25470
+ }
25471
+ newline = buffer.indexOf(`
25472
+ `);
25473
+ }
25474
+ });
25475
+ spawned.stderr?.setEncoding?.("utf8");
25476
+ spawned.stderr?.on("data", (chunk) => {
25477
+ const text = String(chunk).trim();
25478
+ if (text)
25479
+ console.warn(`[code-viewer] watch child: ${text}`);
25480
+ });
25481
+ spawned.on("error", (error) => report(error));
25482
+ spawned.on("exit", () => {
25483
+ if (closed || child !== spawned)
25484
+ return;
25485
+ if (!sawReady)
25486
+ watchFailures++;
25487
+ child = null;
25488
+ const retry = setTimer(start, heartbeatMs);
25489
+ retry.unref?.();
25490
+ });
25491
+ };
25492
+ start();
25493
+ watchdog = setRepeating(() => {
25494
+ if (closed || !child)
25495
+ return;
25496
+ if (now() - lastSignalAt < heartbeatMs * HEARTBEAT_MISS_LIMIT)
25497
+ return;
25498
+ const victim = child;
25499
+ child = null;
25500
+ watchFailures++;
25501
+ report(new Error("watch child stopped reporting; restarting"));
25502
+ abandon(victim);
25503
+ start();
25504
+ }, heartbeatMs);
25505
+ watchdog.unref?.();
25506
+ return {
25507
+ close: () => {
25508
+ closed = true;
25509
+ if (watchdog)
25510
+ clearRepeating(watchdog);
25511
+ watchdog = null;
25512
+ const victim = child;
25513
+ child = null;
25514
+ if (victim)
25515
+ abandon(victim);
25516
+ },
25517
+ pollOnly: () => pollOnly
25518
+ };
25519
+ }
25520
+ var DEFAULT_HEARTBEAT_MS = 5000, DEFAULT_POLL_MS = 15000, HEARTBEAT_MISS_LIMIT = 3, KILL_GRACE_MS = 2000, WATCH_FAILURES_BEFORE_POLL_ONLY = 2;
25521
+ var init_watch_supervisor = () => {};
25522
+
25051
25523
  // web-src/server/state-route.ts
25052
25524
  var exports_state_route = {};
25053
25525
  __export(exports_state_route, {
25054
25526
  handleStateRoute: () => handleStateRoute
25055
25527
  });
25056
- async function parseJsonBody(req) {
25057
- return parseBoundedJsonBody(req, MAX_STATE_PATCH_BODY_BYTES, "state body too large");
25528
+ async function parseJsonBody(req, maxBytes) {
25529
+ return parseBoundedJsonBody(req, maxBytes, "state body too large");
25058
25530
  }
25059
- async function handleSettingsGet(cwd) {
25060
- return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
25061
- }
25062
- async function handleSettingsPatch(cwd, req, onChange) {
25063
- const body = await parseJsonBody(req);
25531
+ async function handleStatePatch(cwd, req, patchState, tooLargeMessage, saveFailedMessage, maxBodyBytes, onChange) {
25532
+ const body = await parseJsonBody(req, maxBodyBytes);
25064
25533
  if (body instanceof Response)
25065
25534
  return body;
25066
25535
  try {
25067
- const next = await patchAppSettingsState(cwd, body);
25536
+ const next = await patchState(cwd, body);
25068
25537
  if (onChange) {
25069
25538
  try {
25070
25539
  onChange(next);
25071
25540
  } catch (notifyErr) {
25072
- console.warn("[code-viewer] settings change notify failed:", notifyErr);
25541
+ console.warn("[code-viewer] state change notify failed:", notifyErr);
25073
25542
  }
25074
25543
  }
25075
25544
  return json(next);
25076
25545
  } catch (err) {
25077
25546
  const message = err instanceof Error ? err.message : String(err);
25078
- if (message === "settings state too large")
25547
+ if (message === tooLargeMessage)
25079
25548
  return textError(message, 413);
25080
25549
  console.error("[code-viewer] state error:", err);
25081
- return textError("failed to save settings state", 500);
25550
+ return textError(saveFailedMessage, 500);
25082
25551
  }
25083
25552
  }
25553
+ async function handleSettingsGet(cwd) {
25554
+ return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
25555
+ }
25556
+ async function handleSettingsPatch(cwd, req, onChange) {
25557
+ return handleStatePatch(cwd, req, patchAppSettingsState, "settings state too large", "failed to save settings state", MAX_STATE_PATCH_BODY_BYTES, onChange);
25558
+ }
25084
25559
  async function handleViewGet(cwd) {
25085
25560
  return jsonLoadResponse(() => loadViewState(cwd), "state", "failed to load view state");
25086
25561
  }
25087
25562
  async function handleViewPatch(cwd, req) {
25088
- const body = await parseJsonBody(req);
25089
- if (body instanceof Response)
25090
- return body;
25091
- try {
25092
- return json(await patchViewState(cwd, body));
25093
- } catch (err) {
25094
- const message = err instanceof Error ? err.message : String(err);
25095
- if (message === "view state too large")
25096
- return textError(message, 413);
25097
- console.error("[code-viewer] state error:", err);
25098
- return textError("failed to save view state", 500);
25099
- }
25563
+ return handleStatePatch(cwd, req, patchViewState, "view state too large", "failed to save view state", MAX_STATE_PATCH_BODY_BYTES);
25564
+ }
25565
+ async function handleToolsGet(cwd) {
25566
+ return jsonLoadResponse(() => loadToolsState(cwd), "state", "failed to load tools state");
25567
+ }
25568
+ async function handleToolsPatch(cwd, req) {
25569
+ return handleStatePatch(cwd, req, patchToolsState, "tools state too large", "failed to save tools state", MAX_TOOLS_PATCH_BODY_BYTES);
25100
25570
  }
25101
25571
  async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {}) {
25102
25572
  return dispatchRoutes(req, url, {
@@ -25109,10 +25579,15 @@ async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {})
25109
25579
  methods: ["GET", "PATCH"],
25110
25580
  sideEffect: (method) => method !== "GET",
25111
25581
  handler: () => req.method === "GET" ? handleViewGet(cwd) : handleViewPatch(cwd, req)
25582
+ },
25583
+ "/_state/tools": {
25584
+ methods: ["GET", "PATCH"],
25585
+ sideEffect: (method) => method !== "GET",
25586
+ handler: () => req.method === "GET" ? handleToolsGet(cwd) : handleToolsPatch(cwd, req)
25112
25587
  }
25113
25588
  }, sideEffectAllowed, (res) => res, (err) => handleError("state", "handle state request", err));
25114
25589
  }
25115
- var MAX_STATE_PATCH_BODY_BYTES = 1e6;
25590
+ var MAX_STATE_PATCH_BODY_BYTES = 1e6, MAX_TOOLS_PATCH_BODY_BYTES = 4000000;
25116
25591
  var init_state_route = __esm(() => {
25117
25592
  init_handle_shared();
25118
25593
  init_state_store();
@@ -25124,7 +25599,7 @@ import {
25124
25599
  closeSync as closeSync2,
25125
25600
  constants as constants3,
25126
25601
  existsSync as existsSync8,
25127
- lstatSync as lstatSync4,
25602
+ lstatSync as lstatSync5,
25128
25603
  mkdirSync as mkdirSync4,
25129
25604
  openSync as openSync2,
25130
25605
  readFileSync as readFileSync8,
@@ -25136,7 +25611,7 @@ import {
25136
25611
  writeFileSync as writeFileSync2
25137
25612
  } from "node:fs";
25138
25613
  import { homedir as homedir3 } from "node:os";
25139
- import { basename as basename3, dirname as dirname6, extname as extname2, join as join20, relative as relative8 } from "node:path";
25614
+ import { basename as basename3, dirname as dirname6, extname as extname2, join as join22, relative as relative8 } from "node:path";
25140
25615
  function parseCli() {
25141
25616
  const rest = [];
25142
25617
  for (let i = 2;i < process.argv.length; i++) {
@@ -25258,7 +25733,7 @@ Examples:
25258
25733
  }
25259
25734
  function warnIfLegacyConfigPresent() {
25260
25735
  try {
25261
- if (existsSync8(join20(cwd, ".code-viewer.json"))) {
25736
+ if (existsSync8(join22(cwd, ".code-viewer.json"))) {
25262
25737
  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.");
25263
25738
  }
25264
25739
  } catch {}
@@ -25338,6 +25813,7 @@ function staticFile(pathname) {
25338
25813
  "/app.js": ["app.js", "application/javascript; charset=utf-8"],
25339
25814
  "/mermaid.js": ["mermaid.js", "application/javascript; charset=utf-8"],
25340
25815
  "/shiki.js": ["shiki.js", "application/javascript; charset=utf-8"],
25816
+ "/yaml.js": ["yaml.js", "application/javascript; charset=utf-8"],
25341
25817
  "/vendor/diff2html/diff2html.min.css": [
25342
25818
  "vendor/diff2html/diff2html.min.css",
25343
25819
  "text/css; charset=utf-8"
@@ -25365,7 +25841,7 @@ function staticFile(pathname) {
25365
25841
  const spec = map[pathname];
25366
25842
  if (!spec)
25367
25843
  return null;
25368
- const full = join20(WEB_ROOT, spec[0]);
25844
+ const full = join22(WEB_ROOT, spec[0]);
25369
25845
  if (!existsSync8(full))
25370
25846
  return text("not found", 404);
25371
25847
  return new Response(readFileSync8(full), {
@@ -25623,7 +26099,7 @@ function safeWorktreePath2(path) {
25623
26099
  return safeWorktreePath(currentSearchEnv(), path);
25624
26100
  }
25625
26101
  function worktreePath(path) {
25626
- return join20(cwd, path);
26102
+ return join22(cwd, path);
25627
26103
  }
25628
26104
  function safeOpenWorktreePath(path) {
25629
26105
  if (path === "") {
@@ -25814,7 +26290,8 @@ async function handleSettings() {
25814
26290
  watch_limit_effective: scopeWatchLimit,
25815
26291
  watch_limit_default: DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT,
25816
26292
  watch_limit_min: MIN_WORKTREE_WATCH_DIRECTORY_LIMIT,
25817
- watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT
26293
+ watch_limit_max: MAX_WORKTREE_WATCH_DIRECTORY_LIMIT,
26294
+ watch_recursive: supportsNativeRecursiveWatch(process.platform)
25818
26295
  }
25819
26296
  });
25820
26297
  }
@@ -25957,7 +26434,7 @@ async function handleLog(url) {
25957
26434
  }
25958
26435
  function blamePathKey(p) {
25959
26436
  try {
25960
- const st = statSync7(join20(cwd, p));
26437
+ const st = statSync7(join22(cwd, p));
25961
26438
  return `${st.mtimeMs}:${st.size}`;
25962
26439
  } catch {
25963
26440
  return "missing";
@@ -26496,7 +26973,7 @@ async function handleUploadFiles(req) {
26496
26973
  total += file.size;
26497
26974
  if (total > MAX_UPLOAD_TOTAL_BYTES)
26498
26975
  return text("upload too large", 413);
26499
- const target = join20(realDir, safeName);
26976
+ const target = join22(realDir, safeName);
26500
26977
  if (relative8(realDir, dirname6(target)) !== "")
26501
26978
  return text("invalid filename", 400);
26502
26979
  if (existsSync8(target))
@@ -26618,9 +27095,9 @@ function triggerUpdate(changedPaths) {
26618
27095
  sendSse("update", data);
26619
27096
  }
26620
27097
  function moveMacPathIntoTrash(path) {
26621
- const trashDir = join20(homedir3(), ".Trash");
27098
+ const trashDir = join22(homedir3(), ".Trash");
26622
27099
  const base = basename3(path) || "code-viewer-trash-item";
26623
- const target = join20(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
27100
+ const target = join22(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
26624
27101
  try {
26625
27102
  mkdirSync4(trashDir, { recursive: true });
26626
27103
  renameSync(path, target);
@@ -26630,7 +27107,7 @@ function moveMacPathIntoTrash(path) {
26630
27107
  }
26631
27108
  }
26632
27109
  async function movePathToTrash(path) {
26633
- lstatSync4(path);
27110
+ lstatSync5(path);
26634
27111
  if (process.platform === "darwin") {
26635
27112
  return moveMacPathIntoTrash(path);
26636
27113
  }
@@ -26662,7 +27139,7 @@ async function restoreTrashPath(originalPath, trashPath) {
26662
27139
  if (!existsSync8(trashPath))
26663
27140
  return { ok: false, error: "trash item not found" };
26664
27141
  try {
26665
- const trashRoot = join20(homedir3(), ".Trash");
27142
+ const trashRoot = join22(homedir3(), ".Trash");
26666
27143
  const trashRelative = relative8(trashRoot, trashPath);
26667
27144
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
26668
27145
  return { ok: false, error: "invalid trash handle" };
@@ -26818,7 +27295,7 @@ async function handleCreateDirectory(req) {
26818
27295
  const targetPath = dir ? `${dir}/${name}` : name;
26819
27296
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
26820
27297
  return text("invalid target", 400);
26821
- const target = join20(parent, name);
27298
+ const target = join22(parent, name);
26822
27299
  if (existsSync8(target))
26823
27300
  return text("already exists", 409);
26824
27301
  try {
@@ -27367,18 +27844,19 @@ async function shutdown(exitCode = 0) {
27367
27844
  }
27368
27845
  function startScopedWorktreeWatch() {
27369
27846
  watchLimitReached = null;
27370
- return startWorktreeUpdateWatch({
27847
+ return startWatchSupervisor({
27371
27848
  root: cwd,
27372
27849
  omitDirNames: scopeOmitDirNames,
27373
27850
  excludeNames: scopeExcludeNames,
27374
- watch,
27375
- initialScanMode: "async",
27376
27851
  maxWatchedDirectories: scopeWatchLimit,
27377
27852
  onUpdate: triggerUpdate,
27378
27853
  onWatchLimit: (limit) => {
27379
27854
  watchLimitReached = limit;
27380
27855
  sendSse("watch-limit", String(limit));
27381
27856
  },
27857
+ onPollOnly: () => {
27858
+ console.warn("[code-viewer] file watching is unavailable; updates now come from periodic polling");
27859
+ },
27382
27860
  onError: (error) => {
27383
27861
  const message = error instanceof Error ? error.message : String(error);
27384
27862
  console.warn(`code-viewer worktree watch skipped: ${message}`);
@@ -27421,9 +27899,10 @@ var init_preview = __esm(async () => {
27421
27899
  init_search_service();
27422
27900
  init_server_registry();
27423
27901
  init_state_store();
27902
+ init_watch_supervisor();
27424
27903
  init_worktree_watcher();
27425
- WEB_ROOT = join20(ROOT, "web");
27426
- VERSION = JSON.parse(readFileSync8(join20(ROOT, "package.json"), "utf8")).version;
27904
+ WEB_ROOT = join22(ROOT, "web");
27905
+ VERSION = JSON.parse(readFileSync8(join22(ROOT, "package.json"), "utf8")).version;
27427
27906
  DEFAULT_ARGS = ["HEAD"];
27428
27907
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
27429
27908
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -27719,6 +28198,9 @@ if (process.argv[2] === "agent-help") {
27719
28198
  } else if (process.argv[2] === "skill") {
27720
28199
  const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
27721
28200
  runSkillCli2(process.argv.slice(3));
28201
+ } else if (process.argv[2] === "watch-child") {
28202
+ const { runWatchChild: runWatchChild2 } = await Promise.resolve().then(() => (init_watch_child(), exports_watch_child));
28203
+ await runWatchChild2();
27722
28204
  } else if (process.argv[2] === "doctor") {
27723
28205
  const { runDoctorCli: runDoctorCli2 } = await Promise.resolve().then(() => (init_doctor_cli(), exports_doctor_cli));
27724
28206
  await runDoctorCli2(process.argv.slice(3));