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.
package/README.md CHANGED
@@ -105,6 +105,15 @@ spindb connect cache # Open redis-cli
105
105
  spindb connect cache --iredis # Enhanced shell
106
106
  ```
107
107
 
108
+ ### Enhanced Shells & Visual Tools
109
+
110
+ ```bash
111
+ spindb connect myapp --pgcli # Enhanced PostgreSQL shell
112
+ spindb connect myapp --dblab # Visual TUI (table browser)
113
+ spindb connect mydb --mycli # Enhanced MySQL/MariaDB shell
114
+ spindb connect mydb --ui # Built-in Web UI (DuckDB)
115
+ ```
116
+
108
117
  ### Any Engine
109
118
 
110
119
  ```bash
@@ -27,6 +27,9 @@ import { getEngineDefaults } from '../../config/defaults'
27
27
  import { promptContainerSelect } from '../ui/prompts'
28
28
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../ui/theme'
29
29
  import { Engine } from '../../types'
30
+ import { configManager } from '../../core/config-manager'
31
+ import { DBLAB_ENGINES, getDblabArgs } from '../../core/dblab-utils'
32
+ import { downloadDblabCli } from './menu/shell-handlers'
30
33
 
31
34
  export const connectCommand = new Command('connect')
32
35
  .alias('shell')
@@ -55,6 +58,9 @@ export const connectCommand = new Command('connect')
55
58
  'Use iredis for enhanced Redis shell (auto-completion, syntax highlighting)',
56
59
  )
57
60
  .option('--install-iredis', 'Install iredis if not present, then connect')
61
+ .option('--dblab', 'Use dblab visual TUI (table browser, query editor)')
62
+ .option('--install-dblab', 'Download dblab if not present, then connect')
63
+ .option('--ui', 'Open built-in Web UI (DuckDB only)')
58
64
  .action(
59
65
  async (
60
66
  name: string | undefined,
@@ -70,6 +76,9 @@ export const connectCommand = new Command('connect')
70
76
  installLitecli?: boolean
71
77
  iredis?: boolean
72
78
  installIredis?: boolean
79
+ dblab?: boolean
80
+ installDblab?: boolean
81
+ ui?: boolean
73
82
  },
74
83
  ) => {
75
84
  try {
@@ -456,6 +465,96 @@ export const connectCommand = new Command('connect')
456
465
  }
457
466
  }
458
467
 
468
+ const useDblab = options.dblab || options.installDblab
469
+ if (useDblab) {
470
+ if (!DBLAB_ENGINES.has(engineName)) {
471
+ console.error(
472
+ uiError(`dblab is not supported for ${engineName} containers`),
473
+ )
474
+ process.exit(1)
475
+ }
476
+
477
+ let dblabPath = await configManager.getBinaryPath('dblab')
478
+
479
+ if (!dblabPath) {
480
+ if (options.installDblab) {
481
+ dblabPath = await downloadDblabCli()
482
+ if (!dblabPath) {
483
+ process.exit(1)
484
+ }
485
+ } else {
486
+ console.error(uiError('dblab is not installed'))
487
+ console.log()
488
+ console.log(chalk.gray('Download dblab:'))
489
+ console.log(chalk.cyan(' spindb connect --install-dblab'))
490
+ console.log()
491
+ console.log(chalk.gray('Or download manually from:'))
492
+ console.log(
493
+ chalk.cyan(' https://github.com/danvergara/dblab/releases'),
494
+ )
495
+ process.exit(1)
496
+ }
497
+ }
498
+
499
+ const dblabArgs = getDblabArgs(config, database)
500
+ const dblabProcess = spawn(dblabPath, dblabArgs, {
501
+ stdio: 'inherit',
502
+ })
503
+
504
+ await new Promise<void>((resolve) => {
505
+ dblabProcess.on('error', (err: NodeJS.ErrnoException) => {
506
+ if (err.code === 'ENOENT') {
507
+ console.log(uiWarning('dblab not found.'))
508
+ console.log(chalk.gray(' Download it with:'))
509
+ console.log(chalk.cyan(' spindb connect --install-dblab'))
510
+ } else {
511
+ console.error(uiError(err.message))
512
+ }
513
+ resolve()
514
+ })
515
+ dblabProcess.on('close', () => resolve())
516
+ })
517
+
518
+ return
519
+ }
520
+
521
+ if (options.ui) {
522
+ if (engineName !== Engine.DuckDB) {
523
+ console.error(
524
+ uiError('--ui is only available for DuckDB containers'),
525
+ )
526
+ process.exit(1)
527
+ }
528
+
529
+ const duckdbPath = await configManager.getBinaryPath('duckdb')
530
+ if (!duckdbPath) {
531
+ console.error(
532
+ uiError(
533
+ 'DuckDB binary not found. Download it with: spindb engines download duckdb',
534
+ ),
535
+ )
536
+ process.exit(1)
537
+ }
538
+
539
+ const uiProcess = spawn(duckdbPath, [config.database, '-ui'], {
540
+ stdio: 'inherit',
541
+ })
542
+
543
+ await new Promise<void>((resolve) => {
544
+ uiProcess.on('error', (err: NodeJS.ErrnoException) => {
545
+ if (err.code === 'ENOENT') {
546
+ console.log(uiWarning('DuckDB binary not found.'))
547
+ } else {
548
+ console.error(uiError(err.message))
549
+ }
550
+ resolve()
551
+ })
552
+ uiProcess.on('close', () => resolve())
553
+ })
554
+
555
+ return
556
+ }
557
+
459
558
  console.log(uiInfo(`Connecting to ${containerName}:${database}...`))
460
559
  console.log()
461
560
 
@@ -53,7 +53,12 @@ import {
53
53
  formatBytes,
54
54
  box,
55
55
  } from '../../ui/theme'
56
- import { handleOpenShell, handleCopyConnectionString } from './shell-handlers'
56
+ import {
57
+ handleOpenShell,
58
+ handleCopyConnectionString,
59
+ stopPgwebProcess,
60
+ } from './shell-handlers'
61
+ import { getPgwebStatus } from '../../../core/pgweb-utils'
57
62
  import { generatePassword } from '../../../core/credential-generator'
58
63
  import {
59
64
  saveCredentials,
@@ -591,16 +596,16 @@ export async function handleList(
591
596
  // Build the full choice list with footer items
592
597
  // IMPORTANT: Containers must come FIRST because filterableCount slices from index 0
593
598
  const summary = `${containers.length} container(s): ${parts.join('; ')}`
599
+ const headerItems = hasServerContainers
600
+ ? [
601
+ new inquirer.Separator(
602
+ chalk.cyan('── [Shift+Tab] toggle start/stop ──'),
603
+ ),
604
+ ]
605
+ : []
594
606
  const allChoices: (FilterableChoice | inquirer.Separator)[] = [
595
607
  ...containerChoices,
596
- // Show toggle hint after containers (before footer) when server-based containers exist
597
- ...(hasServerContainers
598
- ? [
599
- new inquirer.Separator(
600
- chalk.cyan('── [Shift+Tab] toggle start/stop ──'),
601
- ),
602
- ]
603
- : [new inquirer.Separator()]),
608
+ new inquirer.Separator(),
604
609
  new inquirer.Separator(summary),
605
610
  new inquirer.Separator(),
606
611
  { name: `${chalk.green('+')} Create new`, value: 'create' },
@@ -620,6 +625,7 @@ export async function handleList(
620
625
  emptyText: 'No containers match filter',
621
626
  enableToggle: hasServerContainers,
622
627
  defaultValue: options?.focusContainer,
628
+ headerItems,
623
629
  },
624
630
  )
625
631
 
@@ -660,6 +666,8 @@ export async function handleList(
660
666
  const result = await handleCreate()
661
667
  if (result === 'main') {
662
668
  await showMainMenu()
669
+ } else if (result) {
670
+ await showContainerSubmenu(result, showMainMenu)
663
671
  } else {
664
672
  await handleList(showMainMenu)
665
673
  }
@@ -775,6 +783,21 @@ export async function showContainerSubmenu(
775
783
  name: `${chalk.red('■')} Stop container`,
776
784
  value: 'stop',
777
785
  })
786
+
787
+ // Stop pgweb - only for PG-wire-protocol engines when pgweb is running
788
+ if (
789
+ config.engine === 'postgresql' ||
790
+ config.engine === 'cockroachdb' ||
791
+ config.engine === 'ferretdb'
792
+ ) {
793
+ const pgwebStatus = await getPgwebStatus(containerName, config.engine)
794
+ if (pgwebStatus.running) {
795
+ actionChoices.push({
796
+ name: `${chalk.redBright('■')} Stop pgweb (port ${pgwebStatus.port})`,
797
+ value: 'stop-pgweb',
798
+ })
799
+ }
800
+ }
778
801
  }
779
802
 
780
803
  // View logs - available anytime for server-based DBs
@@ -806,11 +829,11 @@ export async function showContainerSubmenu(
806
829
  new inquirer.Separator(chalk.gray(`── ${dataSectionLabel} ──`)),
807
830
  )
808
831
 
809
- // Open shell - requires database selection for multi-db containers
832
+ // Open console - requires database selection for multi-db containers
810
833
  actionChoices.push(
811
834
  canDoDbAction
812
- ? { name: `${chalk.blue('>')} Open shell`, value: 'shell' }
813
- : disabledItem('>', 'Open shell'),
835
+ ? { name: `${chalk.blue('>')} Open console`, value: 'shell' }
836
+ : disabledItem('>', 'Open console'),
814
837
  )
815
838
 
816
839
  // Run script file - requires database selection for multi-db containers
@@ -991,6 +1014,10 @@ export async function showContainerSubmenu(
991
1014
  await handleViewLogs(containerName)
992
1015
  await showContainerSubmenu(containerName, showMainMenu, activeDatabase)
993
1016
  return
1017
+ case 'stop-pgweb':
1018
+ await stopPgwebProcess(containerName, config.engine)
1019
+ await showContainerSubmenu(containerName, showMainMenu, activeDatabase)
1020
+ return
994
1021
  case 'edit': {
995
1022
  const newName = await handleEditContainer(containerName)
996
1023
  if (newName === null) {
@@ -24,7 +24,8 @@ import { handleSettings } from './settings-handlers'
24
24
  import { configManager } from '../../../core/config-manager'
25
25
  import { createSpinner } from '../../ui/spinner'
26
26
  import { type MenuChoice, pressEnterToContinue } from './shared'
27
- import { getPageSize } from '../../constants'
27
+ import { getPageSize, getEngineIcon } from '../../constants'
28
+ import { getContainerPorts } from '../ports'
28
29
 
29
30
  // Track update check state for this session (only check once on first menu load)
30
31
  let updateCheckPromise: Promise<UpdateCheckResult | null> | null = null
@@ -76,6 +77,7 @@ async function showMainMenu(): Promise<void> {
76
77
  ? [
77
78
  { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
78
79
  { name: `${chalk.green('+')} Create container`, value: 'create' },
80
+ { name: `${chalk.magenta('⊞')} Ports`, value: 'ports' },
79
81
  ]
80
82
  : [
81
83
  { name: `${chalk.green('+')} Create container`, value: 'create' },
@@ -138,6 +140,9 @@ async function showMainMenu(): Promise<void> {
138
140
  case 'list':
139
141
  await handleList(showMainMenu)
140
142
  break
143
+ case 'ports':
144
+ await handlePorts()
145
+ break
141
146
  case 'settings':
142
147
  await handleSettings()
143
148
  break
@@ -150,6 +155,72 @@ async function showMainMenu(): Promise<void> {
150
155
  }
151
156
  }
152
157
 
158
+ async function handlePorts(): Promise<void> {
159
+ console.clear()
160
+ console.log(header('Ports'))
161
+ console.log()
162
+
163
+ const containers = await containerManager.list()
164
+
165
+ if (containers.length === 0) {
166
+ console.log(chalk.gray(' No containers found.'))
167
+ console.log()
168
+ await pressEnterToContinue()
169
+ return
170
+ }
171
+
172
+ const results = await Promise.all(
173
+ containers.map(async (config) => {
174
+ const { status, ports } = await getContainerPorts(config)
175
+ return { config, status, ports }
176
+ }),
177
+ )
178
+
179
+ // Only show containers that have ports (skip file-based DBs)
180
+ const withPorts = results.filter((r) => r.ports.length > 0)
181
+
182
+ if (withPorts.length === 0) {
183
+ console.log(chalk.gray(' No port-based containers found.'))
184
+ console.log()
185
+ await pressEnterToContinue()
186
+ return
187
+ }
188
+
189
+ console.log(
190
+ chalk.gray(' ') +
191
+ chalk.bold.white('NAME'.padEnd(22)) +
192
+ chalk.bold.white('ENGINE'.padEnd(18)) +
193
+ chalk.bold.white('PORT(S)'),
194
+ )
195
+ console.log(chalk.gray(' ' + '─'.repeat(66)))
196
+
197
+ for (const { config, status, ports } of withPorts) {
198
+ const engineIcon = getEngineIcon(config.engine)
199
+ const engineName = config.engine.padEnd(13)
200
+
201
+ const parts = ports.map((p, i) =>
202
+ i === 0 ? String(p.port) : `${p.port} ${chalk.gray(`(${p.label})`)}`,
203
+ )
204
+ const portDisplay = parts.join(chalk.gray(', '))
205
+
206
+ const statusIndicator =
207
+ status === 'running' ? chalk.green('●') : chalk.gray('○')
208
+
209
+ console.log(
210
+ chalk.gray(' ') +
211
+ statusIndicator +
212
+ ' ' +
213
+ chalk.cyan(config.name.padEnd(20)) +
214
+ engineIcon +
215
+ chalk.white(engineName) +
216
+ portDisplay,
217
+ )
218
+ }
219
+
220
+ console.log()
221
+ await pressEnterToContinue()
222
+ }
223
+
153
224
  async function handleUpdate(): Promise<void> {
154
225
  console.clear()
155
226
  console.log(header('Update SpinDB'))