@youtyan/code-viewer 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10340,6 +10340,42 @@ function throwIfAborted(signal, message = "operation aborted") {
10340
10340
  if (signal?.aborted)
10341
10341
  throw abortError(message);
10342
10342
  }
10343
+ function waitForAbortableResource(promise, signal, dispose, message = "operation aborted") {
10344
+ const disposeSafely = (resource) => {
10345
+ try {
10346
+ dispose(resource);
10347
+ } catch {}
10348
+ };
10349
+ if (!signal)
10350
+ return promise;
10351
+ if (signal.aborted) {
10352
+ promise.then(disposeSafely, () => {
10353
+ return;
10354
+ });
10355
+ return Promise.reject(abortError(message));
10356
+ }
10357
+ return new Promise((resolve2, reject) => {
10358
+ let aborted = false;
10359
+ const onAbort = () => {
10360
+ aborted = true;
10361
+ signal.removeEventListener("abort", onAbort);
10362
+ reject(abortError(message));
10363
+ };
10364
+ signal.addEventListener("abort", onAbort, { once: true });
10365
+ promise.then((resource) => {
10366
+ signal.removeEventListener("abort", onAbort);
10367
+ if (aborted) {
10368
+ disposeSafely(resource);
10369
+ return;
10370
+ }
10371
+ resolve2(resource);
10372
+ }, (error) => {
10373
+ signal.removeEventListener("abort", onAbort);
10374
+ if (!aborted)
10375
+ reject(error);
10376
+ });
10377
+ });
10378
+ }
10343
10379
  function isAbortLikeError(err, signal, extraTokens = []) {
10344
10380
  if (signal?.aborted)
10345
10381
  return true;
@@ -10796,6 +10832,8 @@ var init_sql_capture = __esm(() => {
10796
10832
 
10797
10833
  // web-src/server/database/adapters/docker.ts
10798
10834
  import { spawnSync as spawnSync3 } from "node:child_process";
10835
+ import mysql from "mysql2/promise";
10836
+ import pg from "pg";
10799
10837
  function dockerDatabasesCacheKey(serviceName, kind, cwd) {
10800
10838
  return `${serviceName}\x00${kind}\x00${cwd}`;
10801
10839
  }
@@ -10821,55 +10859,211 @@ function setDockerSchemasCache(key, value, ttlMs, now = Date.now()) {
10821
10859
  function fallbackDockerDatabases(defaultDb) {
10822
10860
  return defaultDb ? [defaultDb] : [];
10823
10861
  }
10824
- function buildExecArgs(config, sql) {
10862
+ function buildExecInvocation(config, sql) {
10863
+ if (!config.containerName) {
10864
+ throw new Error("direct SQL connections use the built-in driver");
10865
+ }
10825
10866
  if (config.kind === "postgresql") {
10826
- return [
10867
+ return {
10868
+ args: [
10869
+ dockerCommand(),
10870
+ "exec",
10871
+ "-i",
10872
+ "-e",
10873
+ `PGPASSWORD=${config.password}`,
10874
+ config.containerName,
10875
+ "psql",
10876
+ "-U",
10877
+ config.user,
10878
+ "-d",
10879
+ config.database,
10880
+ "-X",
10881
+ "-q",
10882
+ "-t",
10883
+ "-A",
10884
+ "-F",
10885
+ "\t",
10886
+ "-R",
10887
+ PG_RECORD_SEPARATOR,
10888
+ "-v",
10889
+ "ON_ERROR_STOP=1",
10890
+ "-c",
10891
+ sql
10892
+ ],
10893
+ env: process.env
10894
+ };
10895
+ }
10896
+ return {
10897
+ args: [
10827
10898
  dockerCommand(),
10828
10899
  "exec",
10829
10900
  "-i",
10830
10901
  "-e",
10831
- `PGPASSWORD=${config.password}`,
10902
+ `MYSQL_PWD=${config.password}`,
10832
10903
  config.containerName,
10833
- "psql",
10834
- "-U",
10904
+ "mysql",
10905
+ "-u",
10835
10906
  config.user,
10836
- "-d",
10837
10907
  config.database,
10838
- "-X",
10839
- "-q",
10840
- "-t",
10841
- "-A",
10842
- "-F",
10843
- "\t",
10844
- "-R",
10845
- PG_RECORD_SEPARATOR,
10846
- "-v",
10847
- "ON_ERROR_STOP=1",
10848
- "-c",
10908
+ "--batch",
10909
+ "--default-character-set=utf8mb4",
10910
+ "-e",
10849
10911
  sql
10850
- ];
10912
+ ],
10913
+ env: process.env
10914
+ };
10915
+ }
10916
+ function sqlDriverValue(value, nullValue) {
10917
+ if (value === null || value === undefined)
10918
+ return nullValue;
10919
+ if (value instanceof Date)
10920
+ return value.toISOString();
10921
+ if (value instanceof Uint8Array)
10922
+ return Buffer.from(value).toString("hex");
10923
+ if (typeof value === "object")
10924
+ return JSON.stringify(value);
10925
+ return String(value);
10926
+ }
10927
+ function pgDriverResult(raw) {
10928
+ const results = Array.isArray(raw) ? raw : [raw];
10929
+ const reversed = [...results].reverse();
10930
+ const result = reversed.find((item) => !!item && typeof item === "object" && Array.isArray(item.fields) && item.fields.length > 0) ?? reversed.find((item) => !!item && typeof item === "object" && Array.isArray(item.rows) && item.rows.length > 0) ?? raw;
10931
+ if (!result || typeof result !== "object")
10932
+ return { columns: [], rows: [] };
10933
+ const fields = Array.isArray(result.fields) ? result.fields : [];
10934
+ const rows = Array.isArray(result.rows) ? result.rows : [];
10935
+ return {
10936
+ columns: fields.map((field) => String(field.name ?? "")),
10937
+ rows: rows.map((row) => row.map((value) => sqlDriverValue(value, "\\N")))
10938
+ };
10939
+ }
10940
+ function mysqlDriverResult(raw) {
10941
+ if (!Array.isArray(raw) || raw.length < 2)
10942
+ return { columns: [], rows: [] };
10943
+ const [allRows, allFields] = raw;
10944
+ let rows = allRows;
10945
+ let fields = allFields;
10946
+ if (Array.isArray(allFields) && allFields.length > 0 && Array.isArray(allFields[0])) {
10947
+ const fieldSets = allFields;
10948
+ const rowSets = Array.isArray(allRows) ? allRows : [];
10949
+ let index = -1;
10950
+ for (let i = fieldSets.length - 1;i >= 0; i--) {
10951
+ if (Array.isArray(fieldSets[i])) {
10952
+ index = i;
10953
+ break;
10954
+ }
10955
+ }
10956
+ fields = index >= 0 ? fieldSets[index] : [];
10957
+ rows = index >= 0 ? rowSets[index] : [];
10851
10958
  }
10852
- return [
10853
- dockerCommand(),
10854
- "exec",
10855
- "-i",
10856
- "-e",
10857
- `MYSQL_PWD=${config.password}`,
10858
- config.containerName,
10859
- "mysql",
10860
- "-u",
10861
- config.user,
10862
- config.database,
10863
- "--batch",
10864
- "--default-character-set=utf8mb4",
10865
- "-e",
10866
- sql
10867
- ];
10959
+ const fieldList = Array.isArray(fields) ? fields.filter(Boolean) : [];
10960
+ const rowList = Array.isArray(rows) ? rows : [];
10961
+ return {
10962
+ columns: fieldList.map((field) => String(field.name ?? "")),
10963
+ rows: rowList.map((row) => row.map((value) => sqlDriverValue(value, "NULL")))
10964
+ };
10965
+ }
10966
+ function createSqlDriverExecutor(config) {
10967
+ if (!config.host || !config.port) {
10968
+ throw new Error("direct SQL connection requires host and port");
10969
+ }
10970
+ if (config.kind === "postgresql") {
10971
+ const pool2 = createPgPoolImpl({
10972
+ host: config.host,
10973
+ port: config.port,
10974
+ user: config.user,
10975
+ password: config.password,
10976
+ database: config.database,
10977
+ ssl: config.tls,
10978
+ max: 4,
10979
+ connectionTimeoutMillis: 1e4,
10980
+ query_timeout: 1e4
10981
+ });
10982
+ return {
10983
+ async exec(sql, signal) {
10984
+ throwIfAborted(signal, "query aborted");
10985
+ const client = await waitForAbortableResource(pool2.connect(), signal, (lateClient) => lateClient.release(true), "query aborted");
10986
+ let released = false;
10987
+ const abort = () => {
10988
+ if (released)
10989
+ return;
10990
+ released = true;
10991
+ client.release(true);
10992
+ };
10993
+ signal?.addEventListener("abort", abort, { once: true });
10994
+ try {
10995
+ throwIfAborted(signal, "query aborted");
10996
+ return pgDriverResult(await client.query({ text: sql, rowMode: "array" }));
10997
+ } catch (error) {
10998
+ if (signal?.aborted)
10999
+ throw abortError("query aborted");
11000
+ throw error;
11001
+ } finally {
11002
+ signal?.removeEventListener("abort", abort);
11003
+ if (!released)
11004
+ client.release();
11005
+ }
11006
+ },
11007
+ close() {
11008
+ pool2.end().catch(() => {
11009
+ return;
11010
+ });
11011
+ }
11012
+ };
11013
+ }
11014
+ const pool = createMysqlPoolImpl({
11015
+ host: config.host,
11016
+ port: config.port,
11017
+ user: config.user,
11018
+ password: config.password,
11019
+ database: config.database,
11020
+ ssl: config.tls ? {} : undefined,
11021
+ waitForConnections: true,
11022
+ connectionLimit: 4,
11023
+ connectTimeout: 1e4,
11024
+ multipleStatements: true,
11025
+ rowsAsArray: true,
11026
+ supportBigNumbers: true,
11027
+ bigNumberStrings: true,
11028
+ dateStrings: true
11029
+ });
11030
+ return {
11031
+ async exec(sql, signal) {
11032
+ throwIfAborted(signal, "query aborted");
11033
+ const connection = await waitForAbortableResource(pool.getConnection(), signal, (lateConnection) => lateConnection.destroy(), "query aborted");
11034
+ let released = false;
11035
+ const abort = () => {
11036
+ if (released)
11037
+ return;
11038
+ released = true;
11039
+ connection.destroy();
11040
+ };
11041
+ signal?.addEventListener("abort", abort, { once: true });
11042
+ try {
11043
+ throwIfAborted(signal, "query aborted");
11044
+ return mysqlDriverResult(await connection.query({ sql, rowsAsArray: true, timeout: 1e4 }));
11045
+ } catch (error) {
11046
+ if (signal?.aborted)
11047
+ throw abortError("query aborted");
11048
+ throw error;
11049
+ } finally {
11050
+ signal?.removeEventListener("abort", abort);
11051
+ if (!released)
11052
+ connection.release();
11053
+ }
11054
+ },
11055
+ close() {
11056
+ pool.end().catch(() => {
11057
+ return;
11058
+ });
11059
+ }
11060
+ };
10868
11061
  }
10869
11062
  function execInContainer(config, sql, timeoutMs = 1e4) {
10870
- const args = buildExecArgs(config, sql);
11063
+ const { args, env } = buildExecInvocation(config, sql);
10871
11064
  const proc = spawnSyncImpl2(args[0], args.slice(1), {
10872
11065
  encoding: "utf8",
11066
+ env,
10873
11067
  timeout: timeoutMs,
10874
11068
  stdio: ["ignore", "pipe", "pipe"]
10875
11069
  });
@@ -10895,11 +11089,12 @@ async function readStreamText(stream) {
10895
11089
  text += decoder.decode();
10896
11090
  return text;
10897
11091
  }
10898
- async function execWithBunSpawn(spawnFn, args, timeoutMs, signal) {
11092
+ async function execWithBunSpawn(spawnFn, args, env, timeoutMs, signal) {
10899
11093
  throwIfAborted(signal, "query aborted");
10900
11094
  let proc;
10901
11095
  try {
10902
11096
  proc = spawnFn(args, {
11097
+ env,
10903
11098
  stdin: "ignore",
10904
11099
  stdout: "pipe",
10905
11100
  stderr: "pipe"
@@ -10946,10 +11141,11 @@ async function execWithBunSpawn(spawnFn, args, timeoutMs, signal) {
10946
11141
  signal?.removeEventListener("abort", abort);
10947
11142
  }
10948
11143
  }
10949
- function execWithNodeSpawn(args, timeoutMs, signal) {
11144
+ function execWithNodeSpawn(args, env, timeoutMs, signal) {
10950
11145
  return spawnTextAsync({
10951
11146
  command: args[0],
10952
11147
  args: args.slice(1),
11148
+ env,
10953
11149
  timeoutMs,
10954
11150
  signal,
10955
11151
  killSignal: "SIGKILL",
@@ -10966,10 +11162,11 @@ async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
10966
11162
  throwIfDockerCommandUnavailableResult(result);
10967
11163
  return result;
10968
11164
  }
10969
- const args = buildExecArgs(config, sql);
11165
+ const { args, env } = buildExecInvocation(config, sql);
10970
11166
  const bunSpawn = globalThis.Bun?.spawn;
10971
- result = bunSpawn ? await execWithBunSpawn(bunSpawn, args, timeoutMs, signal) : await execWithNodeSpawn(args, timeoutMs, signal);
10972
- throwIfDockerCommandUnavailableResult(result);
11167
+ result = bunSpawn ? await execWithBunSpawn(bunSpawn, args, env, timeoutMs, signal) : await execWithNodeSpawn(args, env, timeoutMs, signal);
11168
+ if (config.containerName)
11169
+ throwIfDockerCommandUnavailableResult(result);
10973
11170
  return result;
10974
11171
  }
10975
11172
  function stripFinalLineBreak(text) {
@@ -11107,8 +11304,13 @@ function observeBackgroundRejection(promise) {
11107
11304
  });
11108
11305
  return promise;
11109
11306
  }
11110
- function createDockerAdapter(config) {
11307
+ function createSqlCliAdapter(config) {
11308
+ const driver = config.containerName ? null : createSqlDriverExecutor(config);
11111
11309
  async function execAsync(sql, signal) {
11310
+ if (driver) {
11311
+ recordSql(sql);
11312
+ return driver.exec(sql, signal);
11313
+ }
11112
11314
  const result = await execInContainerAsync(config, sql, 1e4, signal);
11113
11315
  if (result.code !== 0) {
11114
11316
  throw new Error(result.stderr.trim() || "query failed");
@@ -11544,6 +11746,7 @@ function createDockerAdapter(config) {
11544
11746
  close() {
11545
11747
  columnCache.clear();
11546
11748
  tableMetaCache.invalidate();
11749
+ driver?.close();
11547
11750
  },
11548
11751
  async* iterateForSnapshot(table, signal) {
11549
11752
  const columns = await adapter.getColumnsAsync(table, signal);
@@ -11713,7 +11916,7 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
11713
11916
  ...kind === "postgresql" && schema ? { schema } : {}
11714
11917
  });
11715
11918
  }
11716
- var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES, SUPABASE_LOCAL_DB_USER = "postgres", SUPABASE_LOCAL_DB_PASSWORD = "postgres", SUPABASE_LOCAL_DB_NAME = "postgres";
11919
+ var createPgPoolImpl = (config) => new pg.Pool(config), createMysqlPoolImpl = (config) => mysql.createPool(config), COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES, createDockerAdapter, SUPABASE_LOCAL_DB_USER = "postgres", SUPABASE_LOCAL_DB_PASSWORD = "postgres", SUPABASE_LOCAL_DB_NAME = "postgres";
11717
11920
  var init_docker = __esm(() => {
11718
11921
  init_mutate();
11719
11922
  init_sql_snapshot();
@@ -11735,6 +11938,7 @@ var init_docker = __esm(() => {
11735
11938
  "geometrycollection",
11736
11939
  "geomcollection"
11737
11940
  ]);
11941
+ createDockerAdapter = createSqlCliAdapter;
11738
11942
  });
11739
11943
 
11740
11944
  // web-src/server/database/adapters/elasticsearch.ts
@@ -11743,8 +11947,13 @@ __export(exports_elasticsearch, {
11743
11947
  quoteCurlConfigString: () => quoteCurlConfigString,
11744
11948
  openElasticsearchAdapterAsync: () => openElasticsearchAdapterAsync,
11745
11949
  isReadOnlyEsPath: () => isReadOnlyEsPath,
11746
- canonicalizeEsSnapshotContainer: () => canonicalizeEsSnapshotContainer
11950
+ createElasticsearchAdapter: () => createElasticsearchAdapter,
11951
+ canonicalizeEsSnapshotContainer: () => canonicalizeEsSnapshotContainer,
11952
+ __setEsFetchForTest: () => __setEsFetchForTest
11747
11953
  });
11954
+ function __setEsFetchForTest(fetchForTest) {
11955
+ esFetchImpl = fetchForTest ?? globalThis.fetch;
11956
+ }
11748
11957
  function isReadOnlyEsPath(rawPath) {
11749
11958
  const path = rawPath.split("?")[0].replace(/\/+$/, "");
11750
11959
  const segments = path.split("/").filter(Boolean);
@@ -11764,13 +11973,11 @@ function quoteCurlConfigString(value) {
11764
11973
  }
11765
11974
  function buildEsRequestInvocation(config, method, path, body) {
11766
11975
  const hasPassword = !!config.password;
11767
- const url = `http://localhost:9200${path.startsWith("/") ? "" : "/"}${path}`;
11768
- const curlConfig = hasPassword ? `user = ${quoteCurlConfigString(`elastic:${config.password}`)}
11976
+ const endpoint = config.endpoint?.replace(/\/$/, "") || "http://localhost:9200";
11977
+ const url = `${endpoint}${path.startsWith("/") ? "" : "/"}${path}`;
11978
+ const curlConfig = hasPassword ? `user = ${quoteCurlConfigString(`${config.username || "elastic"}:${config.password}`)}
11769
11979
  ` : undefined;
11770
- const args = [
11771
- "exec",
11772
- "-i",
11773
- config.containerName,
11980
+ const curlArgs = [
11774
11981
  "curl",
11775
11982
  "-s",
11776
11983
  "-S",
@@ -11786,13 +11993,64 @@ __ES_STATUS__:%{http_code}
11786
11993
  ...hasPassword ? ["-K", "-"] : [],
11787
11994
  ...body !== undefined ? ["--data-binary", JSON.stringify(body)] : []
11788
11995
  ];
11789
- return { args, input: curlConfig };
11996
+ if (!config.containerName) {
11997
+ throw new Error("direct Elasticsearch connections use the built-in HTTP client");
11998
+ }
11999
+ return {
12000
+ command: dockerCommand(),
12001
+ args: ["exec", "-i", config.containerName, ...curlArgs],
12002
+ input: curlConfig
12003
+ };
11790
12004
  }
11791
12005
  async function execEsRequestAsync(config, method, path, body, timeoutMs = 15000, signal) {
11792
12006
  throwIfAborted(signal, "elasticsearch request aborted");
12007
+ if (!config.containerName) {
12008
+ const endpoint = config.endpoint?.replace(/\/$/, "") || "http://localhost:9200";
12009
+ const url = `${endpoint}${path.startsWith("/") ? "" : "/"}${path}`;
12010
+ const controller = new AbortController;
12011
+ let timedOut = false;
12012
+ const timer = setTimeout(() => {
12013
+ timedOut = true;
12014
+ controller.abort();
12015
+ }, timeoutMs);
12016
+ const abort = () => controller.abort();
12017
+ signal?.addEventListener("abort", abort, { once: true });
12018
+ try {
12019
+ const headers = new Headers({ "Content-Type": "application/json" });
12020
+ if (config.password) {
12021
+ headers.set("Authorization", `Basic ${Buffer.from(`${config.username || "elastic"}:${config.password}`).toString("base64")}`);
12022
+ }
12023
+ const response = await esFetchImpl(url, {
12024
+ method,
12025
+ headers,
12026
+ body: body === undefined ? undefined : JSON.stringify(body),
12027
+ signal: controller.signal,
12028
+ redirect: "error"
12029
+ });
12030
+ const text = await response.text();
12031
+ return {
12032
+ code: 0,
12033
+ stdout: `${text}
12034
+ __ES_STATUS__:${response.status}
12035
+ `,
12036
+ stderr: ""
12037
+ };
12038
+ } catch (error) {
12039
+ if (signal?.aborted)
12040
+ throw new Error("elasticsearch request aborted");
12041
+ return {
12042
+ code: 1,
12043
+ stdout: "",
12044
+ stderr: timedOut ? `elasticsearch request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error)
12045
+ };
12046
+ } finally {
12047
+ clearTimeout(timer);
12048
+ signal?.removeEventListener("abort", abort);
12049
+ }
12050
+ }
11793
12051
  const invocation = buildEsRequestInvocation(config, method, path, body);
11794
12052
  const result = await spawnTextAsync({
11795
- command: dockerCommand(),
12053
+ command: invocation.command,
11796
12054
  args: invocation.args,
11797
12055
  env: process.env,
11798
12056
  input: invocation.input,
@@ -11802,7 +12060,8 @@ async function execEsRequestAsync(config, method, path, body, timeoutMs = 15000,
11802
12060
  timeoutMessage: `elasticsearch request timed out after ${timeoutMs}ms`,
11803
12061
  rejectOnError: false
11804
12062
  });
11805
- throwIfDockerCommandUnavailableResult(result);
12063
+ if (config.containerName)
12064
+ throwIfDockerCommandUnavailableResult(result);
11806
12065
  return result;
11807
12066
  }
11808
12067
  function parseEsResponse(stdout) {
@@ -11829,7 +12088,7 @@ function createElasticsearchAdapter(config) {
11829
12088
  async function callJsonAsync(method, path, body, label, signal) {
11830
12089
  const r = await execEsRequestAsync(config, method, path, body, 15000, signal);
11831
12090
  if (r.code !== 0) {
11832
- throw new Error(r.stderr.trim() || `${label}: curl exit ${r.code}`);
12091
+ throw new Error(r.stderr.trim() || `${label}: request failed`);
11833
12092
  }
11834
12093
  const { status, body: text } = parseEsResponse(r.stdout);
11835
12094
  if (status < 200 || status >= 300) {
@@ -11914,7 +12173,7 @@ function createElasticsearchAdapter(config) {
11914
12173
  }
11915
12174
  const r = await execEsRequestAsync(config, "GET", `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`, undefined, 15000, opts.signal);
11916
12175
  if (r.code !== 0) {
11917
- throw new Error(r.stderr.trim() || `_doc: curl exit ${r.code}`);
12176
+ throw new Error(r.stderr.trim() || "_doc: request failed");
11918
12177
  }
11919
12178
  const { status, body: text } = parseEsResponse(r.stdout);
11920
12179
  if (status === 404) {
@@ -12013,7 +12272,7 @@ function createElasticsearchAdapter(config) {
12013
12272
  const r = await execEsRequestAsync(config, input.method, input.path, input.body, 15000, signal);
12014
12273
  const elapsedMs = Date.now() - start;
12015
12274
  if (r.code !== 0) {
12016
- throw new Error(r.stderr.trim() || `query: curl exit ${r.code}`);
12275
+ throw new Error(r.stderr.trim() || "query: request failed");
12017
12276
  }
12018
12277
  const { status, body: text } = parseEsResponse(r.stdout);
12019
12278
  let body = text;
@@ -12099,10 +12358,11 @@ async function openElasticsearchAdapterAsync(serviceName, env, cwd, signal) {
12099
12358
  const password = env.ELASTIC_PASSWORD || "";
12100
12359
  return createElasticsearchAdapter({ containerName, password });
12101
12360
  }
12102
- var ES_QUERY_ALLOWED_SUBPATHS, ES_DEFAULT_SIZE = 200;
12361
+ var esFetchImpl, ES_QUERY_ALLOWED_SUBPATHS, ES_DEFAULT_SIZE = 200;
12103
12362
  var init_elasticsearch = __esm(() => {
12104
12363
  init_docker_utils();
12105
12364
  init_spawn_runner();
12365
+ esFetchImpl = globalThis.fetch;
12106
12366
  ES_QUERY_ALLOWED_SUBPATHS = new Set([
12107
12367
  "_search",
12108
12368
  "_count",
@@ -12119,9 +12379,14 @@ var exports_redis = {};
12119
12379
  __export(exports_redis, {
12120
12380
  openRedisExplorerAsync: () => openRedisExplorerAsync,
12121
12381
  createRedisAdapter: () => createRedisAdapter,
12122
- canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
12382
+ canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer,
12383
+ __setRedisClientFactoryForTest: () => __setRedisClientFactoryForTest
12123
12384
  });
12124
12385
  import { createHash as createHash3 } from "node:crypto";
12386
+ import { createClient } from "@redis/client";
12387
+ function __setRedisClientFactoryForTest(factory) {
12388
+ createRedisClientImpl = factory ?? ((options) => createClient(options));
12389
+ }
12125
12390
  function canonicalizeRedisSnapshotContainer(container) {
12126
12391
  const { db, pattern } = parseSnapshotContainer(container);
12127
12392
  return JSON.stringify({ db, pattern });
@@ -12139,20 +12404,25 @@ function parseSnapshotContainer(container) {
12139
12404
  }
12140
12405
  function buildRedisCliInvocation(config, args) {
12141
12406
  const hasPassword = !!config.password;
12142
- const dockerArgs = [
12143
- "exec",
12144
- "-i",
12145
- ...hasPassword ? ["-e", "REDISCLI_AUTH"] : [],
12146
- config.containerName,
12147
- "redis-cli",
12148
- "-3",
12149
- ...args
12150
- ];
12151
12407
  const spawnEnv = hasPassword ? { ...process.env, REDISCLI_AUTH: config.password } : process.env;
12152
- return { args: dockerArgs, env: spawnEnv };
12408
+ if (!config.containerName) {
12409
+ throw new Error("direct Redis connections use the built-in driver");
12410
+ }
12411
+ return {
12412
+ args: [
12413
+ "exec",
12414
+ "-i",
12415
+ ...hasPassword ? ["-e", "REDISCLI_AUTH"] : [],
12416
+ config.containerName,
12417
+ "redis-cli",
12418
+ "-3",
12419
+ ...args
12420
+ ],
12421
+ env: spawnEnv
12422
+ };
12153
12423
  }
12154
- async function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
12155
- throwIfAborted(signal, "redis-cli aborted");
12424
+ async function execRedisCliProcessAsync(config, args, timeoutMs = 1e4, signal) {
12425
+ throwIfAborted(signal, "redis request aborted");
12156
12426
  const invocation = buildRedisCliInvocation(config, args);
12157
12427
  const result = await spawnTextAsync({
12158
12428
  command: dockerCommand(),
@@ -12160,13 +12430,115 @@ async function execRedisCliAsync(config, args, timeoutMs = 1e4, signal) {
12160
12430
  env: invocation.env,
12161
12431
  timeoutMs,
12162
12432
  signal,
12163
- abortMessage: "redis-cli aborted",
12164
- timeoutMessage: `redis-cli timed out after ${timeoutMs}ms`,
12433
+ abortMessage: "redis request aborted",
12434
+ timeoutMessage: `redis request timed out after ${timeoutMs}ms`,
12165
12435
  rejectOnError: false
12166
12436
  });
12167
- throwIfDockerCommandUnavailableResult(result);
12437
+ if (config.containerName)
12438
+ throwIfDockerCommandUnavailableResult(result);
12168
12439
  return result;
12169
12440
  }
12441
+ function redisReplyText(reply) {
12442
+ if (reply === null || reply === undefined)
12443
+ return "";
12444
+ if (reply instanceof Uint8Array)
12445
+ return Buffer.from(reply).toString("utf8");
12446
+ if (Array.isArray(reply))
12447
+ return JSON.stringify(reply);
12448
+ if (typeof reply === "object")
12449
+ return JSON.stringify(reply);
12450
+ return String(reply);
12451
+ }
12452
+ function createRedisDriverExecutor(config) {
12453
+ if (!config.host || !config.port) {
12454
+ throw new Error("direct Redis connection requires host and port");
12455
+ }
12456
+ const clients = new Map;
12457
+ function clientForDatabase(database) {
12458
+ const existing = clients.get(database);
12459
+ if (existing)
12460
+ return existing;
12461
+ const socket = config.tls ? {
12462
+ host: config.host,
12463
+ port: config.port,
12464
+ tls: true,
12465
+ connectTimeout: 1e4
12466
+ } : {
12467
+ host: config.host,
12468
+ port: config.port,
12469
+ connectTimeout: 1e4
12470
+ };
12471
+ const client = createRedisClientImpl({
12472
+ username: config.username || undefined,
12473
+ password: config.password || undefined,
12474
+ database,
12475
+ socket
12476
+ });
12477
+ client.on("error", () => {});
12478
+ const entry = { client, connected: client.connect() };
12479
+ clients.set(database, entry);
12480
+ return entry;
12481
+ }
12482
+ return {
12483
+ async exec(args, timeoutMs = 1e4, signal) {
12484
+ throwIfAborted(signal, "redis request aborted");
12485
+ let database = 0;
12486
+ let command = args;
12487
+ if (args[0] === "-n" && args.length >= 3) {
12488
+ database = Number(args[1]) || 0;
12489
+ command = args.slice(2);
12490
+ }
12491
+ const entry = clientForDatabase(database);
12492
+ let connectDisposed = false;
12493
+ const disposeConnectingClient = () => {
12494
+ if (connectDisposed)
12495
+ return;
12496
+ connectDisposed = true;
12497
+ if (clients.get(database) === entry)
12498
+ clients.delete(database);
12499
+ entry.client.destroy();
12500
+ };
12501
+ const abortConnect = () => disposeConnectingClient();
12502
+ signal?.addEventListener("abort", abortConnect, { once: true });
12503
+ const timeoutController = new AbortController;
12504
+ let timedOut = false;
12505
+ const timer = setTimeout(() => {
12506
+ timedOut = true;
12507
+ timeoutController.abort();
12508
+ }, timeoutMs);
12509
+ const abort = () => timeoutController.abort();
12510
+ signal?.addEventListener("abort", abort, { once: true });
12511
+ let connected = false;
12512
+ try {
12513
+ await waitForAbortableResource(entry.connected.then(() => entry.client), signal, disposeConnectingClient, "redis request aborted");
12514
+ connected = true;
12515
+ signal?.removeEventListener("abort", abortConnect);
12516
+ throwIfAborted(signal, "redis request aborted");
12517
+ const reply = await entry.client.withAbortSignal(timeoutController.signal).sendCommand(command);
12518
+ return { stdout: redisReplyText(reply), stderr: "", code: 0 };
12519
+ } catch (error) {
12520
+ if (!connected)
12521
+ disposeConnectingClient();
12522
+ if (signal?.aborted)
12523
+ throw abortError("redis request aborted");
12524
+ return {
12525
+ stdout: "",
12526
+ stderr: timedOut ? `redis request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error),
12527
+ code: 1
12528
+ };
12529
+ } finally {
12530
+ clearTimeout(timer);
12531
+ signal?.removeEventListener("abort", abortConnect);
12532
+ signal?.removeEventListener("abort", abort);
12533
+ }
12534
+ },
12535
+ close() {
12536
+ for (const { client } of clients.values())
12537
+ client.destroy();
12538
+ clients.clear();
12539
+ }
12540
+ };
12541
+ }
12170
12542
  function parseInfoKeyspace(stdout) {
12171
12543
  const counts = new Map;
12172
12544
  for (const line of stdout.split(/\r?\n/)) {
@@ -12201,6 +12573,8 @@ function decodeHexItem(hex) {
12201
12573
  return { binaryBase64: buf.toString("base64") };
12202
12574
  }
12203
12575
  function createRedisAdapter(config) {
12576
+ const driver = config.containerName ? null : createRedisDriverExecutor(config);
12577
+ const execRedisCliAsync = (_config, args, timeoutMs = 1e4, signal) => driver ? driver.exec(args, timeoutMs, signal) : execRedisCliProcessAsync(config, args, timeoutMs, signal);
12204
12578
  function parseDatabasesResult(result) {
12205
12579
  if (result.code !== 0) {
12206
12580
  throw new Error(result.stderr.trim() || "INFO keyspace failed");
@@ -12691,7 +13065,9 @@ function createRedisAdapter(config) {
12691
13065
  deleteKeyAsync,
12692
13066
  iterateForSnapshot,
12693
13067
  listSnapshotContainers,
12694
- close() {}
13068
+ close() {
13069
+ driver?.close();
13070
+ }
12695
13071
  };
12696
13072
  }
12697
13073
  async function openRedisExplorerAsync(serviceName, env, cwd, signal) {
@@ -12699,7 +13075,7 @@ async function openRedisExplorerAsync(serviceName, env, cwd, signal) {
12699
13075
  const password = env.REDIS_PASSWORD || "";
12700
13076
  return createRedisAdapter({ containerName, password });
12701
13077
  }
12702
- var DEFAULT_DATABASES = 16, REDIS_STRING_BYTE_LIMIT = 65536, REDIS_COLLECTION_LIMIT = 200, SCAN_WITH_TYPES_LUA = `local s = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2], 'COUNT', ARGV[3]); local types = {}; for i, k in ipairs(s[2]) do types[i] = redis.call('TYPE', k).ok end; return cjson.encode({cursor=s[1], keys=s[2], types=types})`, LUA_TOHEX_PRELUDE = `local function tohex(s) local t = {} for i = 1, #s do t[i] = string.format('%02x', string.byte(s, i)) end return table.concat(t) end`, TYPE_OR_STRING_LUA, LUA_HEX_KEY_PRELUDE = `local function tohex(s) local t = {} for i = 1, #s do t[i] = string.format('%02x', string.byte(s, i)) end return table.concat(t) end local function fromhex(h) local b = {} for i = 1, #h, 2 do b[#b+1] = string.char(tonumber(string.sub(h, i, i+1), 16)) end return table.concat(b) end`, SCAN_HEX_KEYS_LUA;
13078
+ var createRedisClientImpl = (options) => createClient(options), DEFAULT_DATABASES = 16, REDIS_STRING_BYTE_LIMIT = 65536, REDIS_COLLECTION_LIMIT = 200, SCAN_WITH_TYPES_LUA = `local s = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2], 'COUNT', ARGV[3]); local types = {}; for i, k in ipairs(s[2]) do types[i] = redis.call('TYPE', k).ok end; return cjson.encode({cursor=s[1], keys=s[2], types=types})`, LUA_TOHEX_PRELUDE = `local function tohex(s) local t = {} for i = 1, #s do t[i] = string.format('%02x', string.byte(s, i)) end return table.concat(t) end`, TYPE_OR_STRING_LUA, LUA_HEX_KEY_PRELUDE = `local function tohex(s) local t = {} for i = 1, #s do t[i] = string.format('%02x', string.byte(s, i)) end return table.concat(t) end local function fromhex(h) local b = {} for i = 1, #h, 2 do b[#b+1] = string.char(tonumber(string.sub(h, i, i+1), 16)) end return table.concat(b) end`, SCAN_HEX_KEYS_LUA;
12703
13079
  var init_redis = __esm(() => {
12704
13080
  init_docker_utils();
12705
13081
  init_spawn_runner();
@@ -15779,185 +16155,33 @@ function asAsyncDoc(source) {
15779
16155
  };
15780
16156
  }
15781
16157
 
15782
- // web-src/server/database/connection-pool.ts
15783
- function setAdapterFactory(f) {
15784
- factory = f;
16158
+ // web-src/server/database/adapters/dynamodb.ts
16159
+ import { spawnSync as spawnSync5 } from "node:child_process";
16160
+ import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
16161
+ function createDynamoDbRequestDeadline() {
16162
+ const timeoutMs = dynamoDbRequestTimeoutMs;
16163
+ return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15785
16164
  }
15786
- function evictOldest() {
15787
- let oldestKey = null;
15788
- let oldestTime = Infinity;
15789
- for (const [key, entry] of pool) {
15790
- if (entry.lastUsed < oldestTime) {
15791
- oldestTime = entry.lastUsed;
15792
- oldestKey = key;
15793
- }
15794
- }
15795
- if (oldestKey) {
15796
- const entry = pool.get(oldestKey);
15797
- if (entry) {
15798
- clearTimeout(entry.timer);
15799
- try {
15800
- entry.adapter.close();
15801
- } catch {}
15802
- pool.delete(oldestKey);
15803
- }
15804
- }
16165
+ function createDynamoDbDockerCurlDeadline() {
16166
+ const timeoutMs = dynamoDbDockerCurlTimeoutMs;
16167
+ return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15805
16168
  }
15806
- function scheduleEviction(key, entry) {
15807
- clearTimeout(entry.timer);
15808
- entry.timer = setTimeout(() => {
15809
- const current = pool.get(key);
15810
- if (current === entry) {
15811
- try {
15812
- current.adapter.close();
15813
- } catch {}
15814
- pool.delete(key);
15815
- }
15816
- }, IDLE_TIMEOUT_MS);
16169
+ function createDynamoDbTransportDeadline(config) {
16170
+ return config.dockerContainerName ? createDynamoDbDockerCurlDeadline() : createDynamoDbRequestDeadline();
15817
16171
  }
15818
- async function getConnection(resolvedPath) {
15819
- if (!factory) {
15820
- throw new Error("No adapter factory configured");
15821
- }
15822
- const existing = pool.get(resolvedPath);
15823
- if (existing) {
15824
- existing.lastUsed = Date.now();
15825
- scheduleEviction(resolvedPath, existing);
15826
- return existing.adapter;
15827
- }
15828
- if (pool.size >= MAX_CONNECTIONS) {
15829
- evictOldest();
15830
- }
15831
- const adapter = await factory.open(resolvedPath);
15832
- const entry = {
15833
- adapter,
15834
- path: resolvedPath,
15835
- lastUsed: Date.now(),
15836
- timer: setTimeout(() => {
15837
- return;
15838
- }, 0)
15839
- };
15840
- pool.set(resolvedPath, entry);
15841
- scheduleEviction(resolvedPath, entry);
15842
- return adapter;
16172
+ function dynamoDbTimeoutError(deadline) {
16173
+ return new DynamoDbHttpError(503, `DynamoDB request timed out after ${deadline?.timeoutMs ?? dynamoDbRequestTimeoutMs}ms`);
15843
16174
  }
15844
- function closeConnection(resolvedPath) {
15845
- const entry = pool.get(resolvedPath);
15846
- if (!entry)
15847
- return false;
15848
- clearTimeout(entry.timer);
15849
- try {
15850
- entry.adapter.close();
15851
- } catch {}
15852
- pool.delete(resolvedPath);
15853
- return true;
16175
+ function remainingDynamoDbTimeoutMs(deadline) {
16176
+ if (!deadline)
16177
+ return dynamoDbRequestTimeoutMs;
16178
+ return Math.max(0, deadline.expiresAt - Date.now());
15854
16179
  }
15855
- var MAX_CONNECTIONS = 8, IDLE_TIMEOUT_MS, pool, factory = null;
15856
- var init_connection_pool = __esm(() => {
15857
- IDLE_TIMEOUT_MS = 5 * 60 * 1000;
15858
- pool = new Map;
15859
- });
15860
-
15861
- // web-src/server/database/global-search.ts
15862
- function isTextLikeType(type) {
15863
- const upper = type.toUpperCase();
15864
- return upper.includes("CHAR") || upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CLOB") || upper.includes("STRING") || upper === "JSON" || upper === "JSONB" || upper === "XML" || upper === "UUID";
16180
+ function hmac2(key, value) {
16181
+ return createHmac2("sha256", key).update(value, "utf8").digest();
15865
16182
  }
15866
- function escapeLikeTerm(term) {
15867
- return term.replace(/=/g, "==").replace(/%/g, "=%").replace(/_/g, "=_");
15868
- }
15869
- async function searchTableAsync(adapter, table, columns, term, maxHits, includeNonText, pkColumns, signal) {
15870
- const kind = adapter.kind;
15871
- const searchCols = includeNonText ? columns.filter((c) => c.type.toUpperCase() !== "BLOB" && c.type.toUpperCase() !== "BYTEA") : columns.filter((c) => isTextLikeType(c.type));
15872
- if (searchCols.length === 0)
15873
- return [];
15874
- const escapedTerm = escapeLikeTerm(term);
15875
- const tbl = sanitizeIdentifier(table, kind);
15876
- const hits = [];
15877
- const db = asAsync(adapter);
15878
- for (const col of searchCols) {
15879
- if (signal?.aborted)
15880
- break;
15881
- if (hits.length >= maxHits)
15882
- break;
15883
- const colId = sanitizeIdentifier(col.name, kind);
15884
- const castCol = kind === "mysql" ? `CAST(${colId} AS CHAR)` : `CAST(${colId} AS TEXT)`;
15885
- let sql;
15886
- const remaining = maxHits - hits.length;
15887
- if (kind === "sqlite") {
15888
- sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '='`;
15889
- } else {
15890
- const likeVal = escapeSqlString(`%${escapedTerm}%`);
15891
- sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '='`;
15892
- }
15893
- try {
15894
- const params = kind === "sqlite" ? [`%${escapedTerm}%`] : undefined;
15895
- const result = await db.readonlyQuery(sql, params, remaining, signal);
15896
- for (const row of result.rows) {
15897
- const colIdx = result.columns.indexOf(col.name);
15898
- const valueRaw = colIdx >= 0 ? serializeDbValue(row[colIdx]) : null;
15899
- const valueStr = valueRaw == null ? "" : String(valueRaw);
15900
- const preview = valueStr.length > 200 ? `${valueStr.slice(0, 200)}...` : valueStr;
15901
- let rowKeyJson;
15902
- if (pkColumns.length > 0) {
15903
- const keyObj = {};
15904
- for (const pk of pkColumns) {
15905
- const pkIdx = result.columns.indexOf(pk);
15906
- if (pkIdx >= 0)
15907
- keyObj[pk] = serializeDbValue(row[pkIdx]);
15908
- }
15909
- rowKeyJson = JSON.stringify(keyObj);
15910
- }
15911
- hits.push({
15912
- table,
15913
- column: col.name,
15914
- rowKeyJson,
15915
- valuePreview: preview,
15916
- rowPreview: serializeDbRow(row)
15917
- });
15918
- }
15919
- } catch (err) {
15920
- if (isAbortLikeError(err, signal))
15921
- throw err;
15922
- }
15923
- }
15924
- return hits;
15925
- }
15926
- function getPrimaryKeyColumnsFromColumns(columns) {
15927
- return columns.filter((c) => c.primaryKey).map((c) => c.name);
15928
- }
15929
- var init_global_search = __esm(() => {
15930
- init_serialize();
15931
- init_sql_utils();
15932
- });
15933
-
15934
- // web-src/server/database/adapters/dynamodb.ts
15935
- import { spawnSync as spawnSync5 } from "node:child_process";
15936
- import { createHash as createHash5, createHmac as createHmac2 } from "node:crypto";
15937
- function createDynamoDbRequestDeadline() {
15938
- const timeoutMs = dynamoDbRequestTimeoutMs;
15939
- return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15940
- }
15941
- function createDynamoDbDockerCurlDeadline() {
15942
- const timeoutMs = dynamoDbDockerCurlTimeoutMs;
15943
- return { expiresAt: Date.now() + timeoutMs, timeoutMs };
15944
- }
15945
- function createDynamoDbTransportDeadline(config) {
15946
- return config.dockerContainerName ? createDynamoDbDockerCurlDeadline() : createDynamoDbRequestDeadline();
15947
- }
15948
- function dynamoDbTimeoutError(deadline) {
15949
- return new DynamoDbHttpError(503, `DynamoDB request timed out after ${deadline?.timeoutMs ?? dynamoDbRequestTimeoutMs}ms`);
15950
- }
15951
- function remainingDynamoDbTimeoutMs(deadline) {
15952
- if (!deadline)
15953
- return dynamoDbRequestTimeoutMs;
15954
- return Math.max(0, deadline.expiresAt - Date.now());
15955
- }
15956
- function hmac2(key, value) {
15957
- return createHmac2("sha256", key).update(value, "utf8").digest();
15958
- }
15959
- function sha2562(value) {
15960
- return createHash5("sha256").update(value, "utf8").digest("hex");
16183
+ function sha2562(value) {
16184
+ return createHash5("sha256").update(value, "utf8").digest("hex");
15961
16185
  }
15962
16186
  function amzDate2(date = new Date) {
15963
16187
  const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
@@ -16393,6 +16617,416 @@ var init_dynamodb = __esm(() => {
16393
16617
  dynamoDbDockerCurlTimeoutMs = DEFAULT_DYNAMODB_DOCKER_CURL_TIMEOUT_MS;
16394
16618
  });
16395
16619
 
16620
+ // web-src/server/database/connection-pool.ts
16621
+ function setAdapterFactory(f) {
16622
+ factory = f;
16623
+ }
16624
+ function evictOldest() {
16625
+ let oldestKey = null;
16626
+ let oldestTime = Infinity;
16627
+ for (const [key, entry] of pool) {
16628
+ if (entry.lastUsed < oldestTime) {
16629
+ oldestTime = entry.lastUsed;
16630
+ oldestKey = key;
16631
+ }
16632
+ }
16633
+ if (oldestKey) {
16634
+ const entry = pool.get(oldestKey);
16635
+ if (entry) {
16636
+ clearTimeout(entry.timer);
16637
+ try {
16638
+ entry.adapter.close();
16639
+ } catch {}
16640
+ pool.delete(oldestKey);
16641
+ }
16642
+ }
16643
+ }
16644
+ function scheduleEviction(key, entry) {
16645
+ clearTimeout(entry.timer);
16646
+ entry.timer = setTimeout(() => {
16647
+ const current = pool.get(key);
16648
+ if (current === entry) {
16649
+ try {
16650
+ current.adapter.close();
16651
+ } catch {}
16652
+ pool.delete(key);
16653
+ }
16654
+ }, IDLE_TIMEOUT_MS);
16655
+ }
16656
+ async function getConnection(resolvedPath) {
16657
+ if (!factory) {
16658
+ throw new Error("No adapter factory configured");
16659
+ }
16660
+ const existing = pool.get(resolvedPath);
16661
+ if (existing) {
16662
+ existing.lastUsed = Date.now();
16663
+ scheduleEviction(resolvedPath, existing);
16664
+ return existing.adapter;
16665
+ }
16666
+ if (pool.size >= MAX_CONNECTIONS) {
16667
+ evictOldest();
16668
+ }
16669
+ const adapter = await factory.open(resolvedPath);
16670
+ const entry = {
16671
+ adapter,
16672
+ path: resolvedPath,
16673
+ lastUsed: Date.now(),
16674
+ timer: setTimeout(() => {
16675
+ return;
16676
+ }, 0)
16677
+ };
16678
+ pool.set(resolvedPath, entry);
16679
+ scheduleEviction(resolvedPath, entry);
16680
+ return adapter;
16681
+ }
16682
+ function closeConnection(resolvedPath) {
16683
+ const entry = pool.get(resolvedPath);
16684
+ if (!entry)
16685
+ return false;
16686
+ clearTimeout(entry.timer);
16687
+ try {
16688
+ entry.adapter.close();
16689
+ } catch {}
16690
+ pool.delete(resolvedPath);
16691
+ return true;
16692
+ }
16693
+ var MAX_CONNECTIONS = 8, IDLE_TIMEOUT_MS, pool, factory = null;
16694
+ var init_connection_pool = __esm(() => {
16695
+ IDLE_TIMEOUT_MS = 5 * 60 * 1000;
16696
+ pool = new Map;
16697
+ });
16698
+
16699
+ // web-src/server/database/connections-store.ts
16700
+ import { randomUUID } from "node:crypto";
16701
+ import { chmod } from "node:fs/promises";
16702
+ import { join as join11 } from "node:path";
16703
+ function secretKey(cwd, id) {
16704
+ return `${cwd}\x00${id}`;
16705
+ }
16706
+ function extractSecrets(value) {
16707
+ return {
16708
+ ...typeof value.user === "string" ? { user: value.user } : {},
16709
+ ...typeof value.username === "string" ? { username: value.username } : {},
16710
+ ...typeof value.accessKeyId === "string" ? { accessKeyId: value.accessKeyId } : {},
16711
+ ...typeof value.password === "string" ? { password: value.password } : {},
16712
+ ...typeof value.secretAccessKey === "string" ? { secretAccessKey: value.secretAccessKey } : {},
16713
+ ...typeof value.sessionToken === "string" ? { sessionToken: value.sessionToken } : {}
16714
+ };
16715
+ }
16716
+ function withRuntimeSecrets(cwd, connection) {
16717
+ return {
16718
+ ...connection,
16719
+ ...runtimeSecrets.get(secretKey(cwd, connection.id)) ?? {}
16720
+ };
16721
+ }
16722
+ function connectionsFilePath(root) {
16723
+ return join11(root, ".code-viewer", CONNECTIONS_FILE_NAME);
16724
+ }
16725
+ function emptyState() {
16726
+ return { version: 1, connections: [] };
16727
+ }
16728
+ function requiredString(value, maxLength = MAX_VALUE_LENGTH) {
16729
+ return typeof value === "string" && value.length <= maxLength ? value : "";
16730
+ }
16731
+ function optionalString2(value, maxLength = MAX_VALUE_LENGTH) {
16732
+ const normalized = requiredString(value, maxLength);
16733
+ return normalized || undefined;
16734
+ }
16735
+ function validPort(value) {
16736
+ const port = Number(value);
16737
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
16738
+ }
16739
+ function validEndpoint(value) {
16740
+ const raw = requiredString(value, MAX_VALUE_LENGTH);
16741
+ if (!raw)
16742
+ return null;
16743
+ try {
16744
+ const url = new URL(raw);
16745
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) {
16746
+ return null;
16747
+ }
16748
+ return url.toString().replace(/\/$/, "");
16749
+ } catch {
16750
+ return null;
16751
+ }
16752
+ }
16753
+ function sanitizeConnection(raw) {
16754
+ if (!raw || typeof raw !== "object")
16755
+ return null;
16756
+ const input = raw;
16757
+ const id = requiredString(input.id, 80);
16758
+ const name = requiredString(input.name, MAX_NAME_LENGTH).trim();
16759
+ if (!/^connection:[a-f0-9-]{16,64}$/.test(id) || !name)
16760
+ return null;
16761
+ const base = { id, name };
16762
+ if (input.kind === "postgresql" || input.kind === "mysql") {
16763
+ const host = requiredString(input.host, MAX_HOST_LENGTH).trim();
16764
+ const port = validPort(input.port);
16765
+ const user = requiredString(input.user).trim();
16766
+ const database = requiredString(input.database).trim();
16767
+ if (!host || !port || !database)
16768
+ return null;
16769
+ return {
16770
+ ...base,
16771
+ kind: input.kind,
16772
+ host,
16773
+ port,
16774
+ user,
16775
+ password: requiredString(input.password),
16776
+ database,
16777
+ ...optionalString2(input.schema)?.trim() ? { schema: optionalString2(input.schema)?.trim() } : {},
16778
+ tls: input.tls === true
16779
+ };
16780
+ }
16781
+ if (input.kind === "redis") {
16782
+ const host = requiredString(input.host, MAX_HOST_LENGTH).trim();
16783
+ const port = validPort(input.port);
16784
+ if (!host || !port)
16785
+ return null;
16786
+ return {
16787
+ ...base,
16788
+ kind: "redis",
16789
+ host,
16790
+ port,
16791
+ ...optionalString2(input.username)?.trim() ? { username: optionalString2(input.username)?.trim() } : {},
16792
+ password: requiredString(input.password),
16793
+ tls: input.tls === true
16794
+ };
16795
+ }
16796
+ if (input.kind === "elasticsearch") {
16797
+ const endpoint = validEndpoint(input.endpoint);
16798
+ if (!endpoint)
16799
+ return null;
16800
+ return {
16801
+ ...base,
16802
+ kind: "elasticsearch",
16803
+ endpoint,
16804
+ ...optionalString2(input.username)?.trim() ? { username: optionalString2(input.username)?.trim() } : {},
16805
+ password: requiredString(input.password)
16806
+ };
16807
+ }
16808
+ if (input.kind === "s3" || input.kind === "dynamodb") {
16809
+ const endpoint = validEndpoint(input.endpoint);
16810
+ const region = requiredString(input.region).trim();
16811
+ const accessKeyId = requiredString(input.accessKeyId).trim();
16812
+ if (!endpoint || !region)
16813
+ return null;
16814
+ return {
16815
+ ...base,
16816
+ kind: input.kind,
16817
+ endpoint,
16818
+ region,
16819
+ accessKeyId,
16820
+ secretAccessKey: requiredString(input.secretAccessKey),
16821
+ ...optionalString2(input.sessionToken) ? { sessionToken: optionalString2(input.sessionToken) } : {}
16822
+ };
16823
+ }
16824
+ return null;
16825
+ }
16826
+ function validateDatastoreConnection(raw, fallbackId = "connection:0000000000000000") {
16827
+ const input = raw && typeof raw === "object" ? raw : {};
16828
+ const connection = sanitizeConnection({
16829
+ ...input,
16830
+ id: typeof input.id === "string" && input.id ? input.id : fallbackId
16831
+ });
16832
+ if (!connection || (connection.kind === "postgresql" || connection.kind === "mysql") && !connection.user || (connection.kind === "s3" || connection.kind === "dynamodb") && !connection.accessKeyId) {
16833
+ throw new Error("invalid datastore connection");
16834
+ }
16835
+ return connection;
16836
+ }
16837
+ function sanitizeState(raw) {
16838
+ if (!raw || typeof raw !== "object")
16839
+ return emptyState();
16840
+ const input = raw;
16841
+ if (input.version !== 1 || !Array.isArray(input.connections)) {
16842
+ return emptyState();
16843
+ }
16844
+ const seen = new Set;
16845
+ const connections = [];
16846
+ for (const candidate of input.connections) {
16847
+ if (connections.length >= MAX_CONNECTIONS2)
16848
+ break;
16849
+ const connection = sanitizeConnection(candidate);
16850
+ if (!connection || seen.has(connection.id))
16851
+ continue;
16852
+ seen.add(connection.id);
16853
+ connections.push(connection);
16854
+ }
16855
+ return { version: 1, connections };
16856
+ }
16857
+ async function protectFile(cwd) {
16858
+ await chmod(connectionsFilePath(cwd), 384).catch(() => {
16859
+ return;
16860
+ });
16861
+ }
16862
+ async function loadDatastoreConnections(cwd) {
16863
+ return (await store.load(cwd)).connections.map((connection) => withRuntimeSecrets(cwd, connection));
16864
+ }
16865
+ async function findDatastoreConnection(cwd, id) {
16866
+ return (await loadDatastoreConnections(cwd)).find((entry) => entry.id === id) ?? null;
16867
+ }
16868
+ async function saveDatastoreConnection(cwd, raw) {
16869
+ const input = raw && typeof raw === "object" ? raw : {};
16870
+ const requestedId = typeof input.id === "string" ? input.id : "";
16871
+ const id = requestedId || `connection:${randomUUID()}`;
16872
+ const result = await store.update(cwd, (state) => {
16873
+ const storedExisting = state.connections.find((entry) => entry.id === id);
16874
+ const existing = storedExisting ? withRuntimeSecrets(cwd, storedExisting) : undefined;
16875
+ const merged = sanitizeConnection({
16876
+ ...existing ?? {},
16877
+ ...input,
16878
+ id,
16879
+ password: input.password === undefined && existing && "password" in existing ? existing.password : input.password,
16880
+ secretAccessKey: input.secretAccessKey === undefined && existing && "secretAccessKey" in existing ? existing.secretAccessKey : input.secretAccessKey,
16881
+ sessionToken: input.sessionToken === undefined && existing && "sessionToken" in existing ? existing.sessionToken : input.sessionToken
16882
+ });
16883
+ if (!merged || (merged.kind === "postgresql" || merged.kind === "mysql") && !merged.user || (merged.kind === "s3" || merged.kind === "dynamodb") && !merged.accessKeyId) {
16884
+ throw new Error("invalid datastore connection");
16885
+ }
16886
+ const connections = state.connections.filter((entry) => entry.id !== id);
16887
+ if (!storedExisting && connections.length >= MAX_CONNECTIONS2) {
16888
+ throw new Error("too many datastore connections");
16889
+ }
16890
+ connections.push(merged);
16891
+ return {
16892
+ state: { version: 1, connections },
16893
+ result: merged
16894
+ };
16895
+ });
16896
+ runtimeSecrets.set(secretKey(cwd, result.id), extractSecrets(result));
16897
+ await protectFile(cwd);
16898
+ return result;
16899
+ }
16900
+ async function deleteDatastoreConnection(cwd, id) {
16901
+ const deleted = await store.update(cwd, (state) => {
16902
+ const connections = state.connections.filter((entry) => entry.id !== id);
16903
+ return {
16904
+ state: { version: 1, connections },
16905
+ result: connections.length !== state.connections.length
16906
+ };
16907
+ });
16908
+ runtimeSecrets.delete(secretKey(cwd, id));
16909
+ await protectFile(cwd);
16910
+ return deleted;
16911
+ }
16912
+ function connectionToFileInfo(connection) {
16913
+ return {
16914
+ id: connection.id,
16915
+ path: "saved connection",
16916
+ name: connection.name,
16917
+ sizeBytes: 0,
16918
+ kind: connection.kind,
16919
+ savedConnection: true
16920
+ };
16921
+ }
16922
+ function publicConnection(connection) {
16923
+ const {
16924
+ password: _password,
16925
+ user: _user,
16926
+ username: _username,
16927
+ accessKeyId: _accessKeyId,
16928
+ ...withoutPassword
16929
+ } = connection;
16930
+ const {
16931
+ secretAccessKey: _secret,
16932
+ sessionToken: _token,
16933
+ ...safe
16934
+ } = withoutPassword;
16935
+ return safe;
16936
+ }
16937
+ var CONNECTIONS_FILE_NAME = "datastore-connections.json", MAX_CONNECTIONS2 = 64, MAX_JSON_BYTES, MAX_NAME_LENGTH = 120, MAX_HOST_LENGTH = 253, MAX_VALUE_LENGTH = 4096, runtimeSecrets, store;
16938
+ var init_connections_store = __esm(() => {
16939
+ init_json_store();
16940
+ MAX_JSON_BYTES = 256 * 1024;
16941
+ runtimeSecrets = new Map;
16942
+ store = createJsonFileStore({
16943
+ filePath: connectionsFilePath,
16944
+ empty: emptyState,
16945
+ sanitize: sanitizeState,
16946
+ maxBytes: MAX_JSON_BYTES,
16947
+ backupSuffix: "bak",
16948
+ sizeErrorMessage: "datastore connections state too large",
16949
+ serialize: (state) => `${JSON.stringify({
16950
+ version: 1,
16951
+ connections: state.connections.map(publicConnection)
16952
+ }, null, 2)}
16953
+ `
16954
+ });
16955
+ });
16956
+
16957
+ // web-src/server/database/global-search.ts
16958
+ function isTextLikeType(type) {
16959
+ const upper = type.toUpperCase();
16960
+ return upper.includes("CHAR") || upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CLOB") || upper.includes("STRING") || upper === "JSON" || upper === "JSONB" || upper === "XML" || upper === "UUID";
16961
+ }
16962
+ function escapeLikeTerm(term) {
16963
+ return term.replace(/=/g, "==").replace(/%/g, "=%").replace(/_/g, "=_");
16964
+ }
16965
+ async function searchTableAsync(adapter, table, columns, term, maxHits, includeNonText, pkColumns, signal) {
16966
+ const kind = adapter.kind;
16967
+ const searchCols = includeNonText ? columns.filter((c) => c.type.toUpperCase() !== "BLOB" && c.type.toUpperCase() !== "BYTEA") : columns.filter((c) => isTextLikeType(c.type));
16968
+ if (searchCols.length === 0)
16969
+ return [];
16970
+ const escapedTerm = escapeLikeTerm(term);
16971
+ const tbl = sanitizeIdentifier(table, kind);
16972
+ const hits = [];
16973
+ const db = asAsync(adapter);
16974
+ for (const col of searchCols) {
16975
+ if (signal?.aborted)
16976
+ break;
16977
+ if (hits.length >= maxHits)
16978
+ break;
16979
+ const colId = sanitizeIdentifier(col.name, kind);
16980
+ const castCol = kind === "mysql" ? `CAST(${colId} AS CHAR)` : `CAST(${colId} AS TEXT)`;
16981
+ let sql;
16982
+ const remaining = maxHits - hits.length;
16983
+ if (kind === "sqlite") {
16984
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '='`;
16985
+ } else {
16986
+ const likeVal = escapeSqlString(`%${escapedTerm}%`);
16987
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '='`;
16988
+ }
16989
+ try {
16990
+ const params = kind === "sqlite" ? [`%${escapedTerm}%`] : undefined;
16991
+ const result = await db.readonlyQuery(sql, params, remaining, signal);
16992
+ for (const row of result.rows) {
16993
+ const colIdx = result.columns.indexOf(col.name);
16994
+ const valueRaw = colIdx >= 0 ? serializeDbValue(row[colIdx]) : null;
16995
+ const valueStr = valueRaw == null ? "" : String(valueRaw);
16996
+ const preview = valueStr.length > 200 ? `${valueStr.slice(0, 200)}...` : valueStr;
16997
+ let rowKeyJson;
16998
+ if (pkColumns.length > 0) {
16999
+ const keyObj = {};
17000
+ for (const pk of pkColumns) {
17001
+ const pkIdx = result.columns.indexOf(pk);
17002
+ if (pkIdx >= 0)
17003
+ keyObj[pk] = serializeDbValue(row[pkIdx]);
17004
+ }
17005
+ rowKeyJson = JSON.stringify(keyObj);
17006
+ }
17007
+ hits.push({
17008
+ table,
17009
+ column: col.name,
17010
+ rowKeyJson,
17011
+ valuePreview: preview,
17012
+ rowPreview: serializeDbRow(row)
17013
+ });
17014
+ }
17015
+ } catch (err) {
17016
+ if (isAbortLikeError(err, signal))
17017
+ throw err;
17018
+ }
17019
+ }
17020
+ return hits;
17021
+ }
17022
+ function getPrimaryKeyColumnsFromColumns(columns) {
17023
+ return columns.filter((c) => c.primaryKey).map((c) => c.name);
17024
+ }
17025
+ var init_global_search = __esm(() => {
17026
+ init_serialize();
17027
+ init_sql_utils();
17028
+ });
17029
+
16396
17030
  // web-src/server/database/handle-shared.ts
16397
17031
  function createDockerAdapterCache(maxEntries = DEFAULT_MAX_DOCKER_ADAPTER_CACHE, idleMs = DEFAULT_DOCKER_ADAPTER_IDLE_MS) {
16398
17032
  const cache = new Map;
@@ -16642,6 +17276,24 @@ async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omi
16642
17276
  }
16643
17277
  return { dbId: dbParam, explorer };
16644
17278
  }
17279
+ async function resolveDatastoreExplorerAsync(cwd, dbParam, kind, cache, openDocker, openSaved, omitDirNames, signal) {
17280
+ if (dbParam?.startsWith("connection:")) {
17281
+ const connection = await findDatastoreConnection(cwd, dbParam);
17282
+ if (!connection || connection.kind !== kind) {
17283
+ return textError(`${kind} connection not found`, 404);
17284
+ }
17285
+ try {
17286
+ const explorer = await waitForCallerAbort(cache.getOrOpenAsync(dbParam, () => openSaved(connection)), signal, `${kind} open aborted`);
17287
+ return { dbId: dbParam, explorer };
17288
+ } catch (err) {
17289
+ if (isAbortLikeError(err, signal)) {
17290
+ return textError(`${kind} open aborted`, 503);
17291
+ }
17292
+ throw err;
17293
+ }
17294
+ }
17295
+ return resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openDocker, omitDirNames, signal);
17296
+ }
16645
17297
  async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
16646
17298
  if (!Object.prototype.hasOwnProperty.call(routes, url.pathname))
16647
17299
  return null;
@@ -16688,6 +17340,7 @@ function handleError(prefix, action, err, signal) {
16688
17340
  var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS, MAX_LOGGED_ERROR_BODY = 500, logQueue;
16689
17341
  var init_handle_shared = __esm(() => {
16690
17342
  init_docker_utils();
17343
+ init_connections_store();
16691
17344
  init_discovery();
16692
17345
  DEFAULT_DOCKER_ADAPTER_IDLE_MS = 5 * 60 * 1000;
16693
17346
  logQueue = Promise.resolve();
@@ -16703,7 +17356,12 @@ function closeDynamoDbAdapter(dbId) {
16703
17356
  dynamoDbAdapterCache.close(dbId);
16704
17357
  }
16705
17358
  function resolveDynamoDb(cwd, dbParam, signal, omitDirNames) {
16706
- return resolveDockerExplorerAsync(cwd, dbParam, "dynamodb", dynamoDbAdapterCache, (info) => openDynamoDbExplorerAsync(info), omitDirNames, signal);
17359
+ return resolveDatastoreExplorerAsync(cwd, dbParam, "dynamodb", dynamoDbAdapterCache, (info) => openDynamoDbExplorerAsync(info), (connection) => {
17360
+ if (connection.kind !== "dynamodb") {
17361
+ throw new Error("invalid DynamoDB connection");
17362
+ }
17363
+ return createDynamoDbAdapter(connection);
17364
+ }, omitDirNames, signal);
16707
17365
  }
16708
17366
  function dynamoDbErrorResponse(err, action, signal) {
16709
17367
  if (isAbortLikeError(err, signal)) {
@@ -16944,7 +17602,12 @@ function closeElasticsearchAdapter(dbId) {
16944
17602
  esAdapterCache.close(dbId);
16945
17603
  }
16946
17604
  function resolveEs(cwd, dbParam, signal, omitDirNames) {
16947
- return resolveDockerExplorerAsync(cwd, dbParam, "elasticsearch", esAdapterCache, (info) => openElasticsearchAdapterAsync(info.serviceName, info.env, info.composeDir), omitDirNames, signal);
17605
+ return resolveDatastoreExplorerAsync(cwd, dbParam, "elasticsearch", esAdapterCache, (info) => openElasticsearchAdapterAsync(info.serviceName, info.env, info.composeDir), (connection) => {
17606
+ if (connection.kind !== "elasticsearch") {
17607
+ throw new Error("invalid Elasticsearch connection");
17608
+ }
17609
+ return createElasticsearchAdapter(connection);
17610
+ }, omitDirNames, signal);
16948
17611
  }
16949
17612
  async function handleIndices(req, cwd, url, omitDirNames) {
16950
17613
  const r = await resolveEs(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
@@ -17189,7 +17852,11 @@ function closeRedisAdapter(dbId) {
17189
17852
  redisAdapterCache.close(dbId);
17190
17853
  }
17191
17854
  function resolveRedis(cwd, dbParam, signal, omitDirNames) {
17192
- return resolveDockerExplorerAsync(cwd, dbParam, "redis", redisAdapterCache, (info) => openRedisExplorerAsync(info.serviceName, info.env, info.composeDir), omitDirNames, signal);
17855
+ return resolveDatastoreExplorerAsync(cwd, dbParam, "redis", redisAdapterCache, (info) => openRedisExplorerAsync(info.serviceName, info.env, info.composeDir), (connection) => {
17856
+ if (connection.kind !== "redis")
17857
+ throw new Error("invalid Redis connection");
17858
+ return createRedisAdapter(connection);
17859
+ }, omitDirNames, signal);
17193
17860
  }
17194
17861
  async function handleDatabases(req, cwd, url, omitDirNames) {
17195
17862
  const r = await resolveRedis(cwd, url.searchParams.get("db"), req.signal, omitDirNames);
@@ -17381,7 +18048,11 @@ function closeS3Adapter(dbId) {
17381
18048
  s3AdapterCache.close(dbId);
17382
18049
  }
17383
18050
  function resolveS3(cwd, dbParam, signal, omitDirNames) {
17384
- return resolveDockerExplorerAsync(cwd, dbParam, "s3", s3AdapterCache, (info) => openS3ExplorerAsync(info), omitDirNames, signal);
18051
+ return resolveDatastoreExplorerAsync(cwd, dbParam, "s3", s3AdapterCache, (info) => openS3ExplorerAsync(info), (connection) => {
18052
+ if (connection.kind !== "s3")
18053
+ throw new Error("invalid S3 connection");
18054
+ return createS3Adapter(connection);
18055
+ }, omitDirNames, signal);
17385
18056
  }
17386
18057
  function validateBucket(value) {
17387
18058
  if (!value)
@@ -17831,11 +18502,11 @@ var init_handle_s3 = __esm(() => {
17831
18502
  });
17832
18503
 
17833
18504
  // web-src/server/database/query-history.ts
17834
- import { join as join11 } from "node:path";
18505
+ import { join as join12 } from "node:path";
17835
18506
  function historyFilePath(root) {
17836
- return join11(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18507
+ return join12(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
17837
18508
  }
17838
- function emptyState() {
18509
+ function emptyState2() {
17839
18510
  return { version: 1, entries: [] };
17840
18511
  }
17841
18512
  function serializeHistoryState(state) {
@@ -17845,14 +18516,14 @@ function serializeHistoryState(state) {
17845
18516
  };
17846
18517
  let content = `${JSON.stringify(normalized, null, 2)}
17847
18518
  `;
17848
- while (normalized.entries.length > 1 && Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES) {
18519
+ while (normalized.entries.length > 1 && Buffer.byteLength(content, "utf8") > MAX_JSON_BYTES2) {
17849
18520
  normalized.entries.pop();
17850
18521
  content = `${JSON.stringify(normalized, null, 2)}
17851
18522
  `;
17852
18523
  }
17853
18524
  return content;
17854
18525
  }
17855
- function optionalString2(value, maxLen) {
18526
+ function optionalString3(value, maxLen) {
17856
18527
  if (typeof value !== "string")
17857
18528
  return;
17858
18529
  if (!value || value.length > maxLen || value.includes("\x00"))
@@ -17883,16 +18554,16 @@ function sanitizeEntry(raw) {
17883
18554
  if (!raw || typeof raw !== "object")
17884
18555
  return null;
17885
18556
  const entry = raw;
17886
- const id = optionalString2(entry.id, MAX_ID_LEN);
17887
- const dbId = optionalString2(entry.dbId, MAX_DB_ID_LEN);
17888
- const sql = optionalString2(entry.sql, MAX_SQL_LEN);
18557
+ const id = optionalString3(entry.id, MAX_ID_LEN);
18558
+ const dbId = optionalString3(entry.dbId, MAX_DB_ID_LEN);
18559
+ const sql = optionalString3(entry.sql, MAX_SQL_LEN);
17889
18560
  if (!id || !dbId || !sql)
17890
18561
  return null;
17891
18562
  const columns = Array.isArray(entry.columns) ? entry.columns.filter((col) => typeof col === "string" && !!col).map((col) => col.slice(0, MAX_COLUMN_LEN)).slice(0, MAX_COLUMNS) : [];
17892
18563
  const rowsPreview = sanitizeRows(entry.rowsPreview);
17893
- const schema = optionalString2(entry.schema, MAX_SCHEMA_LEN);
17894
- const title = optionalString2(entry.title, MAX_TEXT_LEN);
17895
- const body = optionalString2(entry.body, MAX_TEXT_LEN);
18564
+ const schema = optionalString3(entry.schema, MAX_SCHEMA_LEN);
18565
+ const title = optionalString3(entry.title, MAX_TEXT_LEN);
18566
+ const body = optionalString3(entry.body, MAX_TEXT_LEN);
17896
18567
  return {
17897
18568
  id,
17898
18569
  dbId,
@@ -17906,17 +18577,17 @@ function sanitizeEntry(raw) {
17906
18577
  savedRows: finiteNumber(entry.savedRows) ?? rowsPreview.length,
17907
18578
  truncated: typeof entry.truncated === "boolean" ? entry.truncated : false,
17908
18579
  elapsedMs: finiteNumber(entry.elapsedMs) ?? 0,
17909
- executedAt: optionalString2(entry.executedAt, 64) ?? new Date(0).toISOString(),
18580
+ executedAt: optionalString3(entry.executedAt, 64) ?? new Date(0).toISOString(),
17910
18581
  executedBy: entry.executedBy === "ai" ? "ai" : "user",
17911
18582
  source: entry.source === "cli" ? "cli" : "browser"
17912
18583
  };
17913
18584
  }
17914
18585
  function sanitizeHistoryState(raw) {
17915
18586
  if (!raw || typeof raw !== "object")
17916
- return emptyState();
18587
+ return emptyState2();
17917
18588
  const entriesRaw = raw.entries;
17918
18589
  if (!Array.isArray(entriesRaw))
17919
- return emptyState();
18590
+ return emptyState2();
17920
18591
  const entries = [];
17921
18592
  for (const entry of entriesRaw) {
17922
18593
  if (entries.length >= MAX_ENTRIES2)
@@ -17955,7 +18626,7 @@ function deleteQueryHistoryEntry(state, id) {
17955
18626
  }
17956
18627
  function clearQueryHistory(state, dbId, schema) {
17957
18628
  if (!dbId)
17958
- return emptyState();
18629
+ return emptyState2();
17959
18630
  return {
17960
18631
  version: 1,
17961
18632
  entries: state.entries.filter((e) => {
@@ -17967,14 +18638,14 @@ function clearQueryHistory(state, dbId, schema) {
17967
18638
  })
17968
18639
  };
17969
18640
  }
17970
- var CODE_VIEWER_DIR3 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES = 1e6, MAX_ID_LEN = 128, MAX_DB_ID_LEN = 2048, MAX_SCHEMA_LEN = 512, MAX_SQL_LEN = 64000, MAX_TEXT_LEN = 64000, MAX_COLUMN_LEN = 512, MAX_COLUMNS = 500, historyStore;
18641
+ var CODE_VIEWER_DIR3 = ".code-viewer", HISTORY_FILE_NAME = "query-history.json", MAX_ENTRIES2 = 200, MAX_PREVIEW_ROWS = 100, MAX_JSON_BYTES2 = 1e6, MAX_ID_LEN = 128, MAX_DB_ID_LEN = 2048, MAX_SCHEMA_LEN = 512, MAX_SQL_LEN = 64000, MAX_TEXT_LEN = 64000, MAX_COLUMN_LEN = 512, MAX_COLUMNS = 500, historyStore;
17971
18642
  var init_query_history = __esm(() => {
17972
18643
  init_json_store();
17973
18644
  historyStore = createJsonFileStore({
17974
18645
  filePath: historyFilePath,
17975
- empty: emptyState,
18646
+ empty: emptyState2,
17976
18647
  sanitize: sanitizeHistoryState,
17977
- maxBytes: MAX_JSON_BYTES,
18648
+ maxBytes: MAX_JSON_BYTES2,
17978
18649
  backupSuffix: "corrupt",
17979
18650
  serialize: serializeHistoryState
17980
18651
  });
@@ -17983,9 +18654,9 @@ var init_query_history = __esm(() => {
17983
18654
  // web-src/server/database/snapshot-store.ts
17984
18655
  import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
17985
18656
  import { mkdirSync as mkdirSync3 } from "node:fs";
17986
- import { join as join12 } from "node:path";
18657
+ import { join as join13 } from "node:path";
17987
18658
  async function getStoreDb(cwd) {
17988
- const dbPath = join12(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
18659
+ const dbPath = join13(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
17989
18660
  if (storeDb && storeDbPath === dbPath)
17990
18661
  return storeDb;
17991
18662
  if (storeDb) {
@@ -17993,7 +18664,7 @@ async function getStoreDb(cwd) {
17993
18664
  storeDb.close();
17994
18665
  } catch {}
17995
18666
  }
17996
- mkdirSync3(join12(cwd, CODE_VIEWER_DIR4), { recursive: true });
18667
+ mkdirSync3(join13(cwd, CODE_VIEWER_DIR4), { recursive: true });
17997
18668
  const DbClass = await loadSqliteClass();
17998
18669
  storeDb = new DbClass(dbPath);
17999
18670
  storeDbPath = dbPath;
@@ -18599,11 +19270,11 @@ var init_snapshot_runner = __esm(() => {
18599
19270
  });
18600
19271
 
18601
19272
  // web-src/server/database/tabs-store.ts
18602
- import { join as join13 } from "node:path";
19273
+ import { join as join14 } from "node:path";
18603
19274
  function tabsFilePath(root) {
18604
- return join13(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19275
+ return join14(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
18605
19276
  }
18606
- function emptyState2() {
19277
+ function emptyState3() {
18607
19278
  return { version: 1, tabs: [], activeTabId: null };
18608
19279
  }
18609
19280
  function isValidCssSize(s) {
@@ -18725,10 +19396,10 @@ function sanitizeDynamodb(v) {
18725
19396
  }
18726
19397
  function sanitize(input) {
18727
19398
  if (!input || typeof input !== "object")
18728
- return emptyState2();
19399
+ return emptyState3();
18729
19400
  const obj = input;
18730
19401
  if (obj.version !== 1)
18731
- return emptyState2();
19402
+ return emptyState3();
18732
19403
  const rawTabs = Array.isArray(obj.tabs) ? obj.tabs : [];
18733
19404
  const seenIds = new Set;
18734
19405
  const tabs = [];
@@ -18796,7 +19467,7 @@ async function loadTabsAsync(cwd) {
18796
19467
  async function saveTabsAsync(cwd, state) {
18797
19468
  return tabsStore.save(cwd, state);
18798
19469
  }
18799
- var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES2 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN2 = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_DYNAMODB_TABLE_LEN = 255, MAX_DYNAMODB_EXPRESSION_LEN = 4096, MAX_DYNAMODB_ATTRIBUTE_VALUES_LEN = 16000, MAX_DYNAMODB_ITEM_KEY_LEN = 4096, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
19470
+ var CODE_VIEWER_DIR5 = ".code-viewer", TABS_FILE_NAME = "tabs.json", MAX_TABS = 64, MAX_JSON_BYTES3 = 1e6, MAX_SQL_DRAFT_LEN = 16000, MAX_ES_QUERY_LEN = 16000, MAX_TAB_ID_LEN = 128, MAX_DB_ID_LEN2 = 2048, MAX_SCHEMA_NAME_LEN = 512, MAX_TABLE_NAME_LEN2 = 512, MAX_REDIS_KEY_LEN = 1024, MAX_REDIS_KEY_FILTER_LEN = 512, MAX_INDEX_NAME_LEN = 256, MAX_S3_BUCKET_LEN = 256, MAX_S3_KEY_LEN = 2048, MAX_S3_QUERY_LEN = 2048, MAX_DYNAMODB_TABLE_LEN = 255, MAX_DYNAMODB_EXPRESSION_LEN = 4096, MAX_DYNAMODB_ATTRIBUTE_VALUES_LEN = 16000, MAX_DYNAMODB_ITEM_KEY_LEN = 4096, MAX_CSS_SIZE_LEN = 16, VALID_VIEWS, tabsStore;
18800
19471
  var init_tabs_store = __esm(() => {
18801
19472
  init_json_store();
18802
19473
  VALID_VIEWS = new Set([
@@ -18809,9 +19480,9 @@ var init_tabs_store = __esm(() => {
18809
19480
  ]);
18810
19481
  tabsStore = createJsonFileStore({
18811
19482
  filePath: tabsFilePath,
18812
- empty: emptyState2,
19483
+ empty: emptyState3,
18813
19484
  sanitize,
18814
- maxBytes: MAX_JSON_BYTES2,
19485
+ maxBytes: MAX_JSON_BYTES3,
18815
19486
  backupSuffix: "bak",
18816
19487
  sizeErrorMessage: "tabs state too large"
18817
19488
  });
@@ -18839,6 +19510,10 @@ function ensureInit() {
18839
19510
  initialized = true;
18840
19511
  }
18841
19512
  async function getAdapter(r, _cwd, signal) {
19513
+ if (r.saved) {
19514
+ const cacheKey = r.schema ? `${r.dbId}\x00schema=${r.schema}` : r.dbId;
19515
+ return dockerAdapterCache.getOrOpenAsync(cacheKey, () => createSqlCliAdapter({ ...r.saved, schema: r.schema }));
19516
+ }
18842
19517
  if (r.docker) {
18843
19518
  const docker = r.docker;
18844
19519
  const cacheKey = r.schema ? `${r.dbId}\x00schema=${r.schema}` : r.dbId;
@@ -18886,6 +19561,24 @@ async function resolveSupabaseSchema(info, requestedSchema, signal) {
18886
19561
  async function resolveDb(cwd, dbParam, omitDirNames, schemaParam, signal) {
18887
19562
  if (!dbParam)
18888
19563
  return textError("missing db parameter", 400);
19564
+ if (dbParam.startsWith("connection:")) {
19565
+ const connection = await findDatastoreConnection(cwd, dbParam);
19566
+ if (!connection)
19567
+ return textError("datastore connection not found", 404);
19568
+ if (connection.kind !== "postgresql" && connection.kind !== "mysql") {
19569
+ return textError(`${connection.kind} must use its datastore routes`, 400);
19570
+ }
19571
+ const requestedSchema = normalizeSchemaParam(schemaParam);
19572
+ if (requestedSchema instanceof Response)
19573
+ return requestedSchema;
19574
+ const schema = connection.kind === "postgresql" ? requestedSchema || connection.schema || "public" : undefined;
19575
+ return {
19576
+ resolved: dbParam,
19577
+ dbId: dbParam,
19578
+ saved: connection,
19579
+ ...schema ? { schema } : {}
19580
+ };
19581
+ }
18889
19582
  if (dbParam.startsWith("supabase:")) {
18890
19583
  const parsed = parseSupabaseDbId(dbParam);
18891
19584
  if (!parsed)
@@ -18993,10 +19686,11 @@ async function expandDockerServicesForFiles(dockerServices, listDockerDatabases,
18993
19686
  }
18994
19687
  async function createDbFilesResponse(cwd, omitDirNames, signal, deps = DEFAULT_DB_FILE_DISCOVERY_DEPS) {
18995
19688
  ensureInit();
18996
- const [sqliteSettled, dockerSettled, supabaseSettled] = await Promise.allSettled([
19689
+ const [sqliteSettled, dockerSettled, supabaseSettled, connectionsSettled] = await Promise.allSettled([
18997
19690
  deps.discoverSqliteFiles(cwd, omitDirNames, signal),
18998
19691
  deps.discoverDockerDatabases(cwd, omitDirNames, signal),
18999
- deps.discoverSupabaseCliProjects(cwd, omitDirNames, signal)
19692
+ deps.discoverSupabaseCliProjects(cwd, omitDirNames, signal),
19693
+ (deps.loadConnections ?? loadDatastoreConnections)(cwd)
19000
19694
  ]);
19001
19695
  if (sqliteSettled.status === "rejected") {
19002
19696
  throw sqliteSettled.reason;
@@ -19004,11 +19698,15 @@ async function createDbFilesResponse(cwd, omitDirNames, signal, deps = DEFAULT_D
19004
19698
  if (supabaseSettled.status === "rejected") {
19005
19699
  throw supabaseSettled.reason;
19006
19700
  }
19701
+ if (connectionsSettled.status === "rejected") {
19702
+ throw connectionsSettled.reason;
19703
+ }
19007
19704
  if (dockerSettled.status === "rejected" && isAbortLikeError(dockerSettled.reason, signal)) {
19008
19705
  throw dockerSettled.reason;
19009
19706
  }
19010
19707
  const sqliteFiles = sqliteSettled.value;
19011
19708
  const supabaseProjects = supabaseSettled.value;
19709
+ const savedConnections = connectionsSettled.value;
19012
19710
  const dockerServices = dockerSettled.status === "fulfilled" ? dockerSettled.value : [];
19013
19711
  const dockerErrors = [];
19014
19712
  if (dockerSettled.status === "rejected") {
@@ -19027,7 +19725,8 @@ async function createDbFilesResponse(cwd, omitDirNames, signal, deps = DEFAULT_D
19027
19725
  kind: "sqlite"
19028
19726
  })),
19029
19727
  ...dockerEntries.map(toFileInfo),
19030
- ...supabaseProjects.map(toFileInfo)
19728
+ ...supabaseProjects.map(toFileInfo),
19729
+ ...savedConnections.map(connectionToFileInfo)
19031
19730
  ],
19032
19731
  ...dockerTruncated ? { truncated: true } : {},
19033
19732
  ...dockerErrors.length > 0 ? { dockerError: dockerErrors.join("; ") } : {}
@@ -19052,6 +19751,26 @@ async function createDbSchemasResponse(cwd, dbParam, schemaParam, omitDirNames,
19052
19751
  };
19053
19752
  return { ok: true, value: body2 };
19054
19753
  }
19754
+ if (r.saved?.kind === "postgresql") {
19755
+ try {
19756
+ const adapter = await getAdapter(r, cwd, signal);
19757
+ const { result, executedSql: executedSql2 } = await captureSql(() => adapter.executeReadonlyQueryAsync("SELECT schema_name FROM information_schema.schemata ORDER BY schema_name", undefined, 1000, signal));
19758
+ return {
19759
+ ok: true,
19760
+ value: {
19761
+ dbId: r.dbId,
19762
+ schemas: result.rows.map((row) => ({ name: String(row[0]) })),
19763
+ selectedSchema: r.schema,
19764
+ executedSql: executedSql2
19765
+ }
19766
+ };
19767
+ } catch (err) {
19768
+ return {
19769
+ ok: false,
19770
+ response: handleError("database", "list schemas", err, signal)
19771
+ };
19772
+ }
19773
+ }
19055
19774
  if (!r.docker || r.docker.kind !== "postgresql") {
19056
19775
  const body2 = { dbId: r.dbId, schemas: [] };
19057
19776
  return { ok: true, value: body2 };
@@ -20054,6 +20773,118 @@ async function handleTabsPut(cwd, req) {
20054
20773
  async function handleDbUiGet(cwd) {
20055
20774
  return jsonLoadResponse(() => loadDbUiState(cwd), "db UI", "failed to load db UI state");
20056
20775
  }
20776
+ function closeSavedConnection(id, kind) {
20777
+ if (kind === "postgresql" || kind === "mysql") {
20778
+ dockerAdapterCache.close(id);
20779
+ dockerAdapterCache.closePrefix(`${id}\x00`);
20780
+ return;
20781
+ }
20782
+ DOCKER_CLOSE_REGISTRY[kind]?.(id);
20783
+ }
20784
+ async function handleConnections(cwd, req) {
20785
+ if (req.method === "GET") {
20786
+ const connections = await loadDatastoreConnections(cwd);
20787
+ return json({ connections: connections.map(publicConnection) });
20788
+ }
20789
+ const body = await parseBoundedJsonBody(req, 65536, "connection payload too large");
20790
+ if (body instanceof Response)
20791
+ return body;
20792
+ if (req.method === "PUT") {
20793
+ try {
20794
+ const connection = await saveDatastoreConnection(cwd, body);
20795
+ closeSavedConnection(connection.id, connection.kind);
20796
+ return json({ connection: publicConnection(connection) });
20797
+ } catch (err) {
20798
+ const message = err instanceof Error ? err.message : "invalid datastore connection";
20799
+ return textError(message, message === "too many datastore connections" ? 409 : 400);
20800
+ }
20801
+ }
20802
+ const id = body && typeof body === "object" && "id" in body ? body.id : undefined;
20803
+ if (typeof id !== "string" || !id.startsWith("connection:")) {
20804
+ return textError("invalid datastore connection id", 400);
20805
+ }
20806
+ const existing = await findDatastoreConnection(cwd, id);
20807
+ if (!existing)
20808
+ return textError("datastore connection not found", 404);
20809
+ await deleteDatastoreConnection(cwd, id);
20810
+ closeSavedConnection(id, existing.kind);
20811
+ return json({ ok: true });
20812
+ }
20813
+ async function probeDatastoreConnection(connection, signal) {
20814
+ if (connection.kind === "postgresql" || connection.kind === "mysql") {
20815
+ const adapter2 = createSqlCliAdapter(connection);
20816
+ try {
20817
+ await adapter2.getTablesAsync(signal);
20818
+ } finally {
20819
+ adapter2.close();
20820
+ }
20821
+ return;
20822
+ }
20823
+ if (connection.kind === "redis") {
20824
+ const adapter2 = createRedisAdapter(connection);
20825
+ try {
20826
+ await adapter2.listDatabasesAsync(signal);
20827
+ } finally {
20828
+ adapter2.close();
20829
+ }
20830
+ return;
20831
+ }
20832
+ if (connection.kind === "elasticsearch") {
20833
+ const adapter2 = createElasticsearchAdapter(connection);
20834
+ try {
20835
+ await adapter2.listIndicesAsync(signal);
20836
+ } finally {
20837
+ adapter2.close();
20838
+ }
20839
+ return;
20840
+ }
20841
+ if (connection.kind === "s3") {
20842
+ const adapter2 = createS3Adapter(connection);
20843
+ try {
20844
+ await adapter2.listBuckets(signal);
20845
+ } finally {
20846
+ adapter2.close();
20847
+ }
20848
+ return;
20849
+ }
20850
+ if (connection.kind !== "dynamodb") {
20851
+ throw new Error("invalid datastore connection");
20852
+ }
20853
+ const adapter = createDynamoDbAdapter(connection);
20854
+ try {
20855
+ await adapter.listTablesAsync({ limit: 1, signal });
20856
+ } finally {
20857
+ adapter.close();
20858
+ }
20859
+ }
20860
+ async function handleConnectionTest(cwd, req) {
20861
+ const body = await parseBoundedJsonBody(req, 65536, "connection payload too large");
20862
+ if (body instanceof Response)
20863
+ return body;
20864
+ const input = body && typeof body === "object" ? body : {};
20865
+ let existing = null;
20866
+ if (typeof input.id === "string") {
20867
+ existing = await findDatastoreConnection(cwd, input.id);
20868
+ if (!existing)
20869
+ return textError("datastore connection not found", 404);
20870
+ }
20871
+ try {
20872
+ const connection = validateDatastoreConnection({
20873
+ ...existing ?? {},
20874
+ ...input
20875
+ });
20876
+ await probeDatastoreConnection(connection, req.signal);
20877
+ return json({ ok: true });
20878
+ } catch (err) {
20879
+ if (isAbortLikeError(err, req.signal)) {
20880
+ return textError("connection test aborted", 503);
20881
+ }
20882
+ if (err instanceof Error && err.message === "invalid datastore connection") {
20883
+ return textError(err.message, 400);
20884
+ }
20885
+ return textError("connection failed", 400);
20886
+ }
20887
+ }
20057
20888
  async function handleDbUiPatch(cwd, req) {
20058
20889
  const body = await parseBoundedJsonBody(req, MAX_DB_UI_BODY_BYTES, "db UI body too large");
20059
20890
  if (body instanceof Response)
@@ -20074,6 +20905,12 @@ async function handleClose(cwd, req, omitDirNames) {
20074
20905
  return body;
20075
20906
  if (!body.db)
20076
20907
  return textError("missing db", 400);
20908
+ if (body.db.startsWith("connection:")) {
20909
+ const connection = await findDatastoreConnection(cwd, body.db);
20910
+ if (connection)
20911
+ closeSavedConnection(body.db, connection.kind);
20912
+ return json({ ok: true });
20913
+ }
20077
20914
  if (body.db.startsWith("docker:")) {
20078
20915
  const parsed = parseDockerDbId(body.db);
20079
20916
  if (!parsed)
@@ -20174,6 +21011,16 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
20174
21011
  methods: ["GET"],
20175
21012
  handler: () => handleFiles(cwd, omitDirNames, req.signal)
20176
21013
  },
21014
+ "/_db/connections": {
21015
+ methods: ["GET", "PUT", "DELETE"],
21016
+ sideEffect: (requestMethod) => requestMethod !== "GET",
21017
+ handler: () => handleConnections(cwd, req)
21018
+ },
21019
+ "/_db/connections/test": {
21020
+ methods: ["POST"],
21021
+ sideEffect: true,
21022
+ handler: () => handleConnectionTest(cwd, req)
21023
+ },
20177
21024
  "/_db/schemas": {
20178
21025
  methods: ["GET"],
20179
21026
  handler: () => handleSchemas(cwd, url, omitDirNames, req.signal)
@@ -20294,9 +21141,14 @@ var init_handle = __esm(() => {
20294
21141
  init_state_store();
20295
21142
  init_docker();
20296
21143
  init_docker_utils();
21144
+ init_dynamodb();
21145
+ init_elasticsearch();
21146
+ init_redis();
21147
+ init_s3();
20297
21148
  init_sql_capture();
20298
21149
  init_sqlite();
20299
21150
  init_connection_pool();
21151
+ init_connections_store();
20300
21152
  init_discovery();
20301
21153
  init_global_search();
20302
21154
  init_handle_dynamodb();
@@ -20314,7 +21166,8 @@ var init_handle = __esm(() => {
20314
21166
  discoverSqliteFiles: discoverSqliteFilesAsync,
20315
21167
  discoverDockerDatabases: discoverDockerDatabasesAsync,
20316
21168
  listDockerDatabases: listDockerDatabasesAsync,
20317
- discoverSupabaseCliProjects: discoverSupabaseCliProjectsAsync
21169
+ discoverSupabaseCliProjects: discoverSupabaseCliProjectsAsync,
21170
+ loadConnections: loadDatastoreConnections
20318
21171
  };
20319
21172
  searchJobs = new Map;
20320
21173
  snapshotJobs = new Map;
@@ -20356,7 +21209,7 @@ var init_handle = __esm(() => {
20356
21209
 
20357
21210
  // web-src/server/doctor.ts
20358
21211
  import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
20359
- import { dirname as dirname4, join as join14, relative as relative5 } from "node:path";
21212
+ import { dirname as dirname4, join as join15, relative as relative5 } from "node:path";
20360
21213
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20361
21214
  function statusWorse(a, b) {
20362
21215
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -20450,7 +21303,7 @@ function findCodeViewerPackageJson() {
20450
21303
  cursor = dirname4(process.argv[1] || ".");
20451
21304
  }
20452
21305
  for (let depth = 0;depth < 8; depth += 1) {
20453
- const candidate = join14(cursor, "package.json");
21306
+ const candidate = join15(cursor, "package.json");
20454
21307
  try {
20455
21308
  const raw = readFileSync6(candidate, "utf8");
20456
21309
  const pkg = JSON.parse(raw);
@@ -20546,7 +21399,7 @@ async function checkSqlite(cwd) {
20546
21399
  return { id: "sqlite", title: "SQLite driver", rows };
20547
21400
  }
20548
21401
  async function trySnapshotDbOpen(cwd) {
20549
- const dbPath = join14(cwd, SNAPSHOT_DB_REL);
21402
+ const dbPath = join15(cwd, SNAPSHOT_DB_REL);
20550
21403
  try {
20551
21404
  statSync5(dbPath);
20552
21405
  } catch {
@@ -20567,7 +21420,7 @@ async function trySnapshotDbOpen(cwd) {
20567
21420
  }
20568
21421
  }
20569
21422
  function checkSnapshotStore(cwd) {
20570
- const dbPath = join14(cwd, SNAPSHOT_DB_REL);
21423
+ const dbPath = join15(cwd, SNAPSHOT_DB_REL);
20571
21424
  const dir = dirname4(dbPath);
20572
21425
  let dirStatus = "ok";
20573
21426
  let dirDetail = dir;
@@ -21571,7 +22424,7 @@ function normalizeNewDirectoryName(name) {
21571
22424
 
21572
22425
  // web-src/server/cache.ts
21573
22426
  import { lstatSync as lstatSync3 } from "node:fs";
21574
- import { join as join15 } from "node:path";
22427
+ import { join as join16 } from "node:path";
21575
22428
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
21576
22429
  return !!cached && now - cached.storedAt <= ttlMs;
21577
22430
  }
@@ -21586,7 +22439,7 @@ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MA
21586
22439
  }
21587
22440
  function worktreeFileSignature(path, cwd) {
21588
22441
  try {
21589
- const stats = lstatSync3(join15(cwd, path));
22442
+ const stats = lstatSync3(join16(cwd, path));
21590
22443
  const inode = "ino" in stats ? stats.ino : 0;
21591
22444
  return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
21592
22445
  } catch {
@@ -21632,12 +22485,12 @@ function startDevAssetReload(options) {
21632
22485
  var init_dev_assets = () => {};
21633
22486
 
21634
22487
  // web-src/server/journal.ts
21635
- import { join as join16 } from "node:path";
22488
+ import { join as join17 } from "node:path";
21636
22489
  function dailyJournalFilePath(root) {
21637
- return join16(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
22490
+ return join17(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
21638
22491
  }
21639
22492
  function journalTasksFilePath(root) {
21640
- return join16(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
22493
+ return join17(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
21641
22494
  }
21642
22495
  function emptyDailyJournalState() {
21643
22496
  return { version: 1, entries: [] };
@@ -21650,7 +22503,7 @@ function makeJournalId(prefix) {
21650
22503
  const time = Date.now().toString(36);
21651
22504
  return `${prefix}-${time}${random}`;
21652
22505
  }
21653
- function optionalString3(value, maxLen) {
22506
+ function optionalString4(value, maxLen) {
21654
22507
  if (typeof value !== "string")
21655
22508
  return;
21656
22509
  if (value.includes("\x00"))
@@ -21676,12 +22529,12 @@ function normalizeJournalEntry(raw) {
21676
22529
  if (!raw || typeof raw !== "object")
21677
22530
  return null;
21678
22531
  const entry = raw;
21679
- const id = optionalString3(entry.id, 128);
22532
+ const id = optionalString4(entry.id, 128);
21680
22533
  const date = isIsoDate(entry.date) ? entry.date : undefined;
21681
22534
  const body = optionalBody(entry.body, JOURNAL_ENTRY_BODY_MAX_BYTES);
21682
22535
  if (!id || !date || body === undefined)
21683
22536
  return null;
21684
- const title = optionalString3(entry.title, JOURNAL_TITLE_MAX_CHARS);
22537
+ const title = optionalString4(entry.title, JOURNAL_TITLE_MAX_CHARS);
21685
22538
  return {
21686
22539
  id,
21687
22540
  date,
@@ -21689,8 +22542,8 @@ function normalizeJournalEntry(raw) {
21689
22542
  body,
21690
22543
  labels: normalizeJournalLabels(entry.labels),
21691
22544
  source: normalizeSource(entry.source),
21692
- created_at: optionalString3(entry.created_at, 64) ?? new Date(0).toISOString(),
21693
- updated_at: optionalString3(entry.updated_at, 64) ?? new Date(0).toISOString()
22545
+ created_at: optionalString4(entry.created_at, 64) ?? new Date(0).toISOString(),
22546
+ updated_at: optionalString4(entry.updated_at, 64) ?? new Date(0).toISOString()
21694
22547
  };
21695
22548
  }
21696
22549
  function normalizeDailyJournalState(raw) {
@@ -21714,13 +22567,13 @@ function normalizeTaskNote(raw) {
21714
22567
  if (!raw || typeof raw !== "object")
21715
22568
  return null;
21716
22569
  const note = raw;
21717
- const id = optionalString3(note.id, 128);
22570
+ const id = optionalString4(note.id, 128);
21718
22571
  const body = optionalBody(note.body, JOURNAL_TASK_NOTE_MAX_BYTES);
21719
22572
  if (!id || body === undefined)
21720
22573
  return null;
21721
22574
  return {
21722
22575
  id,
21723
- at: optionalString3(note.at, 64) ?? new Date(0).toISOString(),
22576
+ at: optionalString4(note.at, 64) ?? new Date(0).toISOString(),
21724
22577
  body,
21725
22578
  source: normalizeSource(note.source)
21726
22579
  };
@@ -21729,9 +22582,9 @@ function normalizeTaskClaim(raw) {
21729
22582
  if (!raw || typeof raw !== "object")
21730
22583
  return;
21731
22584
  const claim = raw;
21732
- const by = optionalString3(claim.by, 128);
21733
- const claimedAt = optionalString3(claim.claimed_at, 64);
21734
- const leaseExpiresAt = optionalString3(claim.lease_expires_at, 64);
22585
+ const by = optionalString4(claim.by, 128);
22586
+ const claimedAt = optionalString4(claim.claimed_at, 64);
22587
+ const leaseExpiresAt = optionalString4(claim.lease_expires_at, 64);
21735
22588
  if (!by || !claimedAt || !leaseExpiresAt)
21736
22589
  return;
21737
22590
  return {
@@ -21744,8 +22597,8 @@ function normalizeJournalTask(raw) {
21744
22597
  if (!raw || typeof raw !== "object")
21745
22598
  return null;
21746
22599
  const task = raw;
21747
- const id = optionalString3(task.id, 128);
21748
- const title = optionalString3(task.title, JOURNAL_TITLE_MAX_CHARS);
22600
+ const id = optionalString4(task.id, 128);
22601
+ const title = optionalString4(task.title, JOURNAL_TITLE_MAX_CHARS);
21749
22602
  if (!id || !title)
21750
22603
  return null;
21751
22604
  const status = isJournalTaskStatus(task.status) ? task.status : "todo";
@@ -21753,8 +22606,8 @@ function normalizeJournalTask(raw) {
21753
22606
  const body = optionalBody(task.body, JOURNAL_TASK_BODY_MAX_BYTES) ?? "";
21754
22607
  const dueDate = isIsoDate(task.due_date) ? task.due_date : undefined;
21755
22608
  const sourceDate = isIsoDate(task.source_date) ? task.source_date : undefined;
21756
- const journalEntryId = optionalString3(task.journal_entry_id, 128);
21757
- const completedAt = optionalString3(task.completed_at, 64);
22609
+ const journalEntryId = optionalString4(task.journal_entry_id, 128);
22610
+ const completedAt = optionalString4(task.completed_at, 64);
21758
22611
  const notes = Array.isArray(task.notes) ? task.notes.slice(0, MAX_NOTES_PER_TASK).map(normalizeTaskNote).filter((note) => note !== null) : [];
21759
22612
  const claim = normalizeTaskClaim(task.claim);
21760
22613
  return {
@@ -21764,8 +22617,8 @@ function normalizeJournalTask(raw) {
21764
22617
  status,
21765
22618
  priority,
21766
22619
  labels: normalizeJournalLabels(task.labels),
21767
- created_at: optionalString3(task.created_at, 64) ?? new Date(0).toISOString(),
21768
- updated_at: optionalString3(task.updated_at, 64) ?? new Date(0).toISOString(),
22620
+ created_at: optionalString4(task.created_at, 64) ?? new Date(0).toISOString(),
22621
+ updated_at: optionalString4(task.updated_at, 64) ?? new Date(0).toISOString(),
21769
22622
  ...dueDate ? { due_date: dueDate } : {},
21770
22623
  ...sourceDate ? { source_date: sourceDate } : {},
21771
22624
  ...journalEntryId ? { journal_entry_id: journalEntryId } : {},
@@ -21846,7 +22699,7 @@ function addDailyJournalEntry(state, input, now, makeId3 = makeJournalId) {
21846
22699
  const valid = validateEntryInput(input);
21847
22700
  if (valid.ok === false)
21848
22701
  return valid;
21849
- const title = optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS);
22702
+ const title = optionalString4(input.title, JOURNAL_TITLE_MAX_CHARS);
21850
22703
  const entry = {
21851
22704
  id: makeId3("j"),
21852
22705
  date: input.date,
@@ -21877,7 +22730,7 @@ function updateDailyJournalEntry(state, id, patch, now) {
21877
22730
  next.date = patch.date;
21878
22731
  }
21879
22732
  if (patch.title !== undefined) {
21880
- const title = optionalString3(patch.title, JOURNAL_TITLE_MAX_CHARS);
22733
+ const title = optionalString4(patch.title, JOURNAL_TITLE_MAX_CHARS);
21881
22734
  if (title)
21882
22735
  next.title = title;
21883
22736
  else
@@ -21910,7 +22763,7 @@ function deleteDailyJournalEntry(state, id) {
21910
22763
  };
21911
22764
  }
21912
22765
  function validateTaskInput(input) {
21913
- if (!optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS))
22766
+ if (!optionalString4(input.title, JOURNAL_TITLE_MAX_CHARS))
21914
22767
  return { ok: false, error: "title is required" };
21915
22768
  if (input.body !== undefined && optionalBody(input.body, JOURNAL_TASK_BODY_MAX_BYTES) === undefined)
21916
22769
  return { ok: false, error: "body is too large" };
@@ -21955,7 +22808,7 @@ function addJournalTask(state, input, now, makeId3 = makeJournalId) {
21955
22808
  const status = input.status || anchor?.status || "todo";
21956
22809
  const task = {
21957
22810
  id: makeId3("t"),
21958
- title: optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS) || "Untitled task",
22811
+ title: optionalString4(input.title, JOURNAL_TITLE_MAX_CHARS) || "Untitled task",
21959
22812
  body: input.body || "",
21960
22813
  status,
21961
22814
  priority: input.priority || "p2",
@@ -21980,7 +22833,7 @@ function updateJournalTask(state, id, patch, now) {
21980
22833
  return { ok: false, error: "task not found" };
21981
22834
  const next = { ...task, updated_at: now };
21982
22835
  if (patch.title !== undefined) {
21983
- const title = optionalString3(patch.title, JOURNAL_TITLE_MAX_CHARS);
22836
+ const title = optionalString4(patch.title, JOURNAL_TITLE_MAX_CHARS);
21984
22837
  if (!title)
21985
22838
  return { ok: false, error: "title is required" };
21986
22839
  next.title = title;
@@ -22022,7 +22875,7 @@ function updateJournalTask(state, id, patch, now) {
22022
22875
  return { ok: false, error: "source date must be YYYY-MM-DD" };
22023
22876
  }
22024
22877
  if (patch.journal_entry_id !== undefined) {
22025
- const journalEntryId = optionalString3(patch.journal_entry_id, 128);
22878
+ const journalEntryId = optionalString4(patch.journal_entry_id, 128);
22026
22879
  if (journalEntryId)
22027
22880
  next.journal_entry_id = journalEntryId;
22028
22881
  else
@@ -22072,10 +22925,10 @@ function linkGithubIssueTask(state, input, now, makeId3 = makeJournalId) {
22072
22925
  if (!Number.isInteger(input.issue_number) || input.issue_number < 1) {
22073
22926
  return { ok: false, error: "issue number must be a positive integer" };
22074
22927
  }
22075
- const title = optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS) || `GitHub issue #${input.issue_number}`;
22076
- const repo = optionalString3(input.repo, 120);
22077
- const issueUrl = optionalString3(input.url, 240);
22078
- const memoLabel = optionalString3(input.memo_label, 80);
22928
+ const title = optionalString4(input.title, JOURNAL_TITLE_MAX_CHARS) || `GitHub issue #${input.issue_number}`;
22929
+ const repo = optionalString4(input.repo, 120);
22930
+ const issueUrl = optionalString4(input.url, 240);
22931
+ const memoLabel = optionalString4(input.memo_label, 80);
22079
22932
  const requiredLabels = githubIssueRequiredLabels(input.issue_number, repo, input.labels);
22080
22933
  const linkLabel = journalIssueLabel(input.issue_number);
22081
22934
  const repoLabel = journalIssueRepoLabel(repo);
@@ -22153,7 +23006,7 @@ function claimJournalTask(state, id, input, now) {
22153
23006
  ok: false,
22154
23007
  error: "only todo or expired doing tasks can be claimed"
22155
23008
  };
22156
- const by = optionalString3(input.by, 128) || "ai";
23009
+ const by = optionalString4(input.by, 128) || "ai";
22157
23010
  const wipLimit = input.wip_limit;
22158
23011
  if (wipLimit !== undefined && wipLimit > 0) {
22159
23012
  const activeDoing = state.tasks.filter((item) => {
@@ -22198,7 +23051,7 @@ function completeJournalTask(state, id, input, now, makeId3 = makeJournalId) {
22198
23051
  const activeClaim = task.claim && Number.isFinite(Date.parse(task.claim.lease_expires_at)) && Date.parse(task.claim.lease_expires_at) > nowMs;
22199
23052
  if (!activeClaim)
22200
23053
  return { ok: false, error: "task must be claimed before completion" };
22201
- const by = optionalString3(input.by, 128);
23054
+ const by = optionalString4(input.by, 128);
22202
23055
  if (!by)
22203
23056
  return { ok: false, error: "task completion requires claim owner" };
22204
23057
  if (task.claim?.by !== by)
@@ -22270,7 +23123,7 @@ var init_journal2 = __esm(() => {
22270
23123
 
22271
23124
  // web-src/server/search-service.ts
22272
23125
  import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "node:fs";
22273
- import { join as join17, relative as relative6 } from "node:path";
23126
+ import { join as join18, relative as relative6 } from "node:path";
22274
23127
  async function rgAvailableAsync(cwd) {
22275
23128
  if (rgAvailableCache !== null)
22276
23129
  return rgAvailableCache;
@@ -22299,7 +23152,7 @@ function safeWorktreePath(env, path) {
22299
23152
  return null;
22300
23153
  if (isGitInternalPath(path))
22301
23154
  return null;
22302
- const full = join17(env.cwd, path);
23155
+ const full = join18(env.cwd, path);
22303
23156
  if (!existsSync7(full))
22304
23157
  return null;
22305
23158
  let realCwd;
@@ -22491,7 +23344,7 @@ var init_search_service = __esm(() => {
22491
23344
 
22492
23345
  // web-src/server/mcp.ts
22493
23346
  import { readFileSync as readFileSync8 } from "node:fs";
22494
- import { join as join18 } from "node:path";
23347
+ import { join as join19 } from "node:path";
22495
23348
  function defaultMcpTools(options = {}) {
22496
23349
  return [
22497
23350
  {
@@ -23965,7 +24818,7 @@ var init_mcp = __esm(() => {
23965
24818
  init_search_cli();
23966
24819
  init_search_service();
23967
24820
  init_status_cli();
23968
- PACKAGE_VERSION = JSON.parse(readFileSync8(join18(ROOT, "package.json"), "utf8")).version;
24821
+ PACKAGE_VERSION = JSON.parse(readFileSync8(join19(ROOT, "package.json"), "utf8")).version;
23969
24822
  MCP_SERVER_INFO = {
23970
24823
  name: "code-viewer",
23971
24824
  title: "code-viewer",
@@ -24061,7 +24914,7 @@ import {
24061
24914
  writeFileSync as writeFileSync2
24062
24915
  } from "node:fs";
24063
24916
  import { homedir as homedir3 } from "node:os";
24064
- import { basename as basename3, dirname as dirname5, extname as extname2, join as join19, relative as relative7 } from "node:path";
24917
+ import { basename as basename3, dirname as dirname5, extname as extname2, join as join20, relative as relative7 } from "node:path";
24065
24918
  function parseCli() {
24066
24919
  const rest = [];
24067
24920
  for (let i = 2;i < process.argv.length; i++) {
@@ -24183,7 +25036,7 @@ Examples:
24183
25036
  }
24184
25037
  function warnIfLegacyConfigPresent() {
24185
25038
  try {
24186
- if (existsSync8(join19(cwd, ".code-viewer.json"))) {
25039
+ if (existsSync8(join20(cwd, ".code-viewer.json"))) {
24187
25040
  console.warn("[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed.");
24188
25041
  }
24189
25042
  } catch {}
@@ -24290,7 +25143,7 @@ function staticFile(pathname) {
24290
25143
  const spec = map[pathname];
24291
25144
  if (!spec)
24292
25145
  return null;
24293
- const full = join19(WEB_ROOT, spec[0]);
25146
+ const full = join20(WEB_ROOT, spec[0]);
24294
25147
  if (!existsSync8(full))
24295
25148
  return text("not found", 404);
24296
25149
  return new Response(readFileSync9(full), {
@@ -24541,7 +25394,7 @@ function safeWorktreePath2(path) {
24541
25394
  return safeWorktreePath(currentSearchEnv(), path);
24542
25395
  }
24543
25396
  function worktreePath(path) {
24544
- return join19(cwd, path);
25397
+ return join20(cwd, path);
24545
25398
  }
24546
25399
  function safeOpenWorktreePath(path) {
24547
25400
  if (path === "") {
@@ -24830,7 +25683,7 @@ async function handleLog(url) {
24830
25683
  }
24831
25684
  function blamePathKey(p) {
24832
25685
  try {
24833
- const st = statSync6(join19(cwd, p));
25686
+ const st = statSync6(join20(cwd, p));
24834
25687
  return `${st.mtimeMs}:${st.size}`;
24835
25688
  } catch {
24836
25689
  return "missing";
@@ -25363,7 +26216,7 @@ async function handleUploadFiles(req) {
25363
26216
  total += file.size;
25364
26217
  if (total > MAX_UPLOAD_TOTAL_BYTES)
25365
26218
  return text("upload too large", 413);
25366
- const target = join19(realDir, safeName);
26219
+ const target = join20(realDir, safeName);
25367
26220
  if (relative7(realDir, dirname5(target)) !== "")
25368
26221
  return text("invalid filename", 400);
25369
26222
  if (existsSync8(target))
@@ -25485,9 +26338,9 @@ function triggerUpdate(changedPaths) {
25485
26338
  sendSse("update", data);
25486
26339
  }
25487
26340
  function moveMacPathIntoTrash(path) {
25488
- const trashDir = join19(homedir3(), ".Trash");
26341
+ const trashDir = join20(homedir3(), ".Trash");
25489
26342
  const base = basename3(path) || "code-viewer-trash-item";
25490
- const target = join19(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
26343
+ const target = join20(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
25491
26344
  try {
25492
26345
  mkdirSync4(trashDir, { recursive: true });
25493
26346
  renameSync(path, target);
@@ -25529,7 +26382,7 @@ async function restoreTrashPath(originalPath, trashPath) {
25529
26382
  if (!existsSync8(trashPath))
25530
26383
  return { ok: false, error: "trash item not found" };
25531
26384
  try {
25532
- const trashRoot = join19(homedir3(), ".Trash");
26385
+ const trashRoot = join20(homedir3(), ".Trash");
25533
26386
  const trashRelative = relative7(trashRoot, trashPath);
25534
26387
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
25535
26388
  return { ok: false, error: "invalid trash handle" };
@@ -25679,7 +26532,7 @@ async function handleCreateDirectory(req) {
25679
26532
  const targetPath = dir ? `${dir}/${name}` : name;
25680
26533
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
25681
26534
  return text("invalid target", 400);
25682
- const target = join19(parent, name);
26535
+ const target = join20(parent, name);
25683
26536
  if (existsSync8(target))
25684
26537
  return text("already exists", 409);
25685
26538
  try {
@@ -26277,8 +27130,8 @@ var init_preview = __esm(async () => {
26277
27130
  init_server_registry();
26278
27131
  init_state_store();
26279
27132
  init_worktree_watcher();
26280
- WEB_ROOT = join19(ROOT, "web");
26281
- VERSION = JSON.parse(readFileSync9(join19(ROOT, "package.json"), "utf8")).version;
27133
+ WEB_ROOT = join20(ROOT, "web");
27134
+ VERSION = JSON.parse(readFileSync9(join20(ROOT, "package.json"), "utf8")).version;
26282
27135
  DEFAULT_ARGS = ["HEAD"];
26283
27136
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
26284
27137
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;