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
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
package/core/config-manager.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/engines/base-engine.ts
CHANGED
|
@@ -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
|
|
@@ -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
|
-
//
|
|
632
|
-
|
|
631
|
+
// Check if PostgreSQL backend is already running in this data dir
|
|
632
|
+
let pgAlreadyRunning = false
|
|
633
633
|
try {
|
|
634
|
-
await spawnAsync(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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.
|
|
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
|