@youtyan/code-viewer 0.1.50 → 0.1.52

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.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __returnValue = (v) => v;
4
5
  function __exportSetter(name, newValue) {
@@ -14,6 +15,7 @@ var __export = (target, all) => {
14
15
  });
15
16
  };
16
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
19
 
18
20
  // web-src/server/annotations.ts
19
21
  import {
@@ -1745,6 +1747,368 @@ var init_annotate_cli = __esm(() => {
1745
1747
  init_server_registry();
1746
1748
  });
1747
1749
 
1750
+ // web-src/server/query-cli.ts
1751
+ var exports_query_cli = {};
1752
+ __export(exports_query_cli, {
1753
+ runQueryCli: () => runQueryCli,
1754
+ parseQueryArgs: () => parseQueryArgs,
1755
+ QUERY_HELP: () => QUERY_HELP,
1756
+ QUERY_AGENT_HELP: () => QUERY_AGENT_HELP
1757
+ });
1758
+ import { realpathSync as realpathSync2 } from "node:fs";
1759
+ function takeValue2(argv, index, flag) {
1760
+ const value = argv[index + 1];
1761
+ if (value === undefined)
1762
+ return { error: `${flag} requires a value` };
1763
+ return { value, next: index + 1 };
1764
+ }
1765
+ function parseQueryArgs(argv) {
1766
+ const rest = [];
1767
+ let cwd;
1768
+ let server;
1769
+ const options = new Map;
1770
+ const flags = new Set;
1771
+ const valueFlags = new Set([
1772
+ "--db",
1773
+ "--sql",
1774
+ "--title",
1775
+ "--body",
1776
+ "--max-rows"
1777
+ ]);
1778
+ for (let i = 0;i < argv.length; i++) {
1779
+ const arg = argv[i];
1780
+ if (arg === "--help" || arg === "-h")
1781
+ return { ok: true, args: { command: { kind: "help" } } };
1782
+ if (arg === "--cwd" || arg === "--server") {
1783
+ const taken = takeValue2(argv, i, arg);
1784
+ if ("error" in taken)
1785
+ return { ok: false, error: taken.error };
1786
+ if (arg === "--cwd")
1787
+ cwd = taken.value;
1788
+ else
1789
+ server = taken.value;
1790
+ i = taken.next;
1791
+ } else if (valueFlags.has(arg)) {
1792
+ const taken = takeValue2(argv, i, arg);
1793
+ if ("error" in taken)
1794
+ return { ok: false, error: taken.error };
1795
+ options.set(arg, taken.value);
1796
+ i = taken.next;
1797
+ } else if (arg === "--json" || arg === "--no-save") {
1798
+ flags.add(arg);
1799
+ } else if (arg.startsWith("-")) {
1800
+ return { ok: false, error: `unknown option: ${arg}` };
1801
+ } else {
1802
+ rest.push(arg);
1803
+ }
1804
+ }
1805
+ const subcommand = rest[0];
1806
+ if (!subcommand)
1807
+ return { ok: true, args: { command: { kind: "help" } } };
1808
+ if (subcommand === "agent-help") {
1809
+ return { ok: true, args: { command: { kind: "agent-help" } } };
1810
+ }
1811
+ if (subcommand === "exec") {
1812
+ const db = options.get("--db");
1813
+ if (!db)
1814
+ return { ok: false, error: "exec requires --db <path>" };
1815
+ const sql = options.get("--sql");
1816
+ if (!sql)
1817
+ return { ok: false, error: "exec requires --sql <sql>" };
1818
+ const maxRowsRaw = options.get("--max-rows");
1819
+ const maxRows = maxRowsRaw ? Number(maxRowsRaw) || undefined : undefined;
1820
+ return {
1821
+ ok: true,
1822
+ args: {
1823
+ command: {
1824
+ kind: "exec",
1825
+ db,
1826
+ sql,
1827
+ title: options.get("--title"),
1828
+ body: options.get("--body"),
1829
+ save: !flags.has("--no-save"),
1830
+ maxRows
1831
+ },
1832
+ cwd,
1833
+ server
1834
+ }
1835
+ };
1836
+ }
1837
+ if (subcommand === "list") {
1838
+ return {
1839
+ ok: true,
1840
+ args: {
1841
+ command: {
1842
+ kind: "list",
1843
+ json: flags.has("--json"),
1844
+ db: options.get("--db")
1845
+ },
1846
+ cwd,
1847
+ server
1848
+ }
1849
+ };
1850
+ }
1851
+ if (subcommand === "clear") {
1852
+ return {
1853
+ ok: true,
1854
+ args: {
1855
+ command: { kind: "clear", db: options.get("--db") },
1856
+ cwd,
1857
+ server
1858
+ }
1859
+ };
1860
+ }
1861
+ return { ok: false, error: `unknown query command: ${subcommand}` };
1862
+ }
1863
+ function resolveRepoRoot2(cwdOption) {
1864
+ const base = cwdOption || process.cwd();
1865
+ try {
1866
+ return repoRoot(base) || realpathSync2(base);
1867
+ } catch {
1868
+ console.error(`--cwd must point to an existing directory: ${base}`);
1869
+ process.exit(1);
1870
+ }
1871
+ }
1872
+ async function serverReachable2(serverUrl) {
1873
+ try {
1874
+ const res = await fetch(`${serverUrl}/_db/files`, {
1875
+ signal: AbortSignal.timeout(1500)
1876
+ });
1877
+ return res.ok;
1878
+ } catch {
1879
+ return false;
1880
+ }
1881
+ }
1882
+ async function ensureServerUrl2(root, override) {
1883
+ if (override) {
1884
+ const url = override.replace(/\/+$/, "");
1885
+ if (await serverReachable2(url))
1886
+ return url;
1887
+ console.error(`could not reach the code-viewer server at ${url}.`);
1888
+ process.exit(1);
1889
+ }
1890
+ const registered = readServerRegistry(root);
1891
+ if (registered) {
1892
+ const url = registered.url.replace(/\/+$/, "");
1893
+ if (await serverReachable2(url))
1894
+ return url;
1895
+ }
1896
+ console.error(`no running code-viewer server for this repository.
1897
+ ` + `Start one manually (from ${root}):
1898
+ ` + " code-viewer");
1899
+ process.exit(1);
1900
+ }
1901
+ async function request2(serverUrl, path, method, body) {
1902
+ const url = `${serverUrl}${path}`;
1903
+ const origin = new URL(serverUrl).origin;
1904
+ let res;
1905
+ try {
1906
+ res = await fetch(url, {
1907
+ method,
1908
+ headers: method === "POST" ? {
1909
+ "Content-Type": "application/json",
1910
+ Origin: origin,
1911
+ "X-Code-Viewer-Action": "1"
1912
+ } : {},
1913
+ body: body === undefined ? undefined : JSON.stringify(body)
1914
+ });
1915
+ } catch {
1916
+ console.error(`could not reach the code-viewer server at ${serverUrl}.`);
1917
+ process.exit(1);
1918
+ }
1919
+ const data = await res.json();
1920
+ return { ok: res.ok, status: res.status, data };
1921
+ }
1922
+ async function runQueryCli(argv) {
1923
+ const parsed = parseQueryArgs(argv);
1924
+ if (parsed.ok === false) {
1925
+ console.error(parsed.error);
1926
+ console.error('Run "code-viewer query --help" for usage.');
1927
+ process.exit(1);
1928
+ }
1929
+ const { command, cwd, server } = parsed.args;
1930
+ if (command.kind === "help") {
1931
+ console.log(QUERY_HELP);
1932
+ return;
1933
+ }
1934
+ if (command.kind === "agent-help") {
1935
+ console.log(QUERY_AGENT_HELP);
1936
+ return;
1937
+ }
1938
+ const root = resolveRepoRoot2(cwd);
1939
+ const serverUrl = await ensureServerUrl2(root, server);
1940
+ if (command.kind === "exec") {
1941
+ const reqBody = {
1942
+ db: command.db,
1943
+ sql: command.sql,
1944
+ saveHistory: command.save,
1945
+ executedBy: "ai",
1946
+ source: "cli"
1947
+ };
1948
+ if (command.title)
1949
+ reqBody.title = command.title;
1950
+ if (command.body)
1951
+ reqBody.body = command.body;
1952
+ if (command.maxRows)
1953
+ reqBody.maxRows = command.maxRows;
1954
+ const result = await request2(serverUrl, "/_db/query", "POST", reqBody);
1955
+ const data = result.data;
1956
+ if (!result.ok || data.error) {
1957
+ console.error(`query error: ${typeof data.error === "string" ? data.error : JSON.stringify(data)}`);
1958
+ process.exit(1);
1959
+ }
1960
+ console.log(JSON.stringify({
1961
+ columns: data.columns,
1962
+ rows: data.rows,
1963
+ rowCount: data.rowCount,
1964
+ elapsedMs: data.elapsedMs
1965
+ }, null, 2));
1966
+ return;
1967
+ }
1968
+ if (command.kind === "list") {
1969
+ const params = command.db ? `?db=${encodeURIComponent(command.db)}` : "";
1970
+ const result = await request2(serverUrl, `/_db/history${params}`, "GET");
1971
+ if (!result.ok) {
1972
+ console.error("failed to fetch query history");
1973
+ process.exit(1);
1974
+ }
1975
+ const state = result.data;
1976
+ if (command.json) {
1977
+ console.log(JSON.stringify(state, null, 2));
1978
+ } else {
1979
+ if (!state.entries.length) {
1980
+ console.log("no query history");
1981
+ return;
1982
+ }
1983
+ for (const entry of state.entries) {
1984
+ const by = entry.executedBy === "ai" ? "[AI]" : "";
1985
+ const title = entry.title ? ` ${entry.title}` : "";
1986
+ const sql = typeof entry.sql === "string" ? entry.sql.length > 80 ? `${entry.sql.slice(0, 80)}...` : entry.sql : "";
1987
+ console.log(`${entry.executedAt} ${by}${title} ${entry.rowCount} rows (${entry.elapsedMs}ms)`);
1988
+ console.log(` ${sql}`);
1989
+ }
1990
+ }
1991
+ return;
1992
+ }
1993
+ if (command.kind === "clear") {
1994
+ const reqBody = {};
1995
+ if (command.db)
1996
+ reqBody.db = command.db;
1997
+ await request2(serverUrl, "/_db/history/clear", "POST", reqBody);
1998
+ console.log("cleared query history");
1999
+ return;
2000
+ }
2001
+ }
2002
+ var QUERY_HELP = `code-viewer query — execute read-only SQL queries against local databases
2003
+
2004
+ Usage:
2005
+ code-viewer query exec --db <path> --sql <sql> [--title <text>] [--body <markdown>] [--no-save] [--max-rows <n>]
2006
+ code-viewer query list [--json] [--db <path>]
2007
+ code-viewer query clear [--db <path>]
2008
+ code-viewer query search --db <path> --term <text> [--tables t1,t2,...] [--include-non-text] [--max-hits <n>]
2009
+ code-viewer query snapshot create --db <path> [--tables t1,t2,...] [--note <text>]
2010
+ code-viewer query snapshot list [--json] [--db <path>]
2011
+ code-viewer query snapshot delete --id <snapshot-id>
2012
+ code-viewer query snapshot note --id <snapshot-id> --note <text>
2013
+ code-viewer query diff create --before <id> --after <id> [--note <text>]
2014
+ code-viewer query diff list [--json] [--db <path>]
2015
+ code-viewer query diff tables --id <diff-id>
2016
+ code-viewer query diff rows --id <diff-id> --table <name> [--type inserted|updated|deleted] [--limit <n>]
2017
+ code-viewer query diff delete --id <diff-id>
2018
+ code-viewer query agent-help
2019
+
2020
+ Global options:
2021
+ --cwd <dir> repository directory (default: current directory)
2022
+ --server <url> code-viewer server URL (default: auto-discovered)
2023
+
2024
+ Examples:
2025
+ code-viewer query exec --db data.sqlite3 --sql "SELECT * FROM users LIMIT 10"
2026
+ code-viewer query search --db app.db --term "john@example.com"
2027
+ code-viewer query snapshot create --db app.db --tables users,orders --note "Before migration"
2028
+ code-viewer query diff create --before snap-abc123 --after snap-def456
2029
+ code-viewer query diff rows --id diff-xyz789 --table users --type updated
2030
+ `, QUERY_AGENT_HELP = `code-viewer query — execute read-only SQL queries against local databases
2031
+
2032
+ You are an AI coding agent. Use this tool to investigate database contents
2033
+ when a human asks about their data. Results are saved to the project's
2034
+ .code-viewer/query-history.json and appear in the browser's Database > Query
2035
+ History tab, so the human can review what you queried.
2036
+
2037
+ ## When to use
2038
+
2039
+ - Answering "what does this data look like?"
2040
+ - Checking schema, row counts, sample data
2041
+ - Investigating data quality or anomalies
2042
+ - Searching for a value across all tables
2043
+ - Taking snapshots before/after a test to verify DB changes
2044
+
2045
+ ## Requirements
2046
+
2047
+ - A code-viewer server must be running for the repository.
2048
+ - Only SELECT, PRAGMA, EXPLAIN, WITH queries are allowed (for exec).
2049
+ - Results are persisted and visible to the human.
2050
+
2051
+ ## Workflow: SQL Query
2052
+
2053
+ 1. Identify which database file to query (list with: code-viewer query list)
2054
+ 2. Execute:
2055
+ code-viewer query exec --db data.sqlite3 --sql "SELECT * FROM users LIMIT 10" \\
2056
+ --title "Sample user data" --body "Checking what user records look like."
2057
+ 3. The human sees results in the browser's Database > Query History tab.
2058
+
2059
+ ## Workflow: Global Search
2060
+
2061
+ Search for a string across all tables and all text columns:
2062
+ code-viewer query search --db app.db --term "john@example.com"
2063
+
2064
+ Options:
2065
+ --tables users,orders Only search specific tables
2066
+ --include-non-text Also search numeric/date columns
2067
+ --max-hits 20 Max hits per table (default: 50)
2068
+
2069
+ ## Workflow: Snapshot & Diff (for testing)
2070
+
2071
+ Use this to verify that a feature test correctly modifies the expected DB tables.
2072
+
2073
+ 1. Take a "before" snapshot:
2074
+ code-viewer query snapshot create --db app.db --tables users,orders \\
2075
+ --note "Before running user registration test"
2076
+
2077
+ 2. (The human or test runner performs the action)
2078
+
2079
+ 3. Take an "after" snapshot:
2080
+ code-viewer query snapshot create --db app.db --tables users,orders \\
2081
+ --note "After running user registration test"
2082
+
2083
+ 4. List snapshots to get IDs:
2084
+ code-viewer query snapshot list --db app.db
2085
+
2086
+ 5. Create a diff:
2087
+ code-viewer query diff create --before snap-abc123 --after snap-def456 \\
2088
+ --note "User registration test - expected 1 INSERT in users"
2089
+
2090
+ 6. View the diff:
2091
+ code-viewer query diff tables --id diff-xyz789
2092
+ code-viewer query diff rows --id diff-xyz789 --table users --type inserted
2093
+
2094
+ The human can also view all diffs in the browser's Database > Snapshot tab.
2095
+
2096
+ ## Guidelines
2097
+
2098
+ - Always use LIMIT. The server caps rows but be explicit.
2099
+ - Write --title for the human, not for yourself.
2100
+ - Use --body to explain why the query matters.
2101
+ - Do not query broad PII or secrets unless explicitly asked.
2102
+ - Use --no-save for exploratory queries that should not remain in history.
2103
+ - Prefer specific columns over SELECT *.
2104
+ - For snapshots, always specify --tables to avoid scanning unnecessary tables.
2105
+ - Write meaningful --note values — the human uses them to understand context.
2106
+ `;
2107
+ var init_query_cli = __esm(() => {
2108
+ init_git();
2109
+ init_server_registry();
2110
+ });
2111
+
1748
2112
  // web-src/server/root.ts
1749
2113
  import { existsSync as existsSync4 } from "node:fs";
1750
2114
  import { dirname, join as join4, normalize } from "node:path";
@@ -1940,7 +2304,8 @@ var init_routes = __esm(() => {
1940
2304
  "/todiff",
1941
2305
  "/file",
1942
2306
  "/help",
1943
- "/history"
2307
+ "/history",
2308
+ "/database"
1944
2309
  ];
1945
2310
  APP_ENTRY_PATHS = ["/", "/index.html"];
1946
2311
  });
@@ -2323,215 +2688,2576 @@ function parseRgOutput(stdout, max, omitDirNames = [], excludeNames = []) {
2323
2688
  preview: preview.slice(0, 500)
2324
2689
  });
2325
2690
  }
2326
- return matches;
2691
+ return matches;
2692
+ }
2693
+ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames = []) {
2694
+ const prefix = `${ref}:`;
2695
+ const normalized = stdout.split(`
2696
+ `).map((line) => line.startsWith(prefix) ? line.slice(prefix.length) : line).join(`
2697
+ `);
2698
+ return parseRgOutput(normalized, max, omitDirNames, excludeNames);
2699
+ }
2700
+ var GREP_DEFAULT_MAX = 200, GREP_ABSOLUTE_MAX = 500, GREP_MAX_FILE_BYTES, FILE_SEARCH_ABSOLUTE_MAX = 50000, DEFAULT_EXCLUDE_NAMES;
2701
+ var init_search = __esm(() => {
2702
+ GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
2703
+ DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
2704
+ });
2705
+
2706
+ // web-src/server/worktree-watcher.ts
2707
+ import {
2708
+ lstatSync as lstatSync3,
2709
+ readdirSync as nodeReaddirSync,
2710
+ watch as nodeWatch
2711
+ } from "node:fs";
2712
+ import { join as join7, relative } from "node:path";
2713
+ function normalizeRelativePath(path) {
2714
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
2715
+ }
2716
+ function isInsideRoot(root, path) {
2717
+ const rel = relative(root, path).replace(/\\/g, "/");
2718
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
2719
+ }
2720
+ function startWorktreeUpdateWatch(options) {
2721
+ const watch = options.watch || nodeWatch;
2722
+ const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
2723
+ const isDirectory = options.isDirectory || ((path) => {
2724
+ try {
2725
+ return lstatSync3(path).isDirectory();
2726
+ } catch {
2727
+ return false;
2728
+ }
2729
+ });
2730
+ const directorySignature = options.directorySignature || ((path) => {
2731
+ try {
2732
+ const stats = lstatSync3(path);
2733
+ if (!stats.isDirectory())
2734
+ return null;
2735
+ return `${stats.dev}:${stats.ino}`;
2736
+ } catch {
2737
+ return null;
2738
+ }
2739
+ });
2740
+ const setTimer = options.setTimeoutFn || setTimeout;
2741
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
2742
+ const debounceMs = options.debounceMs ?? 250;
2743
+ const watchers = new Map;
2744
+ const signatures = new Map;
2745
+ const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
2746
+ const initialScanQueue = [];
2747
+ let initialScanTimer = null;
2748
+ let timer = null;
2749
+ const pendingChangedPaths = new Set;
2750
+ const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
2751
+ const scheduleUpdate = (changedPath) => {
2752
+ if (changedPath)
2753
+ pendingChangedPaths.add(changedPath);
2754
+ if (timer)
2755
+ clearTimer(timer);
2756
+ timer = setTimer(() => {
2757
+ timer = null;
2758
+ const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
2759
+ pendingChangedPaths.clear();
2760
+ options.onUpdate(paths);
2761
+ }, debounceMs);
2762
+ };
2763
+ const closeSubtree = (dir) => {
2764
+ for (const [watchedDir, watcher] of [...watchers]) {
2765
+ if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
2766
+ continue;
2767
+ try {
2768
+ watcher.close?.();
2769
+ } catch {}
2770
+ watchers.delete(watchedDir);
2771
+ signatures.delete(watchedDir);
2772
+ }
2773
+ };
2774
+ const closeAll = () => {
2775
+ if (initialScanTimer) {
2776
+ clearTimer(initialScanTimer);
2777
+ initialScanTimer = null;
2778
+ }
2779
+ initialScanQueue.length = 0;
2780
+ for (const watcher of [...watchers.values()]) {
2781
+ try {
2782
+ watcher.close?.();
2783
+ } catch {}
2784
+ }
2785
+ watchers.clear();
2786
+ signatures.clear();
2787
+ };
2788
+ const readChildDirectories = (dir) => {
2789
+ let entries;
2790
+ try {
2791
+ entries = readDirs(dir);
2792
+ } catch (error) {
2793
+ options.onError?.(error);
2794
+ return [];
2795
+ }
2796
+ const children = [];
2797
+ for (const entry of entries) {
2798
+ if (!entry.isDirectory())
2799
+ continue;
2800
+ children.push(join7(dir, entry.name));
2801
+ }
2802
+ return children;
2803
+ };
2804
+ const processInitialScanQueue = () => {
2805
+ initialScanTimer = null;
2806
+ const next = initialScanQueue.shift();
2807
+ if (next)
2808
+ watchDirectory(next, true);
2809
+ if (initialScanQueue.length)
2810
+ initialScanTimer = setTimer(processInitialScanQueue, 50);
2811
+ };
2812
+ const queueInitialChildren = (dir) => {
2813
+ initialScanQueue.push(...readChildDirectories(dir));
2814
+ if (!initialScanTimer)
2815
+ initialScanTimer = setTimer(processInitialScanQueue, 5000);
2816
+ };
2817
+ const watchDirectory = (dir, initialScan = false) => {
2818
+ if (watchers.has(dir))
2819
+ return;
2820
+ const rel = normalizeRelativePath(relative(options.root, dir));
2821
+ if (rel && ignored(rel))
2822
+ return;
2823
+ try {
2824
+ const watcher = watch(dir, { persistent: false }, (_event, filename) => {
2825
+ if (!filename) {
2826
+ scheduleUpdate();
2827
+ return;
2828
+ }
2829
+ const changed = normalizeRelativePath(join7(rel, filename.toString()));
2830
+ if (ignored(changed))
2831
+ return;
2832
+ const fullChangedPath = join7(options.root, changed);
2833
+ if (!isInsideRoot(options.root, fullChangedPath))
2834
+ return;
2835
+ const known = watchers.has(fullChangedPath);
2836
+ if (isDirectory(fullChangedPath)) {
2837
+ if (known) {
2838
+ const signature2 = directorySignature(fullChangedPath);
2839
+ if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
2840
+ closeSubtree(fullChangedPath);
2841
+ watchDirectory(fullChangedPath);
2842
+ }
2843
+ scheduleUpdate(changed);
2844
+ return;
2845
+ }
2846
+ watchDirectory(fullChangedPath);
2847
+ } else if (known) {
2848
+ closeSubtree(fullChangedPath);
2849
+ }
2850
+ scheduleUpdate(changed);
2851
+ }) || {};
2852
+ watchers.set(dir, watcher);
2853
+ const signature = directorySignature(dir);
2854
+ if (signature)
2855
+ signatures.set(dir, signature);
2856
+ watcher.on?.("error", () => {
2857
+ if (watchers.get(dir) === watcher) {
2858
+ watchers.delete(dir);
2859
+ signatures.delete(dir);
2860
+ }
2861
+ });
2862
+ watcher.on?.("close", () => {
2863
+ if (watchers.get(dir) === watcher) {
2864
+ watchers.delete(dir);
2865
+ signatures.delete(dir);
2866
+ }
2867
+ });
2868
+ } catch (error) {
2869
+ options.onError?.(error);
2870
+ return;
2871
+ }
2872
+ if (initialScanAsync && initialScan) {
2873
+ queueInitialChildren(dir);
2874
+ return;
2875
+ }
2876
+ for (const child of readChildDirectories(dir))
2877
+ watchDirectory(child);
2878
+ };
2879
+ watchDirectory(options.root, true);
2880
+ return { started: watchers.size > 0, close: closeAll };
2881
+ }
2882
+ var init_worktree_watcher = __esm(() => {
2883
+ init_search();
2884
+ });
2885
+
2886
+ // web-src/server/database/adapters/docker.ts
2887
+ import { spawnSync as spawnSync2 } from "node:child_process";
2888
+ function execInContainer(config, sql, timeoutMs = 1e4) {
2889
+ let args;
2890
+ if (config.kind === "postgresql") {
2891
+ args = [
2892
+ "docker",
2893
+ "exec",
2894
+ "-i",
2895
+ "-e",
2896
+ `PGPASSWORD=${config.password}`,
2897
+ config.containerName,
2898
+ "psql",
2899
+ "-U",
2900
+ config.user,
2901
+ "-d",
2902
+ config.database,
2903
+ "-X",
2904
+ "-q",
2905
+ "-t",
2906
+ "-A",
2907
+ "-F",
2908
+ "\t",
2909
+ "-v",
2910
+ "ON_ERROR_STOP=1",
2911
+ "-c",
2912
+ sql
2913
+ ];
2914
+ } else {
2915
+ args = [
2916
+ "docker",
2917
+ "exec",
2918
+ "-i",
2919
+ "-e",
2920
+ `MYSQL_PWD=${config.password}`,
2921
+ config.containerName,
2922
+ "mysql",
2923
+ "-u",
2924
+ config.user,
2925
+ config.database,
2926
+ "--batch",
2927
+ "--raw",
2928
+ "--default-character-set=utf8mb4",
2929
+ "-e",
2930
+ sql
2931
+ ];
2932
+ }
2933
+ const proc = spawnSync2(args[0], args.slice(1), {
2934
+ encoding: "utf8",
2935
+ timeout: timeoutMs,
2936
+ stdio: ["ignore", "pipe", "pipe"]
2937
+ });
2938
+ return {
2939
+ stdout: proc.stdout || "",
2940
+ stderr: proc.stderr || "",
2941
+ code: proc.status ?? 1
2942
+ };
2943
+ }
2944
+ function parseTsvOutput(stdout, hasHeader) {
2945
+ const lines = stdout.trim().split(`
2946
+ `).filter(Boolean);
2947
+ if (lines.length === 0)
2948
+ return { columns: [], rows: [] };
2949
+ if (hasHeader) {
2950
+ const columns = lines[0].split("\t");
2951
+ const rows2 = lines.slice(1).map((line) => line.split("\t"));
2952
+ return { columns, rows: rows2 };
2953
+ }
2954
+ const rows = lines.map((line) => line.split("\t"));
2955
+ return { columns: [], rows };
2956
+ }
2957
+ function sanitizeIdentifier(name, kind) {
2958
+ if (kind === "mysql")
2959
+ return `\`${name.replace(/`/g, "``")}\``;
2960
+ return `"${name.replace(/"/g, '""')}"`;
2961
+ }
2962
+ function buildOrderClause(orderBy, kind) {
2963
+ if (!orderBy?.length)
2964
+ return "";
2965
+ const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
2966
+ return ` ORDER BY ${parts.join(", ")}`;
2967
+ }
2968
+ function createDockerAdapter(config) {
2969
+ function exec(sql) {
2970
+ const result = execInContainer(config, sql);
2971
+ if (result.code !== 0) {
2972
+ throw new Error(result.stderr.trim() || "query failed");
2973
+ }
2974
+ return parseTsvOutput(result.stdout, config.kind === "mysql");
2975
+ }
2976
+ function toDbValue(val) {
2977
+ if (val === "NULL" || val === "\\N")
2978
+ return null;
2979
+ return val;
2980
+ }
2981
+ const columnCache = new Map;
2982
+ function fetchColumnsUncached(table) {
2983
+ let sql;
2984
+ if (config.kind === "postgresql") {
2985
+ sql = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${table.replace(/'/g, "''")}' ORDER BY ordinal_position`;
2986
+ } else {
2987
+ sql = `SELECT column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${table.replace(/'/g, "''")}' ORDER BY ordinal_position`;
2988
+ }
2989
+ const result = exec(sql);
2990
+ if (config.kind === "postgresql") {
2991
+ const pkSql = `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '${table.replace(/'/g, "''")}'::regclass AND i.indisprimary`;
2992
+ let pkCols;
2993
+ try {
2994
+ const pkResult = exec(pkSql);
2995
+ pkCols = new Set(pkResult.rows.map((r) => r[0]));
2996
+ } catch {
2997
+ pkCols = new Set;
2998
+ }
2999
+ return result.rows.map((row) => ({
3000
+ name: row[0],
3001
+ type: row[1],
3002
+ nullable: row[2] === "YES",
3003
+ primaryKey: pkCols.has(row[0]),
3004
+ defaultValue: row[3] === "" ? null : row[3]
3005
+ }));
3006
+ }
3007
+ return result.rows.map((row) => ({
3008
+ name: row[0],
3009
+ type: row[1],
3010
+ nullable: row[2] === "YES",
3011
+ primaryKey: row[4] === "PRI",
3012
+ defaultValue: row[3] === "NULL" ? null : row[3]
3013
+ }));
3014
+ }
3015
+ return {
3016
+ kind: config.kind,
3017
+ getTables() {
3018
+ let sql;
3019
+ if (config.kind === "postgresql") {
3020
+ sql = `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name`;
3021
+ } else {
3022
+ sql = `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name`;
3023
+ }
3024
+ const result = exec(sql);
3025
+ return result.rows.map((row) => ({
3026
+ name: row[0],
3027
+ type: row[1] === "VIEW" ? "view" : "table",
3028
+ rowCount: null
3029
+ }));
3030
+ },
3031
+ getColumns(table) {
3032
+ const cached = columnCache.get(table);
3033
+ if (cached)
3034
+ return cached;
3035
+ const cols = fetchColumnsUncached(table);
3036
+ columnCache.set(table, cols);
3037
+ return cols;
3038
+ },
3039
+ getIndexes() {
3040
+ let sql;
3041
+ if (config.kind === "postgresql") {
3042
+ sql = `SELECT indexname, tablename FROM pg_indexes WHERE schemaname = 'public' AND indexname NOT LIKE 'pg_%' ORDER BY indexname`;
3043
+ } else {
3044
+ sql = `SELECT DISTINCT index_name, table_name, non_unique FROM information_schema.statistics WHERE table_schema = DATABASE() ORDER BY index_name`;
3045
+ }
3046
+ const result = exec(sql);
3047
+ if (config.kind === "postgresql") {
3048
+ return result.rows.map((row) => ({
3049
+ name: row[0],
3050
+ table: row[1],
3051
+ columns: [],
3052
+ unique: false
3053
+ }));
3054
+ }
3055
+ return result.rows.map((row) => ({
3056
+ name: row[0],
3057
+ table: row[1],
3058
+ columns: [],
3059
+ unique: row[2] === "0"
3060
+ }));
3061
+ },
3062
+ getForeignKeys() {
3063
+ let sql;
3064
+ if (config.kind === "postgresql") {
3065
+ sql = `SELECT tc.table_name, kcu.column_name, ccu.table_name, ccu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`;
3066
+ } else {
3067
+ sql = `SELECT table_name, column_name, referenced_table_name, referenced_column_name FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND referenced_table_name IS NOT NULL`;
3068
+ }
3069
+ try {
3070
+ const result = exec(sql);
3071
+ return result.rows.map((row) => ({
3072
+ fromTable: row[0],
3073
+ fromColumn: row[1],
3074
+ toTable: row[2],
3075
+ toColumn: row[3]
3076
+ }));
3077
+ } catch {
3078
+ return [];
3079
+ }
3080
+ },
3081
+ getColumnsMulti(tables) {
3082
+ const result = new Map;
3083
+ const uncached = tables.filter((t) => {
3084
+ const c = columnCache.get(t);
3085
+ if (c)
3086
+ result.set(t, c);
3087
+ return !c;
3088
+ });
3089
+ if (uncached.length === 0)
3090
+ return result;
3091
+ let sql;
3092
+ if (config.kind === "postgresql") {
3093
+ const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
3094
+ sql = `SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
3095
+ } else {
3096
+ const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
3097
+ sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
3098
+ }
3099
+ try {
3100
+ const queryResult = exec(sql);
3101
+ const grouped = new Map;
3102
+ for (const row of queryResult.rows) {
3103
+ const tbl = row[0];
3104
+ const existing = grouped.get(tbl) || [];
3105
+ existing.push(row);
3106
+ grouped.set(tbl, existing);
3107
+ }
3108
+ let pkMap = new Map;
3109
+ if (config.kind === "postgresql") {
3110
+ try {
3111
+ const pkInList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
3112
+ const pkResult = exec(`SELECT c.relname, a.attname FROM pg_index i JOIN pg_class c ON c.oid = i.indrelid JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indisprimary AND c.relname IN (${pkInList})`);
3113
+ for (const row of pkResult.rows) {
3114
+ const existing = pkMap.get(row[0]) || new Set;
3115
+ existing.add(row[1]);
3116
+ pkMap.set(row[0], existing);
3117
+ }
3118
+ } catch {
3119
+ pkMap = new Map;
3120
+ }
3121
+ }
3122
+ for (const [tbl, rows] of grouped) {
3123
+ const pkCols = pkMap.get(tbl) || new Set;
3124
+ let cols;
3125
+ if (config.kind === "postgresql") {
3126
+ cols = rows.map((row) => ({
3127
+ name: row[1],
3128
+ type: row[2],
3129
+ nullable: row[3] === "YES",
3130
+ primaryKey: pkCols.has(row[1]),
3131
+ defaultValue: row[4] === "" ? null : row[4]
3132
+ }));
3133
+ } else {
3134
+ cols = rows.map((row) => ({
3135
+ name: row[1],
3136
+ type: row[2],
3137
+ nullable: row[3] === "YES",
3138
+ primaryKey: row[5] === "PRI",
3139
+ defaultValue: row[4] === "NULL" ? null : row[4]
3140
+ }));
3141
+ }
3142
+ columnCache.set(tbl, cols);
3143
+ result.set(tbl, cols);
3144
+ }
3145
+ } catch {
3146
+ for (const t of uncached) {
3147
+ const cols = fetchColumnsUncached(t);
3148
+ columnCache.set(t, cols);
3149
+ result.set(t, cols);
3150
+ }
3151
+ }
3152
+ return result;
3153
+ },
3154
+ getTableRowCount(table) {
3155
+ const id = sanitizeIdentifier(table, config.kind);
3156
+ const result = exec(`SELECT COUNT(*) FROM ${id}`);
3157
+ return result.rows.length > 0 ? Number(result.rows[0][0]) || 0 : 0;
3158
+ },
3159
+ getTableRowCounts(tables) {
3160
+ const result = new Map;
3161
+ if (tables.length === 0)
3162
+ return result;
3163
+ const parts = tables.map((t) => {
3164
+ const id = sanitizeIdentifier(t, config.kind);
3165
+ return `SELECT '${t.replace(/'/g, "''")}' AS tbl, COUNT(*) AS cnt FROM ${id}`;
3166
+ });
3167
+ const sql = parts.join(" UNION ALL ");
3168
+ try {
3169
+ const queryResult = exec(sql);
3170
+ for (const row of queryResult.rows) {
3171
+ result.set(row[0], Number(row[1]) || 0);
3172
+ }
3173
+ } catch {
3174
+ for (const t of tables) {
3175
+ const id = sanitizeIdentifier(t, config.kind);
3176
+ try {
3177
+ const r = exec(`SELECT COUNT(*) FROM ${id}`);
3178
+ result.set(t, r.rows.length > 0 ? Number(r.rows[0][0]) || 0 : 0);
3179
+ } catch {
3180
+ result.set(t, 0);
3181
+ }
3182
+ }
3183
+ }
3184
+ return result;
3185
+ },
3186
+ getTablePage(table, options) {
3187
+ const id = sanitizeIdentifier(table, config.kind);
3188
+ const order = buildOrderClause(options.orderBy, config.kind);
3189
+ const sql = `SELECT * FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
3190
+ const result = exec(sql);
3191
+ const cols = this.getColumns(table);
3192
+ if (result.rows.length === 0) {
3193
+ return {
3194
+ columns: cols.map((c) => c.name),
3195
+ columnTypes: cols.map((c) => c.type),
3196
+ rows: [],
3197
+ rowCount: 0
3198
+ };
3199
+ }
3200
+ const columnNames = config.kind === "mysql" ? result.columns : cols.map((c) => c.name);
3201
+ const typeMap = new Map(cols.map((c) => [c.name, c.type]));
3202
+ return {
3203
+ columns: columnNames,
3204
+ columnTypes: columnNames.map((n) => typeMap.get(n) || "TEXT"),
3205
+ rows: result.rows.map((row) => row.map(toDbValue)),
3206
+ rowCount: result.rows.length
3207
+ };
3208
+ },
3209
+ executeReadonlyQuery(sql, _params, maxRows = 1000) {
3210
+ const trimmed = sql.trim();
3211
+ const upper = trimmed.toUpperCase();
3212
+ const firstWord = upper.split(/\s/)[0];
3213
+ if (firstWord !== "SELECT" && firstWord !== "EXPLAIN" && firstWord !== "WITH" && firstWord !== "SHOW" && firstWord !== "DESCRIBE") {
3214
+ throw new Error("Only SELECT, EXPLAIN, WITH, SHOW, and DESCRIBE queries are allowed");
3215
+ }
3216
+ const BLOCKED_RE = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH|DETACH|REPLACE|VACUUM|TRUNCATE|GRANT|REVOKE)\b/;
3217
+ if (BLOCKED_RE.test(upper)) {
3218
+ throw new Error("Query contains a disallowed statement keyword");
3219
+ }
3220
+ const readOnlyPreamble = config.kind === "postgresql" ? "BEGIN TRANSACTION READ ONLY; " : "SET SESSION TRANSACTION READ ONLY; ";
3221
+ const readOnlyPostamble = config.kind === "postgresql" ? "; COMMIT" : "; SET SESSION TRANSACTION READ WRITE";
3222
+ const stripped = trimmed.replace(/;\s*$/, "");
3223
+ const limited = `${readOnlyPreamble}${stripped} LIMIT ${maxRows}${readOnlyPostamble}`;
3224
+ const result = exec(limited);
3225
+ const columnNames = config.kind === "mysql" && result.columns.length > 0 ? result.columns : result.rows.length > 0 ? Array.from({ length: result.rows[0].length }, (_, i) => `col${i + 1}`) : [];
3226
+ return {
3227
+ columns: columnNames,
3228
+ columnTypes: columnNames.map(() => "TEXT"),
3229
+ rows: result.rows.slice(0, maxRows).map((row) => row.map(toDbValue)),
3230
+ rowCount: Math.min(result.rows.length, maxRows)
3231
+ };
3232
+ },
3233
+ getCreateStatement(table) {
3234
+ if (config.kind === "mysql") {
3235
+ try {
3236
+ const result = exec(`SHOW CREATE TABLE ${sanitizeIdentifier(table, config.kind)}`);
3237
+ return result.rows.length > 0 ? result.rows[0][1] || "" : "";
3238
+ } catch {
3239
+ return "";
3240
+ }
3241
+ }
3242
+ try {
3243
+ const result = exec(`SELECT 'CREATE TABLE ' || '${table.replace(/'/g, "''")}' || ' (...)' AS ddl`);
3244
+ return result.rows.length > 0 ? result.rows[0][0] || "" : "";
3245
+ } catch {
3246
+ return "";
3247
+ }
3248
+ },
3249
+ getTriggers(table) {
3250
+ let sql;
3251
+ if (config.kind === "mysql") {
3252
+ sql = `SELECT trigger_name, action_statement FROM information_schema.triggers WHERE event_object_schema = DATABASE() AND event_object_table = '${table.replace(/'/g, "''")}'`;
3253
+ } else {
3254
+ sql = `SELECT tgname, pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = '${table.replace(/'/g, "''")}'::regclass AND NOT tgisinternal`;
3255
+ }
3256
+ try {
3257
+ const result = exec(sql);
3258
+ return result.rows.map((row) => ({
3259
+ name: row[0],
3260
+ sql: row[1] || ""
3261
+ }));
3262
+ } catch {
3263
+ return [];
3264
+ }
3265
+ },
3266
+ close() {
3267
+ columnCache.clear();
3268
+ }
3269
+ };
3270
+ }
3271
+ function resolveContainerName(serviceName, cwd) {
3272
+ const proc = spawnSync2("docker", ["compose", "ps", "--format", "json", "--status", "running"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], cwd });
3273
+ if (proc.status !== 0)
3274
+ return null;
3275
+ try {
3276
+ const output = proc.stdout.trim();
3277
+ let containers;
3278
+ if (output.startsWith("[")) {
3279
+ containers = JSON.parse(output);
3280
+ } else {
3281
+ containers = output.split(`
3282
+ `).filter(Boolean).map((line) => JSON.parse(line));
3283
+ }
3284
+ const match = containers.find((c) => c.Service === serviceName && c.State === "running");
3285
+ return match?.Name || null;
3286
+ } catch {
3287
+ return null;
3288
+ }
3289
+ }
3290
+ function listDockerDatabases(serviceName, kind, env, cwd) {
3291
+ const containerName = resolveContainerName(serviceName, cwd);
3292
+ if (!containerName)
3293
+ return [];
3294
+ const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
3295
+ const password = env.POSTGRES_PASSWORD || env.MYSQL_PASSWORD || env.MYSQL_ROOT_PASSWORD || env.MARIADB_PASSWORD || env.MARIADB_ROOT_PASSWORD || "";
3296
+ const defaultDb = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || (kind === "postgresql" ? "postgres" : "");
3297
+ const config = {
3298
+ kind,
3299
+ containerName,
3300
+ user,
3301
+ password,
3302
+ database: defaultDb || (kind === "postgresql" ? "postgres" : "mysql")
3303
+ };
3304
+ try {
3305
+ let sql;
3306
+ if (kind === "postgresql") {
3307
+ sql = `SELECT datname FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY datname`;
3308
+ } else {
3309
+ sql = `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','performance_schema','mysql','sys') ORDER BY schema_name`;
3310
+ }
3311
+ const result = execInContainer(config, sql);
3312
+ if (result.code !== 0)
3313
+ return defaultDb ? [defaultDb] : [];
3314
+ const parsed = parseTsvOutput(result.stdout, kind === "mysql");
3315
+ const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
3316
+ return dbs.length > 0 ? dbs : defaultDb ? [defaultDb] : [];
3317
+ } catch {
3318
+ return defaultDb ? [defaultDb] : [];
3319
+ }
3320
+ }
3321
+ function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
3322
+ const containerName = resolveContainerName(serviceName, cwd);
3323
+ if (!containerName) {
3324
+ throw new Error(`Container for service "${serviceName}" is not running. Start it with: docker compose up -d ${serviceName}`);
3325
+ }
3326
+ const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
3327
+ const password = env.POSTGRES_PASSWORD || env.MYSQL_PASSWORD || env.MYSQL_ROOT_PASSWORD || env.MARIADB_PASSWORD || env.MARIADB_ROOT_PASSWORD || "";
3328
+ const database = overrideDatabase || env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || (kind === "postgresql" ? "postgres" : "");
3329
+ return createDockerAdapter({
3330
+ kind,
3331
+ containerName,
3332
+ user,
3333
+ password,
3334
+ database
3335
+ });
3336
+ }
3337
+ var init_docker = () => {};
3338
+
3339
+ // web-src/server/database/adapters/sqlite.ts
3340
+ async function getSqliteClass() {
3341
+ if (cachedDbClass)
3342
+ return cachedDbClass;
3343
+ try {
3344
+ const mod = await import("bun:sqlite");
3345
+ cachedDbClass = mod.Database;
3346
+ return cachedDbClass;
3347
+ } catch {}
3348
+ try {
3349
+ const mod = await Function('return import("better-sqlite3")')();
3350
+ cachedDbClass = mod.default || mod;
3351
+ return cachedDbClass;
3352
+ } catch {}
3353
+ throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
3354
+ }
3355
+ function sanitizeIdentifier2(name) {
3356
+ return `"${name.replace(/"/g, '""')}"`;
3357
+ }
3358
+ function buildOrderClause2(orderBy) {
3359
+ if (!orderBy?.length)
3360
+ return "";
3361
+ const parts = orderBy.map((o) => `${sanitizeIdentifier2(o.column)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
3362
+ return ` ORDER BY ${parts.join(", ")}`;
3363
+ }
3364
+ function queryColumns(db, table) {
3365
+ const rows = db.prepare(`PRAGMA table_info(${sanitizeIdentifier2(table)})`).all();
3366
+ return rows.map((row) => ({
3367
+ name: row.name,
3368
+ type: row.type || "TEXT",
3369
+ nullable: row.notnull === 0,
3370
+ primaryKey: row.pk > 0,
3371
+ defaultValue: row.dflt_value
3372
+ }));
3373
+ }
3374
+ function createSqliteAdapter(db) {
3375
+ return {
3376
+ kind: "sqlite",
3377
+ getTables() {
3378
+ const rows = db.prepare("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
3379
+ return rows.map((row) => ({
3380
+ name: row.name,
3381
+ type: row.type,
3382
+ rowCount: null
3383
+ }));
3384
+ },
3385
+ getColumns(table) {
3386
+ return queryColumns(db, table);
3387
+ },
3388
+ getIndexes() {
3389
+ const rows = db.prepare("SELECT name, tbl_name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
3390
+ return rows.map((row) => {
3391
+ const info = db.prepare(`PRAGMA index_info(${sanitizeIdentifier2(row.name)})`).all();
3392
+ const indexList = db.prepare(`PRAGMA index_list(${sanitizeIdentifier2(row.tbl_name)})`).all();
3393
+ const entry = indexList.find((i) => i.name === row.name);
3394
+ return {
3395
+ name: row.name,
3396
+ table: row.tbl_name,
3397
+ columns: info.map((i) => i.name),
3398
+ unique: entry ? entry.unique === 1 : false
3399
+ };
3400
+ });
3401
+ },
3402
+ getForeignKeys() {
3403
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND sql NOT LIKE '%VIRTUAL%' ORDER BY name").all();
3404
+ const fks = [];
3405
+ for (const t of tables) {
3406
+ try {
3407
+ const rows = db.prepare(`PRAGMA foreign_key_list(${sanitizeIdentifier2(t.name)})`).all();
3408
+ for (const row of rows) {
3409
+ fks.push({
3410
+ fromTable: t.name,
3411
+ fromColumn: row.from,
3412
+ toTable: row.table,
3413
+ toColumn: row.to
3414
+ });
3415
+ }
3416
+ } catch {}
3417
+ }
3418
+ return fks;
3419
+ },
3420
+ getColumnsMulti(tables) {
3421
+ const result = new Map;
3422
+ for (const t of tables) {
3423
+ result.set(t, queryColumns(db, t));
3424
+ }
3425
+ return result;
3426
+ },
3427
+ getTableRowCount(table) {
3428
+ const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${sanitizeIdentifier2(table)}`).get();
3429
+ return row?.cnt ?? 0;
3430
+ },
3431
+ getTableRowCounts(tables) {
3432
+ const result = new Map;
3433
+ if (tables.length === 0)
3434
+ return result;
3435
+ const parts = tables.map((t) => `SELECT '${t.replace(/'/g, "''")}' AS tbl, COUNT(*) AS cnt FROM ${sanitizeIdentifier2(t)}`);
3436
+ const sql = parts.join(" UNION ALL ");
3437
+ try {
3438
+ const rows = db.prepare(sql).all();
3439
+ for (const row of rows) {
3440
+ result.set(row.tbl, row.cnt);
3441
+ }
3442
+ } catch {
3443
+ for (const t of tables) {
3444
+ const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${sanitizeIdentifier2(t)}`).get();
3445
+ result.set(t, row?.cnt ?? 0);
3446
+ }
3447
+ }
3448
+ return result;
3449
+ },
3450
+ getTablePage(table, options) {
3451
+ const order = buildOrderClause2(options.orderBy);
3452
+ const sql = `SELECT * FROM ${sanitizeIdentifier2(table)}${order} LIMIT ? OFFSET ?`;
3453
+ const rows = db.prepare(sql).all(options.limit, options.offset);
3454
+ const cols = queryColumns(db, table);
3455
+ if (rows.length === 0) {
3456
+ return {
3457
+ columns: cols.map((c) => c.name),
3458
+ columnTypes: cols.map((c) => c.type),
3459
+ rows: [],
3460
+ rowCount: 0
3461
+ };
3462
+ }
3463
+ const columnNames = Object.keys(rows[0]);
3464
+ const typeMap = new Map(cols.map((c) => [c.name, c.type]));
3465
+ return {
3466
+ columns: columnNames,
3467
+ columnTypes: columnNames.map((n) => typeMap.get(n) || "TEXT"),
3468
+ rows: rows.map((row) => columnNames.map((col) => row[col])),
3469
+ rowCount: rows.length
3470
+ };
3471
+ },
3472
+ executeReadonlyQuery(sql, params, maxRows = 1000) {
3473
+ const trimmed = sql.trim();
3474
+ const upper = trimmed.toUpperCase();
3475
+ const firstWord = upper.split(/\s/)[0];
3476
+ if (firstWord !== "SELECT" && firstWord !== "PRAGMA" && firstWord !== "EXPLAIN" && firstWord !== "WITH") {
3477
+ throw new Error("Only SELECT, PRAGMA, EXPLAIN, and WITH queries are allowed");
3478
+ }
3479
+ const BLOCKED_RE = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH|DETACH|REPLACE|VACUUM|REINDEX|LOAD_EXTENSION)\b/;
3480
+ if (BLOCKED_RE.test(upper)) {
3481
+ throw new Error("Query contains a disallowed statement keyword");
3482
+ }
3483
+ const limited = trimmed.replace(/;\s*$/, "");
3484
+ const wrappedSql = `SELECT * FROM (${limited}) LIMIT ${maxRows + 1}`;
3485
+ let rows;
3486
+ try {
3487
+ rows = db.prepare(wrappedSql).all(...params || []);
3488
+ } catch (wrapErr) {
3489
+ const fallbackSql = `${limited} LIMIT ${maxRows + 1}`;
3490
+ try {
3491
+ rows = db.prepare(fallbackSql).all(...params || []);
3492
+ } catch {
3493
+ throw wrapErr;
3494
+ }
3495
+ }
3496
+ const truncated = rows.length > maxRows;
3497
+ if (truncated)
3498
+ rows = rows.slice(0, maxRows);
3499
+ if (rows.length === 0) {
3500
+ return { columns: [], columnTypes: [], rows: [], rowCount: 0 };
3501
+ }
3502
+ const columnNames = Object.keys(rows[0]);
3503
+ return {
3504
+ columns: columnNames,
3505
+ columnTypes: columnNames.map(() => "TEXT"),
3506
+ rows: rows.map((row) => columnNames.map((col) => row[col])),
3507
+ rowCount: rows.length
3508
+ };
3509
+ },
3510
+ getCreateStatement(table) {
3511
+ const row = db.prepare("SELECT sql FROM sqlite_master WHERE name = ?").get(table);
3512
+ return row?.sql ?? "";
3513
+ },
3514
+ getTriggers(table) {
3515
+ const rows = db.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?").all(table);
3516
+ return rows.map((row) => ({ name: row.name, sql: row.sql ?? "" }));
3517
+ },
3518
+ close() {
3519
+ db.close();
3520
+ }
3521
+ };
3522
+ }
3523
+ var cachedDbClass = null, sqliteAdapterFactory;
3524
+ var init_sqlite = __esm(() => {
3525
+ sqliteAdapterFactory = {
3526
+ async open(path) {
3527
+ const DbClass = await getSqliteClass();
3528
+ const db = new DbClass(path, { readonly: true, create: false });
3529
+ return createSqliteAdapter(db);
3530
+ }
3531
+ };
3532
+ });
3533
+
3534
+ // web-src/server/database/connection-pool.ts
3535
+ function setAdapterFactory(f) {
3536
+ factory = f;
3537
+ }
3538
+ function evictOldest() {
3539
+ let oldestKey = null;
3540
+ let oldestTime = Infinity;
3541
+ for (const [key, entry] of pool) {
3542
+ if (entry.lastUsed < oldestTime) {
3543
+ oldestTime = entry.lastUsed;
3544
+ oldestKey = key;
3545
+ }
3546
+ }
3547
+ if (oldestKey) {
3548
+ const entry = pool.get(oldestKey);
3549
+ if (entry) {
3550
+ clearTimeout(entry.timer);
3551
+ try {
3552
+ entry.adapter.close();
3553
+ } catch {}
3554
+ pool.delete(oldestKey);
3555
+ }
3556
+ }
3557
+ }
3558
+ function scheduleEviction(key, entry) {
3559
+ clearTimeout(entry.timer);
3560
+ entry.timer = setTimeout(() => {
3561
+ const current = pool.get(key);
3562
+ if (current === entry) {
3563
+ try {
3564
+ current.adapter.close();
3565
+ } catch {}
3566
+ pool.delete(key);
3567
+ }
3568
+ }, IDLE_TIMEOUT_MS);
3569
+ }
3570
+ async function getConnection(resolvedPath) {
3571
+ if (!factory) {
3572
+ throw new Error("No adapter factory configured");
3573
+ }
3574
+ const existing = pool.get(resolvedPath);
3575
+ if (existing) {
3576
+ existing.lastUsed = Date.now();
3577
+ scheduleEviction(resolvedPath, existing);
3578
+ return existing.adapter;
3579
+ }
3580
+ if (pool.size >= MAX_CONNECTIONS) {
3581
+ evictOldest();
3582
+ }
3583
+ const adapter = await factory.open(resolvedPath);
3584
+ const entry = {
3585
+ adapter,
3586
+ path: resolvedPath,
3587
+ lastUsed: Date.now(),
3588
+ timer: setTimeout(() => {}, 0)
3589
+ };
3590
+ pool.set(resolvedPath, entry);
3591
+ scheduleEviction(resolvedPath, entry);
3592
+ return adapter;
3593
+ }
3594
+ function closeConnection(resolvedPath) {
3595
+ const entry = pool.get(resolvedPath);
3596
+ if (!entry)
3597
+ return false;
3598
+ clearTimeout(entry.timer);
3599
+ try {
3600
+ entry.adapter.close();
3601
+ } catch {}
3602
+ pool.delete(resolvedPath);
3603
+ return true;
3604
+ }
3605
+ var MAX_CONNECTIONS = 8, IDLE_TIMEOUT_MS, pool, factory = null;
3606
+ var init_connection_pool = __esm(() => {
3607
+ IDLE_TIMEOUT_MS = 5 * 60 * 1000;
3608
+ pool = new Map;
3609
+ });
3610
+
3611
+ // web-src/server/database/discovery.ts
3612
+ import {
3613
+ closeSync,
3614
+ existsSync as existsSync6,
3615
+ lstatSync as lstatSync4,
3616
+ openSync,
3617
+ readdirSync as readdirSync2,
3618
+ readFileSync as readFileSync5,
3619
+ readSync,
3620
+ realpathSync as realpathSync3,
3621
+ statSync as statSync2
3622
+ } from "node:fs";
3623
+ import { basename as basename2, join as join8, relative as relative2 } from "node:path";
3624
+ function isSqliteFile(fullPath) {
3625
+ try {
3626
+ const stat = statSync2(fullPath);
3627
+ if (!stat.isFile() || stat.size < 16)
3628
+ return false;
3629
+ const buf = Buffer.alloc(16);
3630
+ const fd = openSync(fullPath, "r");
3631
+ try {
3632
+ readSync(fd, buf, 0, 16, 0);
3633
+ } finally {
3634
+ closeSync(fd);
3635
+ }
3636
+ return buf.toString("utf8", 0, 16) === SQLITE_MAGIC;
3637
+ } catch {
3638
+ return false;
3639
+ }
3640
+ }
3641
+ function discoverSqliteFiles(cwd, omitDirNames) {
3642
+ const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
3643
+ omitSet.add(".git");
3644
+ const results = [];
3645
+ function scan(dir, depth) {
3646
+ if (depth > MAX_SCAN_DEPTH || results.length >= MAX_ENTRIES)
3647
+ return;
3648
+ let entries;
3649
+ try {
3650
+ entries = readdirSync2(dir);
3651
+ } catch {
3652
+ return;
3653
+ }
3654
+ for (const entry of entries) {
3655
+ if (results.length >= MAX_ENTRIES)
3656
+ return;
3657
+ if (omitSet.has(entry.toLowerCase()))
3658
+ continue;
3659
+ const full = join8(dir, entry);
3660
+ let stat;
3661
+ try {
3662
+ stat = lstatSync4(full);
3663
+ } catch {
3664
+ continue;
3665
+ }
3666
+ if (stat.isSymbolicLink())
3667
+ continue;
3668
+ if (stat.isDirectory()) {
3669
+ scan(full, depth + 1);
3670
+ } else if (stat.isFile()) {
3671
+ const ext = entry.slice(entry.lastIndexOf(".")).toLowerCase();
3672
+ if (!SQLITE_EXTENSIONS.has(ext))
3673
+ continue;
3674
+ if (!isSqliteFile(full))
3675
+ continue;
3676
+ const rel = relative2(cwd, full);
3677
+ if (rel.startsWith("..") || rel.startsWith("/"))
3678
+ continue;
3679
+ results.push({
3680
+ path: rel,
3681
+ name: basename2(rel),
3682
+ sizeBytes: stat.size
3683
+ });
3684
+ }
3685
+ }
3686
+ }
3687
+ scan(cwd, 0);
3688
+ results.sort((a, b) => {
3689
+ const aInternal = a.path.startsWith(".code-viewer/") ? 1 : 0;
3690
+ const bInternal = b.path.startsWith(".code-viewer/") ? 1 : 0;
3691
+ if (aInternal !== bInternal)
3692
+ return aInternal - bInternal;
3693
+ return a.path.localeCompare(b.path);
3694
+ });
3695
+ return results;
3696
+ }
3697
+ function validateDbPath(cwd, dbPath) {
3698
+ if (!dbPath || dbPath.includes("\x00") || dbPath.startsWith("/") || dbPath.startsWith("\\"))
3699
+ return null;
3700
+ const parts = dbPath.split(/[\\/]+/);
3701
+ if (parts.some((p) => p === ".." || p.toLowerCase() === ".git"))
3702
+ return null;
3703
+ const full = join8(cwd, dbPath);
3704
+ if (!existsSync6(full))
3705
+ return null;
3706
+ let realCwd;
3707
+ let realFull;
3708
+ try {
3709
+ realCwd = realpathSync3(cwd);
3710
+ realFull = realpathSync3(full);
3711
+ } catch {
3712
+ return null;
3713
+ }
3714
+ const rel = relative2(realCwd, realFull);
3715
+ if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
3716
+ return null;
3717
+ if (!isSqliteFile(realFull))
3718
+ return null;
3719
+ return realFull;
3720
+ }
3721
+ function detectDbKind(image) {
3722
+ const lower = image.toLowerCase();
3723
+ if (lower.includes("postgres"))
3724
+ return "postgresql";
3725
+ if (lower.includes("mysql") || lower.includes("mariadb"))
3726
+ return "mysql";
3727
+ return null;
3728
+ }
3729
+ function resolveEnvValue(raw) {
3730
+ return raw.replace(/\$\{([^}]+)\}/g, (_, expr) => {
3731
+ const defaultMatch = expr.match(/^([^:-]+)(?::?-(.*))?$/);
3732
+ if (!defaultMatch)
3733
+ return "";
3734
+ const varName = defaultMatch[1];
3735
+ const fallback = defaultMatch[2] ?? "";
3736
+ return process.env[varName] || fallback;
3737
+ });
3738
+ }
3739
+ function parseComposeEnv(serviceBlock) {
3740
+ const env = {};
3741
+ const envMatch = serviceBlock.match(/^[ \t]+environment:\s*\n((?:[ \t]+(?:- )?[^\n]+\n?)*)/m);
3742
+ if (!envMatch)
3743
+ return env;
3744
+ const block = envMatch[1];
3745
+ for (const line of block.split(`
3746
+ `)) {
3747
+ const trimmed = line.trim();
3748
+ if (!trimmed || trimmed.startsWith("#"))
3749
+ continue;
3750
+ const stripped = trimmed.startsWith("- ") ? trimmed.slice(2) : trimmed;
3751
+ const eqIdx = stripped.indexOf("=");
3752
+ const colonIdx = stripped.indexOf(": ");
3753
+ if (eqIdx > 0) {
3754
+ env[stripped.slice(0, eqIdx).trim()] = resolveEnvValue(stripped.slice(eqIdx + 1).trim());
3755
+ } else if (colonIdx > 0) {
3756
+ env[stripped.slice(0, colonIdx).trim()] = resolveEnvValue(stripped.slice(colonIdx + 2).trim());
3757
+ }
3758
+ }
3759
+ return env;
3760
+ }
3761
+ function parseComposePorts(serviceBlock) {
3762
+ const portsMatch = serviceBlock.match(/^[ \t]+ports:\s*\n((?:[ \t]+- [^\n]+\n?)*)/m);
3763
+ if (!portsMatch)
3764
+ return null;
3765
+ for (const line of portsMatch[1].split(`
3766
+ `)) {
3767
+ const m = line.match(/["']?(\d+):(\d+)["']?/);
3768
+ if (m)
3769
+ return m[1];
3770
+ }
3771
+ return null;
3772
+ }
3773
+ function discoverDockerDatabases(cwd) {
3774
+ const results = [];
3775
+ for (const filename of COMPOSE_FILENAMES) {
3776
+ const filepath = join8(cwd, filename);
3777
+ if (!existsSync6(filepath))
3778
+ continue;
3779
+ let content;
3780
+ try {
3781
+ content = readFileSync5(filepath, "utf-8");
3782
+ } catch {
3783
+ continue;
3784
+ }
3785
+ const servicesMatch = content.match(/^services:\s*\n/m);
3786
+ if (!servicesMatch || servicesMatch.index === undefined)
3787
+ continue;
3788
+ const servicesStart = servicesMatch.index + servicesMatch[0].length;
3789
+ const afterServices = content.slice(servicesStart);
3790
+ const topLevelEnd = afterServices.search(/^\S/m);
3791
+ const servicesBlock = topLevelEnd >= 0 ? afterServices.slice(0, topLevelEnd) : afterServices;
3792
+ const serviceRegex = /^ {2}(\w[\w-]*):\s*\n/gm;
3793
+ const servicePositions = [];
3794
+ for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
3795
+ servicePositions.push({
3796
+ name: match[1],
3797
+ start: match.index
3798
+ });
3799
+ }
3800
+ for (let i = 0;i < servicePositions.length; i++) {
3801
+ const svc = servicePositions[i];
3802
+ const nextStart = i + 1 < servicePositions.length ? servicePositions[i + 1].start : servicesBlock.length;
3803
+ const svcBlock = servicesBlock.slice(svc.start, nextStart);
3804
+ const imageMatch = svcBlock.match(/^\s+image:\s*["']?([^\s"'#]+)/m);
3805
+ if (!imageMatch)
3806
+ continue;
3807
+ const image = imageMatch[1];
3808
+ const kind = detectDbKind(image);
3809
+ if (!kind)
3810
+ continue;
3811
+ const env = parseComposeEnv(svcBlock);
3812
+ const port = parseComposePorts(svcBlock);
3813
+ const dbName = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || svc.name;
3814
+ const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
3815
+ const hostPort = port || (kind === "postgresql" ? "5432" : "3306");
3816
+ const label = `${svc.name} (${image}, ${user}@localhost:${hostPort}/${dbName})`;
3817
+ results.push({
3818
+ id: `docker:${svc.name}`,
3819
+ path: filename,
3820
+ name: label,
3821
+ sizeBytes: 0,
3822
+ kind,
3823
+ serviceName: svc.name,
3824
+ env
3825
+ });
3826
+ }
3827
+ break;
3828
+ }
3829
+ return results;
3830
+ }
3831
+ var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, COMPOSE_FILENAMES;
3832
+ var init_discovery = __esm(() => {
3833
+ SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
3834
+ COMPOSE_FILENAMES = [
3835
+ "docker-compose.yml",
3836
+ "docker-compose.yaml",
3837
+ "compose.yml",
3838
+ "compose.yaml"
3839
+ ];
3840
+ });
3841
+
3842
+ // web-src/server/database/global-search.ts
3843
+ function sanitizeIdentifier3(name, kind) {
3844
+ if (kind === "mysql")
3845
+ return `\`${name.replace(/`/g, "``")}\``;
3846
+ return `"${name.replace(/"/g, '""')}"`;
3847
+ }
3848
+ function escapeSqlString(value) {
3849
+ return `'${value.replace(/'/g, "''")}'`;
3850
+ }
3851
+ function isTextLikeType(type) {
3852
+ const upper = type.toUpperCase();
3853
+ return upper.includes("CHAR") || upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CLOB") || upper.includes("STRING") || upper === "JSON" || upper === "JSONB" || upper === "XML" || upper === "UUID";
3854
+ }
3855
+ function escapeLikeTerm(term) {
3856
+ return term.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
3857
+ }
3858
+ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkColumns) {
3859
+ const kind = adapter.kind;
3860
+ const searchCols = includeNonText ? columns.filter((c) => c.type.toUpperCase() !== "BLOB" && c.type.toUpperCase() !== "BYTEA") : columns.filter((c) => isTextLikeType(c.type));
3861
+ if (searchCols.length === 0)
3862
+ return [];
3863
+ const escapedTerm = escapeLikeTerm(term);
3864
+ const tbl = sanitizeIdentifier3(table, kind);
3865
+ const hits = [];
3866
+ for (const col of searchCols) {
3867
+ if (hits.length >= maxHits)
3868
+ break;
3869
+ const colId = sanitizeIdentifier3(col.name, kind);
3870
+ const castCol = kind === "mysql" ? `CAST(${colId} AS CHAR)` : `CAST(${colId} AS TEXT)`;
3871
+ let sql;
3872
+ const remaining = maxHits - hits.length;
3873
+ if (kind === "sqlite") {
3874
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '\\' LIMIT ${remaining}`;
3875
+ } else {
3876
+ const likeVal = escapeSqlString(`%${escapedTerm}%`);
3877
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '\\' LIMIT ${remaining}`;
3878
+ }
3879
+ try {
3880
+ const result = kind === "sqlite" ? adapter.executeReadonlyQuery(sql, [`%${escapedTerm}%`], remaining) : adapter.executeReadonlyQuery(sql, undefined, remaining);
3881
+ for (const row of result.rows) {
3882
+ const colIdx = result.columns.indexOf(col.name);
3883
+ const valueRaw = colIdx >= 0 ? row[colIdx] : null;
3884
+ const valueStr = valueRaw == null ? "" : String(valueRaw);
3885
+ const preview = valueStr.length > 200 ? `${valueStr.slice(0, 200)}...` : valueStr;
3886
+ let rowKeyJson;
3887
+ if (pkColumns.length > 0) {
3888
+ const keyObj = {};
3889
+ for (const pk of pkColumns) {
3890
+ const pkIdx = result.columns.indexOf(pk);
3891
+ if (pkIdx >= 0)
3892
+ keyObj[pk] = row[pkIdx];
3893
+ }
3894
+ rowKeyJson = JSON.stringify(keyObj);
3895
+ }
3896
+ hits.push({
3897
+ table,
3898
+ column: col.name,
3899
+ rowKeyJson,
3900
+ valuePreview: preview,
3901
+ rowPreview: row
3902
+ });
3903
+ }
3904
+ } catch {}
3905
+ }
3906
+ return hits;
3907
+ }
3908
+ function getPrimaryKeyColumns(adapter, table) {
3909
+ const columns = adapter.getColumns(table);
3910
+ return columns.filter((c) => c.primaryKey).map((c) => c.name);
3911
+ }
3912
+
3913
+ // web-src/server/database/query-history.ts
3914
+ import {
3915
+ existsSync as existsSync7,
3916
+ mkdirSync as mkdirSync4,
3917
+ readFileSync as readFileSync6,
3918
+ renameSync as renameSync2,
3919
+ writeFileSync as writeFileSync3
3920
+ } from "node:fs";
3921
+ import { join as join9 } from "node:path";
3922
+ function historyFilePath(root) {
3923
+ return join9(root, CODE_VIEWER_DIR2, HISTORY_FILE_NAME);
3924
+ }
3925
+ function emptyState() {
3926
+ return { version: 1, entries: [] };
3927
+ }
3928
+ function loadQueryHistory(cwd) {
3929
+ const file = historyFilePath(cwd);
3930
+ if (!existsSync7(file))
3931
+ return emptyState();
3932
+ try {
3933
+ const raw = readFileSync6(file, "utf8");
3934
+ const parsed = JSON.parse(raw);
3935
+ if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
3936
+ return emptyState();
3937
+ }
3938
+ return parsed;
3939
+ } catch {
3940
+ return emptyState();
3941
+ }
3942
+ }
3943
+ function saveQueryHistory(cwd, state) {
3944
+ const dir = join9(cwd, CODE_VIEWER_DIR2);
3945
+ mkdirSync4(dir, { recursive: true });
3946
+ const file = historyFilePath(cwd);
3947
+ const tmp = `${file}.tmp-${process.pid}`;
3948
+ let content = `${JSON.stringify(state, null, 2)}
3949
+ `;
3950
+ if (Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES) {
3951
+ while (state.entries.length > 1 && Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES) {
3952
+ state.entries.pop();
3953
+ content = `${JSON.stringify(state, null, 2)}
3954
+ `;
3955
+ }
3956
+ }
3957
+ writeFileSync3(tmp, content, "utf8");
3958
+ renameSync2(tmp, file);
3959
+ }
3960
+ function clampPreviewRows(rows) {
3961
+ return rows.slice(0, MAX_PREVIEW_ROWS);
3962
+ }
3963
+ function addQueryHistoryEntry(state, entry) {
3964
+ const clamped = {
3965
+ ...entry,
3966
+ rowsPreview: clampPreviewRows(entry.rowsPreview),
3967
+ savedRows: Math.min(entry.rowsPreview.length, MAX_PREVIEW_ROWS)
3968
+ };
3969
+ const entries = [clamped, ...state.entries];
3970
+ if (entries.length > MAX_ENTRIES2)
3971
+ entries.length = MAX_ENTRIES2;
3972
+ return { version: 1, entries };
3973
+ }
3974
+ function deleteQueryHistoryEntry(state, id) {
3975
+ return {
3976
+ version: 1,
3977
+ entries: state.entries.filter((e) => e.id !== id)
3978
+ };
3979
+ }
3980
+ function clearQueryHistory(state, dbId) {
3981
+ if (!dbId)
3982
+ return emptyState();
3983
+ return {
3984
+ version: 1,
3985
+ entries: state.entries.filter((e) => e.dbId !== dbId)
3986
+ };
3987
+ }
3988
+ var CODE_VIEWER_DIR2 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES = 1e6;
3989
+ var init_query_history = () => {};
3990
+
3991
+ // web-src/server/database/snapshot-store.ts
3992
+ import { createHash as createHash2, randomBytes } from "node:crypto";
3993
+ import { mkdirSync as mkdirSync5 } from "node:fs";
3994
+ import { join as join10 } from "node:path";
3995
+ async function getSqliteClass2() {
3996
+ if (cachedDbClass2)
3997
+ return cachedDbClass2;
3998
+ try {
3999
+ const mod = await import("bun:sqlite");
4000
+ cachedDbClass2 = mod.Database;
4001
+ return cachedDbClass2;
4002
+ } catch {}
4003
+ try {
4004
+ const mod = await Function('return import("better-sqlite3")')();
4005
+ cachedDbClass2 = mod.default || mod;
4006
+ return cachedDbClass2;
4007
+ } catch {}
4008
+ throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
4009
+ }
4010
+ async function getStoreDb(cwd) {
4011
+ const dbPath = join10(cwd, CODE_VIEWER_DIR3, SNAPSHOT_DB_NAME);
4012
+ if (storeDb && storeDbPath === dbPath)
4013
+ return storeDb;
4014
+ if (storeDb) {
4015
+ try {
4016
+ storeDb.close();
4017
+ } catch {}
4018
+ }
4019
+ mkdirSync5(join10(cwd, CODE_VIEWER_DIR3), { recursive: true });
4020
+ const DbClass = await getSqliteClass2();
4021
+ storeDb = new DbClass(dbPath);
4022
+ storeDbPath = dbPath;
4023
+ storeDb.exec("PRAGMA journal_mode=WAL");
4024
+ storeDb.exec("PRAGMA foreign_keys=ON");
4025
+ storeDb.exec(SCHEMA_SQL);
4026
+ return storeDb;
4027
+ }
4028
+ function makeId(prefix) {
4029
+ return `${prefix}-${randomBytes(8).toString("hex")}`;
4030
+ }
4031
+ function hashPayload(payloadJson) {
4032
+ return createHash2("sha256").update(payloadJson).digest("hex");
4033
+ }
4034
+ async function createSnapshot(cwd, dbId, kind, tables, note) {
4035
+ const db = await getStoreDb(cwd);
4036
+ const id = makeId("snap");
4037
+ db.prepare("INSERT INTO snapshots (id, db_id, kind, note, created_at, status) VALUES (?, ?, ?, ?, ?, ?)").run(id, dbId, kind, note, new Date().toISOString(), "running");
4038
+ for (const t of tables) {
4039
+ db.prepare("INSERT INTO snapshot_tables (snapshot_id, table_name) VALUES (?, ?)").run(id, t);
4040
+ }
4041
+ return id;
4042
+ }
4043
+ async function addSnapshotTableData(cwd, snapshotId, tableName, pkColumns, rows) {
4044
+ const db = await getStoreDb(cwd);
4045
+ const tableHasher = createHash2("sha256");
4046
+ const insertRow = db.prepare("INSERT OR IGNORE INTO snapshot_rows (snapshot_id, table_name, row_key_hash, row_key_json, row_hash, payload_hash) VALUES (?, ?, ?, ?, ?, ?)");
4047
+ const insertPayload = db.prepare("INSERT OR IGNORE INTO snapshot_payloads (payload_hash, payload_json) VALUES (?, ?)");
4048
+ for (const row of rows) {
4049
+ const rowKeyHash = createHash2("sha256").update(row.rowKeyJson).digest("hex");
4050
+ const payloadHash = hashPayload(row.payloadJson);
4051
+ tableHasher.update(row.rowHash);
4052
+ insertRow.run(snapshotId, tableName, rowKeyHash, row.rowKeyJson, row.rowHash, payloadHash);
4053
+ insertPayload.run(payloadHash, row.payloadJson);
4054
+ }
4055
+ const tableHash = tableHasher.digest("hex");
4056
+ db.prepare("UPDATE snapshot_tables SET row_count = ?, table_hash = ?, pk_columns_json = ? WHERE snapshot_id = ? AND table_name = ?").run(rows.length, tableHash, JSON.stringify(pkColumns), snapshotId, tableName);
4057
+ }
4058
+ async function finalizeSnapshot(cwd, snapshotId, error) {
4059
+ const db = await getStoreDb(cwd);
4060
+ if (error) {
4061
+ db.prepare("UPDATE snapshots SET status = 'error', error_message = ? WHERE id = ?").run(error, snapshotId);
4062
+ } else {
4063
+ db.prepare("UPDATE snapshots SET status = 'done' WHERE id = ?").run(snapshotId);
4064
+ }
4065
+ }
4066
+ async function listSnapshots(cwd, dbId) {
4067
+ const db = await getStoreDb(cwd);
4068
+ let rows;
4069
+ if (dbId) {
4070
+ rows = db.prepare("SELECT id, db_id, kind, note, created_at, status, error_message FROM snapshots WHERE db_id = ? ORDER BY created_at DESC").all(dbId);
4071
+ } else {
4072
+ rows = db.prepare("SELECT id, db_id, kind, note, created_at, status, error_message FROM snapshots ORDER BY created_at DESC").all();
4073
+ }
4074
+ return rows.map((r) => {
4075
+ const tableRows = db.prepare("SELECT table_name FROM snapshot_tables WHERE snapshot_id = ?").all(r.id);
4076
+ return {
4077
+ id: r.id,
4078
+ dbId: r.db_id,
4079
+ kind: r.kind,
4080
+ note: r.note,
4081
+ createdAt: r.created_at,
4082
+ tables: tableRows.map((t) => t.table_name),
4083
+ status: r.status,
4084
+ errorMessage: r.error_message
4085
+ };
4086
+ });
4087
+ }
4088
+ async function updateSnapshotNote(cwd, snapshotId, note) {
4089
+ const db = await getStoreDb(cwd);
4090
+ db.prepare("UPDATE snapshots SET note = ? WHERE id = ?").run(note, snapshotId);
4091
+ }
4092
+ async function deleteSnapshot(cwd, snapshotId) {
4093
+ const db = await getStoreDb(cwd);
4094
+ const payloadHashes = db.prepare("SELECT DISTINCT payload_hash FROM snapshot_rows WHERE snapshot_id = ?").all(snapshotId).map((r) => r.payload_hash);
4095
+ db.prepare("DELETE FROM snapshots WHERE id = ?").run(snapshotId);
4096
+ for (const ph of payloadHashes) {
4097
+ const used = db.prepare("SELECT 1 FROM snapshot_rows WHERE payload_hash = ? LIMIT 1").get(ph);
4098
+ if (!used) {
4099
+ db.prepare("DELETE FROM snapshot_payloads WHERE payload_hash = ?").run(ph);
4100
+ }
4101
+ }
4102
+ }
4103
+ async function computeDiffTables(cwd, beforeId, afterId) {
4104
+ const db = await getStoreDb(cwd);
4105
+ const beforeTables = db.prepare("SELECT table_name, table_hash, row_count FROM snapshot_tables WHERE snapshot_id = ?").all(beforeId);
4106
+ const afterTables = db.prepare("SELECT table_name, table_hash, row_count FROM snapshot_tables WHERE snapshot_id = ?").all(afterId);
4107
+ const beforeMap = new Map(beforeTables.map((t) => [t.table_name, t]));
4108
+ const afterMap = new Map(afterTables.map((t) => [t.table_name, t]));
4109
+ const allTables = new Set([...beforeMap.keys(), ...afterMap.keys()]);
4110
+ const results = [];
4111
+ for (const table of allTables) {
4112
+ const b = beforeMap.get(table);
4113
+ const a = afterMap.get(table);
4114
+ if (b && a && b.table_hash === a.table_hash) {
4115
+ results.push({
4116
+ tableName: table,
4117
+ insertedCount: 0,
4118
+ updatedCount: 0,
4119
+ deletedCount: 0,
4120
+ unchangedCount: b.row_count
4121
+ });
4122
+ continue;
4123
+ }
4124
+ if (!b) {
4125
+ results.push({
4126
+ tableName: table,
4127
+ insertedCount: a.row_count,
4128
+ updatedCount: 0,
4129
+ deletedCount: 0,
4130
+ unchangedCount: 0
4131
+ });
4132
+ continue;
4133
+ }
4134
+ if (!a) {
4135
+ results.push({
4136
+ tableName: table,
4137
+ insertedCount: 0,
4138
+ updatedCount: 0,
4139
+ deletedCount: b.row_count,
4140
+ unchangedCount: 0
4141
+ });
4142
+ continue;
4143
+ }
4144
+ const insertedCount = db.prepare(`SELECT COUNT(*) AS cnt
4145
+ FROM snapshot_rows a
4146
+ LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
4147
+ WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL`).get(beforeId, table, afterId, table).cnt;
4148
+ const deletedCount = db.prepare(`SELECT COUNT(*) AS cnt
4149
+ FROM snapshot_rows b
4150
+ LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
4151
+ WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL`).get(afterId, table, beforeId, table).cnt;
4152
+ const updatedCount = db.prepare(`SELECT COUNT(*) AS cnt
4153
+ FROM snapshot_rows b
4154
+ INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
4155
+ WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash`).get(afterId, table, beforeId, table).cnt;
4156
+ const unchangedCount = b.row_count - deletedCount - updatedCount;
4157
+ results.push({
4158
+ tableName: table,
4159
+ insertedCount,
4160
+ updatedCount,
4161
+ deletedCount,
4162
+ unchangedCount: Math.max(0, unchangedCount)
4163
+ });
4164
+ }
4165
+ results.sort((a, b) => {
4166
+ const aChanges = a.insertedCount + a.updatedCount + a.deletedCount;
4167
+ const bChanges = b.insertedCount + b.updatedCount + b.deletedCount;
4168
+ if (bChanges !== aChanges)
4169
+ return bChanges - aChanges;
4170
+ return a.tableName.localeCompare(b.tableName);
4171
+ });
4172
+ return results;
4173
+ }
4174
+ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit = 200) {
4175
+ const db = await getStoreDb(cwd);
4176
+ const allDiffRows = [];
4177
+ const inserted = db.prepare(`SELECT a.row_key_json, a.payload_hash
4178
+ FROM snapshot_rows a
4179
+ LEFT JOIN snapshot_rows b ON b.snapshot_id = ? AND b.table_name = ? AND b.row_key_hash = a.row_key_hash
4180
+ WHERE a.snapshot_id = ? AND a.table_name = ? AND b.row_key_hash IS NULL
4181
+ ORDER BY a.row_key_json`).all(beforeId, table, afterId, table);
4182
+ for (const r of inserted) {
4183
+ allDiffRows.push({
4184
+ change_type: "inserted",
4185
+ row_key_json: r.row_key_json,
4186
+ before_payload_hash: null,
4187
+ after_payload_hash: r.payload_hash
4188
+ });
4189
+ }
4190
+ const deleted = db.prepare(`SELECT b.row_key_json, b.payload_hash
4191
+ FROM snapshot_rows b
4192
+ LEFT JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
4193
+ WHERE b.snapshot_id = ? AND b.table_name = ? AND a.row_key_hash IS NULL
4194
+ ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
4195
+ for (const r of deleted) {
4196
+ allDiffRows.push({
4197
+ change_type: "deleted",
4198
+ row_key_json: r.row_key_json,
4199
+ before_payload_hash: r.payload_hash,
4200
+ after_payload_hash: null
4201
+ });
4202
+ }
4203
+ const updated = db.prepare(`SELECT b.row_key_json, b.payload_hash AS before_ph, a.payload_hash AS after_ph
4204
+ FROM snapshot_rows b
4205
+ INNER JOIN snapshot_rows a ON a.snapshot_id = ? AND a.table_name = ? AND a.row_key_hash = b.row_key_hash
4206
+ WHERE b.snapshot_id = ? AND b.table_name = ? AND b.row_hash != a.row_hash
4207
+ ORDER BY b.row_key_json`).all(afterId, table, beforeId, table);
4208
+ for (const r of updated) {
4209
+ allDiffRows.push({
4210
+ change_type: "updated",
4211
+ row_key_json: r.row_key_json,
4212
+ before_payload_hash: r.before_ph,
4213
+ after_payload_hash: r.after_ph
4214
+ });
4215
+ }
4216
+ allDiffRows.sort((a, b) => a.row_key_json.localeCompare(b.row_key_json));
4217
+ const total = allDiffRows.length;
4218
+ const page = allDiffRows.slice(offset, offset + limit);
4219
+ const rows = page.map((r) => {
4220
+ let beforeValues;
4221
+ let afterValues;
4222
+ if (r.before_payload_hash) {
4223
+ const payload = db.prepare("SELECT payload_json FROM snapshot_payloads WHERE payload_hash = ?").get(r.before_payload_hash);
4224
+ if (payload)
4225
+ beforeValues = JSON.parse(payload.payload_json);
4226
+ }
4227
+ if (r.after_payload_hash) {
4228
+ const payload = db.prepare("SELECT payload_json FROM snapshot_payloads WHERE payload_hash = ?").get(r.after_payload_hash);
4229
+ if (payload)
4230
+ afterValues = JSON.parse(payload.payload_json);
4231
+ }
4232
+ return {
4233
+ changeType: r.change_type,
4234
+ rowKeyJson: r.row_key_json,
4235
+ beforeValues,
4236
+ afterValues
4237
+ };
4238
+ });
4239
+ return { rows, total };
4240
+ }
4241
+ var CODE_VIEWER_DIR3 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", cachedDbClass2 = null, SCHEMA_SQL = `
4242
+ CREATE TABLE IF NOT EXISTS snapshots (
4243
+ id TEXT PRIMARY KEY,
4244
+ db_id TEXT NOT NULL,
4245
+ kind TEXT NOT NULL,
4246
+ note TEXT NOT NULL DEFAULT '',
4247
+ created_at TEXT NOT NULL,
4248
+ status TEXT NOT NULL DEFAULT 'running',
4249
+ error_message TEXT
4250
+ );
4251
+
4252
+ CREATE TABLE IF NOT EXISTS snapshot_tables (
4253
+ snapshot_id TEXT NOT NULL,
4254
+ table_name TEXT NOT NULL,
4255
+ row_count INTEGER NOT NULL DEFAULT 0,
4256
+ table_hash TEXT NOT NULL DEFAULT '',
4257
+ pk_columns_json TEXT NOT NULL DEFAULT '[]',
4258
+ PRIMARY KEY (snapshot_id, table_name),
4259
+ FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE
4260
+ );
4261
+
4262
+ CREATE TABLE IF NOT EXISTS snapshot_rows (
4263
+ snapshot_id TEXT NOT NULL,
4264
+ table_name TEXT NOT NULL,
4265
+ row_key_hash TEXT NOT NULL,
4266
+ row_key_json TEXT NOT NULL,
4267
+ row_hash TEXT NOT NULL,
4268
+ payload_hash TEXT NOT NULL,
4269
+ PRIMARY KEY (snapshot_id, table_name, row_key_hash),
4270
+ FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE
4271
+ );
4272
+
4273
+ CREATE TABLE IF NOT EXISTS snapshot_payloads (
4274
+ payload_hash TEXT PRIMARY KEY,
4275
+ payload_json TEXT NOT NULL
4276
+ );
4277
+
4278
+ `, storeDb = null, storeDbPath = null;
4279
+ var init_snapshot_store = () => {};
4280
+
4281
+ // web-src/server/database/snapshot-runner.ts
4282
+ import { createHash as createHash3 } from "node:crypto";
4283
+ function normalizeValue(v) {
4284
+ if (v === null)
4285
+ return "\\N";
4286
+ if (v instanceof Uint8Array) {
4287
+ return `\\x${Buffer.from(v).toString("hex")}`;
4288
+ }
4289
+ return String(v);
4290
+ }
4291
+ function rowToPayloadJson(columns, row) {
4292
+ const obj = {};
4293
+ for (let i = 0;i < columns.length; i++) {
4294
+ obj[columns[i]] = row[i] instanceof Uint8Array ? `<blob ${row[i].byteLength} bytes>` : row[i];
4295
+ }
4296
+ return JSON.stringify(obj);
4297
+ }
4298
+ function computeRowHash(columns, row) {
4299
+ const parts = columns.map((_, i) => normalizeValue(row[i]));
4300
+ return createHash3("sha256").update(parts.join("\t")).digest("hex");
4301
+ }
4302
+ function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
4303
+ if (pkColumns.length === 0) {
4304
+ return JSON.stringify({ __rowIndex: rowIndex });
4305
+ }
4306
+ const keyObj = {};
4307
+ for (const pk of pkColumns) {
4308
+ const idx = allColumns.indexOf(pk);
4309
+ if (idx >= 0)
4310
+ keyObj[pk] = row[idx];
4311
+ }
4312
+ return JSON.stringify(keyObj);
4313
+ }
4314
+ async function runSnapshot(cwd, adapter, dbId, tables, note, onProgress) {
4315
+ const snapshotId = await createSnapshot(cwd, dbId, adapter.kind, tables, note);
4316
+ try {
4317
+ for (const table of tables) {
4318
+ onProgress?.(table, false);
4319
+ const columns = adapter.getColumns(table);
4320
+ const colNames = columns.map((c) => c.name);
4321
+ const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
4322
+ let offset = 0;
4323
+ let rowIndex = 0;
4324
+ const allRows = [];
4325
+ for (;; ) {
4326
+ const result = adapter.getTablePage(table, {
4327
+ offset,
4328
+ limit: BATCH_SIZE
4329
+ });
4330
+ if (result.rows.length === 0)
4331
+ break;
4332
+ for (const row of result.rows) {
4333
+ const rowKeyJson = buildRowKeyJson(pkColumns, colNames, row, rowIndex);
4334
+ const rowHash = computeRowHash(colNames, row);
4335
+ const payloadJson = rowToPayloadJson(colNames, row);
4336
+ allRows.push({ rowKeyJson, rowHash, payloadJson });
4337
+ rowIndex++;
4338
+ }
4339
+ offset += result.rows.length;
4340
+ if (result.rows.length < BATCH_SIZE)
4341
+ break;
4342
+ }
4343
+ await addSnapshotTableData(cwd, snapshotId, table, pkColumns, allRows);
4344
+ }
4345
+ await finalizeSnapshot(cwd, snapshotId);
4346
+ onProgress?.("", true);
4347
+ return snapshotId;
4348
+ } catch (err) {
4349
+ const msg = err instanceof Error ? err.message : String(err);
4350
+ await finalizeSnapshot(cwd, snapshotId, msg);
4351
+ throw err;
4352
+ }
4353
+ }
4354
+ var BATCH_SIZE = 500;
4355
+ var init_snapshot_runner = __esm(() => {
4356
+ init_snapshot_store();
4357
+ });
4358
+
4359
+ // web-src/server/database/handle.ts
4360
+ var exports_handle = {};
4361
+ __export(exports_handle, {
4362
+ handleDatabaseRoute: () => handleDatabaseRoute
4363
+ });
4364
+ import { randomBytes as randomBytes2 } from "node:crypto";
4365
+ function ensureInit() {
4366
+ if (initialized)
4367
+ return;
4368
+ setAdapterFactory(sqliteAdapterFactory);
4369
+ initialized = true;
4370
+ }
4371
+ async function getAdapter(r, cwd) {
4372
+ if (r.docker) {
4373
+ const key = r.dbId;
4374
+ const cached = dockerAdapterCache.get(key);
4375
+ if (cached)
4376
+ return cached;
4377
+ const adapter = openDockerAdapter(r.docker.serviceName, r.docker.kind, r.docker.env, cwd, r.docker.database);
4378
+ dockerAdapterCache.set(key, adapter);
4379
+ return adapter;
4380
+ }
4381
+ return getConnection(r.resolved);
4382
+ }
4383
+ function json(data, status = 200) {
4384
+ return new Response(JSON.stringify(data), {
4385
+ status,
4386
+ headers: {
4387
+ "Content-Type": "application/json; charset=utf-8",
4388
+ "Cache-Control": "no-store"
4389
+ }
4390
+ });
4391
+ }
4392
+ function textError(message, status) {
4393
+ return new Response(message, {
4394
+ status,
4395
+ headers: {
4396
+ "Content-Type": "text/plain; charset=utf-8",
4397
+ "Cache-Control": "no-store"
4398
+ }
4399
+ });
4400
+ }
4401
+ function sanitizeFilename(name) {
4402
+ return name.replace(/["\\\r\n\x00-\x1f]/g, "_");
4403
+ }
4404
+ function getDockerDbs(cwd) {
4405
+ if (cachedDockerCwd === cwd && cachedDockerDbs)
4406
+ return cachedDockerDbs;
4407
+ cachedDockerDbs = discoverDockerDatabases(cwd);
4408
+ cachedDockerCwd = cwd;
4409
+ return cachedDockerDbs;
4410
+ }
4411
+ function resolveDb(cwd, dbParam) {
4412
+ if (!dbParam)
4413
+ return textError("missing db parameter", 400);
4414
+ if (dbParam.startsWith("docker:")) {
4415
+ const rest = dbParam.slice(7);
4416
+ const colonIdx = rest.indexOf(":");
4417
+ const serviceName = colonIdx >= 0 ? rest.slice(0, colonIdx) : rest;
4418
+ const dbName = colonIdx >= 0 ? rest.slice(colonIdx + 1) : undefined;
4419
+ const dockerDbs = getDockerDbs(cwd);
4420
+ const info = dockerDbs.find((d) => d.serviceName === serviceName);
4421
+ if (!info)
4422
+ return textError("docker service not found", 404);
4423
+ const resolved2 = dbName ? { ...info, database: dbName } : info;
4424
+ return { resolved: dbParam, dbId: dbParam, docker: resolved2 };
4425
+ }
4426
+ const resolved = validateDbPath(cwd, dbParam);
4427
+ if (!resolved)
4428
+ return textError("invalid database path", 400);
4429
+ return { resolved, dbId: dbParam };
4430
+ }
4431
+ function handleFiles(cwd, omitDirNames) {
4432
+ const sqliteFiles = discoverSqliteFiles(cwd, omitDirNames);
4433
+ const dockerServices = discoverDockerDatabases(cwd);
4434
+ const dockerEntries = [];
4435
+ for (const svc of dockerServices) {
4436
+ const dbs = listDockerDatabases(svc.serviceName, svc.kind, svc.env, cwd);
4437
+ if (dbs.length <= 1) {
4438
+ dockerEntries.push(svc);
4439
+ } else {
4440
+ for (const db of dbs) {
4441
+ dockerEntries.push({
4442
+ ...svc,
4443
+ id: `docker:${svc.serviceName}:${db}`,
4444
+ name: svc.name.replace(/\)$/, ` / ${db})`),
4445
+ database: db
4446
+ });
4447
+ }
4448
+ }
4449
+ }
4450
+ const body = {
4451
+ files: [
4452
+ ...sqliteFiles.map((f) => ({
4453
+ id: f.path,
4454
+ path: f.path,
4455
+ name: f.name,
4456
+ sizeBytes: f.sizeBytes,
4457
+ kind: "sqlite"
4458
+ })),
4459
+ ...dockerEntries
4460
+ ]
4461
+ };
4462
+ return json(body);
4463
+ }
4464
+ async function handleSchema(cwd, url) {
4465
+ const r = resolveDb(cwd, url.searchParams.get("db"));
4466
+ if (r instanceof Response)
4467
+ return r;
4468
+ const includeColumns = url.searchParams.get("includeColumns") === "1";
4469
+ try {
4470
+ const adapter = await getAdapter(r, cwd);
4471
+ const tables = adapter.getTables();
4472
+ const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
4473
+ let countMap;
4474
+ if (adapter.getTableRowCounts) {
4475
+ countMap = adapter.getTableRowCounts(tableNames);
4476
+ } else {
4477
+ countMap = new Map;
4478
+ for (const name of tableNames) {
4479
+ countMap.set(name, adapter.getTableRowCount(name));
4480
+ }
4481
+ }
4482
+ const tablesWithCount = tables.map((t) => ({
4483
+ ...t,
4484
+ rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
4485
+ }));
4486
+ const indexes = adapter.getIndexes();
4487
+ const foreignKeys = adapter.getForeignKeys();
4488
+ const body = {
4489
+ dbId: r.dbId,
4490
+ tables: tablesWithCount,
4491
+ indexes,
4492
+ foreignKeys
4493
+ };
4494
+ if (includeColumns) {
4495
+ let colsMap;
4496
+ if (adapter.getColumnsMulti) {
4497
+ colsMap = adapter.getColumnsMulti(tableNames);
4498
+ } else {
4499
+ colsMap = new Map;
4500
+ for (const name of tableNames) {
4501
+ colsMap.set(name, adapter.getColumns(name));
4502
+ }
4503
+ }
4504
+ body.columnsMap = Object.fromEntries(colsMap);
4505
+ }
4506
+ return json(body);
4507
+ } catch (err) {
4508
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4509
+ return textError(`failed to read schema: ${err instanceof Error ? err.message : String(err)}`, 500);
4510
+ }
4511
+ }
4512
+ function sanitizeIdentifier4(name, kind = "sqlite") {
4513
+ if (kind === "mysql")
4514
+ return `\`${name.replace(/`/g, "``")}\``;
4515
+ return `"${name.replace(/"/g, '""')}"`;
4516
+ }
4517
+ function escapeSqlString2(value) {
4518
+ return `'${value.replace(/'/g, "''")}'`;
4519
+ }
4520
+ function buildFilterWhere(grouped, kind) {
4521
+ const whereParts = [];
4522
+ const params = [];
4523
+ const useParams = kind === "sqlite";
4524
+ for (const [value, cols] of grouped) {
4525
+ const likeVal = useParams ? "?" : escapeSqlString2(`%${value}%`);
4526
+ if (cols.length === 1) {
4527
+ const cast = kind === "mysql" ? `CAST(${sanitizeIdentifier4(cols[0], kind)} AS CHAR)` : `CAST(${sanitizeIdentifier4(cols[0], kind)} AS TEXT)`;
4528
+ whereParts.push(`${cast} LIKE ${likeVal}`);
4529
+ if (useParams)
4530
+ params.push(`%${value}%`);
4531
+ } else {
4532
+ const orParts = cols.map((c) => {
4533
+ const cast = kind === "mysql" ? `CAST(${sanitizeIdentifier4(c, kind)} AS CHAR)` : `CAST(${sanitizeIdentifier4(c, kind)} AS TEXT)`;
4534
+ return `${cast} LIKE ${likeVal}`;
4535
+ });
4536
+ whereParts.push(`(${orParts.join(" OR ")})`);
4537
+ if (useParams) {
4538
+ for (let i = 0;i < cols.length; i++)
4539
+ params.push(`%${value}%`);
4540
+ }
4541
+ }
4542
+ }
4543
+ return { where: whereParts.join(" AND "), params, useParams };
4544
+ }
4545
+ function parseFilters(url) {
4546
+ const raw = url.searchParams.get("filters");
4547
+ if (!raw)
4548
+ return [];
4549
+ try {
4550
+ const parsed = JSON.parse(raw);
4551
+ if (!Array.isArray(parsed))
4552
+ return [];
4553
+ return parsed.filter((f) => !!f && typeof f === "object" && typeof f.column === "string" && typeof f.value === "string");
4554
+ } catch {
4555
+ return [];
4556
+ }
4557
+ }
4558
+ async function handleTable(cwd, url) {
4559
+ const r = resolveDb(cwd, url.searchParams.get("db"));
4560
+ if (r instanceof Response)
4561
+ return r;
4562
+ const table = url.searchParams.get("table");
4563
+ if (!table)
4564
+ return textError("missing table parameter", 400);
4565
+ const offset = Math.max(0, Number(url.searchParams.get("offset") || "0") || 0);
4566
+ const limit = Math.min(1000, Math.max(1, Number(url.searchParams.get("limit") || "200") || 200));
4567
+ let orderBy;
4568
+ const sortCol = url.searchParams.get("sort");
4569
+ const sortDir = url.searchParams.get("dir");
4570
+ if (sortCol) {
4571
+ orderBy = [
4572
+ {
4573
+ column: sortCol,
4574
+ direction: sortDir === "desc" ? "desc" : "asc"
4575
+ }
4576
+ ];
4577
+ }
4578
+ const filters = parseFilters(url);
4579
+ try {
4580
+ const adapter = await getAdapter(r, cwd);
4581
+ const columns = adapter.getColumns(table);
4582
+ const colNames = new Set(columns.map((c) => c.name));
4583
+ if (sortCol && !colNames.has(sortCol)) {
4584
+ return textError(`invalid sort column: ${sortCol}`, 400);
4585
+ }
4586
+ if (filters.length > 0) {
4587
+ const validFilters = filters.filter((f) => colNames.has(f.column));
4588
+ if (validFilters.length > 0) {
4589
+ const grouped = new Map;
4590
+ for (const f of validFilters) {
4591
+ const existing = grouped.get(f.value) || [];
4592
+ existing.push(f.column);
4593
+ grouped.set(f.value, existing);
4594
+ }
4595
+ const k = adapter.kind;
4596
+ const filter = buildFilterWhere(grouped, k);
4597
+ const order = orderBy ? ` ORDER BY ${sanitizeIdentifier4(orderBy[0].column, k)} ${orderBy[0].direction === "desc" ? "DESC" : "ASC"}` : "";
4598
+ const tbl = sanitizeIdentifier4(table, k);
4599
+ const countSql = `SELECT COUNT(*) AS cnt FROM ${tbl} WHERE ${filter.where}`;
4600
+ const limitOffset = filter.useParams ? "LIMIT ? OFFSET ?" : `LIMIT ${limit} OFFSET ${offset}`;
4601
+ const dataSql = `SELECT * FROM ${tbl} WHERE ${filter.where}${order} ${limitOffset}`;
4602
+ const countResult = adapter.executeReadonlyQuery(countSql, filter.useParams ? filter.params : undefined);
4603
+ const totalRows2 = countResult.rows.length > 0 ? Number(countResult.rows[0][0]) || 0 : 0;
4604
+ const dataResult = adapter.executeReadonlyQuery(dataSql, filter.useParams ? [...filter.params, limit, offset] : undefined);
4605
+ const body2 = {
4606
+ dbId: r.dbId,
4607
+ table,
4608
+ columns,
4609
+ rows: dataResult.rows,
4610
+ totalRows: totalRows2,
4611
+ offset,
4612
+ limit,
4613
+ hasMore: offset + dataResult.rowCount < totalRows2
4614
+ };
4615
+ return json(body2);
4616
+ }
4617
+ }
4618
+ const result = adapter.getTablePage(table, { offset, limit, orderBy });
4619
+ const totalRows = result.rowCount < limit ? offset + result.rowCount : adapter.getTableRowCount(table);
4620
+ const body = {
4621
+ dbId: r.dbId,
4622
+ table,
4623
+ columns,
4624
+ rows: result.rows,
4625
+ totalRows,
4626
+ offset,
4627
+ limit,
4628
+ hasMore: offset + result.rowCount < totalRows
4629
+ };
4630
+ return json(body);
4631
+ } catch (err) {
4632
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4633
+ return textError(`failed to read table: ${err instanceof Error ? err.message : String(err)}`, 500);
4634
+ }
4635
+ }
4636
+ function makeHistoryId() {
4637
+ return `qh-${randomBytes2(8).toString("hex")}`;
4638
+ }
4639
+ async function handleQuery(cwd, req, sendSse) {
4640
+ if (req.method !== "POST")
4641
+ return textError("method not allowed", 405);
4642
+ let body;
4643
+ try {
4644
+ body = await req.json();
4645
+ } catch {
4646
+ return textError("invalid JSON body", 400);
4647
+ }
4648
+ if (!body.db || !body.sql)
4649
+ return textError("missing db or sql", 400);
4650
+ const r = resolveDb(cwd, body.db);
4651
+ if (r instanceof Response)
4652
+ return r;
4653
+ const maxRows = Math.min(1e4, Math.max(1, body.maxRows || 1000));
4654
+ const start = Date.now();
4655
+ try {
4656
+ const adapter = await getAdapter(r, cwd);
4657
+ const result = adapter.executeReadonlyQuery(body.sql, undefined, maxRows);
4658
+ const elapsed = Date.now() - start;
4659
+ const response = {
4660
+ dbId: body.db,
4661
+ columns: result.columns,
4662
+ columnTypes: result.columnTypes,
4663
+ rows: result.rows,
4664
+ rowCount: result.rowCount,
4665
+ truncated: result.rowCount >= maxRows,
4666
+ elapsedMs: elapsed
4667
+ };
4668
+ if (body.saveHistory) {
4669
+ const entry = {
4670
+ id: makeHistoryId(),
4671
+ dbId: body.db,
4672
+ sql: body.sql,
4673
+ title: body.title,
4674
+ body: body.body,
4675
+ columns: result.columns,
4676
+ rowsPreview: result.rows,
4677
+ rowCount: result.rowCount,
4678
+ savedRows: result.rows.length,
4679
+ truncated: result.rowCount >= maxRows,
4680
+ elapsedMs: elapsed,
4681
+ executedAt: new Date().toISOString(),
4682
+ executedBy: body.executedBy || "user",
4683
+ source: body.source || "browser"
4684
+ };
4685
+ const state = loadQueryHistory(cwd);
4686
+ const updated = addQueryHistoryEntry(state, entry);
4687
+ saveQueryHistory(cwd, updated);
4688
+ sendSse?.("db-query", JSON.stringify({ action: "add", id: entry.id }));
4689
+ }
4690
+ return json(response);
4691
+ } catch (err) {
4692
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4693
+ const elapsed = Date.now() - start;
4694
+ const response = {
4695
+ dbId: body.db,
4696
+ columns: [],
4697
+ columnTypes: [],
4698
+ rows: [],
4699
+ rowCount: 0,
4700
+ truncated: false,
4701
+ elapsedMs: elapsed,
4702
+ error: err instanceof Error ? err.message : String(err)
4703
+ };
4704
+ return json(response, 400);
4705
+ }
4706
+ }
4707
+ function handleHistory(cwd, url) {
4708
+ const dbId = url.searchParams.get("db") || undefined;
4709
+ const state = loadQueryHistory(cwd);
4710
+ if (dbId) {
4711
+ return json({
4712
+ version: 1,
4713
+ entries: state.entries.filter((e) => e.dbId === dbId)
4714
+ });
4715
+ }
4716
+ return json(state);
4717
+ }
4718
+ async function handleHistoryDelete(cwd, req, sendSse) {
4719
+ if (req.method !== "POST")
4720
+ return textError("method not allowed", 405);
4721
+ let body;
4722
+ try {
4723
+ body = await req.json();
4724
+ } catch {
4725
+ return textError("invalid JSON body", 400);
4726
+ }
4727
+ if (!body.id)
4728
+ return textError("missing id", 400);
4729
+ const state = loadQueryHistory(cwd);
4730
+ const updated = deleteQueryHistoryEntry(state, body.id);
4731
+ saveQueryHistory(cwd, updated);
4732
+ sendSse?.("db-query", JSON.stringify({ action: "delete", id: body.id }));
4733
+ return json({ ok: true });
4734
+ }
4735
+ async function handleHistoryClear(cwd, req, sendSse) {
4736
+ if (req.method !== "POST")
4737
+ return textError("method not allowed", 405);
4738
+ let body;
4739
+ try {
4740
+ body = await req.json();
4741
+ } catch {
4742
+ body = {};
4743
+ }
4744
+ const state = loadQueryHistory(cwd);
4745
+ const updated = clearQueryHistory(state, body.db);
4746
+ saveQueryHistory(cwd, updated);
4747
+ sendSse?.("db-query", JSON.stringify({ action: "clear" }));
4748
+ return json({ ok: true });
4749
+ }
4750
+ function formatCsvField(value) {
4751
+ if (value === null || value === undefined)
4752
+ return "";
4753
+ if (value instanceof Uint8Array)
4754
+ return `<blob ${value.byteLength} bytes>`;
4755
+ const str = typeof value === "object" ? JSON.stringify(value) : String(value);
4756
+ if (str.includes(",") || str.includes('"') || str.includes(`
4757
+ `) || str.includes("\r")) {
4758
+ return `"${str.replace(/"/g, '""')}"`;
4759
+ }
4760
+ return str;
4761
+ }
4762
+ async function handleExport(cwd, url) {
4763
+ const r = resolveDb(cwd, url.searchParams.get("db"));
4764
+ if (r instanceof Response)
4765
+ return r;
4766
+ const table = url.searchParams.get("table");
4767
+ if (!table)
4768
+ return textError("missing table parameter", 400);
4769
+ const format = url.searchParams.get("format");
4770
+ if (format !== "csv" && format !== "json")
4771
+ return textError("format must be csv or json", 400);
4772
+ let orderBy;
4773
+ const sortCol = url.searchParams.get("sort");
4774
+ const sortDir = url.searchParams.get("dir");
4775
+ if (sortCol) {
4776
+ orderBy = [
4777
+ {
4778
+ column: sortCol,
4779
+ direction: sortDir === "desc" ? "desc" : "asc"
4780
+ }
4781
+ ];
4782
+ }
4783
+ const filters = parseFilters(url);
4784
+ try {
4785
+ const adapter = await getAdapter(r, cwd);
4786
+ const columns = adapter.getColumns(table);
4787
+ const colNames = columns.map((c) => c.name);
4788
+ const colNameSet = new Set(colNames);
4789
+ if (sortCol && !colNameSet.has(sortCol)) {
4790
+ return textError(`invalid sort column: ${sortCol}`, 400);
4791
+ }
4792
+ let rows;
4793
+ if (filters.length > 0) {
4794
+ const validFilters = filters.filter((f) => colNameSet.has(f.column));
4795
+ if (validFilters.length > 0) {
4796
+ const grouped = new Map;
4797
+ for (const f of validFilters) {
4798
+ const existing = grouped.get(f.value) || [];
4799
+ existing.push(f.column);
4800
+ grouped.set(f.value, existing);
4801
+ }
4802
+ const k = adapter.kind;
4803
+ const filter = buildFilterWhere(grouped, k);
4804
+ const order = orderBy ? ` ORDER BY ${sanitizeIdentifier4(orderBy[0].column, k)} ${orderBy[0].direction === "desc" ? "DESC" : "ASC"}` : "";
4805
+ const tbl = sanitizeIdentifier4(table, k);
4806
+ const limitOffset = filter.useParams ? "LIMIT ? OFFSET ?" : `LIMIT ${EXPORT_MAX_ROWS} OFFSET 0`;
4807
+ const dataSql = `SELECT * FROM ${tbl} WHERE ${filter.where}${order} ${limitOffset}`;
4808
+ const result = adapter.executeReadonlyQuery(dataSql, filter.useParams ? [...filter.params, EXPORT_MAX_ROWS, 0] : undefined);
4809
+ rows = result.rows;
4810
+ } else {
4811
+ const result = adapter.getTablePage(table, {
4812
+ offset: 0,
4813
+ limit: EXPORT_MAX_ROWS,
4814
+ orderBy
4815
+ });
4816
+ rows = result.rows;
4817
+ }
4818
+ } else {
4819
+ const result = adapter.getTablePage(table, {
4820
+ offset: 0,
4821
+ limit: EXPORT_MAX_ROWS,
4822
+ orderBy
4823
+ });
4824
+ rows = result.rows;
4825
+ }
4826
+ if (format === "csv") {
4827
+ const lines = [colNames.map(formatCsvField).join(",")];
4828
+ for (const row of rows) {
4829
+ lines.push(row.map(formatCsvField).join(","));
4830
+ }
4831
+ const body2 = lines.join(`
4832
+ `);
4833
+ return new Response(body2, {
4834
+ status: 200,
4835
+ headers: {
4836
+ "Content-Type": "text/csv; charset=utf-8",
4837
+ "Content-Disposition": `attachment; filename="${sanitizeFilename(table)}.csv"`,
4838
+ "Cache-Control": "no-store"
4839
+ }
4840
+ });
4841
+ }
4842
+ const objects = rows.map((row) => {
4843
+ const obj = {};
4844
+ for (let i = 0;i < colNames.length; i++) {
4845
+ const val = row[i];
4846
+ obj[colNames[i]] = val instanceof Uint8Array ? `<blob ${val.byteLength} bytes>` : val;
4847
+ }
4848
+ return obj;
4849
+ });
4850
+ const body = JSON.stringify(objects, null, 2);
4851
+ return new Response(body, {
4852
+ status: 200,
4853
+ headers: {
4854
+ "Content-Type": "application/json; charset=utf-8",
4855
+ "Content-Disposition": `attachment; filename="${sanitizeFilename(table)}.json"`,
4856
+ "Cache-Control": "no-store"
4857
+ }
4858
+ });
4859
+ } catch (err) {
4860
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4861
+ return textError(`failed to export table: ${err instanceof Error ? err.message : String(err)}`, 500);
4862
+ }
4863
+ }
4864
+ async function handleColumns(cwd, url) {
4865
+ const r = resolveDb(cwd, url.searchParams.get("db"));
4866
+ if (r instanceof Response)
4867
+ return r;
4868
+ const table = url.searchParams.get("table");
4869
+ if (!table)
4870
+ return textError("missing table parameter", 400);
4871
+ try {
4872
+ const adapter = await getAdapter(r, cwd);
4873
+ const columns = adapter.getColumns(table);
4874
+ return json({ dbId: r.dbId, table, columns });
4875
+ } catch (err) {
4876
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4877
+ return textError(`failed to get columns: ${err instanceof Error ? err.message : String(err)}`, 500);
4878
+ }
2327
4879
  }
2328
- function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames = []) {
2329
- const prefix = `${ref}:`;
2330
- const normalized = stdout.split(`
2331
- `).map((line) => line.startsWith(prefix) ? line.slice(prefix.length) : line).join(`
2332
- `);
2333
- return parseRgOutput(normalized, max, omitDirNames, excludeNames);
4880
+ async function handleDdl(cwd, url) {
4881
+ const r = resolveDb(cwd, url.searchParams.get("db"));
4882
+ if (r instanceof Response)
4883
+ return r;
4884
+ const table = url.searchParams.get("table");
4885
+ if (!table)
4886
+ return textError("missing table parameter", 400);
4887
+ try {
4888
+ const adapter = await getAdapter(r, cwd);
4889
+ const sql = adapter.getCreateStatement(table);
4890
+ const triggers = adapter.getTriggers(table);
4891
+ return json({ dbId: r.dbId, table, sql, triggers });
4892
+ } catch (err) {
4893
+ console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
4894
+ return textError(`failed to get DDL: ${err instanceof Error ? err.message : String(err)}`, 500);
4895
+ }
2334
4896
  }
2335
- var GREP_DEFAULT_MAX = 200, GREP_ABSOLUTE_MAX = 500, GREP_MAX_FILE_BYTES, FILE_SEARCH_ABSOLUTE_MAX = 50000, DEFAULT_EXCLUDE_NAMES;
2336
- var init_search = __esm(() => {
2337
- GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
2338
- DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
2339
- });
2340
-
2341
- // web-src/server/worktree-watcher.ts
2342
- import {
2343
- lstatSync as lstatSync3,
2344
- readdirSync as nodeReaddirSync,
2345
- watch as nodeWatch
2346
- } from "node:fs";
2347
- import { join as join7, relative } from "node:path";
2348
- function normalizeRelativePath(path) {
2349
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
4897
+ async function handleSearchStart(cwd, req) {
4898
+ if (req.method !== "POST")
4899
+ return textError("method not allowed", 405);
4900
+ let body;
4901
+ try {
4902
+ body = await req.json();
4903
+ } catch {
4904
+ return textError("invalid JSON body", 400);
4905
+ }
4906
+ if (!body.db || !body.term)
4907
+ return textError("missing db or term", 400);
4908
+ const r = resolveDb(cwd, body.db);
4909
+ if (r instanceof Response)
4910
+ return r;
4911
+ const jobId = `search-${randomBytes2(8).toString("hex")}`;
4912
+ const ac = new AbortController;
4913
+ const job = {
4914
+ id: jobId,
4915
+ dbId: body.db,
4916
+ scannedTables: 0,
4917
+ totalTables: 0,
4918
+ hits: [],
4919
+ done: false,
4920
+ abortController: ac
4921
+ };
4922
+ searchJobs.set(jobId, job);
4923
+ setTimeout(() => searchJobs.delete(jobId), 5 * 60000);
4924
+ const term = body.term;
4925
+ const maxHitsPerTable = body.maxHitsPerTable ?? 50;
4926
+ const includeNonText = body.includeNonText ?? false;
4927
+ const filterTables = body.tables;
4928
+ (async () => {
4929
+ try {
4930
+ const adapter = await getAdapter(r, cwd);
4931
+ let tables = adapter.getTables().filter((t) => t.type === "table").map((t) => t.name);
4932
+ if (filterTables && filterTables.length > 0) {
4933
+ const allowed = new Set(filterTables);
4934
+ tables = tables.filter((t) => allowed.has(t));
4935
+ }
4936
+ let countMap;
4937
+ if (adapter.getTableRowCounts) {
4938
+ countMap = adapter.getTableRowCounts(tables);
4939
+ } else {
4940
+ countMap = new Map;
4941
+ for (const t of tables)
4942
+ countMap.set(t, adapter.getTableRowCount(t));
4943
+ }
4944
+ tables.sort((a, b) => (countMap.get(a) ?? 0) - (countMap.get(b) ?? 0));
4945
+ job.totalTables = tables.length;
4946
+ for (const table of tables) {
4947
+ if (ac.signal.aborted)
4948
+ break;
4949
+ job.currentTable = table;
4950
+ const pkCols = getPrimaryKeyColumns(adapter, table);
4951
+ const columns = adapter.getColumns(table);
4952
+ const hits = searchTable(adapter, table, columns, term, maxHitsPerTable, includeNonText, pkCols);
4953
+ job.hits.push(...hits);
4954
+ job.scannedTables++;
4955
+ }
4956
+ job.done = true;
4957
+ job.currentTable = undefined;
4958
+ } catch (err) {
4959
+ job.error = err instanceof Error ? err.message : String(err);
4960
+ job.done = true;
4961
+ }
4962
+ })();
4963
+ return json({ jobId });
4964
+ }
4965
+ function handleSearchStatus(url) {
4966
+ const jobId = url.searchParams.get("id");
4967
+ if (!jobId)
4968
+ return textError("missing id", 400);
4969
+ const job = searchJobs.get(jobId);
4970
+ if (!job)
4971
+ return textError("job not found", 404);
4972
+ const result = {
4973
+ jobId: job.id,
4974
+ dbId: job.dbId,
4975
+ scannedTables: job.scannedTables,
4976
+ totalTables: job.totalTables,
4977
+ currentTable: job.currentTable,
4978
+ hits: job.hits,
4979
+ done: job.done,
4980
+ error: job.error
4981
+ };
4982
+ if (job.done) {
4983
+ setTimeout(() => searchJobs.delete(jobId), 60000);
4984
+ }
4985
+ return json(result);
2350
4986
  }
2351
- function isInsideRoot(root, path) {
2352
- const rel = relative(root, path).replace(/\\/g, "/");
2353
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
4987
+ async function handleSearchCancel(req) {
4988
+ if (req.method !== "POST")
4989
+ return textError("method not allowed", 405);
4990
+ let body;
4991
+ try {
4992
+ body = await req.json();
4993
+ } catch {
4994
+ return textError("invalid JSON body", 400);
4995
+ }
4996
+ if (!body.id)
4997
+ return textError("missing id", 400);
4998
+ const job = searchJobs.get(body.id);
4999
+ if (!job)
5000
+ return textError("job not found", 404);
5001
+ job.abortController.abort();
5002
+ job.done = true;
5003
+ return json({ ok: true });
2354
5004
  }
2355
- function startWorktreeUpdateWatch(options) {
2356
- const watch = options.watch || nodeWatch;
2357
- const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
2358
- const isDirectory = options.isDirectory || ((path) => {
5005
+ async function handleSnapshotList(cwd, url) {
5006
+ const dbId = url.searchParams.get("db") || undefined;
5007
+ const snapshots = await listSnapshots(cwd, dbId);
5008
+ return json({ snapshots });
5009
+ }
5010
+ async function handleSnapshotCreate(cwd, req, sendSse) {
5011
+ if (req.method !== "POST")
5012
+ return textError("method not allowed", 405);
5013
+ let body;
5014
+ try {
5015
+ body = await req.json();
5016
+ } catch {
5017
+ return textError("invalid JSON body", 400);
5018
+ }
5019
+ if (!body.db)
5020
+ return textError("missing db", 400);
5021
+ const r = resolveDb(cwd, body.db);
5022
+ if (r instanceof Response)
5023
+ return r;
5024
+ const adapter = await getAdapter(r, cwd);
5025
+ let tables = body.tables;
5026
+ if (!tables || tables.length === 0) {
5027
+ tables = adapter.getTables().filter((t) => t.type === "table").map((t) => t.name);
5028
+ }
5029
+ const note = body.note ?? "";
5030
+ (async () => {
2359
5031
  try {
2360
- return lstatSync3(path).isDirectory();
2361
- } catch {
2362
- return false;
5032
+ const snapshotId = await runSnapshot(cwd, adapter, body.db, tables, note, (table, done) => {
5033
+ sendSse?.("db-snapshot", JSON.stringify({ action: "progress", table, done }));
5034
+ });
5035
+ sendSse?.("db-snapshot", JSON.stringify({ action: "created", id: snapshotId }));
5036
+ } catch (err) {
5037
+ console.error("[code-viewer] snapshot error:", err instanceof Error ? err.message : String(err));
5038
+ sendSse?.("db-snapshot", JSON.stringify({
5039
+ action: "error",
5040
+ error: err instanceof Error ? err.message : String(err)
5041
+ }));
2363
5042
  }
2364
- });
2365
- const directorySignature = options.directorySignature || ((path) => {
2366
- try {
2367
- const stats = lstatSync3(path);
2368
- if (!stats.isDirectory())
2369
- return null;
2370
- return `${stats.dev}:${stats.ino}`;
2371
- } catch {
2372
- return null;
5043
+ })();
5044
+ return json({ ok: true, message: "snapshot started" });
5045
+ }
5046
+ async function handleSnapshotUpdateNote(cwd, req) {
5047
+ if (req.method !== "POST")
5048
+ return textError("method not allowed", 405);
5049
+ let body;
5050
+ try {
5051
+ body = await req.json();
5052
+ } catch {
5053
+ return textError("invalid JSON body", 400);
5054
+ }
5055
+ if (!body.id)
5056
+ return textError("missing id", 400);
5057
+ await updateSnapshotNote(cwd, body.id, body.note ?? "");
5058
+ return json({ ok: true });
5059
+ }
5060
+ async function handleSnapshotDelete(cwd, req) {
5061
+ if (req.method !== "POST")
5062
+ return textError("method not allowed", 405);
5063
+ let body;
5064
+ try {
5065
+ body = await req.json();
5066
+ } catch {
5067
+ return textError("invalid JSON body", 400);
5068
+ }
5069
+ if (!body.id)
5070
+ return textError("missing id", 400);
5071
+ await deleteSnapshot(cwd, body.id);
5072
+ return json({ ok: true });
5073
+ }
5074
+ async function handleDiffTables(cwd, url) {
5075
+ const beforeId = url.searchParams.get("before");
5076
+ const afterId = url.searchParams.get("after");
5077
+ if (!beforeId || !afterId)
5078
+ return textError("missing before or after parameter", 400);
5079
+ try {
5080
+ const tables = await computeDiffTables(cwd, beforeId, afterId);
5081
+ return json({ beforeId, afterId, tables });
5082
+ } catch (err) {
5083
+ return textError(`failed to compute diff: ${err instanceof Error ? err.message : String(err)}`, 500);
5084
+ }
5085
+ }
5086
+ async function handleDiffRows(cwd, url) {
5087
+ const beforeId = url.searchParams.get("before");
5088
+ const afterId = url.searchParams.get("after");
5089
+ const table = url.searchParams.get("table");
5090
+ if (!beforeId || !afterId || !table)
5091
+ return textError("missing before, after, or table parameter", 400);
5092
+ const offset = Math.max(0, Number(url.searchParams.get("offset") || "0") || 0);
5093
+ const limit = Math.min(1000, Math.max(1, Number(url.searchParams.get("limit") || "200") || 200));
5094
+ try {
5095
+ const result = await computeDiffRows(cwd, beforeId, afterId, table, offset, limit);
5096
+ return json({ beforeId, afterId, table, ...result });
5097
+ } catch (err) {
5098
+ return textError(`failed to compute diff rows: ${err instanceof Error ? err.message : String(err)}`, 500);
5099
+ }
5100
+ }
5101
+ async function handleClose(cwd, req) {
5102
+ if (req.method !== "POST")
5103
+ return textError("method not allowed", 405);
5104
+ let body;
5105
+ try {
5106
+ body = await req.json();
5107
+ } catch {
5108
+ return textError("invalid JSON body", 400);
5109
+ }
5110
+ if (!body.db)
5111
+ return textError("missing db", 400);
5112
+ const r = resolveDb(cwd, body.db);
5113
+ if (r instanceof Response)
5114
+ return r;
5115
+ if (r.docker) {
5116
+ const cached = dockerAdapterCache.get(r.dbId);
5117
+ if (cached) {
5118
+ cached.close();
5119
+ dockerAdapterCache.delete(r.dbId);
2373
5120
  }
2374
- });
2375
- const setTimer = options.setTimeoutFn || setTimeout;
2376
- const clearTimer = options.clearTimeoutFn || clearTimeout;
2377
- const debounceMs = options.debounceMs ?? 250;
2378
- const watchers = new Map;
2379
- const signatures = new Map;
2380
- const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
2381
- const initialScanQueue = [];
2382
- let initialScanTimer = null;
2383
- let timer = null;
2384
- const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
2385
- const scheduleUpdate = () => {
2386
- if (timer)
2387
- clearTimer(timer);
2388
- timer = setTimer(() => {
2389
- timer = null;
2390
- options.onUpdate();
2391
- }, debounceMs);
5121
+ } else {
5122
+ closeConnection(r.resolved);
5123
+ }
5124
+ return json({ ok: true });
5125
+ }
5126
+ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
5127
+ ensureInit();
5128
+ const path = url.pathname;
5129
+ const start = Date.now();
5130
+ const method = req.method;
5131
+ const qs = url.search ? url.search.slice(0, 120) : "";
5132
+ const log = (status) => {
5133
+ const ms = Date.now() - start;
5134
+ console.log(`[code-viewer] ${method} ${path}${qs} ${status} ${ms}ms`);
2392
5135
  };
2393
- const closeSubtree = (dir) => {
2394
- for (const [watchedDir, watcher] of [...watchers]) {
2395
- if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
2396
- continue;
2397
- try {
2398
- watcher.close?.();
2399
- } catch {}
2400
- watchers.delete(watchedDir);
2401
- signatures.delete(watchedDir);
2402
- }
5136
+ const wrapResponse = async (handler) => {
5137
+ const res = await handler;
5138
+ if (res)
5139
+ log(res.status);
5140
+ return res;
2403
5141
  };
2404
- const closeAll = () => {
2405
- if (initialScanTimer) {
2406
- clearTimer(initialScanTimer);
2407
- initialScanTimer = null;
5142
+ if (path === "/_db/files")
5143
+ return wrapResponse(handleFiles(cwd, omitDirNames));
5144
+ if (path === "/_db/schema")
5145
+ return wrapResponse(handleSchema(cwd, url));
5146
+ if (path === "/_db/table")
5147
+ return wrapResponse(handleTable(cwd, url));
5148
+ if (path === "/_db/columns")
5149
+ return wrapResponse(handleColumns(cwd, url));
5150
+ if (path === "/_db/export")
5151
+ return wrapResponse(handleExport(cwd, url));
5152
+ if (path === "/_db/ddl")
5153
+ return wrapResponse(handleDdl(cwd, url));
5154
+ if (path === "/_db/query") {
5155
+ if (!sideEffectAllowed(req)) {
5156
+ log(403);
5157
+ return textError("forbidden", 403);
2408
5158
  }
2409
- initialScanQueue.length = 0;
2410
- for (const watcher of [...watchers.values()]) {
2411
- try {
2412
- watcher.close?.();
2413
- } catch {}
5159
+ return wrapResponse(handleQuery(cwd, req, sendSse));
5160
+ }
5161
+ if (path === "/_db/close") {
5162
+ if (!sideEffectAllowed(req)) {
5163
+ log(403);
5164
+ return textError("forbidden", 403);
2414
5165
  }
2415
- watchers.clear();
2416
- signatures.clear();
2417
- };
2418
- const readChildDirectories = (dir) => {
2419
- let entries;
2420
- try {
2421
- entries = readDirs(dir);
2422
- } catch (error) {
2423
- options.onError?.(error);
2424
- return [];
5166
+ return wrapResponse(handleClose(cwd, req));
5167
+ }
5168
+ if (path === "/_db/history")
5169
+ return wrapResponse(handleHistory(cwd, url));
5170
+ if (path === "/_db/history/delete") {
5171
+ if (!sideEffectAllowed(req)) {
5172
+ log(403);
5173
+ return textError("forbidden", 403);
2425
5174
  }
2426
- const children = [];
2427
- for (const entry of entries) {
2428
- if (!entry.isDirectory())
2429
- continue;
2430
- children.push(join7(dir, entry.name));
5175
+ return wrapResponse(handleHistoryDelete(cwd, req, sendSse));
5176
+ }
5177
+ if (path === "/_db/history/clear") {
5178
+ if (!sideEffectAllowed(req)) {
5179
+ log(403);
5180
+ return textError("forbidden", 403);
2431
5181
  }
2432
- return children;
2433
- };
2434
- const processInitialScanQueue = () => {
2435
- initialScanTimer = null;
2436
- const next = initialScanQueue.shift();
2437
- if (next)
2438
- watchDirectory(next, true);
2439
- if (initialScanQueue.length)
2440
- initialScanTimer = setTimer(processInitialScanQueue, 50);
2441
- };
2442
- const queueInitialChildren = (dir) => {
2443
- initialScanQueue.push(...readChildDirectories(dir));
2444
- if (!initialScanTimer)
2445
- initialScanTimer = setTimer(processInitialScanQueue, 5000);
2446
- };
2447
- const watchDirectory = (dir, initialScan = false) => {
2448
- if (watchers.has(dir))
2449
- return;
2450
- const rel = normalizeRelativePath(relative(options.root, dir));
2451
- if (rel && ignored(rel))
2452
- return;
2453
- try {
2454
- const watcher = watch(dir, { persistent: false }, (_event, filename) => {
2455
- if (!filename) {
2456
- scheduleUpdate();
2457
- return;
2458
- }
2459
- const changed = normalizeRelativePath(join7(rel, filename.toString()));
2460
- if (ignored(changed))
2461
- return;
2462
- const fullChangedPath = join7(options.root, changed);
2463
- if (!isInsideRoot(options.root, fullChangedPath))
2464
- return;
2465
- const known = watchers.has(fullChangedPath);
2466
- if (isDirectory(fullChangedPath)) {
2467
- if (known) {
2468
- const signature2 = directorySignature(fullChangedPath);
2469
- if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
2470
- closeSubtree(fullChangedPath);
2471
- watchDirectory(fullChangedPath);
2472
- }
2473
- scheduleUpdate();
2474
- return;
2475
- }
2476
- watchDirectory(fullChangedPath);
2477
- } else if (known) {
2478
- closeSubtree(fullChangedPath);
2479
- }
2480
- scheduleUpdate();
2481
- }) || {};
2482
- watchers.set(dir, watcher);
2483
- const signature = directorySignature(dir);
2484
- if (signature)
2485
- signatures.set(dir, signature);
2486
- watcher.on?.("error", () => {
2487
- if (watchers.get(dir) === watcher) {
2488
- watchers.delete(dir);
2489
- signatures.delete(dir);
2490
- }
2491
- });
2492
- watcher.on?.("close", () => {
2493
- if (watchers.get(dir) === watcher) {
2494
- watchers.delete(dir);
2495
- signatures.delete(dir);
2496
- }
2497
- });
2498
- } catch (error) {
2499
- options.onError?.(error);
2500
- return;
5182
+ return wrapResponse(handleHistoryClear(cwd, req, sendSse));
5183
+ }
5184
+ if (path === "/_db/search/start") {
5185
+ if (!sideEffectAllowed(req)) {
5186
+ log(403);
5187
+ return textError("forbidden", 403);
2501
5188
  }
2502
- if (initialScanAsync && initialScan) {
2503
- queueInitialChildren(dir);
2504
- return;
5189
+ return wrapResponse(handleSearchStart(cwd, req));
5190
+ }
5191
+ if (path === "/_db/search/status")
5192
+ return wrapResponse(handleSearchStatus(url));
5193
+ if (path === "/_db/search/cancel") {
5194
+ if (!sideEffectAllowed(req)) {
5195
+ log(403);
5196
+ return textError("forbidden", 403);
2505
5197
  }
2506
- for (const child of readChildDirectories(dir))
2507
- watchDirectory(child);
2508
- };
2509
- watchDirectory(options.root, true);
2510
- return { started: watchers.size > 0, close: closeAll };
5198
+ return wrapResponse(handleSearchCancel(req));
5199
+ }
5200
+ if (path === "/_db/snapshot/list")
5201
+ return wrapResponse(handleSnapshotList(cwd, url));
5202
+ if (path === "/_db/snapshot/create") {
5203
+ if (!sideEffectAllowed(req)) {
5204
+ log(403);
5205
+ return textError("forbidden", 403);
5206
+ }
5207
+ return wrapResponse(handleSnapshotCreate(cwd, req, sendSse));
5208
+ }
5209
+ if (path === "/_db/snapshot/update-note") {
5210
+ if (!sideEffectAllowed(req)) {
5211
+ log(403);
5212
+ return textError("forbidden", 403);
5213
+ }
5214
+ return wrapResponse(handleSnapshotUpdateNote(cwd, req));
5215
+ }
5216
+ if (path === "/_db/snapshot/delete") {
5217
+ if (!sideEffectAllowed(req)) {
5218
+ log(403);
5219
+ return textError("forbidden", 403);
5220
+ }
5221
+ return wrapResponse(handleSnapshotDelete(cwd, req));
5222
+ }
5223
+ if (path === "/_db/snapshot/diff/tables")
5224
+ return wrapResponse(handleDiffTables(cwd, url));
5225
+ if (path === "/_db/snapshot/diff/rows")
5226
+ return wrapResponse(handleDiffRows(cwd, url));
5227
+ return null;
2511
5228
  }
2512
- var init_worktree_watcher = __esm(() => {
2513
- init_search();
5229
+ var initialized = false, dockerAdapterCache, cachedDockerDbs = null, cachedDockerCwd = null, EXPORT_MAX_ROWS = 1e5, searchJobs;
5230
+ var init_handle = __esm(() => {
5231
+ init_docker();
5232
+ init_sqlite();
5233
+ init_connection_pool();
5234
+ init_discovery();
5235
+ init_query_history();
5236
+ init_snapshot_runner();
5237
+ init_snapshot_store();
5238
+ dockerAdapterCache = new Map;
5239
+ searchJobs = new Map;
2514
5240
  });
2515
5241
 
2516
5242
  // web-src/server/preview.ts
2517
5243
  var exports_preview = {};
2518
5244
  import {
2519
- closeSync,
5245
+ closeSync as closeSync2,
2520
5246
  constants,
2521
- existsSync as existsSync6,
2522
- lstatSync as lstatSync4,
2523
- mkdirSync as mkdirSync4,
2524
- openSync,
2525
- readFileSync as readFileSync5,
2526
- realpathSync as realpathSync2,
2527
- renameSync as renameSync2,
2528
- statSync as statSync2,
5247
+ existsSync as existsSync8,
5248
+ lstatSync as lstatSync5,
5249
+ mkdirSync as mkdirSync6,
5250
+ openSync as openSync2,
5251
+ readFileSync as readFileSync7,
5252
+ realpathSync as realpathSync4,
5253
+ renameSync as renameSync3,
5254
+ statSync as statSync3,
2529
5255
  unlinkSync as unlinkSync2,
2530
5256
  watch,
2531
- writeFileSync as writeFileSync3
5257
+ writeFileSync as writeFileSync4
2532
5258
  } from "node:fs";
2533
5259
  import { homedir as homedir3 } from "node:os";
2534
- import { basename as basename2, dirname as dirname2, extname, join as join8, relative as relative2 } from "node:path";
5260
+ import { basename as basename3, dirname as dirname2, extname, join as join11, relative as relative3 } from "node:path";
2535
5261
  function parseCli() {
2536
5262
  const rest = [];
2537
5263
  for (let i = 2;i < process.argv.length; i++) {
@@ -2561,7 +5287,7 @@ Examples:
2561
5287
  process.exit(1);
2562
5288
  }
2563
5289
  try {
2564
- cwd = repoRoot(next) || realpathSync2(next);
5290
+ cwd = repoRoot(next) || realpathSync4(next);
2565
5291
  } catch {
2566
5292
  console.error("--cwd must point to an existing directory");
2567
5293
  process.exit(1);
@@ -2603,7 +5329,7 @@ Examples:
2603
5329
  if (configScopeExcludeNames)
2604
5330
  scopeExcludeNames = configScopeExcludeNames;
2605
5331
  }
2606
- function json(data, init = {}) {
5332
+ function json2(data, init = {}) {
2607
5333
  return new Response(JSON.stringify(data), {
2608
5334
  ...init,
2609
5335
  headers: {
@@ -2670,10 +5396,10 @@ function staticFile(pathname) {
2670
5396
  const spec = map[pathname];
2671
5397
  if (!spec)
2672
5398
  return null;
2673
- const full = join8(WEB_ROOT, spec[0]);
2674
- if (!existsSync6(full))
5399
+ const full = join11(WEB_ROOT, spec[0]);
5400
+ if (!existsSync8(full))
2675
5401
  return text("not found", 404);
2676
- return new Response(readFileSync5(full), {
5402
+ return new Response(readFileSync7(full), {
2677
5403
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
2678
5404
  });
2679
5405
  }
@@ -2770,7 +5496,7 @@ function computePayload(extras, range) {
2770
5496
  files: [],
2771
5497
  totals: { files: 0, additions: 0, deletions: 0 },
2772
5498
  range: "worktree .. worktree",
2773
- project: basename2(cwd),
5499
+ project: basename3(cwd),
2774
5500
  branch: currentBranch(cwd) || undefined,
2775
5501
  generation
2776
5502
  };
@@ -2803,7 +5529,7 @@ function computePayload(extras, range) {
2803
5529
  files: meta,
2804
5530
  totals,
2805
5531
  range: label || "HEAD",
2806
- project: basename2(cwd),
5532
+ project: basename3(cwd),
2807
5533
  branch: currentBranch(cwd) || undefined,
2808
5534
  generation
2809
5535
  };
@@ -2904,21 +5630,21 @@ function parseScopeExcludeNamesQuery(value) {
2904
5630
  return normalizeScopeExcludeNames(names);
2905
5631
  }
2906
5632
  function loadProjectConfig() {
2907
- const full = join8(cwd, ".code-viewer.json");
2908
- if (!existsSync6(full))
5633
+ const full = join11(cwd, ".code-viewer.json");
5634
+ if (!existsSync8(full))
2909
5635
  return null;
2910
5636
  let realCwd;
2911
5637
  let realConfig;
2912
5638
  try {
2913
- realCwd = realpathSync2(cwd);
2914
- realConfig = realpathSync2(full);
5639
+ realCwd = realpathSync4(cwd);
5640
+ realConfig = realpathSync4(full);
2915
5641
  } catch {
2916
5642
  return null;
2917
5643
  }
2918
- if (dirname2(realConfig) !== realCwd || basename2(realConfig) !== ".code-viewer.json")
5644
+ if (dirname2(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
2919
5645
  return null;
2920
5646
  try {
2921
- const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
5647
+ const parsed = JSON.parse(readFileSync7(realConfig, "utf8"));
2922
5648
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
2923
5649
  return null;
2924
5650
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -2969,18 +5695,18 @@ function safeWorktreePath(path) {
2969
5695
  return null;
2970
5696
  if (isGitInternalPath(path))
2971
5697
  return null;
2972
- const full = join8(cwd, path);
2973
- if (!existsSync6(full))
5698
+ const full = join11(cwd, path);
5699
+ if (!existsSync8(full))
2974
5700
  return null;
2975
5701
  let realCwd;
2976
5702
  let realFull;
2977
5703
  try {
2978
- realCwd = realpathSync2(cwd);
2979
- realFull = realpathSync2(full);
5704
+ realCwd = realpathSync4(cwd);
5705
+ realFull = realpathSync4(full);
2980
5706
  } catch {
2981
5707
  return null;
2982
5708
  }
2983
- const rel = relative2(realCwd, realFull);
5709
+ const rel = relative3(realCwd, realFull);
2984
5710
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
2985
5711
  return null;
2986
5712
  if (isGitInternalPath(rel))
@@ -2988,12 +5714,12 @@ function safeWorktreePath(path) {
2988
5714
  return realFull;
2989
5715
  }
2990
5716
  function worktreePath(path) {
2991
- return join8(cwd, path);
5717
+ return join11(cwd, path);
2992
5718
  }
2993
5719
  function safeOpenWorktreePath(path) {
2994
5720
  if (path === "") {
2995
5721
  try {
2996
- const realCwd = realpathSync2(cwd);
5722
+ const realCwd = realpathSync4(cwd);
2997
5723
  if (isGitInternalPath(realCwd))
2998
5724
  return null;
2999
5725
  return realCwd;
@@ -3015,7 +5741,7 @@ function worktreeFileMetadata(path, knownSize) {
3015
5741
  if (!full)
3016
5742
  return {};
3017
5743
  try {
3018
- const stat = statSync2(full);
5744
+ const stat = statSync3(full);
3019
5745
  return {
3020
5746
  size: knownSize ?? stat.size,
3021
5747
  created_at: isoDate(stat.birthtimeMs),
@@ -3040,7 +5766,7 @@ function directoryMetadata(target, path) {
3040
5766
  if (!full)
3041
5767
  return {};
3042
5768
  try {
3043
- const stat = statSync2(full);
5769
+ const stat = statSync3(full);
3044
5770
  return {
3045
5771
  created_at: isoDate(stat.birthtimeMs),
3046
5772
  updated_at: isoDate(stat.mtimeMs)
@@ -3081,7 +5807,7 @@ function readReadme(target, dirPath) {
3081
5807
  if (!full)
3082
5808
  continue;
3083
5809
  try {
3084
- return { path, text: readFileSync5(full, "utf8") };
5810
+ return { path, text: readFileSync7(full, "utf8") };
3085
5811
  } catch {
3086
5812
  continue;
3087
5813
  }
@@ -3112,10 +5838,10 @@ function handleTree(url) {
3112
5838
  omitDirNames: scopeOmitDirNamesFromQuery(url),
3113
5839
  excludeNames
3114
5840
  }).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
3115
- return json({
5841
+ return json2({
3116
5842
  ref: target,
3117
5843
  path,
3118
- project: basename2(cwd),
5844
+ project: basename3(cwd),
3119
5845
  branch: currentBranch(cwd) || undefined,
3120
5846
  entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
3121
5847
  readme: readReadme(target, path),
@@ -3123,8 +5849,8 @@ function handleTree(url) {
3123
5849
  });
3124
5850
  }
3125
5851
  function handleSettings() {
3126
- return json({
3127
- project: basename2(cwd),
5852
+ return json2({
5853
+ project: basename3(cwd),
3128
5854
  repo_web_url: remoteWebUrl(cwd),
3129
5855
  scope: {
3130
5856
  omit_dirs_effective: scopeOmitDirNames,
@@ -3135,7 +5861,7 @@ function handleSettings() {
3135
5861
  }
3136
5862
  });
3137
5863
  }
3138
- function handleFiles(url) {
5864
+ function handleFiles2(url) {
3139
5865
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
3140
5866
  if (target !== "worktree" && !verifyTreeRef(target, cwd))
3141
5867
  return text("invalid target", 400);
@@ -3148,7 +5874,7 @@ function handleFiles(url) {
3148
5874
  const key = `${target || "worktree"}\x00${omitDirNames.join("\x00")}\x00${excludeNames.join("\x00")}`;
3149
5875
  const cached = fileListCache.get(key);
3150
5876
  if (cached && cached.generation === generation)
3151
- return json(cached.body);
5877
+ return json2(cached.body);
3152
5878
  const ref = target || "worktree";
3153
5879
  const entries = listTree(ref, "", cwd, {
3154
5880
  recursive: true,
@@ -3157,7 +5883,7 @@ function handleFiles(url) {
3157
5883
  }).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
3158
5884
  const body = buildFileSearchList(ref, generation, entries);
3159
5885
  fileListCache.set(key, { generation, body });
3160
- return json(body);
5886
+ return json2(body);
3161
5887
  }
3162
5888
  function parseGrepPaths(url, omitDirNames, excludeNames) {
3163
5889
  return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
@@ -3186,7 +5912,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
3186
5912
  continue;
3187
5913
  let stat;
3188
5914
  try {
3189
- stat = lstatSync4(full);
5915
+ stat = lstatSync5(full);
3190
5916
  } catch {
3191
5917
  continue;
3192
5918
  }
@@ -3194,7 +5920,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
3194
5920
  continue;
3195
5921
  let data;
3196
5922
  try {
3197
- data = readFileSync5(full);
5923
+ data = readFileSync7(full);
3198
5924
  } catch {
3199
5925
  continue;
3200
5926
  }
@@ -3269,23 +5995,23 @@ function handleGrep(url) {
3269
5995
  const paths = parseGrepPaths(url, omitDirNames, excludeNames);
3270
5996
  const regex = url.searchParams.get("regex") === "1";
3271
5997
  if (!query.trim())
3272
- return json({
5998
+ return json2({
3273
5999
  ref,
3274
6000
  engine: ref === "worktree" ? "fallback" : "git",
3275
6001
  truncated: false,
3276
6002
  matches: []
3277
6003
  });
3278
6004
  if (ref === "worktree" || ref === "")
3279
- return json(grepWorktree(query, max, paths, regex, omitDirNames, excludeNames));
6005
+ return json2(grepWorktree(query, max, paths, regex, omitDirNames, excludeNames));
3280
6006
  if (!verifyTreeRef(ref, cwd))
3281
6007
  return text("invalid target", 400);
3282
- return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames));
6008
+ return json2(grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames));
3283
6009
  }
3284
6010
  function handleRefCommits(url) {
3285
6011
  const query = url.searchParams.get("q") || "";
3286
6012
  const parsedMax = Number(url.searchParams.get("max") || "");
3287
6013
  const max = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : undefined;
3288
- return json({ commits: refCommits(cwd, query, max) });
6014
+ return json2({ commits: refCommits(cwd, query, max) });
3289
6015
  }
3290
6016
  function handleLog(url) {
3291
6017
  const ref = url.searchParams.get("ref") || "HEAD";
@@ -3299,7 +6025,7 @@ function handleLog(url) {
3299
6025
  });
3300
6026
  if (result.error)
3301
6027
  return text(result.error, 400);
3302
- return json({ commits: result.commits, hasMore: result.hasMore });
6028
+ return json2({ commits: result.commits, hasMore: result.hasMore });
3303
6029
  }
3304
6030
  function handleFileDiff(url) {
3305
6031
  const path = url.searchParams.get("path") || "";
@@ -3316,7 +6042,7 @@ function handleFileDiff(url) {
3316
6042
  to: url.searchParams.get("to") || ""
3317
6043
  };
3318
6044
  if (isSameWorktreeRange(range)) {
3319
- return json({
6045
+ return json2({
3320
6046
  path,
3321
6047
  old_path: url.searchParams.get("old_path") || "",
3322
6048
  status: url.searchParams.get("status") || "",
@@ -3378,11 +6104,11 @@ function handleFileDiff(url) {
3378
6104
  error: errText,
3379
6105
  generation
3380
6106
  };
3381
- return json(body);
6107
+ return json2(body);
3382
6108
  }
3383
6109
  function worktreeLineIndexSignature(full) {
3384
6110
  try {
3385
- const stat = statSync2(full);
6111
+ const stat = statSync3(full);
3386
6112
  return `size:${stat.size}|mtime:${stat.mtimeMs}|ctime:${stat.ctimeMs}|ino:${stat.ino || 0}`;
3387
6113
  } catch {
3388
6114
  return null;
@@ -3398,7 +6124,7 @@ async function getWorktreeLineIndex(full) {
3398
6124
  lineIndexCache.set(full, cached);
3399
6125
  return cached.index;
3400
6126
  }
3401
- const stat = statSync2(full);
6127
+ const stat = statSync3(full);
3402
6128
  if (stat.size > LINE_INDEX_MAX_FILE_BYTES)
3403
6129
  return null;
3404
6130
  const index = await buildLineOffsetIndexFromStream(fileReadableStream(full), stat.size);
@@ -3541,7 +6267,7 @@ async function handleFileRange(url) {
3541
6267
  complete: result.complete,
3542
6268
  generation
3543
6269
  };
3544
- return json(body);
6270
+ return json2(body);
3545
6271
  } else {
3546
6272
  if (!verifyTreeRef(ref, cwd))
3547
6273
  return text("invalid ref", 400);
@@ -3564,7 +6290,7 @@ async function handleFileRange(url) {
3564
6290
  complete: result.complete,
3565
6291
  generation
3566
6292
  };
3567
- return json(body);
6293
+ return json2(body);
3568
6294
  }
3569
6295
  }
3570
6296
  function handleRawFile(req, url) {
@@ -3643,7 +6369,7 @@ function rawFileSize(path, ref) {
3643
6369
  if (!full)
3644
6370
  return null;
3645
6371
  try {
3646
- return statSync2(full).size;
6372
+ return statSync3(full).size;
3647
6373
  } catch {
3648
6374
  return null;
3649
6375
  }
@@ -3742,7 +6468,7 @@ async function handleUploadFiles(req) {
3742
6468
  const realDir = safeOpenWorktreePath(dir);
3743
6469
  if (!realDir)
3744
6470
  return text("not found", 404);
3745
- const stats = statSync2(realDir);
6471
+ const stats = statSync3(realDir);
3746
6472
  if (!stats.isDirectory())
3747
6473
  return text("not a directory", 400);
3748
6474
  const files = form.getAll("files").filter((item) => item instanceof File);
@@ -3766,21 +6492,21 @@ async function handleUploadFiles(req) {
3766
6492
  total += file.size;
3767
6493
  if (total > MAX_UPLOAD_TOTAL_BYTES)
3768
6494
  return text("upload too large", 413);
3769
- const target = join8(realDir, safeName);
3770
- if (relative2(realDir, dirname2(target)) !== "")
6495
+ const target = join11(realDir, safeName);
6496
+ if (relative3(realDir, dirname2(target)) !== "")
3771
6497
  return text("invalid filename", 400);
3772
- if (existsSync6(target))
6498
+ if (existsSync8(target))
3773
6499
  return text("file exists", 409);
3774
6500
  uploads.push({ file, name: safeName, target });
3775
6501
  }
3776
6502
  const written = [];
3777
6503
  try {
3778
6504
  for (const upload of uploads) {
3779
- const fd = openSync(upload.target, uploadOpenFlags(), 420);
6505
+ const fd = openSync2(upload.target, uploadOpenFlags(), 420);
3780
6506
  try {
3781
- writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
6507
+ writeFileSync4(fd, new Uint8Array(await upload.file.arrayBuffer()));
3782
6508
  } finally {
3783
- closeSync(fd);
6509
+ closeSync2(fd);
3784
6510
  }
3785
6511
  written.push(upload.target);
3786
6512
  }
@@ -3795,7 +6521,7 @@ async function handleUploadFiles(req) {
3795
6521
  return text("upload failed", 500);
3796
6522
  }
3797
6523
  triggerUpdate();
3798
- return json({
6524
+ return json2({
3799
6525
  ok: true,
3800
6526
  files: uploads.map((upload) => upload.name),
3801
6527
  generation
@@ -3881,25 +6607,26 @@ function clearMutableCaches() {
3881
6607
  metaCache.clear();
3882
6608
  fileListCache.clear();
3883
6609
  }
3884
- function triggerUpdate() {
6610
+ function triggerUpdate(changedPaths) {
3885
6611
  generation++;
3886
6612
  clearMutableCaches();
3887
- sendSse("update");
6613
+ const data = changedPaths && changedPaths.length && changedPaths.length <= 50 ? JSON.stringify({ generation, paths: changedPaths }) : "tick";
6614
+ sendSse("update", data);
3888
6615
  }
3889
6616
  function moveMacPathIntoTrash(path) {
3890
- const trashDir = join8(homedir3(), ".Trash");
3891
- const base = basename2(path) || "code-viewer-trash-item";
3892
- const target = join8(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
6617
+ const trashDir = join11(homedir3(), ".Trash");
6618
+ const base = basename3(path) || "code-viewer-trash-item";
6619
+ const target = join11(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3893
6620
  try {
3894
- mkdirSync4(trashDir, { recursive: true });
3895
- renameSync2(path, target);
6621
+ mkdirSync6(trashDir, { recursive: true });
6622
+ renameSync3(path, target);
3896
6623
  return { ok: true, trashPath: target };
3897
6624
  } catch (error) {
3898
6625
  return { ok: false, error: String(error) };
3899
6626
  }
3900
6627
  }
3901
6628
  function movePathToTrash(path) {
3902
- lstatSync4(path);
6629
+ lstatSync5(path);
3903
6630
  if (process.platform === "darwin") {
3904
6631
  return moveMacPathIntoTrash(path);
3905
6632
  }
@@ -3923,20 +6650,20 @@ function restoreTrashPath(originalPath, trashPath) {
3923
6650
  if (!parentFullPath)
3924
6651
  return { ok: false, error: "invalid restore target" };
3925
6652
  const original = worktreePath(originalPath);
3926
- if (existsSync6(original))
6653
+ if (existsSync8(original))
3927
6654
  return { ok: false, error: "restore target exists" };
3928
6655
  if (trashPath) {
3929
6656
  if (process.platform !== "darwin")
3930
6657
  return { ok: false, error: "invalid trash handle" };
3931
- if (!existsSync6(trashPath))
6658
+ if (!existsSync8(trashPath))
3932
6659
  return { ok: false, error: "trash item not found" };
3933
6660
  try {
3934
- const trashRoot = join8(homedir3(), ".Trash");
3935
- const trashRelative = relative2(trashRoot, trashPath);
6661
+ const trashRoot = join11(homedir3(), ".Trash");
6662
+ const trashRelative = relative3(trashRoot, trashPath);
3936
6663
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
3937
6664
  return { ok: false, error: "invalid trash handle" };
3938
- mkdirSync4(dirname2(original), { recursive: true });
3939
- renameSync2(trashPath, original);
6665
+ mkdirSync6(dirname2(original), { recursive: true });
6666
+ renameSync3(trashPath, original);
3940
6667
  return { ok: true };
3941
6668
  } catch (error) {
3942
6669
  return { ok: false, error: String(error) };
@@ -3990,11 +6717,11 @@ async function handleOpenPath(req) {
3990
6717
  const target = safeOpenWorktreePath(targetPath);
3991
6718
  if (!target)
3992
6719
  return text("not found", 404);
3993
- const stats = statSync2(target);
6720
+ const stats = statSync3(target);
3994
6721
  if (!stats.isDirectory())
3995
6722
  return text("not a directory", 400);
3996
6723
  openOsPath(target);
3997
- return json({ ok: true });
6724
+ return json2({ ok: true });
3998
6725
  }
3999
6726
  async function handleTrashPath(req) {
4000
6727
  if (req.method !== "POST")
@@ -4039,7 +6766,7 @@ async function handleTrashPath(req) {
4039
6766
  }
4040
6767
  };
4041
6768
  triggerUpdate();
4042
- return json({ ok: true, generation, undo });
6769
+ return json2({ ok: true, generation, undo });
4043
6770
  }
4044
6771
  async function handleCreateDirectory(req) {
4045
6772
  if (req.method !== "POST")
@@ -4075,24 +6802,24 @@ async function handleCreateDirectory(req) {
4075
6802
  const parent = safeOpenWorktreePath(dir);
4076
6803
  if (!parent)
4077
6804
  return text("not found", 404);
4078
- const stats = statSync2(parent);
6805
+ const stats = statSync3(parent);
4079
6806
  if (!stats.isDirectory())
4080
6807
  return text("not a directory", 400);
4081
6808
  const targetPath = dir ? `${dir}/${name}` : name;
4082
6809
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
4083
6810
  return text("invalid target", 400);
4084
- const target = join8(parent, name);
4085
- if (existsSync6(target))
6811
+ const target = join11(parent, name);
6812
+ if (existsSync8(target))
4086
6813
  return text("already exists", 409);
4087
6814
  try {
4088
- mkdirSync4(target, { recursive: false });
6815
+ mkdirSync6(target, { recursive: false });
4089
6816
  } catch (error) {
4090
6817
  if (error.code === "EEXIST")
4091
6818
  return text("already exists", 409);
4092
6819
  return text("create failed", 500);
4093
6820
  }
4094
6821
  triggerUpdate();
4095
- return json({ ok: true, path: targetPath, generation });
6822
+ return json2({ ok: true, path: targetPath, generation });
4096
6823
  }
4097
6824
  async function handleRestoreTrash(req) {
4098
6825
  if (req.method !== "POST")
@@ -4124,14 +6851,14 @@ async function handleRestoreTrash(req) {
4124
6851
  if (!restored.ok)
4125
6852
  return text(restored.error || "undo failed", 409);
4126
6853
  triggerUpdate();
4127
- return json({ ok: true, generation });
6854
+ return json2({ ok: true, generation });
4128
6855
  }
4129
6856
  function annotationSse(kind, sessionId, entryId) {
4130
6857
  sendSse("annotation", JSON.stringify({ kind, session_id: sessionId, entry_id: entryId }));
4131
6858
  }
4132
6859
  async function handleAnnotations(req) {
4133
6860
  if (req.method === "GET")
4134
- return json(loadAnnotationsState(cwd));
6861
+ return json2(loadAnnotationsState(cwd));
4135
6862
  if (req.method !== "POST")
4136
6863
  return text("method not allowed", 405);
4137
6864
  if (!sideEffectRequestAllowed(req))
@@ -4158,7 +6885,7 @@ async function handleAnnotations(req) {
4158
6885
  const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
4159
6886
  saveAnnotationsState(cwd, started.state);
4160
6887
  annotationSse("start", started.session.id);
4161
- return json({ ok: true, session: started.session });
6888
+ return json2({ ok: true, session: started.session });
4162
6889
  }
4163
6890
  if (action === "add") {
4164
6891
  const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
@@ -4179,7 +6906,7 @@ async function handleAnnotations(req) {
4179
6906
  return text(result.error, 400);
4180
6907
  saveAnnotationsState(cwd, result.state);
4181
6908
  annotationSse("add", result.session.id, result.entry.id);
4182
- return json({
6909
+ return json2({
4183
6910
  ok: true,
4184
6911
  session_id: result.session.id,
4185
6912
  session_title: result.session.title,
@@ -4196,7 +6923,7 @@ async function handleAnnotations(req) {
4196
6923
  saveAnnotationsState(cwd, result.state);
4197
6924
  annotationSse("delete");
4198
6925
  }
4199
- return json({ ok: true, removed: result.removed });
6926
+ return json2({ ok: true, removed: result.removed });
4200
6927
  }
4201
6928
  if (action === "rename") {
4202
6929
  const id = typeof body.id === "string" ? body.id : "";
@@ -4208,7 +6935,7 @@ async function handleAnnotations(req) {
4208
6935
  return text("session not found", 404);
4209
6936
  saveAnnotationsState(cwd, result.state);
4210
6937
  annotationSse("update", id);
4211
- return json({ ok: true });
6938
+ return json2({ ok: true });
4212
6939
  }
4213
6940
  if (action === "update") {
4214
6941
  const id = typeof body.id === "string" ? body.id : "";
@@ -4222,12 +6949,12 @@ async function handleAnnotations(req) {
4222
6949
  return text(result.error, 400);
4223
6950
  saveAnnotationsState(cwd, result.state);
4224
6951
  annotationSse("update", undefined, id);
4225
- return json({ ok: true, entry: result.entry });
6952
+ return json2({ ok: true, entry: result.entry });
4226
6953
  }
4227
6954
  if (action === "clear") {
4228
6955
  saveAnnotationsState(cwd, emptyAnnotationsState());
4229
6956
  annotationSse("clear");
4230
- return json({ ok: true });
6957
+ return json2({ ok: true });
4231
6958
  }
4232
6959
  return text("invalid action", 400);
4233
6960
  }
@@ -4263,8 +6990,8 @@ var init_preview = __esm(async () => {
4263
6990
  init_search();
4264
6991
  init_server_registry();
4265
6992
  init_worktree_watcher();
4266
- WEB_ROOT = join8(ROOT, "web");
4267
- VERSION = JSON.parse(readFileSync5(join8(ROOT, "package.json"), "utf8")).version;
6993
+ WEB_ROOT = join11(ROOT, "web");
6994
+ VERSION = JSON.parse(readFileSync7(join11(ROOT, "package.json"), "utf8")).version;
4268
6995
  DEFAULT_ARGS = ["HEAD"];
4269
6996
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
4270
6997
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -4338,7 +7065,7 @@ var init_preview = __esm(async () => {
4338
7065
  if (url.pathname === "/_tree")
4339
7066
  return handleTree(url);
4340
7067
  if (url.pathname === "/_files")
4341
- return handleFiles(url);
7068
+ return handleFiles2(url);
4342
7069
  if (url.pathname === "/_grep")
4343
7070
  return handleGrep(url);
4344
7071
  if (url.pathname === "/_commits")
@@ -4361,15 +7088,21 @@ var init_preview = __esm(async () => {
4361
7088
  return handleCreateDirectory(req);
4362
7089
  if (url.pathname === "/_upload_files")
4363
7090
  return handleUploadFiles(req);
7091
+ if (url.pathname.startsWith("/_db/")) {
7092
+ const { handleDatabaseRoute: handleDatabaseRoute2 } = await Promise.resolve().then(() => (init_handle(), exports_handle));
7093
+ const dbResponse = await handleDatabaseRoute2(req, url, cwd, scopeOmitDirNames, sideEffectRequestAllowed, sendSse);
7094
+ if (dbResponse)
7095
+ return dbResponse;
7096
+ }
4364
7097
  if (url.pathname === "/_annotations")
4365
7098
  return handleAnnotations(req);
4366
7099
  if (url.pathname === "/_refs")
4367
- return json(refs(cwd));
7100
+ return json2(refs(cwd));
4368
7101
  if (url.pathname === "/refresh" && req.method === "POST") {
4369
7102
  if (!sideEffectRequestAllowed(req))
4370
7103
  return text("forbidden", 403);
4371
7104
  triggerUpdate();
4372
- return json({ ok: true, generation });
7105
+ return json2({ ok: true, generation });
4373
7106
  }
4374
7107
  if (url.pathname === "/events") {
4375
7108
  let ctrl;
@@ -4464,6 +7197,9 @@ data: ok
4464
7197
  if (process.argv[2] === "annotate") {
4465
7198
  const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
4466
7199
  await runAnnotateCli2(process.argv.slice(3));
7200
+ } else if (process.argv[2] === "query") {
7201
+ const { runQueryCli: runQueryCli2 } = await Promise.resolve().then(() => (init_query_cli(), exports_query_cli));
7202
+ await runQueryCli2(process.argv.slice(3));
4467
7203
  } else if (process.argv[2] === "skill") {
4468
7204
  const { runSkillCli: runSkillCli2 } = await Promise.resolve().then(() => (init_skill_cli(), exports_skill_cli));
4469
7205
  runSkillCli2(process.argv.slice(3));