@youtyan/code-viewer 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,8 @@ Requires Node.js 20 or newer when installed from npm. Development uses
34
34
  viewer.
35
35
  - Browse SQLite, PostgreSQL, MySQL, Redis, Elasticsearch, and S3-compatible
36
36
  object storage (MinIO, LocalStack) with a built-in datastore viewer.
37
+ Local Supabase CLI (`supabase start`) Postgres projects are auto-discovered
38
+ too, without needing a `docker-compose.yml`.
37
39
  - Read the built-in Help page for getting started, the `.code-viewer/`
38
40
  project files, AI annotations, datastores, the agent skill, and
39
41
  keybindings.
@@ -42,8 +44,9 @@ Requires Node.js 20 or newer when installed from npm. Development uses
42
44
  `@youtyan/code-viewer` version and execution origin (npx cache vs
43
45
  local), SQLite driver and snapshot store, Git, discovery summary,
44
46
  per-source datastore connectivity (each discovered SQLite / docker
45
- SQL / Redis / Elasticsearch / S3 source gets one row with a 2s
46
- minimal-read probe; failure rows include a paste-safe retry hint),
47
+ SQL / Supabase CLI / Redis / Elasticsearch / S3 source gets one row
48
+ with a 2s minimal-read probe; failure rows include a paste-safe retry
49
+ hint),
47
50
  Docker / Compose health (config dry-parse + `compose ps` per service),
48
51
  and the listening port. Useful when `npx` cache mismatch (e.g.
49
52
  `NODE_MODULE_VERSION` errors) needs a remediation hint, or when a
@@ -219,6 +222,15 @@ Services whose names collide across subdirectories are kept distinct via
219
222
  `docker:<service>@<relDir>` ids (cwd-direct compose files keep the historical
220
223
  `docker:<service>` id for backward compatibility).
221
224
 
225
+ **Supabase CLI** (`supabase start`) local projects are also auto-discovered,
226
+ even though the Supabase CLI does not write a `docker-compose.yml` into the
227
+ project directory. Any `supabase/config.toml` found by the same recursive scan
228
+ is picked up (`project_id` and `[db] port` are read from it), and the running
229
+ Postgres container is resolved directly via `docker ps` (matched by container
230
+ name and the `com.supabase.cli.project` label) instead of `docker compose ps`.
231
+ Connection defaults to the Supabase CLI's documented local credentials
232
+ (`postgres`/`postgres`/`postgres`).
233
+
222
234
  **Redis** support: browse DB 0–15, SCAN keys, and view values per
223
235
  type (string/hash/list as dedicated panes, set/zset/stream as JSON views).
224
236
  Edit values, delete keys, and create new keys (string/hash/list/set/zset/stream)
@@ -9612,7 +9612,7 @@ var init_spawn_runner = () => {};
9612
9612
  // web-src/server/database/adapters/docker-utils.ts
9613
9613
  import { spawnSync as spawnSync2 } from "node:child_process";
9614
9614
  function isDockerComposeServiceUnavailableError(err) {
9615
- return err instanceof DockerComposeServiceUnavailableError || err instanceof DockerCommandUnavailableError;
9615
+ return err instanceof DockerComposeServiceUnavailableError || err instanceof DockerCommandUnavailableError || err instanceof SupabaseDbContainerUnavailableError;
9616
9616
  }
9617
9617
  function dockerCommand() {
9618
9618
  return commandForExternal("docker");
@@ -9629,10 +9629,15 @@ function throwIfCachedComposeDockerCommandUnavailable(cwd) {
9629
9629
  const failure = composePsCache.get(cwd)?.error || "";
9630
9630
  throwIfDockerCommandUnavailableResult({ code: 1, stderr: failure });
9631
9631
  }
9632
- function parseComposePsOutput(stdout) {
9632
+ function parseDockerJsonLines(stdout) {
9633
9633
  const output = stdout.trim();
9634
- const containers = output.startsWith("[") ? JSON.parse(output) : output.split(`
9634
+ if (!output)
9635
+ return [];
9636
+ return output.startsWith("[") ? JSON.parse(output) : output.split(`
9635
9637
  `).filter(Boolean).map((line) => JSON.parse(line));
9638
+ }
9639
+ function parseComposePsOutput(stdout) {
9640
+ const containers = parseDockerJsonLines(stdout);
9636
9641
  const byService = new Map;
9637
9642
  for (const container of containers) {
9638
9643
  if (container.Service && container.Name && container.State === "running") {
@@ -9651,15 +9656,14 @@ function cacheComposePsFailure(cwd, result, now) {
9651
9656
  error: stderr
9652
9657
  });
9653
9658
  }
9654
- function runComposePsAsync(cwd, signal) {
9655
- throwIfAborted(signal, "docker compose ps aborted");
9659
+ function runDockerCliAsync(args, opts) {
9660
+ throwIfAborted(opts.signal, opts.abortMessage);
9656
9661
  if (spawnSyncImpl !== spawnSync2) {
9657
- const command = dockerCommand();
9658
- const proc = spawnSyncImpl(command, ["compose", "ps", "--format", "json", "--status", "running"], {
9662
+ const proc = spawnSyncImpl(dockerCommand(), args, {
9659
9663
  encoding: "utf8",
9660
9664
  timeout: 5000,
9661
9665
  stdio: ["ignore", "pipe", "pipe"],
9662
- cwd
9666
+ ...opts.cwd ? { cwd: opts.cwd } : {}
9663
9667
  });
9664
9668
  return Promise.resolve({
9665
9669
  stdout: String(proc.stdout || ""),
@@ -9670,16 +9674,24 @@ ${proc.error.message}` : ""}`,
9670
9674
  }
9671
9675
  return spawnTextAsync({
9672
9676
  command: dockerCommand(),
9673
- args: ["compose", "ps", "--format", "json", "--status", "running"],
9674
- cwd,
9677
+ args,
9678
+ ...opts.cwd ? { cwd: opts.cwd } : {},
9675
9679
  timeoutMs: 5000,
9676
- signal,
9680
+ signal: opts.signal,
9677
9681
  killSignal: "SIGKILL",
9678
- abortMessage: "docker compose ps aborted",
9679
- timeoutMessage: "docker compose ps timed out",
9682
+ abortMessage: opts.abortMessage,
9683
+ timeoutMessage: opts.timeoutMessage,
9680
9684
  rejectOnError: false
9681
9685
  });
9682
9686
  }
9687
+ function runComposePsAsync(cwd, signal) {
9688
+ return runDockerCliAsync(["compose", "ps", "--format", "json", "--status", "running"], {
9689
+ cwd,
9690
+ signal,
9691
+ abortMessage: "docker compose ps aborted",
9692
+ timeoutMessage: "docker compose ps timed out"
9693
+ });
9694
+ }
9683
9695
  async function resolveRunningComposeContainerNameAsync(serviceName, cwd, signal) {
9684
9696
  throwIfAborted(signal, "docker compose ps aborted");
9685
9697
  const now = Date.now();
@@ -9777,7 +9789,128 @@ async function resolveRunningComposeContainerNameOrThrowAsync(serviceName, cwd,
9777
9789
  }
9778
9790
  return containerName;
9779
9791
  }
9780
- var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, COMPOSE_PS_FAILURE_TTL_MS = 15000, composePsCache, composePsPending, spawnSyncImpl, DockerComposeServiceUnavailableError, DockerCommandUnavailableError;
9792
+ function supabaseDbContainerName(projectId) {
9793
+ return `supabase_db_${projectId}`;
9794
+ }
9795
+ function parseDockerPsOutput(stdout) {
9796
+ return parseDockerJsonLines(stdout);
9797
+ }
9798
+ function runDockerPsAsync(args, signal) {
9799
+ return runDockerCliAsync(args, {
9800
+ signal,
9801
+ abortMessage: "docker ps aborted",
9802
+ timeoutMessage: "docker ps timed out"
9803
+ });
9804
+ }
9805
+ async function resolveRunningSupabaseDbContainerAsync(projectId, signal) {
9806
+ throwIfAborted(signal, "docker ps aborted");
9807
+ const containerName = supabaseDbContainerName(projectId);
9808
+ const now = Date.now();
9809
+ const cached = supabaseContainerCache.get(projectId);
9810
+ if (cached) {
9811
+ if (cached.containerName && cached.positiveExpiresAt > now) {
9812
+ return cached.containerName;
9813
+ }
9814
+ if (!cached.containerName && cached.negativeExpiresAt > now) {
9815
+ return null;
9816
+ }
9817
+ }
9818
+ let pending = supabaseContainerPending.get(projectId);
9819
+ if (!pending) {
9820
+ const controller = new AbortController;
9821
+ pending = {
9822
+ controller,
9823
+ refs: 0,
9824
+ done: false,
9825
+ promise: (async () => {
9826
+ const startedAt = Date.now();
9827
+ const cacheMiss = (negativeTtlMs) => {
9828
+ supabaseContainerCache.set(projectId, {
9829
+ containerName: null,
9830
+ positiveExpiresAt: startedAt,
9831
+ negativeExpiresAt: startedAt + negativeTtlMs
9832
+ });
9833
+ return null;
9834
+ };
9835
+ let proc;
9836
+ try {
9837
+ proc = await runDockerPsAsync([
9838
+ "ps",
9839
+ "--filter",
9840
+ `name=^/${containerName}$`,
9841
+ "--filter",
9842
+ `label=com.supabase.cli.project=${projectId}`,
9843
+ "--filter",
9844
+ "status=running",
9845
+ "--format",
9846
+ "json"
9847
+ ], controller.signal);
9848
+ } catch (err) {
9849
+ if (isAbortLikeError(err, controller.signal))
9850
+ throw err;
9851
+ return cacheMiss(SUPABASE_CONTAINER_NEGATIVE_TTL_MS);
9852
+ }
9853
+ if (proc.code !== 0) {
9854
+ throwIfDockerCommandUnavailableResult(proc);
9855
+ return cacheMiss(SUPABASE_CONTAINER_NEGATIVE_TTL_MS);
9856
+ }
9857
+ let containers;
9858
+ try {
9859
+ containers = parseDockerPsOutput(proc.stdout);
9860
+ } catch {
9861
+ return cacheMiss(SUPABASE_CONTAINER_NEGATIVE_TTL_MS);
9862
+ }
9863
+ const match = containers.some((c) => (c.Names || "").replace(/^\//, "") === containerName);
9864
+ if (!match)
9865
+ return cacheMiss(SUPABASE_CONTAINER_NEGATIVE_TTL_MS);
9866
+ supabaseContainerCache.set(projectId, {
9867
+ containerName,
9868
+ positiveExpiresAt: startedAt + SUPABASE_CONTAINER_POSITIVE_TTL_MS,
9869
+ negativeExpiresAt: startedAt
9870
+ });
9871
+ return containerName;
9872
+ })().finally(() => {
9873
+ if (supabaseContainerPending.get(projectId) === pending) {
9874
+ supabaseContainerPending.delete(projectId);
9875
+ }
9876
+ if (pending)
9877
+ pending.done = true;
9878
+ })
9879
+ };
9880
+ supabaseContainerPending.set(projectId, pending);
9881
+ }
9882
+ pending.refs++;
9883
+ let released = false;
9884
+ const release = () => {
9885
+ if (released)
9886
+ return;
9887
+ released = true;
9888
+ pending.refs--;
9889
+ if (pending.refs <= 0 && !pending.done) {
9890
+ pending.controller.abort();
9891
+ }
9892
+ };
9893
+ signal?.addEventListener("abort", release, { once: true });
9894
+ if (signal?.aborted) {
9895
+ signal.removeEventListener("abort", release);
9896
+ release();
9897
+ throwIfAborted(signal, "docker ps aborted");
9898
+ }
9899
+ try {
9900
+ return await pending.promise;
9901
+ } finally {
9902
+ signal?.removeEventListener("abort", release);
9903
+ release();
9904
+ }
9905
+ }
9906
+ async function resolveRunningSupabaseDbContainerOrThrowAsync(projectId, signal) {
9907
+ const containerName = await resolveRunningSupabaseDbContainerAsync(projectId, signal);
9908
+ if (!containerName) {
9909
+ throw new SupabaseDbContainerUnavailableError(projectId);
9910
+ }
9911
+ return containerName;
9912
+ }
9913
+ var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, COMPOSE_PS_FAILURE_TTL_MS = 15000, composePsCache, composePsPending, spawnSyncImpl, DockerComposeServiceUnavailableError, DockerCommandUnavailableError, SupabaseDbContainerUnavailableError, SUPABASE_CONTAINER_POSITIVE_TTL_MS = 15000, SUPABASE_CONTAINER_NEGATIVE_TTL_MS = 3000, supabaseContainerCache, supabaseContainerPending;
9781
9914
  var init_docker_utils = __esm(() => {
9782
9915
  init_command_resolver();
9783
9916
  init_spawn_runner();
@@ -9802,6 +9935,17 @@ var init_docker_utils = __esm(() => {
9802
9935
  this.name = "DockerCommandUnavailableError";
9803
9936
  }
9804
9937
  };
9938
+ SupabaseDbContainerUnavailableError = class SupabaseDbContainerUnavailableError extends Error {
9939
+ projectId;
9940
+ status = 503;
9941
+ constructor(projectId) {
9942
+ super(`Supabase local DB container for project "${projectId}" is not running. Start it with: supabase start`);
9943
+ this.name = "SupabaseDbContainerUnavailableError";
9944
+ this.projectId = projectId;
9945
+ }
9946
+ };
9947
+ supabaseContainerCache = new Map;
9948
+ supabaseContainerPending = new Map;
9805
9949
  });
9806
9950
 
9807
9951
  // web-src/server/database/adapters/sql-capture.ts
@@ -10674,13 +10818,9 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
10674
10818
  throwIfCachedComposeDockerCommandUnavailable(cwd);
10675
10819
  return setDockerSchemasCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
10676
10820
  }
10677
- const config = {
10678
- kind,
10679
- containerName,
10680
- user,
10681
- password,
10682
- database
10683
- };
10821
+ return fetchPostgresSchemasViaContainerAsync({ kind, containerName, user, password, database }, cacheKey, now, signal);
10822
+ }
10823
+ async function fetchPostgresSchemasViaContainerAsync(config, cacheKey, now, signal) {
10684
10824
  try {
10685
10825
  const sql = `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema') AND schema_name NOT LIKE 'pg_toast%' AND schema_name NOT LIKE 'pg_temp_%' AND schema_name NOT LIKE 'pg_toast_temp_%' AND has_schema_privilege(schema_name, 'USAGE') ORDER BY CASE WHEN schema_name = 'public' THEN 0 ELSE 1 END, schema_name`;
10686
10826
  const result = await execInContainerAsync(config, sql, 1e4, signal);
@@ -10699,6 +10839,38 @@ async function listDockerSchemasAsync(serviceName, kind, env, cwd, overrideDatab
10699
10839
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
10700
10840
  }
10701
10841
  }
10842
+ function supabaseSchemasCacheKey(projectId) {
10843
+ return `supabase\x00${projectId}`;
10844
+ }
10845
+ async function listSupabaseSchemasAsync(projectId, signal) {
10846
+ const cacheKey = supabaseSchemasCacheKey(projectId);
10847
+ const now = Date.now();
10848
+ const cached = dockerSchemasCache.get(cacheKey);
10849
+ if (cached && cached.expiresAt > now)
10850
+ return [...cached.value];
10851
+ const containerName = await resolveRunningSupabaseDbContainerAsync(projectId, signal);
10852
+ if (!containerName) {
10853
+ return setDockerSchemasCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
10854
+ }
10855
+ return fetchPostgresSchemasViaContainerAsync({
10856
+ kind: "postgresql",
10857
+ containerName,
10858
+ user: SUPABASE_LOCAL_DB_USER,
10859
+ password: SUPABASE_LOCAL_DB_PASSWORD,
10860
+ database: SUPABASE_LOCAL_DB_NAME
10861
+ }, cacheKey, now, signal);
10862
+ }
10863
+ async function openSupabaseDockerAdapterAsync(projectId, schema, signal) {
10864
+ const containerName = await resolveRunningSupabaseDbContainerOrThrowAsync(projectId, signal);
10865
+ return createDockerAdapter({
10866
+ kind: "postgresql",
10867
+ containerName,
10868
+ user: SUPABASE_LOCAL_DB_USER,
10869
+ password: SUPABASE_LOCAL_DB_PASSWORD,
10870
+ database: SUPABASE_LOCAL_DB_NAME,
10871
+ ...schema ? { schema } : {}
10872
+ });
10873
+ }
10702
10874
  async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatabase, schema, signal) {
10703
10875
  const containerName = await resolveRunningComposeContainerNameOrThrowAsync(serviceName, cwd, signal);
10704
10876
  const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
@@ -10713,7 +10885,7 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
10713
10885
  ...kind === "postgresql" && schema ? { schema } : {}
10714
10886
  });
10715
10887
  }
10716
- 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;
10888
+ 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";
10717
10889
  var init_docker = __esm(() => {
10718
10890
  init_mutate();
10719
10891
  init_sql_snapshot();
@@ -13599,6 +13771,39 @@ async function pathExistsAsync(path) {
13599
13771
  return false;
13600
13772
  }
13601
13773
  }
13774
+ async function walkForMarkerFileAsync(dir, depth, omitSet, hasCapacity, visitDir, signal) {
13775
+ if (signal?.aborted || !hasCapacity())
13776
+ return;
13777
+ if (depth > MAX_SCAN_DEPTH)
13778
+ return;
13779
+ await visitDir(dir);
13780
+ if (signal?.aborted || !hasCapacity())
13781
+ return;
13782
+ let entries;
13783
+ try {
13784
+ entries = await readdir(dir);
13785
+ } catch {
13786
+ return;
13787
+ }
13788
+ for (const entry of entries) {
13789
+ if (signal?.aborted || !hasCapacity())
13790
+ return;
13791
+ if (omitSet.has(entry.toLowerCase()))
13792
+ continue;
13793
+ const full = join8(dir, entry);
13794
+ let entryStat;
13795
+ try {
13796
+ entryStat = await lstat(full);
13797
+ } catch {
13798
+ continue;
13799
+ }
13800
+ if (entryStat.isSymbolicLink())
13801
+ continue;
13802
+ if (entryStat.isDirectory()) {
13803
+ await walkForMarkerFileAsync(full, depth + 1, omitSet, hasCapacity, visitDir, signal);
13804
+ }
13805
+ }
13806
+ }
13602
13807
  async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
13603
13808
  const cacheKey = dockerDiscoveryCacheKey(cwd, omitDirNames);
13604
13809
  const now = Date.now();
@@ -13610,13 +13815,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
13610
13815
  const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
13611
13816
  omitSet.add(".git");
13612
13817
  omitSet.add("node_modules");
13613
- async function scan(dir, depth) {
13614
- if (signal?.aborted)
13615
- return;
13616
- if (results.length >= MAX_DOCKER_SERVICES)
13617
- return;
13618
- if (depth > MAX_SCAN_DEPTH)
13619
- return;
13818
+ await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_DOCKER_SERVICES, async (dir) => {
13620
13819
  for (const filename of COMPOSE_FILENAMES) {
13621
13820
  const filepath = join8(dir, filename);
13622
13821
  if (await pathExistsAsync(filepath)) {
@@ -13624,37 +13823,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
13624
13823
  break;
13625
13824
  }
13626
13825
  }
13627
- if (signal?.aborted)
13628
- return;
13629
- if (results.length >= MAX_DOCKER_SERVICES)
13630
- return;
13631
- let entries;
13632
- try {
13633
- entries = await readdir(dir);
13634
- } catch {
13635
- return;
13636
- }
13637
- for (const entry of entries) {
13638
- if (signal?.aborted)
13639
- return;
13640
- if (results.length >= MAX_DOCKER_SERVICES)
13641
- return;
13642
- if (omitSet.has(entry.toLowerCase()))
13643
- continue;
13644
- const full = join8(dir, entry);
13645
- let entryStat;
13646
- try {
13647
- entryStat = await lstat(full);
13648
- } catch {
13649
- continue;
13650
- }
13651
- if (entryStat.isSymbolicLink())
13652
- continue;
13653
- if (entryStat.isDirectory())
13654
- await scan(full, depth + 1);
13655
- }
13656
- }
13657
- await scan(cwd, 0);
13826
+ }, signal);
13658
13827
  if (signal?.aborted)
13659
13828
  return cloneDockerDiscoveryResult(results);
13660
13829
  if (results.length >= MAX_DOCKER_SERVICES) {
@@ -13749,7 +13918,120 @@ function isSafeDockerDatabaseName(value) {
13749
13918
  return false;
13750
13919
  return /^[A-Za-z0-9_$.-]+$/.test(value);
13751
13920
  }
13752
- var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache;
13921
+ function cloneSupabaseDiscoveryResult(result) {
13922
+ return result.map((entry) => ({ ...entry }));
13923
+ }
13924
+ function parseSupabaseConfigToml(content) {
13925
+ let section = null;
13926
+ let projectId = null;
13927
+ let dbPort = null;
13928
+ for (const rawLine of content.split(`
13929
+ `)) {
13930
+ const line = rawLine.trim();
13931
+ if (!line || line.startsWith("#"))
13932
+ continue;
13933
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
13934
+ if (sectionMatch) {
13935
+ section = sectionMatch[1];
13936
+ continue;
13937
+ }
13938
+ const kvMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
13939
+ if (!kvMatch)
13940
+ continue;
13941
+ const value = stripScalarSyntax(kvMatch[2]);
13942
+ if (section === null && kvMatch[1] === "project_id") {
13943
+ projectId = value;
13944
+ } else if (section === "db" && kvMatch[1] === "port") {
13945
+ dbPort = value;
13946
+ }
13947
+ }
13948
+ if (!projectId || !isSafeDockerServiceName(projectId))
13949
+ return null;
13950
+ return {
13951
+ projectId,
13952
+ dbPort: dbPort && /^\d+$/.test(dbPort) ? dbPort : DEFAULT_SUPABASE_DB_PORT
13953
+ };
13954
+ }
13955
+ async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal) {
13956
+ const cacheKey = discoveryCacheKey(cwd, omitDirNames);
13957
+ const now = Date.now();
13958
+ const cached = supabaseDiscoveryCache.get(cacheKey);
13959
+ if (cached && cached.expiresAt > now) {
13960
+ return cloneSupabaseDiscoveryResult(cached.result);
13961
+ }
13962
+ const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
13963
+ omitSet.add(".git");
13964
+ omitSet.add("node_modules");
13965
+ const results = [];
13966
+ await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
13967
+ const configPath = join8(dir, "supabase", "config.toml");
13968
+ if (!await pathExistsAsync(configPath))
13969
+ return;
13970
+ try {
13971
+ const content = await readFile2(configPath, "utf-8");
13972
+ const parsed = parseSupabaseConfigToml(content);
13973
+ if (!parsed)
13974
+ return;
13975
+ const relDir = relative3(cwd, dir);
13976
+ const isRoot = relDir === "" || relDir === ".";
13977
+ const relDirSlash = relDir.replace(/\\/g, "/");
13978
+ const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
13979
+ const labelPath = isRoot ? "" : ` — ${relDirSlash}`;
13980
+ results.push({
13981
+ id,
13982
+ path: isRoot ? "supabase/config.toml" : `${relDirSlash}/supabase/config.toml`,
13983
+ name: `${parsed.projectId} (Supabase CLI, postgres@127.0.0.1:${parsed.dbPort}/postgres${labelPath})`,
13984
+ sizeBytes: 0,
13985
+ kind: "postgresql",
13986
+ projectId: parsed.projectId,
13987
+ relDirSlash,
13988
+ dbPort: parsed.dbPort
13989
+ });
13990
+ } catch {}
13991
+ }, signal);
13992
+ if (signal?.aborted)
13993
+ return cloneSupabaseDiscoveryResult(results);
13994
+ supabaseDiscoveryCache.set(cacheKey, {
13995
+ expiresAt: now + SUPABASE_DISCOVERY_TTL_MS,
13996
+ result: cloneSupabaseDiscoveryResult(results)
13997
+ });
13998
+ return cloneSupabaseDiscoveryResult(results);
13999
+ }
14000
+ function parseSupabaseDbId(dbId) {
14001
+ if (!dbId.startsWith("supabase:"))
14002
+ return null;
14003
+ const rest = dbId.slice("supabase:".length);
14004
+ if (!rest)
14005
+ return null;
14006
+ const atIdx = rest.indexOf("@");
14007
+ let projectId;
14008
+ let relDir = "";
14009
+ if (atIdx >= 0) {
14010
+ if (rest.indexOf("@", atIdx + 1) >= 0)
14011
+ return null;
14012
+ projectId = rest.slice(0, atIdx);
14013
+ try {
14014
+ relDir = decodeURIComponent(rest.slice(atIdx + 1));
14015
+ } catch {
14016
+ return null;
14017
+ }
14018
+ if (!isSafeDockerRelDir(relDir))
14019
+ return null;
14020
+ } else {
14021
+ projectId = rest;
14022
+ }
14023
+ if (!isSafeDockerServiceName(projectId))
14024
+ return null;
14025
+ return { projectId, relDir };
14026
+ }
14027
+ async function findSupabaseCliProjectByDbIdAsync(cwd, dbId, omitDirNames, signal) {
14028
+ const parsed = parseSupabaseDbId(dbId);
14029
+ if (!parsed)
14030
+ return null;
14031
+ const projects = await discoverSupabaseCliProjectsAsync(cwd, omitDirNames, signal);
14032
+ return projects.find((p) => p.projectId === parsed.projectId && p.relDirSlash === parsed.relDir) || null;
14033
+ }
14034
+ var SQLITE_EXTENSIONS, SQLITE_MAGIC = "SQLite format 3\x00", MAX_SCAN_DEPTH = 3, MAX_ENTRIES = 50, DOCKER_DISCOVERY_TTL_MS = 5000, SQLITE_DISCOVERY_TTL_MS = 5000, sqliteDiscoveryCache, COMPOSE_FILENAMES, MAX_DOCKER_SERVICES = 30, dockerDiscoveryCache, MAX_SUPABASE_PROJECTS = 30, SUPABASE_DISCOVERY_TTL_MS = 5000, DEFAULT_SUPABASE_DB_PORT = "54322", supabaseDiscoveryCache;
13753
14035
  var init_discovery = __esm(() => {
13754
14036
  SQLITE_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".s3db"]);
13755
14037
  sqliteDiscoveryCache = new Map;
@@ -13760,6 +14042,7 @@ var init_discovery = __esm(() => {
13760
14042
  "compose.yaml"
13761
14043
  ];
13762
14044
  dockerDiscoveryCache = new Map;
14045
+ supabaseDiscoveryCache = new Map;
13763
14046
  });
13764
14047
 
13765
14048
  // web-src/core/id.ts
@@ -16754,8 +17037,9 @@ function sanitizeCssSize(v) {
16754
17037
  return isValidCssSize(v) ? v : undefined;
16755
17038
  }
16756
17039
  function isToolInternalDbId(dbId) {
16757
- if (!dbId || dbId.startsWith("docker:"))
17040
+ if (!dbId || dbId.startsWith("docker:") || dbId.startsWith("supabase:")) {
16758
17041
  return false;
17042
+ }
16759
17043
  return dbId.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
16760
17044
  }
16761
17045
  function sanitizeRedis(v) {
@@ -16934,6 +17218,11 @@ async function getAdapter(r, _cwd, signal) {
16934
17218
  const cacheKey = r.schema ? `${r.dbId}\x00schema=${r.schema}` : r.dbId;
16935
17219
  return dockerAdapterCache.getOrOpenAsync(cacheKey, () => openDockerAdapterAsync(docker.serviceName, docker.kind, docker.env, docker.composeDir, docker.database, r.schema, signal));
16936
17220
  }
17221
+ if (r.supabase) {
17222
+ const projectId = r.supabase.projectId;
17223
+ const cacheKey = r.schema ? `${r.dbId}\x00schema=${r.schema}` : r.dbId;
17224
+ return dockerAdapterCache.getOrOpenAsync(cacheKey, () => openSupabaseDockerAdapterAsync(projectId, r.schema, signal));
17225
+ }
16937
17226
  return getConnection(r.resolved);
16938
17227
  }
16939
17228
  function sanitizeFilename(name) {
@@ -16960,9 +17249,35 @@ async function resolvePostgresSchema(info, requestedSchema, signal) {
16960
17249
  return "public";
16961
17250
  return schemas[0] || "public";
16962
17251
  }
17252
+ async function resolveSupabaseSchema(info, requestedSchema, signal) {
17253
+ if (requestedSchema)
17254
+ return requestedSchema;
17255
+ const schemas = await listSupabaseSchemasAsync(info.projectId, signal);
17256
+ if (schemas.includes("public"))
17257
+ return "public";
17258
+ return schemas[0] || "public";
17259
+ }
16963
17260
  async function resolveDb(cwd, dbParam, omitDirNames, schemaParam, signal) {
16964
17261
  if (!dbParam)
16965
17262
  return textError("missing db parameter", 400);
17263
+ if (dbParam.startsWith("supabase:")) {
17264
+ const parsed = parseSupabaseDbId(dbParam);
17265
+ if (!parsed)
17266
+ return textError("invalid supabase db id", 400);
17267
+ const info = await findSupabaseCliProjectByDbIdAsync(cwd, dbParam, omitDirNames, signal);
17268
+ if (!info)
17269
+ return textError("supabase project not found", 404);
17270
+ const requestedSchema = normalizeSchemaParam(schemaParam);
17271
+ if (requestedSchema instanceof Response)
17272
+ return requestedSchema;
17273
+ const schema = await resolveSupabaseSchema(info, requestedSchema, signal);
17274
+ return {
17275
+ resolved: dbParam,
17276
+ dbId: dbParam,
17277
+ supabase: info,
17278
+ ...schema ? { schema } : {}
17279
+ };
17280
+ }
16966
17281
  if (dbParam.startsWith("docker:")) {
16967
17282
  const parsed = parseDockerDbId(dbParam);
16968
17283
  if (!parsed)
@@ -17049,17 +17364,22 @@ async function expandDockerServicesForFiles(dockerServices, listDockerDatabases,
17049
17364
  }
17050
17365
  async function createDbFilesResponse(cwd, omitDirNames, signal, deps = DEFAULT_DB_FILE_DISCOVERY_DEPS) {
17051
17366
  ensureInit();
17052
- const [sqliteSettled, dockerSettled] = await Promise.allSettled([
17367
+ const [sqliteSettled, dockerSettled, supabaseSettled] = await Promise.allSettled([
17053
17368
  deps.discoverSqliteFiles(cwd, omitDirNames, signal),
17054
- deps.discoverDockerDatabases(cwd, omitDirNames, signal)
17369
+ deps.discoverDockerDatabases(cwd, omitDirNames, signal),
17370
+ deps.discoverSupabaseCliProjects(cwd, omitDirNames, signal)
17055
17371
  ]);
17056
17372
  if (sqliteSettled.status === "rejected") {
17057
17373
  throw sqliteSettled.reason;
17058
17374
  }
17375
+ if (supabaseSettled.status === "rejected") {
17376
+ throw supabaseSettled.reason;
17377
+ }
17059
17378
  if (dockerSettled.status === "rejected" && isAbortLikeError(dockerSettled.reason, signal)) {
17060
17379
  throw dockerSettled.reason;
17061
17380
  }
17062
17381
  const sqliteFiles = sqliteSettled.value;
17382
+ const supabaseProjects = supabaseSettled.value;
17063
17383
  const dockerServices = dockerSettled.status === "fulfilled" ? dockerSettled.value : [];
17064
17384
  const dockerErrors = [];
17065
17385
  if (dockerSettled.status === "rejected") {
@@ -17077,7 +17397,8 @@ async function createDbFilesResponse(cwd, omitDirNames, signal, deps = DEFAULT_D
17077
17397
  sizeBytes: f.sizeBytes,
17078
17398
  kind: "sqlite"
17079
17399
  })),
17080
- ...dockerEntries.map(toFileInfo)
17400
+ ...dockerEntries.map(toFileInfo),
17401
+ ...supabaseProjects.map(toFileInfo)
17081
17402
  ],
17082
17403
  ...dockerTruncated ? { truncated: true } : {},
17083
17404
  ...dockerErrors.length > 0 ? { dockerError: dockerErrors.join("; ") } : {}
@@ -17091,6 +17412,17 @@ async function createDbSchemasResponse(cwd, dbParam, schemaParam, omitDirNames,
17091
17412
  const r = await resolveDb(cwd, dbParam, omitDirNames, schemaParam, signal);
17092
17413
  if (r instanceof Response)
17093
17414
  return { ok: false, response: r };
17415
+ if (r.supabase) {
17416
+ const projectId = r.supabase.projectId;
17417
+ const { result: schemas2, executedSql: executedSql2 } = await captureSql(() => listSupabaseSchemasAsync(projectId, signal));
17418
+ const body2 = {
17419
+ dbId: r.dbId,
17420
+ schemas: schemas2.map((name) => ({ name })),
17421
+ selectedSchema: r.schema,
17422
+ executedSql: executedSql2
17423
+ };
17424
+ return { ok: true, value: body2 };
17425
+ }
17094
17426
  if (!r.docker || r.docker.kind !== "postgresql") {
17095
17427
  const body2 = { dbId: r.dbId, schemas: [] };
17096
17428
  return { ok: true, value: body2 };
@@ -18132,7 +18464,7 @@ async function handleClose(cwd, req, omitDirNames) {
18132
18464
  const r = await resolveDb(cwd, body.db, omitDirNames, undefined, req.signal);
18133
18465
  if (r instanceof Response)
18134
18466
  return r;
18135
- if (r.docker) {
18467
+ if (r.docker || r.supabase) {
18136
18468
  dockerAdapterCache.close(r.dbId);
18137
18469
  dockerAdapterCache.closePrefix(`${r.dbId}\x00`);
18138
18470
  } else {
@@ -18347,7 +18679,8 @@ var init_handle = __esm(() => {
18347
18679
  DEFAULT_DB_FILE_DISCOVERY_DEPS = {
18348
18680
  discoverSqliteFiles: discoverSqliteFilesAsync,
18349
18681
  discoverDockerDatabases: discoverDockerDatabasesAsync,
18350
- listDockerDatabases: listDockerDatabasesAsync
18682
+ listDockerDatabases: listDockerDatabasesAsync,
18683
+ discoverSupabaseCliProjects: discoverSupabaseCliProjectsAsync
18351
18684
  };
18352
18685
  searchJobs = new Map;
18353
18686
  snapshotJobs = new Map;
@@ -19085,6 +19418,9 @@ async function checkDiscovery(cwd, scopeOmitDirNames, signal) {
19085
19418
  };
19086
19419
  }
19087
19420
  async function defaultDatastoreProbe(file, cwd, signal) {
19421
+ if (file.id.startsWith("supabase:")) {
19422
+ return probeSupabaseSource(file, cwd, signal);
19423
+ }
19088
19424
  switch (file.kind) {
19089
19425
  case "sqlite":
19090
19426
  return probeSqliteSource(file, cwd, signal);
@@ -19135,6 +19471,23 @@ async function probeDockerSqlSource(file, cwd, signal) {
19135
19471
  } catch {}
19136
19472
  }
19137
19473
  }
19474
+ async function probeSupabaseSource(file, cwd, signal) {
19475
+ const parsed = parseSupabaseDbId(file.id);
19476
+ if (!parsed)
19477
+ throw new Error("invalid supabase db id");
19478
+ const info = await findSupabaseCliProjectByDbIdAsync(cwd, file.id, undefined, signal);
19479
+ if (!info)
19480
+ throw new Error("supabase project not found");
19481
+ const adapter = await openSupabaseDockerAdapterAsync(info.projectId, undefined, signal);
19482
+ try {
19483
+ signal.throwIfAborted();
19484
+ await adapter.getTablesAsync(signal);
19485
+ } finally {
19486
+ try {
19487
+ adapter.close();
19488
+ } catch {}
19489
+ }
19490
+ }
19138
19491
  async function probeRedisSource(file, cwd, signal) {
19139
19492
  const info = await findDockerServiceByDbIdAsync(cwd, file.id, "redis", undefined, signal);
19140
19493
  if (!info)
@@ -21734,7 +22087,7 @@ function validateMcpOptionalDbId(raw) {
21734
22087
  const parsed = validateMcpDbId(raw);
21735
22088
  if (parsed.ok !== true)
21736
22089
  return parsed;
21737
- if (!parsed.value.startsWith("docker:")) {
22090
+ if (!parsed.value.startsWith("docker:") && !parsed.value.startsWith("supabase:")) {
21738
22091
  const pathError = validateMcpPath(parsed.value);
21739
22092
  if (pathError)
21740
22093
  return { ok: false, error: "invalid database path" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,7 +39,9 @@ browser's Database > Search tab, so the human can review the same workflow.
39
39
  1. Discover the datastore ids the running server has detected. This is the
40
40
  AI-friendly equivalent of opening the browser's Database tab and reading
41
41
  the sidebar — it lists every SQLite file plus PostgreSQL / MySQL / Redis
42
- / Elasticsearch / S3 service that any nearby `docker-compose` exposes.
42
+ / Elasticsearch / S3 service that any nearby `docker-compose` exposes, as
43
+ well as any local Supabase CLI (`supabase start`) project found via a
44
+ `supabase/config.toml` (id form `supabase:<project_id>`).
43
45
  Use the printed `id` as `--db` on every other command. Credentials and
44
46
  internal config are stripped server-side.
45
47
 
package/web/app.js CHANGED
@@ -18724,6 +18724,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
18724
18724
  const service = rest.split(/[@:]/, 1)[0];
18725
18725
  return service || "Docker";
18726
18726
  }
18727
+ if (dbId.startsWith("supabase:")) {
18728
+ const rest = dbId.slice("supabase:".length);
18729
+ const projectId = rest.split(/[@]/, 1)[0];
18730
+ return projectId || "Supabase";
18731
+ }
18727
18732
  const normalized = dbId.replace(/\\/g, "/");
18728
18733
  const lastSlash = normalized.lastIndexOf("/");
18729
18734
  return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
@@ -20016,7 +20021,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20016
20021
  const opt = document.createElement("option");
20017
20022
  opt.value = f2.id;
20018
20023
  const isDocker = f2.id.startsWith("docker:");
20019
- const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
20024
+ const isSupabase = f2.id.startsWith("supabase:");
20025
+ const label = isDocker ? `${f2.name} (Docker)` : isSupabase ? `${f2.name} (Supabase)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
20020
20026
  opt.textContent = label;
20021
20027
  optionsFragment.appendChild(opt);
20022
20028
  }
@@ -20206,7 +20212,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20206
20212
  if (!currentDbInfo)
20207
20213
  return labelFromDbId(initial.dbId);
20208
20214
  const suffix = currentSchema ? ` / ${currentSchema}` : "";
20209
- if (currentDbInfo.id.startsWith("docker:")) {
20215
+ if (currentDbInfo.id.startsWith("docker:") || currentDbInfo.id.startsWith("supabase:")) {
20210
20216
  const m = currentDbInfo.name.match(/^(\S+)/);
20211
20217
  return `${m ? m[1] : currentDbInfo.name}${suffix}`;
20212
20218
  }
@@ -24709,7 +24715,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24709
24715
  ],
24710
24716
  [
24711
24717
  "PostgreSQL",
24712
- "Detected from compose files. Multiple databases per server, plus a schema selector for switching schemas without reopening. Same inline row edit / insert / delete as SQLite."
24718
+ "Detected from compose files. Multiple databases per server, plus a schema selector for switching schemas without reopening. Same inline row edit / insert / delete as SQLite. Local Supabase CLI (`supabase start`) projects are also auto-discovered from `supabase/config.toml`, without needing a docker-compose file."
24713
24719
  ],
24714
24720
  [
24715
24721
  "Redis",
@@ -25368,7 +25374,7 @@ code-viewer annotate add-db --db app.db --tab query \\
25368
25374
  ],
25369
25375
  [
25370
25376
  "PostgreSQL",
25371
- "compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。SQLite と同じく行のインライン編集 / 追加 / 削除に対応。"
25377
+ "compose ファイルから検出。同一サーバー上の複数データベースに対応し、スキーマ切替セレクターで再オープンせずスキーマを切り替えられます。SQLite と同じく行のインライン編集 / 追加 / 削除に対応。ローカルの Supabase CLI (`supabase start`) プロジェクトも `supabase/config.toml` から自動検出され、docker-compose ファイルは不要です。"
25372
25378
  ],
25373
25379
  [
25374
25380
  "Redis",