spindb 0.4.1 → 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 +237 -156
- package/cli/commands/delete.ts +3 -1
- package/cli/commands/list.ts +14 -3
- package/cli/commands/menu.ts +103 -46
- package/cli/commands/restore.ts +56 -19
- package/cli/commands/start.ts +30 -4
- package/cli/commands/stop.ts +3 -1
- package/cli/ui/prompts.ts +94 -31
- package/config/defaults.ts +40 -15
- package/config/engine-defaults.ts +84 -0
- package/config/os-dependencies.ts +68 -19
- package/config/paths.ts +77 -22
- package/core/binary-manager.ts +30 -5
- package/core/container-manager.ts +124 -60
- package/core/port-manager.ts +42 -31
- 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
package/core/binary-manager.ts
CHANGED
|
@@ -62,7 +62,12 @@ export class BinaryManager {
|
|
|
62
62
|
platform: string,
|
|
63
63
|
arch: string,
|
|
64
64
|
): Promise<boolean> {
|
|
65
|
-
const binPath = paths.getBinaryPath(
|
|
65
|
+
const binPath = paths.getBinaryPath({
|
|
66
|
+
engine: 'postgresql',
|
|
67
|
+
version,
|
|
68
|
+
platform,
|
|
69
|
+
arch,
|
|
70
|
+
})
|
|
66
71
|
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
67
72
|
return existsSync(postgresPath)
|
|
68
73
|
}
|
|
@@ -109,7 +114,12 @@ export class BinaryManager {
|
|
|
109
114
|
onProgress?: ProgressCallback,
|
|
110
115
|
): Promise<string> {
|
|
111
116
|
const url = this.getDownloadUrl(version, platform, arch)
|
|
112
|
-
const binPath = paths.getBinaryPath(
|
|
117
|
+
const binPath = paths.getBinaryPath({
|
|
118
|
+
engine: 'postgresql',
|
|
119
|
+
version,
|
|
120
|
+
platform,
|
|
121
|
+
arch,
|
|
122
|
+
})
|
|
113
123
|
const tempDir = join(paths.bin, `temp-${version}-${platform}-${arch}`)
|
|
114
124
|
const jarFile = join(tempDir, 'postgres.jar')
|
|
115
125
|
|
|
@@ -190,7 +200,12 @@ export class BinaryManager {
|
|
|
190
200
|
platform: string,
|
|
191
201
|
arch: string,
|
|
192
202
|
): Promise<boolean> {
|
|
193
|
-
const binPath = paths.getBinaryPath(
|
|
203
|
+
const binPath = paths.getBinaryPath({
|
|
204
|
+
engine: 'postgresql',
|
|
205
|
+
version,
|
|
206
|
+
platform,
|
|
207
|
+
arch,
|
|
208
|
+
})
|
|
194
209
|
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
195
210
|
|
|
196
211
|
if (!existsSync(postgresPath)) {
|
|
@@ -241,7 +256,12 @@ export class BinaryManager {
|
|
|
241
256
|
arch: string,
|
|
242
257
|
binary: string,
|
|
243
258
|
): string {
|
|
244
|
-
const binPath = paths.getBinaryPath(
|
|
259
|
+
const binPath = paths.getBinaryPath({
|
|
260
|
+
engine: 'postgresql',
|
|
261
|
+
version,
|
|
262
|
+
platform,
|
|
263
|
+
arch,
|
|
264
|
+
})
|
|
245
265
|
return join(binPath, 'bin', binary)
|
|
246
266
|
}
|
|
247
267
|
|
|
@@ -259,7 +279,12 @@ export class BinaryManager {
|
|
|
259
279
|
stage: 'cached',
|
|
260
280
|
message: 'Using cached PostgreSQL binaries',
|
|
261
281
|
})
|
|
262
|
-
return paths.getBinaryPath(
|
|
282
|
+
return paths.getBinaryPath({
|
|
283
|
+
engine: 'postgresql',
|
|
284
|
+
version,
|
|
285
|
+
platform,
|
|
286
|
+
arch,
|
|
287
|
+
})
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
return this.download(version, platform, arch, onProgress)
|
|
@@ -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
|
|
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
|
}
|
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
|
/**
|