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 +9 -0
- package/cli/commands/connect.ts +99 -0
- package/cli/commands/menu/container-handlers.ts +39 -12
- package/cli/commands/menu/index.ts +72 -1
- package/cli/commands/menu/shell-handlers.ts +549 -11
- package/cli/commands/ports.ts +211 -0
- package/cli/constants.ts +3 -3
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +4 -2
- package/core/config-manager.ts +8 -0
- package/core/dblab-utils.ts +113 -0
- package/core/dependency-manager.ts +4 -0
- package/core/pgweb-utils.ts +62 -0
- package/engines/base-engine.ts +9 -0
- package/engines/cockroachdb/index.ts +3 -0
- package/engines/ferretdb/index.ts +46 -27
- package/engines/postgresql/index.ts +3 -0
- package/package.json +1 -1
- package/types/index.ts +8 -0
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
|
package/cli/commands/connect.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
832
|
+
// Open console - requires database selection for multi-db containers
|
|
810
833
|
actionChoices.push(
|
|
811
834
|
canDoDbAction
|
|
812
|
-
? { name: `${chalk.blue('>')} Open
|
|
813
|
-
: disabledItem('>', 'Open
|
|
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'))
|