spindb 0.4.0 → 0.5.2
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 +77 -100
- package/cli/commands/clone.ts +3 -1
- package/cli/commands/connect.ts +50 -24
- package/cli/commands/create.ts +265 -112
- package/cli/commands/delete.ts +3 -1
- package/cli/commands/list.ts +14 -3
- package/cli/commands/menu.ts +250 -84
- package/cli/commands/restore.ts +142 -38
- package/cli/commands/start.ts +30 -4
- package/cli/commands/stop.ts +3 -1
- package/cli/ui/prompts.ts +95 -32
- package/config/defaults.ts +40 -15
- package/config/engine-defaults.ts +84 -0
- package/config/os-dependencies.ts +68 -19
- package/config/paths.ts +116 -23
- package/core/binary-manager.ts +30 -5
- package/core/container-manager.ts +124 -60
- package/core/dependency-manager.ts +44 -22
- package/core/port-manager.ts +42 -31
- package/core/postgres-binary-manager.ts +10 -9
- package/core/process-manager.ts +14 -6
- package/engines/index.ts +7 -2
- package/engines/mysql/binary-detection.ts +248 -0
- package/engines/mysql/index.ts +699 -0
- package/engines/postgresql/index.ts +13 -6
- package/package.json +4 -2
- package/types/index.ts +29 -5
|
@@ -3,10 +3,12 @@ import { mkdir, readdir, readFile, writeFile, rm, cp } from 'fs/promises'
|
|
|
3
3
|
import { paths } from '../config/paths'
|
|
4
4
|
import { processManager } from './process-manager'
|
|
5
5
|
import { portManager } from './port-manager'
|
|
6
|
-
import
|
|
6
|
+
import { getEngineDefaults, getSupportedEngines } from '../config/defaults'
|
|
7
|
+
import { getEngine } from '../engines'
|
|
8
|
+
import type { ContainerConfig, EngineName } from '../types'
|
|
7
9
|
|
|
8
10
|
export type CreateOptions = {
|
|
9
|
-
engine:
|
|
11
|
+
engine: EngineName
|
|
10
12
|
version: string
|
|
11
13
|
port: number
|
|
12
14
|
database: string
|
|
@@ -30,14 +32,14 @@ export class ContainerManager {
|
|
|
30
32
|
)
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
// Check if container already exists
|
|
34
|
-
if (await this.exists(name)) {
|
|
35
|
-
throw new Error(`Container "${name}" already exists`)
|
|
35
|
+
// Check if container already exists (for this engine)
|
|
36
|
+
if (await this.exists(name, { engine })) {
|
|
37
|
+
throw new Error(`Container "${name}" already exists for engine ${engine}`)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Create container directory
|
|
39
|
-
const containerPath = paths.getContainerPath(name)
|
|
40
|
-
const dataPath = paths.getContainerDataPath(name)
|
|
40
|
+
// Create container directory (engine-scoped)
|
|
41
|
+
const containerPath = paths.getContainerPath(name, { engine })
|
|
42
|
+
const dataPath = paths.getContainerDataPath(name, { engine })
|
|
41
43
|
|
|
42
44
|
await mkdir(containerPath, { recursive: true })
|
|
43
45
|
await mkdir(dataPath, { recursive: true })
|
|
@@ -53,30 +55,54 @@ export class ContainerManager {
|
|
|
53
55
|
status: 'created',
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
await this.saveConfig(name, config)
|
|
58
|
+
await this.saveConfig(name, { engine }, config)
|
|
57
59
|
|
|
58
60
|
return config
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Get container configuration
|
|
65
|
+
* If engine is not provided, searches all engine directories
|
|
63
66
|
*/
|
|
64
|
-
async getConfig(
|
|
65
|
-
|
|
67
|
+
async getConfig(
|
|
68
|
+
name: string,
|
|
69
|
+
options?: { engine?: string },
|
|
70
|
+
): Promise<ContainerConfig | null> {
|
|
71
|
+
const { engine } = options || {}
|
|
72
|
+
|
|
73
|
+
if (engine) {
|
|
74
|
+
// Look in specific engine directory
|
|
75
|
+
const configPath = paths.getContainerConfigPath(name, { engine })
|
|
76
|
+
if (!existsSync(configPath)) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
const content = await readFile(configPath, 'utf8')
|
|
80
|
+
return JSON.parse(content) as ContainerConfig
|
|
81
|
+
}
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
// Search all engine directories
|
|
84
|
+
const engines = getSupportedEngines()
|
|
85
|
+
for (const eng of engines) {
|
|
86
|
+
const configPath = paths.getContainerConfigPath(name, { engine: eng })
|
|
87
|
+
if (existsSync(configPath)) {
|
|
88
|
+
const content = await readFile(configPath, 'utf8')
|
|
89
|
+
return JSON.parse(content) as ContainerConfig
|
|
90
|
+
}
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
return JSON.parse(content) as ContainerConfig
|
|
93
|
+
return null
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
/**
|
|
76
97
|
* Save container configuration
|
|
77
98
|
*/
|
|
78
|
-
async saveConfig(
|
|
79
|
-
|
|
99
|
+
async saveConfig(
|
|
100
|
+
name: string,
|
|
101
|
+
options: { engine: string },
|
|
102
|
+
config: ContainerConfig,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const { engine } = options
|
|
105
|
+
const configPath = paths.getContainerConfigPath(name, { engine })
|
|
80
106
|
await writeFile(configPath, JSON.stringify(config, null, 2))
|
|
81
107
|
}
|
|
82
108
|
|
|
@@ -93,20 +119,36 @@ export class ContainerManager {
|
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
const updatedConfig = { ...config, ...updates }
|
|
96
|
-
await this.saveConfig(name, updatedConfig)
|
|
122
|
+
await this.saveConfig(name, { engine: config.engine }, updatedConfig)
|
|
97
123
|
return updatedConfig
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
/**
|
|
101
127
|
* Check if a container exists
|
|
128
|
+
* If engine is not provided, checks all engine directories
|
|
102
129
|
*/
|
|
103
|
-
async exists(name: string): Promise<boolean> {
|
|
104
|
-
const
|
|
105
|
-
|
|
130
|
+
async exists(name: string, options?: { engine?: string }): Promise<boolean> {
|
|
131
|
+
const { engine } = options || {}
|
|
132
|
+
|
|
133
|
+
if (engine) {
|
|
134
|
+
const configPath = paths.getContainerConfigPath(name, { engine })
|
|
135
|
+
return existsSync(configPath)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check all engine directories
|
|
139
|
+
const engines = getSupportedEngines()
|
|
140
|
+
for (const eng of engines) {
|
|
141
|
+
const configPath = paths.getContainerConfigPath(name, { engine: eng })
|
|
142
|
+
if (existsSync(configPath)) {
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
/**
|
|
109
|
-
* List all containers
|
|
151
|
+
* List all containers across all engines
|
|
110
152
|
*/
|
|
111
153
|
async list(): Promise<ContainerConfig[]> {
|
|
112
154
|
const containersDir = paths.containers
|
|
@@ -115,19 +157,30 @@ export class ContainerManager {
|
|
|
115
157
|
return []
|
|
116
158
|
}
|
|
117
159
|
|
|
118
|
-
const entries = await readdir(containersDir, { withFileTypes: true })
|
|
119
160
|
const containers: ContainerConfig[] = []
|
|
161
|
+
const engines = getSupportedEngines()
|
|
162
|
+
|
|
163
|
+
for (const engine of engines) {
|
|
164
|
+
const engineDir = paths.getEngineContainersPath(engine)
|
|
165
|
+
if (!existsSync(engineDir)) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
const entries = await readdir(engineDir, { withFileTypes: true })
|
|
170
|
+
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
const config = await this.getConfig(entry.name, { engine })
|
|
174
|
+
if (config) {
|
|
175
|
+
// Check if actually running
|
|
176
|
+
const running = await processManager.isRunning(entry.name, {
|
|
177
|
+
engine,
|
|
178
|
+
})
|
|
179
|
+
containers.push({
|
|
180
|
+
...config,
|
|
181
|
+
status: running ? 'running' : 'stopped',
|
|
182
|
+
})
|
|
183
|
+
}
|
|
131
184
|
}
|
|
132
185
|
}
|
|
133
186
|
}
|
|
@@ -141,19 +194,23 @@ export class ContainerManager {
|
|
|
141
194
|
async delete(name: string, options: DeleteOptions = {}): Promise<void> {
|
|
142
195
|
const { force = false } = options
|
|
143
196
|
|
|
144
|
-
|
|
197
|
+
// Get container config to find engine
|
|
198
|
+
const config = await this.getConfig(name)
|
|
199
|
+
if (!config) {
|
|
145
200
|
throw new Error(`Container "${name}" not found`)
|
|
146
201
|
}
|
|
147
202
|
|
|
203
|
+
const { engine } = config
|
|
204
|
+
|
|
148
205
|
// Check if running
|
|
149
|
-
const running = await processManager.isRunning(name)
|
|
206
|
+
const running = await processManager.isRunning(name, { engine })
|
|
150
207
|
if (running && !force) {
|
|
151
208
|
throw new Error(
|
|
152
209
|
`Container "${name}" is running. Stop it first or use --force`,
|
|
153
210
|
)
|
|
154
211
|
}
|
|
155
212
|
|
|
156
|
-
const containerPath = paths.getContainerPath(name)
|
|
213
|
+
const containerPath = paths.getContainerPath(name, { engine })
|
|
157
214
|
await rm(containerPath, { recursive: true, force: true })
|
|
158
215
|
}
|
|
159
216
|
|
|
@@ -171,18 +228,21 @@ export class ContainerManager {
|
|
|
171
228
|
)
|
|
172
229
|
}
|
|
173
230
|
|
|
174
|
-
//
|
|
175
|
-
|
|
231
|
+
// Get source config
|
|
232
|
+
const sourceConfig = await this.getConfig(sourceName)
|
|
233
|
+
if (!sourceConfig) {
|
|
176
234
|
throw new Error(`Source container "${sourceName}" not found`)
|
|
177
235
|
}
|
|
178
236
|
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
const { engine } = sourceConfig
|
|
238
|
+
|
|
239
|
+
// Check target doesn't exist (for this engine)
|
|
240
|
+
if (await this.exists(targetName, { engine })) {
|
|
181
241
|
throw new Error(`Target container "${targetName}" already exists`)
|
|
182
242
|
}
|
|
183
243
|
|
|
184
244
|
// Check source is not running
|
|
185
|
-
const running = await processManager.isRunning(sourceName)
|
|
245
|
+
const running = await processManager.isRunning(sourceName, { engine })
|
|
186
246
|
if (running) {
|
|
187
247
|
throw new Error(
|
|
188
248
|
`Source container "${sourceName}" is running. Stop it first`,
|
|
@@ -190,13 +250,13 @@ export class ContainerManager {
|
|
|
190
250
|
}
|
|
191
251
|
|
|
192
252
|
// Copy container directory
|
|
193
|
-
const sourcePath = paths.getContainerPath(sourceName)
|
|
194
|
-
const targetPath = paths.getContainerPath(targetName)
|
|
253
|
+
const sourcePath = paths.getContainerPath(sourceName, { engine })
|
|
254
|
+
const targetPath = paths.getContainerPath(targetName, { engine })
|
|
195
255
|
|
|
196
256
|
await cp(sourcePath, targetPath, { recursive: true })
|
|
197
257
|
|
|
198
258
|
// Update target config
|
|
199
|
-
const config = await this.getConfig(targetName)
|
|
259
|
+
const config = await this.getConfig(targetName, { engine })
|
|
200
260
|
if (!config) {
|
|
201
261
|
throw new Error('Failed to read cloned container config')
|
|
202
262
|
}
|
|
@@ -206,10 +266,13 @@ export class ContainerManager {
|
|
|
206
266
|
config.clonedFrom = sourceName
|
|
207
267
|
|
|
208
268
|
// Assign new port (excluding ports already used by other containers)
|
|
209
|
-
const
|
|
269
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
270
|
+
const { port } = await portManager.findAvailablePortExcludingContainers({
|
|
271
|
+
portRange: engineDefaults.portRange,
|
|
272
|
+
})
|
|
210
273
|
config.port = port
|
|
211
274
|
|
|
212
|
-
await this.saveConfig(targetName, config)
|
|
275
|
+
await this.saveConfig(targetName, { engine }, config)
|
|
213
276
|
|
|
214
277
|
return config
|
|
215
278
|
}
|
|
@@ -225,37 +288,40 @@ export class ContainerManager {
|
|
|
225
288
|
)
|
|
226
289
|
}
|
|
227
290
|
|
|
228
|
-
//
|
|
229
|
-
|
|
291
|
+
// Get source config
|
|
292
|
+
const sourceConfig = await this.getConfig(oldName)
|
|
293
|
+
if (!sourceConfig) {
|
|
230
294
|
throw new Error(`Container "${oldName}" not found`)
|
|
231
295
|
}
|
|
232
296
|
|
|
297
|
+
const { engine } = sourceConfig
|
|
298
|
+
|
|
233
299
|
// Check target doesn't exist
|
|
234
|
-
if (await this.exists(newName)) {
|
|
300
|
+
if (await this.exists(newName, { engine })) {
|
|
235
301
|
throw new Error(`Container "${newName}" already exists`)
|
|
236
302
|
}
|
|
237
303
|
|
|
238
304
|
// Check container is not running
|
|
239
|
-
const running = await processManager.isRunning(oldName)
|
|
305
|
+
const running = await processManager.isRunning(oldName, { engine })
|
|
240
306
|
if (running) {
|
|
241
307
|
throw new Error(`Container "${oldName}" is running. Stop it first`)
|
|
242
308
|
}
|
|
243
309
|
|
|
244
310
|
// Rename directory
|
|
245
|
-
const oldPath = paths.getContainerPath(oldName)
|
|
246
|
-
const newPath = paths.getContainerPath(newName)
|
|
311
|
+
const oldPath = paths.getContainerPath(oldName, { engine })
|
|
312
|
+
const newPath = paths.getContainerPath(newName, { engine })
|
|
247
313
|
|
|
248
314
|
await cp(oldPath, newPath, { recursive: true })
|
|
249
315
|
await rm(oldPath, { recursive: true, force: true })
|
|
250
316
|
|
|
251
317
|
// Update config with new name
|
|
252
|
-
const config = await this.getConfig(newName)
|
|
318
|
+
const config = await this.getConfig(newName, { engine })
|
|
253
319
|
if (!config) {
|
|
254
320
|
throw new Error('Failed to read renamed container config')
|
|
255
321
|
}
|
|
256
322
|
|
|
257
323
|
config.name = newName
|
|
258
|
-
await this.saveConfig(newName, config)
|
|
324
|
+
await this.saveConfig(newName, { engine }, config)
|
|
259
325
|
|
|
260
326
|
return config
|
|
261
327
|
}
|
|
@@ -269,13 +335,11 @@ export class ContainerManager {
|
|
|
269
335
|
|
|
270
336
|
/**
|
|
271
337
|
* Get connection string for a container
|
|
338
|
+
* Delegates to the appropriate engine
|
|
272
339
|
*/
|
|
273
|
-
getConnectionString(
|
|
274
|
-
config
|
|
275
|
-
|
|
276
|
-
): string {
|
|
277
|
-
const { port } = config
|
|
278
|
-
return `postgresql://postgres@localhost:${port}/${database}`
|
|
340
|
+
getConnectionString(config: ContainerConfig, database?: string): string {
|
|
341
|
+
const engine = getEngine(config.engine)
|
|
342
|
+
return engine.getConnectionString(config, database)
|
|
279
343
|
}
|
|
280
344
|
}
|
|
281
345
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* for database engines.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { exec } from 'child_process'
|
|
8
|
+
import { exec, spawnSync } from 'child_process'
|
|
9
9
|
import { promisify } from 'util'
|
|
10
10
|
import {
|
|
11
11
|
type PackageManagerId,
|
|
@@ -173,30 +173,51 @@ export async function getAllMissingDependencies(): Promise<Dependency[]> {
|
|
|
173
173
|
// =============================================================================
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* Check if stdin is a TTY (interactive terminal)
|
|
177
177
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
function hasTTY(): boolean {
|
|
179
|
+
return process.stdin.isTTY === true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if running as root
|
|
184
|
+
*/
|
|
185
|
+
function isRoot(): boolean {
|
|
186
|
+
return process.getuid?.() === 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute command with inherited stdio (for TTY support with sudo)
|
|
191
|
+
* Uses spawnSync to properly connect to the terminal for password prompts
|
|
192
|
+
*/
|
|
193
|
+
function execWithInheritedStdio(command: string): void {
|
|
194
|
+
let cmdToRun = command
|
|
195
|
+
|
|
196
|
+
// If already running as root, strip sudo from the command
|
|
197
|
+
if (isRoot() && command.startsWith('sudo ')) {
|
|
198
|
+
cmdToRun = command.replace(/^sudo\s+/, '')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if we need a TTY for sudo password prompts
|
|
202
|
+
if (!hasTTY() && cmdToRun.includes('sudo')) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'Cannot run sudo commands without an interactive terminal. Please run the install command manually:\n' +
|
|
205
|
+
` ${command}`,
|
|
193
206
|
)
|
|
207
|
+
}
|
|
194
208
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}, timeoutMs)
|
|
209
|
+
const result = spawnSync(cmdToRun, [], {
|
|
210
|
+
shell: true,
|
|
211
|
+
stdio: 'inherit',
|
|
199
212
|
})
|
|
213
|
+
|
|
214
|
+
if (result.error) {
|
|
215
|
+
throw result.error
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.status !== 0) {
|
|
219
|
+
throw new Error(`Command failed with exit code ${result.status}: ${cmdToRun}`)
|
|
220
|
+
}
|
|
200
221
|
}
|
|
201
222
|
|
|
202
223
|
/**
|
|
@@ -246,7 +267,8 @@ export async function installDependency(
|
|
|
246
267
|
const commands = buildInstallCommand(dependency, packageManager)
|
|
247
268
|
|
|
248
269
|
for (const cmd of commands) {
|
|
249
|
-
|
|
270
|
+
// Use inherited stdio so sudo can prompt for password in terminal
|
|
271
|
+
execWithInheritedStdio(cmd)
|
|
250
272
|
}
|
|
251
273
|
|
|
252
274
|
// Verify installation
|
package/core/port-manager.ts
CHANGED
|
@@ -3,12 +3,20 @@ import { exec } from 'child_process'
|
|
|
3
3
|
import { promisify } from 'util'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
5
|
import { readdir, readFile } from 'fs/promises'
|
|
6
|
-
import { defaults } from '../config/defaults'
|
|
6
|
+
import { defaults, getSupportedEngines } from '../config/defaults'
|
|
7
7
|
import { paths } from '../config/paths'
|
|
8
8
|
import type { ContainerConfig, PortResult } from '../types'
|
|
9
9
|
|
|
10
10
|
const execAsync = promisify(exec)
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Options for finding an available port
|
|
14
|
+
*/
|
|
15
|
+
type FindPortOptions = {
|
|
16
|
+
preferredPort?: number
|
|
17
|
+
portRange?: { start: number; end: number }
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export class PortManager {
|
|
13
21
|
/**
|
|
14
22
|
* Check if a specific port is available
|
|
@@ -36,12 +44,13 @@ export class PortManager {
|
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
|
-
* Find the next available port starting from the
|
|
47
|
+
* Find the next available port starting from the preferred port
|
|
40
48
|
* Returns the port number and whether it's the default port
|
|
41
49
|
*/
|
|
42
|
-
async findAvailablePort(
|
|
43
|
-
preferredPort
|
|
44
|
-
|
|
50
|
+
async findAvailablePort(options: FindPortOptions = {}): Promise<PortResult> {
|
|
51
|
+
const preferredPort = options.preferredPort ?? defaults.port
|
|
52
|
+
const portRange = options.portRange ?? defaults.portRange
|
|
53
|
+
|
|
45
54
|
// First try the preferred port
|
|
46
55
|
if (await this.isPortAvailable(preferredPort)) {
|
|
47
56
|
return {
|
|
@@ -51,11 +60,7 @@ export class PortManager {
|
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
// Scan for available ports in the range
|
|
54
|
-
for (
|
|
55
|
-
let port = defaults.portRange.start;
|
|
56
|
-
port <= defaults.portRange.end;
|
|
57
|
-
port++
|
|
58
|
-
) {
|
|
63
|
+
for (let port = portRange.start; port <= portRange.end; port++) {
|
|
59
64
|
if (port === preferredPort) continue // Already tried this one
|
|
60
65
|
|
|
61
66
|
if (await this.isPortAvailable(port)) {
|
|
@@ -67,7 +72,7 @@ export class PortManager {
|
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
throw new Error(
|
|
70
|
-
`No available ports found in range ${
|
|
75
|
+
`No available ports found in range ${portRange.start}-${portRange.end}`,
|
|
71
76
|
)
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -84,7 +89,7 @@ export class PortManager {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
|
-
* Get all ports currently assigned to containers
|
|
92
|
+
* Get all ports currently assigned to containers across all engines
|
|
88
93
|
*/
|
|
89
94
|
async getContainerPorts(): Promise<number[]> {
|
|
90
95
|
const containersDir = paths.containers
|
|
@@ -94,18 +99,26 @@ export class PortManager {
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
const ports: number[] = []
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
for (const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const engines = getSupportedEngines()
|
|
103
|
+
|
|
104
|
+
for (const engine of engines) {
|
|
105
|
+
const engineDir = paths.getEngineContainersPath(engine)
|
|
106
|
+
if (!existsSync(engineDir)) continue
|
|
107
|
+
|
|
108
|
+
const entries = await readdir(engineDir, { withFileTypes: true })
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
const configPath = paths.getContainerConfigPath(entry.name, {
|
|
112
|
+
engine,
|
|
113
|
+
})
|
|
114
|
+
if (existsSync(configPath)) {
|
|
115
|
+
try {
|
|
116
|
+
const content = await readFile(configPath, 'utf8')
|
|
117
|
+
const config = JSON.parse(content) as ContainerConfig
|
|
118
|
+
ports.push(config.port)
|
|
119
|
+
} catch {
|
|
120
|
+
// Skip invalid configs
|
|
121
|
+
}
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
}
|
|
@@ -118,8 +131,10 @@ export class PortManager {
|
|
|
118
131
|
* Find an available port that's not in use by any process AND not assigned to any container
|
|
119
132
|
*/
|
|
120
133
|
async findAvailablePortExcludingContainers(
|
|
121
|
-
|
|
134
|
+
options: FindPortOptions = {},
|
|
122
135
|
): Promise<PortResult> {
|
|
136
|
+
const preferredPort = options.preferredPort ?? defaults.port
|
|
137
|
+
const portRange = options.portRange ?? defaults.portRange
|
|
123
138
|
const containerPorts = await this.getContainerPorts()
|
|
124
139
|
|
|
125
140
|
// First try the preferred port
|
|
@@ -134,11 +149,7 @@ export class PortManager {
|
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
// Scan for available ports in the range
|
|
137
|
-
for (
|
|
138
|
-
let port = defaults.portRange.start;
|
|
139
|
-
port <= defaults.portRange.end;
|
|
140
|
-
port++
|
|
141
|
-
) {
|
|
152
|
+
for (let port = portRange.start; port <= portRange.end; port++) {
|
|
142
153
|
if (containerPorts.includes(port)) continue // Skip ports used by containers
|
|
143
154
|
if (port === preferredPort) continue // Already tried this one
|
|
144
155
|
|
|
@@ -151,7 +162,7 @@ export class PortManager {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
throw new Error(
|
|
154
|
-
`No available ports found in range ${
|
|
165
|
+
`No available ports found in range ${portRange.start}-${portRange.end}`,
|
|
155
166
|
)
|
|
156
167
|
}
|
|
157
168
|
}
|
|
@@ -308,29 +308,30 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
308
308
|
|
|
309
309
|
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
314
|
-
|
|
311
|
+
// Don't use a spinner during installation - it blocks TTY access for sudo password prompts
|
|
312
|
+
console.log(chalk.cyan(` Installing PostgreSQL client tools with ${packageManager.name}...`))
|
|
313
|
+
console.log(chalk.gray(' You may be prompted for your password.'))
|
|
314
|
+
console.log()
|
|
315
315
|
|
|
316
316
|
try {
|
|
317
317
|
const results = await installEngineDependencies('postgresql', packageManager)
|
|
318
318
|
const allSuccess = results.every((r) => r.success)
|
|
319
319
|
|
|
320
320
|
if (allSuccess) {
|
|
321
|
-
|
|
322
|
-
console.log(success('
|
|
321
|
+
console.log()
|
|
322
|
+
console.log(success('PostgreSQL client tools installed successfully'))
|
|
323
323
|
return true
|
|
324
324
|
} else {
|
|
325
325
|
const failed = results.filter((r) => !r.success)
|
|
326
|
-
|
|
326
|
+
console.log()
|
|
327
|
+
console.log(themeError('Some installations failed:'))
|
|
327
328
|
for (const f of failed) {
|
|
328
|
-
console.log(themeError(`
|
|
329
|
+
console.log(themeError(` ${f.dependency.name}: ${f.error}`))
|
|
329
330
|
}
|
|
330
331
|
return false
|
|
331
332
|
}
|
|
332
333
|
} catch (error: unknown) {
|
|
333
|
-
|
|
334
|
+
console.log()
|
|
334
335
|
console.log(themeError('Failed to install PostgreSQL client tools'))
|
|
335
336
|
console.log(warning('Please install manually'))
|
|
336
337
|
if (error instanceof Error) {
|
package/core/process-manager.ts
CHANGED
|
@@ -202,10 +202,14 @@ export class ProcessManager {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
* Check if
|
|
205
|
+
* Check if a database server is running by looking for PID file
|
|
206
206
|
*/
|
|
207
|
-
async isRunning(
|
|
208
|
-
|
|
207
|
+
async isRunning(
|
|
208
|
+
containerName: string,
|
|
209
|
+
options: { engine: string },
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
const { engine } = options
|
|
212
|
+
const pidFile = paths.getContainerPidPath(containerName, { engine })
|
|
209
213
|
if (!existsSync(pidFile)) {
|
|
210
214
|
return false
|
|
211
215
|
}
|
|
@@ -223,10 +227,14 @@ export class ProcessManager {
|
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
/**
|
|
226
|
-
* Get the PID of a running
|
|
230
|
+
* Get the PID of a running database server
|
|
227
231
|
*/
|
|
228
|
-
async getPid(
|
|
229
|
-
|
|
232
|
+
async getPid(
|
|
233
|
+
containerName: string,
|
|
234
|
+
options: { engine: string },
|
|
235
|
+
): Promise<number | null> {
|
|
236
|
+
const { engine } = options
|
|
237
|
+
const pidFile = paths.getContainerPidPath(containerName, { engine })
|
|
230
238
|
if (!existsSync(pidFile)) {
|
|
231
239
|
return null
|
|
232
240
|
}
|
package/engines/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { postgresqlEngine } from './postgresql'
|
|
2
|
+
import { mysqlEngine } from './mysql'
|
|
2
3
|
import type { BaseEngine } from './base-engine'
|
|
3
4
|
import type { EngineInfo } from '../types'
|
|
4
5
|
|
|
@@ -6,9 +7,13 @@ import type { EngineInfo } from '../types'
|
|
|
6
7
|
* Registry of available database engines
|
|
7
8
|
*/
|
|
8
9
|
export const engines: Record<string, BaseEngine> = {
|
|
10
|
+
// PostgreSQL and aliases
|
|
9
11
|
postgresql: postgresqlEngine,
|
|
10
|
-
postgres: postgresqlEngine,
|
|
11
|
-
pg: postgresqlEngine,
|
|
12
|
+
postgres: postgresqlEngine,
|
|
13
|
+
pg: postgresqlEngine,
|
|
14
|
+
// MySQL and aliases
|
|
15
|
+
mysql: mysqlEngine,
|
|
16
|
+
mariadb: mysqlEngine, // MariaDB is MySQL-compatible
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/**
|