@youtyan/code-viewer 0.1.51 → 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.
- package/dist/code-viewer.js +3007 -271
- package/package.json +1 -1
- package/skills/code-viewer-query/SKILL.md +59 -0
- package/web/app.js +3863 -207
- package/web/index.html +21 -0
- package/web/style.css +2101 -42
package/dist/code-viewer.js
CHANGED
|
@@ -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
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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
|
|
2352
|
-
|
|
2353
|
-
|
|
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
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
|
|
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
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
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
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
const
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
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
|
|
2513
|
-
|
|
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
|
|
2522
|
-
lstatSync as
|
|
2523
|
-
mkdirSync as
|
|
2524
|
-
openSync,
|
|
2525
|
-
readFileSync as
|
|
2526
|
-
realpathSync as
|
|
2527
|
-
renameSync as
|
|
2528
|
-
statSync as
|
|
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
|
|
5257
|
+
writeFileSync as writeFileSync4
|
|
2532
5258
|
} from "node:fs";
|
|
2533
5259
|
import { homedir as homedir3 } from "node:os";
|
|
2534
|
-
import { basename as
|
|
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) ||
|
|
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
|
|
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 =
|
|
2674
|
-
if (!
|
|
5399
|
+
const full = join11(WEB_ROOT, spec[0]);
|
|
5400
|
+
if (!existsSync8(full))
|
|
2675
5401
|
return text("not found", 404);
|
|
2676
|
-
return new Response(
|
|
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:
|
|
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:
|
|
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 =
|
|
2908
|
-
if (!
|
|
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 =
|
|
2914
|
-
realConfig =
|
|
5639
|
+
realCwd = realpathSync4(cwd);
|
|
5640
|
+
realConfig = realpathSync4(full);
|
|
2915
5641
|
} catch {
|
|
2916
5642
|
return null;
|
|
2917
5643
|
}
|
|
2918
|
-
if (dirname2(realConfig) !== realCwd ||
|
|
5644
|
+
if (dirname2(realConfig) !== realCwd || basename3(realConfig) !== ".code-viewer.json")
|
|
2919
5645
|
return null;
|
|
2920
5646
|
try {
|
|
2921
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
2973
|
-
if (!
|
|
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 =
|
|
2979
|
-
realFull =
|
|
5704
|
+
realCwd = realpathSync4(cwd);
|
|
5705
|
+
realFull = realpathSync4(full);
|
|
2980
5706
|
} catch {
|
|
2981
5707
|
return null;
|
|
2982
5708
|
}
|
|
2983
|
-
const rel =
|
|
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
|
|
5717
|
+
return join11(cwd, path);
|
|
2992
5718
|
}
|
|
2993
5719
|
function safeOpenWorktreePath(path) {
|
|
2994
5720
|
if (path === "") {
|
|
2995
5721
|
try {
|
|
2996
|
-
const realCwd =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
5841
|
+
return json2({
|
|
3116
5842
|
ref: target,
|
|
3117
5843
|
path,
|
|
3118
|
-
project:
|
|
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
|
|
3127
|
-
project:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6107
|
+
return json2(body);
|
|
3382
6108
|
}
|
|
3383
6109
|
function worktreeLineIndexSignature(full) {
|
|
3384
6110
|
try {
|
|
3385
|
-
const stat =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
3770
|
-
if (
|
|
6495
|
+
const target = join11(realDir, safeName);
|
|
6496
|
+
if (relative3(realDir, dirname2(target)) !== "")
|
|
3771
6497
|
return text("invalid filename", 400);
|
|
3772
|
-
if (
|
|
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 =
|
|
6505
|
+
const fd = openSync2(upload.target, uploadOpenFlags(), 420);
|
|
3780
6506
|
try {
|
|
3781
|
-
|
|
6507
|
+
writeFileSync4(fd, new Uint8Array(await upload.file.arrayBuffer()));
|
|
3782
6508
|
} finally {
|
|
3783
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3891
|
-
const base =
|
|
3892
|
-
const target =
|
|
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
|
-
|
|
3895
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
6658
|
+
if (!existsSync8(trashPath))
|
|
3932
6659
|
return { ok: false, error: "trash item not found" };
|
|
3933
6660
|
try {
|
|
3934
|
-
const trashRoot =
|
|
3935
|
-
const trashRelative =
|
|
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
|
-
|
|
3939
|
-
|
|
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 =
|
|
6720
|
+
const stats = statSync3(target);
|
|
3994
6721
|
if (!stats.isDirectory())
|
|
3995
6722
|
return text("not a directory", 400);
|
|
3996
6723
|
openOsPath(target);
|
|
3997
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
4085
|
-
if (
|
|
6811
|
+
const target = join11(parent, name);
|
|
6812
|
+
if (existsSync8(target))
|
|
4086
6813
|
return text("already exists", 409);
|
|
4087
6814
|
try {
|
|
4088
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4267
|
-
VERSION = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
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));
|