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
|
@@ -4,7 +4,7 @@ import { spawn } from 'child_process'
|
|
|
4
4
|
import { escapeablePrompt } from '../../ui/prompts'
|
|
5
5
|
import { getPageSize } from '../../constants'
|
|
6
6
|
import { existsSync } from 'fs'
|
|
7
|
-
import { mkdir, writeFile, rm } from 'fs/promises'
|
|
7
|
+
import { chmod, mkdir, writeFile, rm } from 'fs/promises'
|
|
8
8
|
import { join, dirname, resolve, sep } from 'path'
|
|
9
9
|
import { containerManager } from '../../../core/container-manager'
|
|
10
10
|
import {
|
|
@@ -26,7 +26,19 @@ import {
|
|
|
26
26
|
getIredisManualInstructions,
|
|
27
27
|
} from '../../../core/dependency-manager'
|
|
28
28
|
import { platformService } from '../../../core/platform-service'
|
|
29
|
+
import { portManager } from '../../../core/port-manager'
|
|
29
30
|
import { configManager } from '../../../core/config-manager'
|
|
31
|
+
import {
|
|
32
|
+
getPgwebStatus,
|
|
33
|
+
stopPgweb,
|
|
34
|
+
PGWEB_VERSION,
|
|
35
|
+
} from '../../../core/pgweb-utils'
|
|
36
|
+
import {
|
|
37
|
+
DBLAB_ENGINES,
|
|
38
|
+
DBLAB_VERSION,
|
|
39
|
+
getDblabArgs,
|
|
40
|
+
getDblabPlatformSuffix,
|
|
41
|
+
} from '../../../core/dblab-utils'
|
|
30
42
|
import { getEngine } from '../../../engines'
|
|
31
43
|
import { createSpinner } from '../../ui/spinner'
|
|
32
44
|
import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
|
|
@@ -134,6 +146,12 @@ export async function handleOpenShell(
|
|
|
134
146
|
| 'browser'
|
|
135
147
|
| 'api-info'
|
|
136
148
|
| 'install-webui'
|
|
149
|
+
| 'pgweb'
|
|
150
|
+
| 'install-pgweb'
|
|
151
|
+
| 'stop-pgweb'
|
|
152
|
+
| 'dblab'
|
|
153
|
+
| 'install-dblab'
|
|
154
|
+
| 'duckdb-ui'
|
|
137
155
|
| 'usql'
|
|
138
156
|
| 'install-usql'
|
|
139
157
|
| 'pgcli'
|
|
@@ -340,15 +358,6 @@ export async function handleOpenShell(
|
|
|
340
358
|
})
|
|
341
359
|
}
|
|
342
360
|
|
|
343
|
-
// Add browser option for ClickHouse (Play UI on HTTP port = native port + 1)
|
|
344
|
-
if (config.engine === 'clickhouse') {
|
|
345
|
-
const httpPort = config.port + 1
|
|
346
|
-
choices.push({
|
|
347
|
-
name: `◎ Open Play UI in browser (port ${httpPort})`,
|
|
348
|
-
value: 'browser',
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
|
|
352
361
|
// Only show engine-specific CLI option if one exists (MongoDB's mongosh IS the default)
|
|
353
362
|
if (engineSpecificCli !== null) {
|
|
354
363
|
if (engineSpecificInstalled) {
|
|
@@ -380,6 +389,72 @@ export async function handleOpenShell(
|
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
|
|
392
|
+
// dblab visual TUI (supports PostgreSQL, MySQL, MariaDB, CockroachDB, SQLite, QuestDB)
|
|
393
|
+
if (DBLAB_ENGINES.has(config.engine)) {
|
|
394
|
+
const dblabPath = await configManager.getBinaryPath('dblab')
|
|
395
|
+
if (dblabPath) {
|
|
396
|
+
choices.push({
|
|
397
|
+
name: '⚡ Use dblab (visual TUI)',
|
|
398
|
+
value: 'dblab',
|
|
399
|
+
})
|
|
400
|
+
} else {
|
|
401
|
+
choices.push({
|
|
402
|
+
name: '↓ Download dblab (visual TUI)',
|
|
403
|
+
value: 'install-dblab',
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Web Panel section for engines with browser-based UIs
|
|
409
|
+
if (config.engine === 'clickhouse') {
|
|
410
|
+
const httpPort = config.port + 1
|
|
411
|
+
choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)))
|
|
412
|
+
choices.push({
|
|
413
|
+
name: `◎ Open Play UI (port ${httpPort})`,
|
|
414
|
+
value: 'browser',
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (config.engine === 'duckdb') {
|
|
419
|
+
choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)))
|
|
420
|
+
choices.push({
|
|
421
|
+
name: `◎ Open Web UI (built-in, port 4213)`,
|
|
422
|
+
value: 'duckdb-ui',
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (
|
|
427
|
+
config.engine === 'postgresql' ||
|
|
428
|
+
config.engine === 'cockroachdb' ||
|
|
429
|
+
config.engine === 'ferretdb'
|
|
430
|
+
) {
|
|
431
|
+
choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)))
|
|
432
|
+
const pgwebPath = await configManager.getBinaryPath('pgweb')
|
|
433
|
+
if (pgwebPath) {
|
|
434
|
+
const pgwebStatus = await getPgwebStatus(containerName, config.engine)
|
|
435
|
+
if (pgwebStatus.running) {
|
|
436
|
+
choices.push({
|
|
437
|
+
name: `◎ Open pgweb (port ${pgwebStatus.port})`,
|
|
438
|
+
value: 'pgweb',
|
|
439
|
+
})
|
|
440
|
+
choices.push({
|
|
441
|
+
name: `■ Stop pgweb`,
|
|
442
|
+
value: 'stop-pgweb',
|
|
443
|
+
})
|
|
444
|
+
} else {
|
|
445
|
+
choices.push({
|
|
446
|
+
name: `◎ Open pgweb`,
|
|
447
|
+
value: 'pgweb',
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
choices.push({
|
|
452
|
+
name: `↓ Download pgweb`,
|
|
453
|
+
value: 'install-pgweb',
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
383
458
|
choices.push(new inquirer.Separator())
|
|
384
459
|
choices.push({
|
|
385
460
|
name: `${chalk.blue('←')} Back`,
|
|
@@ -390,7 +465,7 @@ export async function handleOpenShell(
|
|
|
390
465
|
{
|
|
391
466
|
type: 'list',
|
|
392
467
|
name: 'shellChoice',
|
|
393
|
-
message: 'Select
|
|
468
|
+
message: 'Select console option:',
|
|
394
469
|
choices,
|
|
395
470
|
pageSize: getPageSize(),
|
|
396
471
|
},
|
|
@@ -416,6 +491,56 @@ export async function handleOpenShell(
|
|
|
416
491
|
return
|
|
417
492
|
}
|
|
418
493
|
|
|
494
|
+
// Handle DuckDB built-in Web UI (duckdb -ui)
|
|
495
|
+
if (shellChoice === 'duckdb-ui') {
|
|
496
|
+
const duckdbPath = await configManager.getBinaryPath('duckdb')
|
|
497
|
+
if (!duckdbPath) {
|
|
498
|
+
console.error(
|
|
499
|
+
uiError(
|
|
500
|
+
'DuckDB binary not found. Download it with: spindb engines download duckdb',
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
await pressEnterToContinue()
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log()
|
|
508
|
+
console.log(uiInfo('Launching DuckDB Web UI...'))
|
|
509
|
+
console.log(chalk.gray(' http://localhost:4213'))
|
|
510
|
+
console.log()
|
|
511
|
+
|
|
512
|
+
const uiProcess = spawn(duckdbPath, [config.database, '-ui'], {
|
|
513
|
+
stdio: 'inherit',
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
await new Promise<void>((resolve) => {
|
|
517
|
+
let settled = false
|
|
518
|
+
const settle = () => {
|
|
519
|
+
if (!settled) {
|
|
520
|
+
settled = true
|
|
521
|
+
resolve()
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
uiProcess.on('error', (err: NodeJS.ErrnoException) => {
|
|
526
|
+
if (err.code === 'ENOENT') {
|
|
527
|
+
console.log(uiWarning('DuckDB binary not found.'))
|
|
528
|
+
} else {
|
|
529
|
+
console.log(uiError(`Failed to start DuckDB UI: ${err.message}`))
|
|
530
|
+
}
|
|
531
|
+
settle()
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
uiProcess.on('close', () => {
|
|
535
|
+
if (process.stdout.isTTY) {
|
|
536
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H')
|
|
537
|
+
}
|
|
538
|
+
settle()
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
|
|
419
544
|
// Handle Qdrant/Meilisearch API info display
|
|
420
545
|
if (shellChoice === 'api-info') {
|
|
421
546
|
console.log()
|
|
@@ -670,6 +795,42 @@ export async function handleOpenShell(
|
|
|
670
795
|
return
|
|
671
796
|
}
|
|
672
797
|
|
|
798
|
+
// Handle dblab download → launch immediately after install
|
|
799
|
+
if (shellChoice === 'install-dblab') {
|
|
800
|
+
const dblabBinaryPath = await downloadDblabCli()
|
|
801
|
+
if (dblabBinaryPath) {
|
|
802
|
+
await launchDblab(config, activeDatabase)
|
|
803
|
+
}
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Handle dblab launch
|
|
808
|
+
if (shellChoice === 'dblab') {
|
|
809
|
+
await launchDblab(config, activeDatabase)
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Handle pgweb download → launch immediately after install
|
|
814
|
+
if (shellChoice === 'install-pgweb') {
|
|
815
|
+
const pgwebBinaryPath = await downloadPgweb()
|
|
816
|
+
if (pgwebBinaryPath) {
|
|
817
|
+
await launchPgweb(containerName, config, activeDatabase)
|
|
818
|
+
}
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Handle pgweb launch
|
|
823
|
+
if (shellChoice === 'pgweb') {
|
|
824
|
+
await launchPgweb(containerName, config, activeDatabase)
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Handle pgweb stop
|
|
829
|
+
if (shellChoice === 'stop-pgweb') {
|
|
830
|
+
await stopPgwebProcess(containerName, config.engine)
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
|
|
673
834
|
// Handle install-webui option for Qdrant
|
|
674
835
|
if (shellChoice === 'install-webui') {
|
|
675
836
|
if (config.engine === 'qdrant') {
|
|
@@ -809,6 +970,383 @@ async function downloadQdrantWebUI(containerName: string): Promise<void> {
|
|
|
809
970
|
await pressEnterToContinue()
|
|
810
971
|
}
|
|
811
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Stop a running pgweb process for a container (with UI feedback)
|
|
975
|
+
*/
|
|
976
|
+
export async function stopPgwebProcess(
|
|
977
|
+
containerName: string,
|
|
978
|
+
engine: string,
|
|
979
|
+
): Promise<void> {
|
|
980
|
+
const stopped = await stopPgweb(containerName, engine)
|
|
981
|
+
|
|
982
|
+
console.log()
|
|
983
|
+
if (stopped) {
|
|
984
|
+
console.log(uiSuccess('pgweb stopped'))
|
|
985
|
+
} else {
|
|
986
|
+
console.log(uiInfo('pgweb is not running'))
|
|
987
|
+
}
|
|
988
|
+
console.log()
|
|
989
|
+
await pressEnterToContinue()
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Download and install pgweb from GitHub releases
|
|
994
|
+
*/
|
|
995
|
+
async function downloadPgweb(): Promise<string | null> {
|
|
996
|
+
console.log()
|
|
997
|
+
const spinner = createSpinner('Downloading pgweb...')
|
|
998
|
+
spinner.start()
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
const platform = process.platform
|
|
1002
|
+
const arch = process.arch
|
|
1003
|
+
let suffix: string
|
|
1004
|
+
|
|
1005
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
1006
|
+
suffix = 'darwin_arm64'
|
|
1007
|
+
} else if (platform === 'darwin' && arch === 'x64') {
|
|
1008
|
+
suffix = 'darwin_amd64'
|
|
1009
|
+
} else if (platform === 'linux' && arch === 'arm64') {
|
|
1010
|
+
suffix = 'linux_arm64'
|
|
1011
|
+
} else if (platform === 'linux' && arch === 'x64') {
|
|
1012
|
+
suffix = 'linux_amd64'
|
|
1013
|
+
} else if (platform === 'win32' && arch === 'x64') {
|
|
1014
|
+
suffix = 'windows_amd64.exe'
|
|
1015
|
+
} else {
|
|
1016
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const zipUrl = `https://github.com/sosedoff/pgweb/releases/download/v${PGWEB_VERSION}/pgweb_${suffix}.zip`
|
|
1020
|
+
|
|
1021
|
+
spinner.text = `Downloading pgweb v${PGWEB_VERSION}...`
|
|
1022
|
+
|
|
1023
|
+
const response = await fetch(zipUrl)
|
|
1024
|
+
if (!response.ok || !response.body) {
|
|
1025
|
+
throw new Error(`Failed to download: ${response.status}`)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const isWin = platform === 'win32'
|
|
1029
|
+
const binaryName = isWin ? 'pgweb.exe' : 'pgweb'
|
|
1030
|
+
const platformArch = `${platform}-${arch}`
|
|
1031
|
+
const installDir = join(
|
|
1032
|
+
paths.bin,
|
|
1033
|
+
`pgweb-${PGWEB_VERSION}-${platformArch}`,
|
|
1034
|
+
'bin',
|
|
1035
|
+
)
|
|
1036
|
+
await mkdir(installDir, { recursive: true })
|
|
1037
|
+
|
|
1038
|
+
const tempZip = join(paths.bin, 'pgweb-temp.zip')
|
|
1039
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
1040
|
+
await writeFile(tempZip, buffer)
|
|
1041
|
+
|
|
1042
|
+
spinner.text = 'Extracting pgweb...'
|
|
1043
|
+
|
|
1044
|
+
try {
|
|
1045
|
+
const unzipper = await import('unzipper')
|
|
1046
|
+
const directory = await unzipper.Open.file(tempZip)
|
|
1047
|
+
|
|
1048
|
+
const resolvedInstallDir = resolve(installDir)
|
|
1049
|
+
let extracted = false
|
|
1050
|
+
|
|
1051
|
+
for (const entry of directory.files) {
|
|
1052
|
+
if (entry.type === 'Directory') continue
|
|
1053
|
+
|
|
1054
|
+
// Zip-slip protection
|
|
1055
|
+
const targetPath = resolve(installDir, binaryName)
|
|
1056
|
+
if (!targetPath.startsWith(resolvedInstallDir + sep)) {
|
|
1057
|
+
continue
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// The zip contains the binary (possibly named pgweb_<platform>_<arch> or pgweb_<platform>_<arch>.exe)
|
|
1061
|
+
const content = await entry.buffer()
|
|
1062
|
+
await writeFile(targetPath, content)
|
|
1063
|
+
extracted = true
|
|
1064
|
+
break // Only one file in the zip
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (!extracted) {
|
|
1068
|
+
throw new Error('Could not find pgweb binary in zip archive')
|
|
1069
|
+
}
|
|
1070
|
+
} finally {
|
|
1071
|
+
await rm(tempZip, { force: true })
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const binaryPath = join(installDir, binaryName)
|
|
1075
|
+
|
|
1076
|
+
// chmod on Unix
|
|
1077
|
+
if (!isWin) {
|
|
1078
|
+
await chmod(binaryPath, 0o755)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Register in config
|
|
1082
|
+
await configManager.setBinaryPath('pgweb', binaryPath, 'bundled')
|
|
1083
|
+
|
|
1084
|
+
spinner.succeed(`pgweb v${PGWEB_VERSION} installed`)
|
|
1085
|
+
console.log()
|
|
1086
|
+
|
|
1087
|
+
return binaryPath
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
spinner.fail('Failed to download pgweb')
|
|
1090
|
+
console.error(uiError((error as Error).message))
|
|
1091
|
+
console.log()
|
|
1092
|
+
console.log(chalk.gray('You can manually download from:'))
|
|
1093
|
+
console.log(chalk.cyan(' https://github.com/sosedoff/pgweb/releases'))
|
|
1094
|
+
console.log()
|
|
1095
|
+
await pressEnterToContinue()
|
|
1096
|
+
return null
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Download and install dblab from GitHub releases.
|
|
1102
|
+
* Exported as downloadDblabCli for use from the CLI connect command.
|
|
1103
|
+
*/
|
|
1104
|
+
export async function downloadDblabCli(): Promise<string | null> {
|
|
1105
|
+
console.log()
|
|
1106
|
+
const spinner = createSpinner('Downloading dblab...')
|
|
1107
|
+
spinner.start()
|
|
1108
|
+
|
|
1109
|
+
try {
|
|
1110
|
+
const suffix = getDblabPlatformSuffix()
|
|
1111
|
+
const tarUrl = `https://github.com/danvergara/dblab/releases/download/v${DBLAB_VERSION}/dblab_${DBLAB_VERSION}_${suffix}.tar.gz`
|
|
1112
|
+
|
|
1113
|
+
spinner.text = `Downloading dblab v${DBLAB_VERSION}...`
|
|
1114
|
+
|
|
1115
|
+
const response = await fetch(tarUrl)
|
|
1116
|
+
if (!response.ok || !response.body) {
|
|
1117
|
+
throw new Error(`Failed to download: ${response.status}`)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const isWin = process.platform === 'win32'
|
|
1121
|
+
const binaryName = isWin ? 'dblab.exe' : 'dblab'
|
|
1122
|
+
const platformArch = `${process.platform}-${process.arch}`
|
|
1123
|
+
const installDir = join(
|
|
1124
|
+
paths.bin,
|
|
1125
|
+
`dblab-${DBLAB_VERSION}-${platformArch}`,
|
|
1126
|
+
'bin',
|
|
1127
|
+
)
|
|
1128
|
+
await mkdir(installDir, { recursive: true })
|
|
1129
|
+
|
|
1130
|
+
const tempTar = join(paths.bin, 'dblab-temp.tar.gz')
|
|
1131
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
1132
|
+
await writeFile(tempTar, buffer)
|
|
1133
|
+
|
|
1134
|
+
spinner.text = 'Extracting dblab...'
|
|
1135
|
+
|
|
1136
|
+
try {
|
|
1137
|
+
const { spawnSync } = await import('child_process')
|
|
1138
|
+
const result = spawnSync('tar', ['-xzf', tempTar, '-C', installDir], {
|
|
1139
|
+
stdio: 'pipe',
|
|
1140
|
+
})
|
|
1141
|
+
if (result.status !== 0) {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`tar extraction failed: ${result.stderr?.toString() || 'unknown error'}`,
|
|
1144
|
+
)
|
|
1145
|
+
}
|
|
1146
|
+
} finally {
|
|
1147
|
+
await rm(tempTar, { force: true })
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const binaryPath = join(installDir, binaryName)
|
|
1151
|
+
|
|
1152
|
+
if (!existsSync(binaryPath)) {
|
|
1153
|
+
throw new Error('Could not find dblab binary after extraction')
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// chmod on Unix
|
|
1157
|
+
if (!isWin) {
|
|
1158
|
+
await chmod(binaryPath, 0o755)
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Register in config
|
|
1162
|
+
await configManager.setBinaryPath('dblab', binaryPath, 'bundled')
|
|
1163
|
+
|
|
1164
|
+
spinner.succeed(`dblab v${DBLAB_VERSION} installed`)
|
|
1165
|
+
console.log()
|
|
1166
|
+
|
|
1167
|
+
return binaryPath
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
spinner.fail('Failed to download dblab')
|
|
1170
|
+
console.error(uiError((error as Error).message))
|
|
1171
|
+
console.log()
|
|
1172
|
+
console.log(chalk.gray('You can manually download from:'))
|
|
1173
|
+
console.log(chalk.cyan(' https://github.com/danvergara/dblab/releases'))
|
|
1174
|
+
console.log()
|
|
1175
|
+
await pressEnterToContinue()
|
|
1176
|
+
return null
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Launch dblab visual TUI for a container
|
|
1182
|
+
*/
|
|
1183
|
+
async function launchDblab(
|
|
1184
|
+
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
|
|
1185
|
+
database: string,
|
|
1186
|
+
): Promise<void> {
|
|
1187
|
+
const dblabPath = await configManager.getBinaryPath('dblab')
|
|
1188
|
+
if (!dblabPath) {
|
|
1189
|
+
console.error(uiError('dblab not found. Download it first.'))
|
|
1190
|
+
await pressEnterToContinue()
|
|
1191
|
+
return
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const args = getDblabArgs(config, database)
|
|
1195
|
+
|
|
1196
|
+
console.log()
|
|
1197
|
+
console.log(chalk.gray(' dblab keybindings:'))
|
|
1198
|
+
console.log(
|
|
1199
|
+
chalk.gray(
|
|
1200
|
+
' Ctrl+Space: run query | Ctrl+H/J/K/L: navigate panels | Ctrl+S: structure view',
|
|
1201
|
+
),
|
|
1202
|
+
)
|
|
1203
|
+
console.log()
|
|
1204
|
+
await escapeablePrompt([
|
|
1205
|
+
{
|
|
1206
|
+
type: 'input',
|
|
1207
|
+
name: 'continue',
|
|
1208
|
+
message: chalk.gray('Press Enter to launch dblab...'),
|
|
1209
|
+
},
|
|
1210
|
+
])
|
|
1211
|
+
|
|
1212
|
+
const dblabProcess = spawn(dblabPath, args, {
|
|
1213
|
+
stdio: 'inherit',
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
await new Promise<void>((resolve) => {
|
|
1217
|
+
let settled = false
|
|
1218
|
+
|
|
1219
|
+
const settle = () => {
|
|
1220
|
+
if (!settled) {
|
|
1221
|
+
settled = true
|
|
1222
|
+
resolve()
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
dblabProcess.on('error', (err: NodeJS.ErrnoException) => {
|
|
1227
|
+
if (err.code === 'ENOENT') {
|
|
1228
|
+
console.log(uiWarning('dblab not found on your system.'))
|
|
1229
|
+
console.log()
|
|
1230
|
+
console.log(chalk.gray(' Download it with:'))
|
|
1231
|
+
console.log(chalk.cyan(' spindb connect --install-dblab'))
|
|
1232
|
+
} else {
|
|
1233
|
+
console.log(uiError(`Failed to start dblab: ${err.message}`))
|
|
1234
|
+
}
|
|
1235
|
+
settle()
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
dblabProcess.on('close', () => {
|
|
1239
|
+
if (process.stdout.isTTY) {
|
|
1240
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H')
|
|
1241
|
+
}
|
|
1242
|
+
settle()
|
|
1243
|
+
})
|
|
1244
|
+
})
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Launch pgweb for a PostgreSQL-compatible container
|
|
1249
|
+
*/
|
|
1250
|
+
async function launchPgweb(
|
|
1251
|
+
containerName: string,
|
|
1252
|
+
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
|
|
1253
|
+
database: string,
|
|
1254
|
+
): Promise<void> {
|
|
1255
|
+
const pgwebPath = await configManager.getBinaryPath('pgweb')
|
|
1256
|
+
if (!pgwebPath) {
|
|
1257
|
+
console.error(uiError('pgweb not found. Download it first.'))
|
|
1258
|
+
await pressEnterToContinue()
|
|
1259
|
+
return
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const containerDir = paths.getContainerPath(containerName, {
|
|
1263
|
+
engine: config.engine,
|
|
1264
|
+
})
|
|
1265
|
+
const pidFile = join(containerDir, 'pgweb.pid')
|
|
1266
|
+
const portFile = join(containerDir, 'pgweb.port')
|
|
1267
|
+
|
|
1268
|
+
// Check if already running — just open browser
|
|
1269
|
+
const status = await getPgwebStatus(containerName, config.engine)
|
|
1270
|
+
if (status.running && status.port) {
|
|
1271
|
+
const url = `http://127.0.0.1:${status.port}`
|
|
1272
|
+
console.log()
|
|
1273
|
+
console.log(uiInfo(`Opening pgweb`))
|
|
1274
|
+
console.log(chalk.gray(` ${url}`))
|
|
1275
|
+
console.log()
|
|
1276
|
+
openInBrowser(url)
|
|
1277
|
+
await pressEnterToContinue()
|
|
1278
|
+
return
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Find available port starting at 8081
|
|
1282
|
+
let port = 8081
|
|
1283
|
+
while (!(await portManager.isPortAvailable(port)) && port < 8200) {
|
|
1284
|
+
port++
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (port >= 8200) {
|
|
1288
|
+
console.error(
|
|
1289
|
+
uiError(
|
|
1290
|
+
'Could not find an available port for pgweb (scanned 8081–8199). ' +
|
|
1291
|
+
'Check for other pgweb or server processes using those ports.',
|
|
1292
|
+
),
|
|
1293
|
+
)
|
|
1294
|
+
await pressEnterToContinue()
|
|
1295
|
+
return
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Build connection URL
|
|
1299
|
+
let connectionUrl: string
|
|
1300
|
+
if (config.engine === 'ferretdb') {
|
|
1301
|
+
// FerretDB has a PostgreSQL backend on backendPort — always connects to 'ferretdb' database
|
|
1302
|
+
if (!config.backendPort) {
|
|
1303
|
+
console.log()
|
|
1304
|
+
console.error(
|
|
1305
|
+
uiError(
|
|
1306
|
+
'PostgreSQL backend port not set — restart the container first',
|
|
1307
|
+
),
|
|
1308
|
+
)
|
|
1309
|
+
console.log()
|
|
1310
|
+
await pressEnterToContinue()
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
connectionUrl = `postgresql://postgres@127.0.0.1:${config.backendPort}/ferretdb?sslmode=disable`
|
|
1314
|
+
} else if (config.engine === 'cockroachdb') {
|
|
1315
|
+
connectionUrl = `postgresql://root@127.0.0.1:${config.port}/${database}?sslmode=disable`
|
|
1316
|
+
} else {
|
|
1317
|
+
connectionUrl = `postgresql://postgres@127.0.0.1:${config.port}/${database}?sslmode=disable`
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Spawn pgweb detached
|
|
1321
|
+
const pgwebProcess = spawn(
|
|
1322
|
+
pgwebPath,
|
|
1323
|
+
['--url', connectionUrl, '--bind', '127.0.0.1', '--listen', String(port)],
|
|
1324
|
+
{
|
|
1325
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1326
|
+
detached: true,
|
|
1327
|
+
},
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
pgwebProcess.unref()
|
|
1331
|
+
|
|
1332
|
+
// Write PID and port files
|
|
1333
|
+
if (pgwebProcess.pid) {
|
|
1334
|
+
await writeFile(pidFile, String(pgwebProcess.pid))
|
|
1335
|
+
await writeFile(portFile, String(port))
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Brief wait for startup
|
|
1339
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
1340
|
+
|
|
1341
|
+
const url = `http://127.0.0.1:${port}`
|
|
1342
|
+
console.log()
|
|
1343
|
+
console.log(uiSuccess(`pgweb started on ${url}`))
|
|
1344
|
+
console.log(chalk.gray(` PID: ${pgwebProcess.pid}`))
|
|
1345
|
+
console.log()
|
|
1346
|
+
openInBrowser(url)
|
|
1347
|
+
await pressEnterToContinue()
|
|
1348
|
+
}
|
|
1349
|
+
|
|
812
1350
|
async function launchShell(
|
|
813
1351
|
containerName: string,
|
|
814
1352
|
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
|