spindb 0.33.1 → 0.34.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.
@@ -0,0 +1,211 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { existsSync } from 'fs'
4
+ import { containerManager } from '../../core/container-manager'
5
+ import { processManager } from '../../core/process-manager'
6
+ import { getPgwebStatus } from '../../core/pgweb-utils'
7
+ import { uiError, uiInfo } from '../ui/theme'
8
+ import { getEngineIcon } from '../constants'
9
+ import { Engine, type ContainerConfig } from '../../types'
10
+ import { loadEnginesJson } from '../../config/engines-registry'
11
+
12
+ function getSecondaryPorts(
13
+ config: ContainerConfig,
14
+ ): Array<{ port: number; label: string }> {
15
+ const ports: Array<{ port: number; label: string }> = []
16
+ switch (config.engine) {
17
+ case 'cockroachdb':
18
+ ports.push({ port: config.port + 1, label: 'HTTP UI' })
19
+ break
20
+ case 'clickhouse':
21
+ ports.push({ port: config.port + 1, label: 'HTTP' })
22
+ break
23
+ case 'qdrant':
24
+ ports.push({ port: config.port + 1, label: 'gRPC' })
25
+ break
26
+ case 'typedb':
27
+ ports.push({ port: config.port + 6271, label: 'HTTP' })
28
+ break
29
+ case 'questdb':
30
+ ports.push({ port: config.port + 188, label: 'HTTP Console' })
31
+ ports.push({ port: config.port + 197, label: 'ILP' })
32
+ break
33
+ case 'ferretdb':
34
+ if (config.backendPort) {
35
+ ports.push({ port: config.backendPort, label: 'PostgreSQL backend' })
36
+ }
37
+ break
38
+ }
39
+ return ports
40
+ }
41
+
42
+ export type PortEntry = { port: number; label: string }
43
+
44
+ export async function getContainerPorts(config: ContainerConfig): Promise<{
45
+ status: 'running' | 'stopped' | 'available' | 'missing'
46
+ ports: PortEntry[]
47
+ }> {
48
+ const isFileBasedDB =
49
+ config.engine === Engine.SQLite || config.engine === Engine.DuckDB
50
+
51
+ if (isFileBasedDB) {
52
+ const fileExists = existsSync(config.database)
53
+ return {
54
+ status: fileExists ? 'available' : 'missing',
55
+ ports: [],
56
+ }
57
+ }
58
+
59
+ const isRunning = await processManager.isRunning(config.name, {
60
+ engine: config.engine,
61
+ })
62
+
63
+ const enginesJson = await loadEnginesJson()
64
+ const engineConfig = enginesJson.engines[config.engine]
65
+ const displayName = engineConfig?.displayName || config.engine
66
+
67
+ const ports: PortEntry[] = [{ port: config.port, label: displayName }]
68
+
69
+ // Add secondary ports
70
+ ports.push(...getSecondaryPorts(config))
71
+
72
+ // Check for pgweb (PG-wire-protocol engines only)
73
+ if (
74
+ config.engine === 'postgresql' ||
75
+ config.engine === 'cockroachdb' ||
76
+ config.engine === 'ferretdb'
77
+ ) {
78
+ const pgweb = await getPgwebStatus(config.name, config.engine)
79
+ if (pgweb.running && pgweb.port) {
80
+ ports.push({ port: pgweb.port, label: 'pgweb' })
81
+ }
82
+ }
83
+
84
+ return {
85
+ status: isRunning ? 'running' : 'stopped',
86
+ ports,
87
+ }
88
+ }
89
+
90
+ export const portsCommand = new Command('ports')
91
+ .description('Show ports used by containers')
92
+ .argument('[name]', 'Container name (shows all if omitted)')
93
+ .option('--json', 'Output as JSON')
94
+ .option('--running', 'Only show running containers')
95
+ .action(
96
+ async (
97
+ name: string | undefined,
98
+ options: { json?: boolean; running?: boolean },
99
+ ) => {
100
+ try {
101
+ let containers: ContainerConfig[]
102
+
103
+ if (name) {
104
+ const config = await containerManager.getConfig(name)
105
+ if (!config) {
106
+ if (options.json) {
107
+ console.log(
108
+ JSON.stringify({ error: `Container "${name}" not found` }),
109
+ )
110
+ } else {
111
+ console.error(uiError(`Container "${name}" not found`))
112
+ }
113
+ process.exit(1)
114
+ }
115
+ containers = [config]
116
+ } else {
117
+ containers = await containerManager.list()
118
+ }
119
+
120
+ // Gather port info for all containers
121
+ const results = await Promise.all(
122
+ containers.map(async (config) => {
123
+ const { status, ports } = await getContainerPorts(config)
124
+ return { config, status, ports }
125
+ }),
126
+ )
127
+
128
+ // Filter to running only if requested
129
+ const filtered = options.running
130
+ ? results.filter((r) => r.status === 'running')
131
+ : results
132
+
133
+ if (options.json) {
134
+ const jsonOutput = filtered.map((r) => ({
135
+ name: r.config.name,
136
+ engine: r.config.engine,
137
+ status: r.status,
138
+ ports: r.ports,
139
+ }))
140
+ console.log(JSON.stringify(jsonOutput, null, 2))
141
+ return
142
+ }
143
+
144
+ if (filtered.length === 0) {
145
+ console.log(
146
+ uiInfo(
147
+ options.running
148
+ ? 'No running containers found.'
149
+ : 'No containers found. Create one with: spindb create',
150
+ ),
151
+ )
152
+ return
153
+ }
154
+
155
+ console.log()
156
+ console.log(
157
+ chalk.gray(' ') +
158
+ chalk.bold.white('NAME'.padEnd(22)) +
159
+ chalk.bold.white('ENGINE'.padEnd(18)) +
160
+ chalk.bold.white('STATUS'.padEnd(12)) +
161
+ chalk.bold.white('PORT(S)'),
162
+ )
163
+ console.log(chalk.gray(' ' + '─'.repeat(78)))
164
+
165
+ for (const { config, status, ports } of filtered) {
166
+ const engineIcon = getEngineIcon(config.engine)
167
+ const engineName = config.engine.padEnd(13)
168
+
169
+ const statusDisplay =
170
+ status === 'running'
171
+ ? chalk.green('● running'.padEnd(12))
172
+ : status === 'available'
173
+ ? chalk.blue('● available'.padEnd(12))
174
+ : status === 'missing'
175
+ ? chalk.gray('○ missing'.padEnd(12))
176
+ : chalk.gray('○ stopped'.padEnd(12))
177
+
178
+ let portDisplay: string
179
+ if (ports.length === 0) {
180
+ portDisplay = chalk.gray('—')
181
+ } else {
182
+ const parts = ports.map((p, i) =>
183
+ i === 0
184
+ ? String(p.port)
185
+ : `${p.port} ${chalk.gray(`(${p.label})`)}`,
186
+ )
187
+ portDisplay = parts.join(chalk.gray(', '))
188
+ }
189
+
190
+ console.log(
191
+ chalk.gray(' ') +
192
+ chalk.cyan(config.name.padEnd(22)) +
193
+ engineIcon +
194
+ chalk.white(engineName) +
195
+ statusDisplay +
196
+ portDisplay,
197
+ )
198
+ }
199
+
200
+ console.log()
201
+ } catch (error) {
202
+ const e = error as Error
203
+ if (options.json) {
204
+ console.log(JSON.stringify({ error: e.message }))
205
+ } else {
206
+ console.error(uiError(e.message))
207
+ }
208
+ process.exit(1)
209
+ }
210
+ },
211
+ )
package/cli/constants.ts CHANGED
@@ -3,12 +3,12 @@ import { Engine, type IconMode } from '../types'
3
3
 
4
4
  /**
5
5
  * Get the page size for list prompts based on terminal height.
6
- * Returns 20 for tall terminals (>= 30 rows), 15 for shorter ones.
7
- * This provides a consistent experience while avoiding rendering issues on small terminals.
6
+ * Scales dynamically with the terminal reserves ~8 lines for header, prompt, and margin,
7
+ * then uses the remaining height for list items (clamped to 10–30).
8
8
  */
9
9
  export function getPageSize(): number {
10
10
  const terminalHeight = process.stdout.rows || 24
11
- return terminalHeight >= 30 ? 20 : 15
11
+ return Math.max(10, Math.min(30, terminalHeight - 8))
12
12
  }
13
13
 
14
14
  // Engine icons with three display modes:
package/cli/index.ts CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from 'module'
3
3
  import chalk from 'chalk'
4
4
  import { createCommand } from './commands/create'
5
5
  import { listCommand } from './commands/list'
6
+ import { portsCommand } from './commands/ports'
6
7
  import { startCommand } from './commands/start'
7
8
  import { stopCommand } from './commands/stop'
8
9
  import { deleteCommand } from './commands/delete'
@@ -117,6 +118,7 @@ export async function run(): Promise<void> {
117
118
 
118
119
  program.addCommand(createCommand)
119
120
  program.addCommand(listCommand)
121
+ program.addCommand(portsCommand)
120
122
  program.addCommand(startCommand)
121
123
  program.addCommand(stopCommand)
122
124
  program.addCommand(deleteCommand)
package/cli/ui/prompts.ts CHANGED
@@ -301,6 +301,7 @@ export async function filterableListPrompt(
301
301
  emptyText?: string
302
302
  enableToggle?: boolean
303
303
  defaultValue?: string // Pre-select this value (cursor starts here)
304
+ headerItems?: (FilterableChoice | inquirer.Separator)[] // Shown above filterable items
304
305
  },
305
306
  ): Promise<string> {
306
307
  // Split choices into filterable items and static footer (separators, back buttons, etc.)
@@ -318,6 +319,7 @@ export async function filterableListPrompt(
318
319
  }
319
320
 
320
321
  // Source function for autocomplete - filters items based on input
322
+ const header = options.headerItems || []
321
323
  async function source(
322
324
  _answers: Record<string, unknown>,
323
325
  input: string | undefined,
@@ -328,7 +330,7 @@ export async function filterableListPrompt(
328
330
 
329
331
  if (!searchTerm) {
330
332
  // No filter - show all items
331
- result = [...filterableItems, ...footerItems]
333
+ result = [...header, ...filterableItems, ...footerItems]
332
334
  } else {
333
335
  // Filter items by matching search term against the display name
334
336
  // Strip ANSI codes for matching but keep them for display
@@ -349,7 +351,7 @@ export async function filterableListPrompt(
349
351
  ...footerItems,
350
352
  ]
351
353
  } else {
352
- result = [...filtered, ...footerItems]
354
+ result = [...header, ...filtered, ...footerItems]
353
355
  }
354
356
  }
355
357
 
@@ -82,6 +82,10 @@ const TYPEDB_TOOLS: BinaryTool[] = ['typedb', 'typedb_console_bin']
82
82
 
83
83
  const INFLUXDB_TOOLS: BinaryTool[] = ['influxdb3']
84
84
 
85
+ const PGWEB_TOOLS: BinaryTool[] = ['pgweb']
86
+
87
+ const DBLAB_TOOLS: BinaryTool[] = ['dblab']
88
+
85
89
  const ENHANCED_SHELLS: BinaryTool[] = [
86
90
  'pgcli',
87
91
  'mycli',
@@ -106,6 +110,8 @@ const ALL_TOOLS: BinaryTool[] = [
106
110
  ...QUESTDB_TOOLS,
107
111
  ...TYPEDB_TOOLS,
108
112
  ...INFLUXDB_TOOLS,
113
+ ...PGWEB_TOOLS,
114
+ ...DBLAB_TOOLS,
109
115
  ...SQLITE_TOOLS,
110
116
  ...DUCKDB_TOOLS,
111
117
  ...ENHANCED_SHELLS,
@@ -621,6 +627,8 @@ export {
621
627
  QUESTDB_TOOLS,
622
628
  TYPEDB_TOOLS,
623
629
  INFLUXDB_TOOLS,
630
+ PGWEB_TOOLS,
631
+ DBLAB_TOOLS,
624
632
  SQLITE_TOOLS,
625
633
  DUCKDB_TOOLS,
626
634
  ENHANCED_SHELLS,
@@ -0,0 +1,113 @@
1
+ import { Engine, type ContainerConfig } from '../types'
2
+
3
+ /** Pinned dblab version — single source of truth for download URL */
4
+ export const DBLAB_VERSION = '0.34.2'
5
+
6
+ /** Engines that support dblab (PostgreSQL, MySQL, or SQLite wire protocol) */
7
+ export const DBLAB_ENGINES = new Set([
8
+ Engine.PostgreSQL,
9
+ Engine.MySQL,
10
+ Engine.MariaDB,
11
+ Engine.CockroachDB,
12
+ Engine.SQLite,
13
+ Engine.QuestDB,
14
+ ])
15
+
16
+ /**
17
+ * Get the platform suffix for the dblab download URL.
18
+ * Returns e.g. 'darwin_arm64', 'linux_amd64', 'windows_amd64'
19
+ */
20
+ export function getDblabPlatformSuffix(): string {
21
+ const platform = process.platform
22
+ const arch = process.arch
23
+
24
+ if (platform === 'darwin' && arch === 'arm64') return 'darwin_arm64'
25
+ if (platform === 'darwin' && arch === 'x64') return 'darwin_amd64'
26
+ if (platform === 'linux' && arch === 'arm64') return 'linux_arm64'
27
+ if (platform === 'linux' && arch === 'x64') return 'linux_amd64'
28
+ if (platform === 'win32' && arch === 'x64') return 'windows_amd64'
29
+
30
+ throw new Error(`Unsupported platform: ${platform} ${arch}`)
31
+ }
32
+
33
+ /**
34
+ * Build the CLI args array for launching dblab against a container.
35
+ * Uses flag-based approach to avoid MySQL tcp() URL wrapper issues.
36
+ */
37
+ export function getDblabArgs(
38
+ config: ContainerConfig,
39
+ database: string,
40
+ ): string[] {
41
+ switch (config.engine) {
42
+ case Engine.PostgreSQL:
43
+ return [
44
+ '--host',
45
+ '127.0.0.1',
46
+ '--port',
47
+ String(config.port),
48
+ '--user',
49
+ 'postgres',
50
+ '--db',
51
+ database,
52
+ '--driver',
53
+ 'postgres',
54
+ '--ssl',
55
+ 'disable',
56
+ ]
57
+
58
+ case Engine.MySQL:
59
+ case Engine.MariaDB:
60
+ return [
61
+ '--host',
62
+ '127.0.0.1',
63
+ '--port',
64
+ String(config.port),
65
+ '--user',
66
+ 'root',
67
+ '--db',
68
+ database,
69
+ '--driver',
70
+ 'mysql',
71
+ ]
72
+
73
+ case Engine.CockroachDB:
74
+ return [
75
+ '--host',
76
+ '127.0.0.1',
77
+ '--port',
78
+ String(config.port),
79
+ '--user',
80
+ 'root',
81
+ '--db',
82
+ database,
83
+ '--driver',
84
+ 'postgres',
85
+ '--ssl',
86
+ 'disable',
87
+ ]
88
+
89
+ case Engine.QuestDB:
90
+ return [
91
+ '--host',
92
+ '127.0.0.1',
93
+ '--port',
94
+ String(config.port),
95
+ '--user',
96
+ 'admin',
97
+ '--pass',
98
+ 'quest',
99
+ '--db',
100
+ database || 'qdb',
101
+ '--driver',
102
+ 'postgres',
103
+ '--ssl',
104
+ 'disable',
105
+ ]
106
+
107
+ case Engine.SQLite:
108
+ return ['--db', config.database, '--driver', 'sqlite3']
109
+
110
+ default:
111
+ throw new Error(`dblab is not supported for engine: ${config.engine}`)
112
+ }
113
+ }
@@ -86,6 +86,10 @@ const KNOWN_BINARY_TOOLS: readonly BinaryTool[] = [
86
86
  'typedb_console_bin',
87
87
  // InfluxDB
88
88
  'influxdb3',
89
+ // Web panels
90
+ 'pgweb',
91
+ // TUI tools
92
+ 'dblab',
89
93
  // Enhanced shells (optional)
90
94
  'pgcli',
91
95
  'mycli',
@@ -0,0 +1,62 @@
1
+ import { existsSync } from 'fs'
2
+ import { readFile, unlink } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { platformService } from './platform-service'
5
+ import { paths } from '../config/paths'
6
+
7
+ /** Pinned pgweb version — single source of truth for download URL */
8
+ export const PGWEB_VERSION = '0.17.0'
9
+
10
+ /**
11
+ * Check if pgweb is running for a container.
12
+ * Reads pgweb.pid/pgweb.port files and verifies the process is alive.
13
+ * Cleans up stale PID/port files if the process is dead.
14
+ */
15
+ export async function getPgwebStatus(
16
+ containerName: string,
17
+ engine: string,
18
+ ): Promise<{ running: boolean; port?: number; pid?: number }> {
19
+ const containerDir = paths.getContainerPath(containerName, { engine })
20
+ const pidFile = join(containerDir, 'pgweb.pid')
21
+ const portFile = join(containerDir, 'pgweb.port')
22
+
23
+ if (!existsSync(pidFile)) return { running: false }
24
+
25
+ try {
26
+ const pid = parseInt(await readFile(pidFile, 'utf8'), 10)
27
+ if (platformService.isProcessRunning(pid)) {
28
+ const port = parseInt(await readFile(portFile, 'utf8'), 10)
29
+ return { running: true, port, pid }
30
+ }
31
+ } catch {
32
+ // PID file invalid or process dead
33
+ }
34
+
35
+ // Clean up stale files
36
+ await unlink(pidFile).catch(() => {})
37
+ await unlink(portFile).catch(() => {})
38
+ return { running: false }
39
+ }
40
+
41
+ /**
42
+ * Stop a running pgweb process for a container (no UI output).
43
+ * Returns true if a process was stopped, false if nothing was running.
44
+ */
45
+ export async function stopPgweb(
46
+ containerName: string,
47
+ engine: string,
48
+ ): Promise<boolean> {
49
+ const status = await getPgwebStatus(containerName, engine)
50
+ if (!status.running || !status.pid) return false
51
+
52
+ try {
53
+ await platformService.terminateProcess(status.pid, false)
54
+ } catch {
55
+ // Already gone
56
+ }
57
+
58
+ const containerDir = paths.getContainerPath(containerName, { engine })
59
+ await unlink(join(containerDir, 'pgweb.pid')).catch(() => {})
60
+ await unlink(join(containerDir, 'pgweb.port')).catch(() => {})
61
+ return true
62
+ }
@@ -13,6 +13,7 @@ import type {
13
13
  UserCredentials,
14
14
  } from '../types'
15
15
  import { UnsupportedOperationError } from '../core/error-handler'
16
+ import { stopPgweb } from '../core/pgweb-utils'
16
17
 
17
18
  /**
18
19
  * Base class for database engines
@@ -267,6 +268,14 @@ export abstract class BaseEngine {
267
268
  // Default: no-op. Override in engines that support connection termination.
268
269
  }
269
270
 
271
+ /**
272
+ * Stop pgweb if running for this container.
273
+ * Called from stop() in engines that support pgweb (PostgreSQL, CockroachDB, FerretDB).
274
+ */
275
+ protected async stopPgweb(containerName: string): Promise<void> {
276
+ await stopPgweb(containerName, this.name)
277
+ }
278
+
270
279
  /**
271
280
  * Execute a query and return results in a structured format.
272
281
  * @param container - The container configuration
@@ -506,6 +506,9 @@ export class CockroachDBEngine extends BaseEngine {
506
506
  }
507
507
  }
508
508
 
509
+ // Kill pgweb if running for this container
510
+ await this.stopPgweb(name)
511
+
509
512
  logDebug('CockroachDB stopped')
510
513
  }
511
514
 
@@ -622,41 +622,57 @@ export class FerretDBEngine extends BaseEngine {
622
622
  let ferretStarted = false
623
623
 
624
624
  try {
625
- // 1. Start PostgreSQL
625
+ // 1. Start PostgreSQL (skip if already running)
626
626
  onProgress?.({
627
627
  stage: 'starting',
628
628
  message: 'Starting PostgreSQL backend...',
629
629
  })
630
630
 
631
- // Use pg_ctl to start PostgreSQL
632
- // Add 60s timeout to prevent hanging if PostgreSQL fails to start (especially on Windows)
631
+ // Check if PostgreSQL backend is already running in this data dir
632
+ let pgAlreadyRunning = false
633
633
  try {
634
- await spawnAsync(
635
- pgCtl,
636
- [
637
- 'start',
638
- '-D',
639
- pgDataDir,
640
- '-l',
641
- pgLogFile,
642
- '-o',
643
- `-p ${backendPort} -h 127.0.0.1`,
644
- '-w', // Wait for startup
645
- ],
646
- { env: pgSpawnEnv, timeout: 60000 },
647
- )
648
- } catch (pgError) {
649
- // Read PostgreSQL log for debugging
650
- let pgLog = ''
634
+ await spawnAsync(pgCtl, ['status', '-D', pgDataDir], {
635
+ env: pgSpawnEnv,
636
+ timeout: 5000,
637
+ })
638
+ // pg_ctl status exits 0 if server is running
639
+ pgAlreadyRunning = true
640
+ logDebug('PostgreSQL backend already running, skipping start')
641
+ } catch {
642
+ // Exit code != 0 means not running — proceed to start
643
+ }
644
+
645
+ if (!pgAlreadyRunning) {
646
+ // Use pg_ctl to start PostgreSQL
647
+ // Add 60s timeout to prevent hanging if PostgreSQL fails to start (especially on Windows)
651
648
  try {
652
- pgLog = await readFile(pgLogFile, 'utf8')
653
- } catch {
654
- pgLog = '(no log available)'
649
+ await spawnAsync(
650
+ pgCtl,
651
+ [
652
+ 'start',
653
+ '-D',
654
+ pgDataDir,
655
+ '-l',
656
+ pgLogFile,
657
+ '-o',
658
+ `-p ${backendPort} -h 127.0.0.1`,
659
+ '-w', // Wait for startup
660
+ ],
661
+ { env: pgSpawnEnv, timeout: 60000 },
662
+ )
663
+ } catch (pgError) {
664
+ // Read PostgreSQL log for debugging
665
+ let pgLog = ''
666
+ try {
667
+ pgLog = await readFile(pgLogFile, 'utf8')
668
+ } catch {
669
+ pgLog = '(no log available)'
670
+ }
671
+ throw new Error(
672
+ `PostgreSQL backend failed to start: ${pgError instanceof Error ? pgError.message : pgError}\n` +
673
+ `PostgreSQL log:\n${pgLog.slice(-2000)}`, // Last 2KB of log
674
+ )
655
675
  }
656
- throw new Error(
657
- `PostgreSQL backend failed to start: ${pgError instanceof Error ? pgError.message : pgError}\n` +
658
- `PostgreSQL log:\n${pgLog.slice(-2000)}`, // Last 2KB of log
659
- )
660
676
  }
661
677
 
662
678
  pgStarted = true
@@ -880,6 +896,9 @@ export class FerretDBEngine extends BaseEngine {
880
896
  await this.stopPostgreSQLProcess(pgCtl, pgDataDir, pgSpawnEnv)
881
897
  }
882
898
 
899
+ // Kill pgweb if running for this container
900
+ await this.stopPgweb(name)
901
+
883
902
  logDebug('FerretDB stopped')
884
903
  }
885
904
 
@@ -461,6 +461,9 @@ export class PostgreSQLEngine extends BaseEngine {
461
461
  const dataDir = paths.getContainerDataPath(name, { engine: this.name })
462
462
 
463
463
  await processManager.stop(pgCtlPath, dataDir)
464
+
465
+ // Kill pgweb if running for this container
466
+ await this.stopPgweb(name)
464
467
  }
465
468
 
466
469
  async status(container: ContainerConfig): Promise<StatusResult> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
package/types/index.ts CHANGED
@@ -427,6 +427,10 @@ export type BinaryTool =
427
427
  | 'typedb_console_bin'
428
428
  // InfluxDB tools
429
429
  | 'influxdb3'
430
+ // Web panels
431
+ | 'pgweb'
432
+ // TUI tools
433
+ | 'dblab'
430
434
  // Enhanced shells (optional)
431
435
  | 'pgcli'
432
436
  | 'mycli'
@@ -519,6 +523,10 @@ export type SpinDBConfig = {
519
523
  typedb_console_bin?: BinaryConfig
520
524
  // InfluxDB tools
521
525
  influxdb3?: BinaryConfig
526
+ // Web panels
527
+ pgweb?: BinaryConfig
528
+ // TUI tools
529
+ dblab?: BinaryConfig
522
530
  // Enhanced shells (optional)
523
531
  pgcli?: BinaryConfig
524
532
  mycli?: BinaryConfig