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/cli/commands/create.ts
CHANGED
|
@@ -5,11 +5,12 @@ import chalk from 'chalk'
|
|
|
5
5
|
import { containerManager } from '../../core/container-manager'
|
|
6
6
|
import { portManager } from '../../core/port-manager'
|
|
7
7
|
import { getEngine } from '../../engines'
|
|
8
|
-
import {
|
|
8
|
+
import { getEngineDefaults } from '../../config/defaults'
|
|
9
9
|
import {
|
|
10
10
|
promptCreateOptions,
|
|
11
11
|
promptInstallDependencies,
|
|
12
12
|
promptContainerName,
|
|
13
|
+
promptConfirm,
|
|
13
14
|
} from '../ui/prompts'
|
|
14
15
|
import { createSpinner } from '../ui/spinner'
|
|
15
16
|
import { header, error, connectionBox } from '../ui/theme'
|
|
@@ -18,38 +19,42 @@ import { join } from 'path'
|
|
|
18
19
|
import { spawn } from 'child_process'
|
|
19
20
|
import { platform } from 'os'
|
|
20
21
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
22
|
+
import type { EngineName } from '../../types'
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Detect if a location string is a connection string or a file path
|
|
26
|
+
* Also infers engine from connection string scheme
|
|
24
27
|
*/
|
|
25
|
-
function detectLocationType(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
function detectLocationType(location: string): {
|
|
29
|
+
type: 'connection' | 'file' | 'not_found'
|
|
30
|
+
inferredEngine?: EngineName
|
|
31
|
+
} {
|
|
32
|
+
// Check for PostgreSQL connection string
|
|
29
33
|
if (
|
|
30
34
|
location.startsWith('postgresql://') ||
|
|
31
35
|
location.startsWith('postgres://')
|
|
32
36
|
) {
|
|
33
|
-
return 'connection'
|
|
37
|
+
return { type: 'connection', inferredEngine: 'postgresql' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for MySQL connection string
|
|
41
|
+
if (location.startsWith('mysql://')) {
|
|
42
|
+
return { type: 'connection', inferredEngine: 'mysql' }
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// Check if file exists
|
|
37
46
|
if (existsSync(location)) {
|
|
38
|
-
return 'file'
|
|
47
|
+
return { type: 'file' }
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
return 'not_found'
|
|
50
|
+
return { type: 'not_found' }
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export const createCommand = new Command('create')
|
|
45
54
|
.description('Create a new database container')
|
|
46
55
|
.argument('[name]', 'Container name')
|
|
47
|
-
.option('-e, --engine <engine>', 'Database engine
|
|
48
|
-
.option(
|
|
49
|
-
'--pg-version <version>',
|
|
50
|
-
'PostgreSQL version',
|
|
51
|
-
defaults.postgresVersion,
|
|
52
|
-
)
|
|
56
|
+
.option('-e, --engine <engine>', 'Database engine (postgresql, mysql)')
|
|
57
|
+
.option('-v, --version <version>', 'Database version')
|
|
53
58
|
.option('-d, --database <database>', 'Database name')
|
|
54
59
|
.option('-p, --port <port>', 'Port number')
|
|
55
60
|
.option('--no-start', 'Do not start the container after creation')
|
|
@@ -61,8 +66,8 @@ export const createCommand = new Command('create')
|
|
|
61
66
|
async (
|
|
62
67
|
name: string | undefined,
|
|
63
68
|
options: {
|
|
64
|
-
engine
|
|
65
|
-
|
|
69
|
+
engine?: string
|
|
70
|
+
version?: string
|
|
66
71
|
database?: string
|
|
67
72
|
port?: string
|
|
68
73
|
start: boolean
|
|
@@ -73,41 +78,39 @@ export const createCommand = new Command('create')
|
|
|
73
78
|
|
|
74
79
|
try {
|
|
75
80
|
let containerName = name
|
|
76
|
-
let engine = options.engine
|
|
77
|
-
let version = options.
|
|
81
|
+
let engine: EngineName = (options.engine as EngineName) || 'postgresql'
|
|
82
|
+
let version = options.version
|
|
78
83
|
let database = options.database
|
|
79
84
|
|
|
80
|
-
//
|
|
81
|
-
if (!containerName) {
|
|
82
|
-
const answers = await promptCreateOptions()
|
|
83
|
-
containerName = answers.name
|
|
84
|
-
engine = answers.engine
|
|
85
|
-
version = answers.version
|
|
86
|
-
database = answers.database
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Default database name to container name if not specified
|
|
90
|
-
database = database ?? containerName
|
|
91
|
-
|
|
92
|
-
// Validate --from location if provided
|
|
85
|
+
// Validate --from location if provided (before prompts so we can infer engine)
|
|
93
86
|
let restoreLocation: string | null = null
|
|
94
87
|
let restoreType: 'connection' | 'file' | null = null
|
|
95
88
|
|
|
96
89
|
if (options.from) {
|
|
97
|
-
const
|
|
90
|
+
const locationInfo = detectLocationType(options.from)
|
|
98
91
|
|
|
99
|
-
if (
|
|
92
|
+
if (locationInfo.type === 'not_found') {
|
|
100
93
|
console.error(error(`Location not found: ${options.from}`))
|
|
101
94
|
console.log(
|
|
102
95
|
chalk.gray(
|
|
103
|
-
' Provide a valid file path or connection string (postgresql
|
|
96
|
+
' Provide a valid file path or connection string (postgresql://, mysql://)',
|
|
104
97
|
),
|
|
105
98
|
)
|
|
106
99
|
process.exit(1)
|
|
107
100
|
}
|
|
108
101
|
|
|
109
102
|
restoreLocation = options.from
|
|
110
|
-
restoreType =
|
|
103
|
+
restoreType = locationInfo.type
|
|
104
|
+
|
|
105
|
+
// Infer engine from connection string if not explicitly set
|
|
106
|
+
if (!options.engine && locationInfo.inferredEngine) {
|
|
107
|
+
engine = locationInfo.inferredEngine
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.gray(
|
|
110
|
+
` Inferred engine "${engine}" from connection string`,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
111
114
|
|
|
112
115
|
// If using --from, we must start the container
|
|
113
116
|
if (options.start === false) {
|
|
@@ -120,6 +123,26 @@ export const createCommand = new Command('create')
|
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
// Get engine defaults for port range and default version
|
|
127
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
128
|
+
|
|
129
|
+
// Set version to engine default if not specified
|
|
130
|
+
if (!version) {
|
|
131
|
+
version = engineDefaults.defaultVersion
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Interactive mode if no name provided
|
|
135
|
+
if (!containerName) {
|
|
136
|
+
const answers = await promptCreateOptions()
|
|
137
|
+
containerName = answers.name
|
|
138
|
+
engine = answers.engine as EngineName
|
|
139
|
+
version = answers.version
|
|
140
|
+
database = answers.database
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Default database name to container name if not specified
|
|
144
|
+
database = database ?? containerName
|
|
145
|
+
|
|
123
146
|
console.log(header('Creating Database Container'))
|
|
124
147
|
console.log()
|
|
125
148
|
|
|
@@ -178,30 +201,39 @@ export const createCommand = new Command('create')
|
|
|
178
201
|
portSpinner.succeed(`Using port ${port}`)
|
|
179
202
|
} else {
|
|
180
203
|
const { port: foundPort, isDefault } =
|
|
181
|
-
await portManager.findAvailablePort(
|
|
204
|
+
await portManager.findAvailablePort({
|
|
205
|
+
preferredPort: engineDefaults.defaultPort,
|
|
206
|
+
portRange: engineDefaults.portRange,
|
|
207
|
+
})
|
|
182
208
|
port = foundPort
|
|
183
209
|
if (isDefault) {
|
|
184
210
|
portSpinner.succeed(`Using default port ${port}`)
|
|
185
211
|
} else {
|
|
186
|
-
portSpinner.warn(
|
|
212
|
+
portSpinner.warn(
|
|
213
|
+
`Default port ${engineDefaults.defaultPort} is in use, using port ${port}`,
|
|
214
|
+
)
|
|
187
215
|
}
|
|
188
216
|
}
|
|
189
217
|
|
|
190
218
|
// Ensure binaries are available
|
|
191
219
|
const binarySpinner = createSpinner(
|
|
192
|
-
`Checking
|
|
220
|
+
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
193
221
|
)
|
|
194
222
|
binarySpinner.start()
|
|
195
223
|
|
|
196
224
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
197
225
|
if (isInstalled) {
|
|
198
|
-
binarySpinner.succeed(
|
|
226
|
+
binarySpinner.succeed(
|
|
227
|
+
`${dbEngine.displayName} ${version} binaries ready (cached)`,
|
|
228
|
+
)
|
|
199
229
|
} else {
|
|
200
|
-
binarySpinner.text = `Downloading
|
|
230
|
+
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
201
231
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
202
232
|
binarySpinner.text = message
|
|
203
233
|
})
|
|
204
|
-
binarySpinner.succeed(
|
|
234
|
+
binarySpinner.succeed(
|
|
235
|
+
`${dbEngine.displayName} ${version} binaries downloaded`,
|
|
236
|
+
)
|
|
205
237
|
}
|
|
206
238
|
|
|
207
239
|
// Check if container name already exists and prompt for new name if needed
|
|
@@ -217,7 +249,7 @@ export const createCommand = new Command('create')
|
|
|
217
249
|
createSpinnerInstance.start()
|
|
218
250
|
|
|
219
251
|
await containerManager.create(containerName, {
|
|
220
|
-
engine: dbEngine.name,
|
|
252
|
+
engine: dbEngine.name as EngineName,
|
|
221
253
|
version,
|
|
222
254
|
port,
|
|
223
255
|
database,
|
|
@@ -230,28 +262,68 @@ export const createCommand = new Command('create')
|
|
|
230
262
|
initSpinner.start()
|
|
231
263
|
|
|
232
264
|
await dbEngine.initDataDir(containerName, version, {
|
|
233
|
-
superuser:
|
|
265
|
+
superuser: engineDefaults.superuser,
|
|
234
266
|
})
|
|
235
267
|
|
|
236
268
|
initSpinner.succeed('Database cluster initialized')
|
|
237
269
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
// Determine if we should start the container
|
|
271
|
+
// If --from is specified, we must start to restore
|
|
272
|
+
// If --no-start is specified, don't start
|
|
273
|
+
// Otherwise, ask the user
|
|
274
|
+
let shouldStart = false
|
|
275
|
+
if (restoreLocation) {
|
|
276
|
+
// Must start to restore data
|
|
277
|
+
shouldStart = true
|
|
278
|
+
} else if (options.start === false) {
|
|
279
|
+
// User explicitly requested no start
|
|
280
|
+
shouldStart = false
|
|
281
|
+
} else {
|
|
282
|
+
// Ask the user
|
|
283
|
+
console.log()
|
|
284
|
+
shouldStart = await promptConfirm(
|
|
285
|
+
`Start ${containerName} now?`,
|
|
286
|
+
true,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
242
289
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
290
|
+
// Get container config for starting and restoration
|
|
291
|
+
const config = await containerManager.getConfig(containerName)
|
|
292
|
+
|
|
293
|
+
// Start container if requested
|
|
294
|
+
if (shouldStart && config) {
|
|
295
|
+
// Check port availability before starting
|
|
296
|
+
const portAvailable = await portManager.isPortAvailable(config.port)
|
|
297
|
+
if (!portAvailable) {
|
|
298
|
+
// Find a new available port
|
|
299
|
+
const { port: newPort } = await portManager.findAvailablePort({
|
|
300
|
+
portRange: engineDefaults.portRange,
|
|
248
301
|
})
|
|
302
|
+
console.log(
|
|
303
|
+
chalk.yellow(
|
|
304
|
+
` ⚠ Port ${config.port} is in use, switching to port ${newPort}`,
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
config.port = newPort
|
|
308
|
+
port = newPort
|
|
309
|
+
await containerManager.updateConfig(containerName, { port: newPort })
|
|
249
310
|
}
|
|
250
311
|
|
|
251
|
-
startSpinner
|
|
312
|
+
const startSpinner = createSpinner(
|
|
313
|
+
`Starting ${dbEngine.displayName}...`,
|
|
314
|
+
)
|
|
315
|
+
startSpinner.start()
|
|
316
|
+
|
|
317
|
+
await dbEngine.start(config)
|
|
318
|
+
await containerManager.updateConfig(containerName, {
|
|
319
|
+
status: 'running',
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
252
323
|
|
|
253
|
-
// Create the user's database (if different from
|
|
254
|
-
|
|
324
|
+
// Create the user's database (if different from default)
|
|
325
|
+
const defaultDb = engineDefaults.superuser // postgres or root
|
|
326
|
+
if (database !== defaultDb) {
|
|
255
327
|
const dbSpinner = createSpinner(
|
|
256
328
|
`Creating database "${database}"...`,
|
|
257
329
|
)
|
|
@@ -261,117 +333,118 @@ export const createCommand = new Command('create')
|
|
|
261
333
|
|
|
262
334
|
dbSpinner.succeed(`Database "${database}" created`)
|
|
263
335
|
}
|
|
336
|
+
}
|
|
264
337
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
338
|
+
// Handle --from restore if specified (only if started)
|
|
339
|
+
if (restoreLocation && restoreType && config && shouldStart) {
|
|
340
|
+
let backupPath = ''
|
|
341
|
+
|
|
342
|
+
if (restoreType === 'connection') {
|
|
343
|
+
// Create dump from remote database
|
|
344
|
+
const timestamp = Date.now()
|
|
345
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
346
|
+
|
|
347
|
+
let dumpSuccess = false
|
|
348
|
+
let attempts = 0
|
|
349
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
350
|
+
|
|
351
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
352
|
+
attempts++
|
|
353
|
+
const dumpSpinner = createSpinner(
|
|
354
|
+
'Creating dump from remote database...',
|
|
355
|
+
)
|
|
356
|
+
dumpSpinner.start()
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await dbEngine.dumpFromConnectionString(
|
|
360
|
+
restoreLocation,
|
|
361
|
+
tempDumpPath,
|
|
282
362
|
)
|
|
283
|
-
dumpSpinner.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
e.message.includes('pg_dump not found') ||
|
|
300
|
-
e.message.includes('ENOENT')
|
|
301
|
-
) {
|
|
302
|
-
const installed = await promptInstallDependencies('pg_dump')
|
|
303
|
-
if (!installed) {
|
|
304
|
-
process.exit(1)
|
|
305
|
-
}
|
|
306
|
-
// Loop will retry
|
|
307
|
-
continue
|
|
363
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
364
|
+
backupPath = tempDumpPath
|
|
365
|
+
dumpSuccess = true
|
|
366
|
+
} catch (err) {
|
|
367
|
+
const e = err as Error
|
|
368
|
+
dumpSpinner.fail('Failed to create dump')
|
|
369
|
+
|
|
370
|
+
// Check if this is a missing tool error
|
|
371
|
+
if (
|
|
372
|
+
e.message.includes('pg_dump not found') ||
|
|
373
|
+
e.message.includes('ENOENT')
|
|
374
|
+
) {
|
|
375
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
376
|
+
if (!installed) {
|
|
377
|
+
process.exit(1)
|
|
308
378
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.error(error('pg_dump error:'))
|
|
312
|
-
console.log(chalk.gray(` ${e.message}`))
|
|
313
|
-
process.exit(1)
|
|
379
|
+
// Loop will retry
|
|
380
|
+
continue
|
|
314
381
|
}
|
|
315
|
-
}
|
|
316
382
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
console.
|
|
383
|
+
console.log()
|
|
384
|
+
console.error(error('pg_dump error:'))
|
|
385
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
320
386
|
process.exit(1)
|
|
321
387
|
}
|
|
322
|
-
} else {
|
|
323
|
-
backupPath = restoreLocation
|
|
324
388
|
}
|
|
325
389
|
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
390
|
+
// Safety check - should never reach here without backupPath set
|
|
391
|
+
if (!dumpSuccess) {
|
|
392
|
+
console.error(error('Failed to create dump after retries'))
|
|
393
|
+
process.exit(1)
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
backupPath = restoreLocation
|
|
397
|
+
}
|
|
329
398
|
|
|
330
|
-
|
|
331
|
-
|
|
399
|
+
// Detect backup format
|
|
400
|
+
const detectSpinner = createSpinner('Detecting backup format...')
|
|
401
|
+
detectSpinner.start()
|
|
332
402
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
restoreSpinner.start()
|
|
403
|
+
const format = await dbEngine.detectBackupFormat(backupPath)
|
|
404
|
+
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
336
405
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
})
|
|
406
|
+
// Restore backup
|
|
407
|
+
const restoreSpinner = createSpinner('Restoring backup...')
|
|
408
|
+
restoreSpinner.start()
|
|
341
409
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
410
|
+
const result = await dbEngine.restore(config, backupPath, {
|
|
411
|
+
database,
|
|
412
|
+
createDatabase: false, // Already created above
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
if (result.code === 0 || !result.stderr) {
|
|
416
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
417
|
+
} else {
|
|
418
|
+
restoreSpinner.warn('Restore completed with warnings')
|
|
419
|
+
if (result.stderr) {
|
|
420
|
+
console.log(chalk.yellow('\n Warnings:'))
|
|
421
|
+
const lines = result.stderr.split('\n').slice(0, 5)
|
|
422
|
+
lines.forEach((line) => {
|
|
423
|
+
if (line.trim()) {
|
|
424
|
+
console.log(chalk.gray(` ${line}`))
|
|
356
425
|
}
|
|
426
|
+
})
|
|
427
|
+
if (result.stderr.split('\n').length > 5) {
|
|
428
|
+
console.log(chalk.gray(' ...'))
|
|
357
429
|
}
|
|
358
430
|
}
|
|
359
431
|
}
|
|
360
432
|
}
|
|
361
433
|
|
|
362
434
|
// Show success message
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
const connectionString = dbEngine.getConnectionString(
|
|
435
|
+
const finalConfig = await containerManager.getConfig(containerName)
|
|
436
|
+
if (finalConfig) {
|
|
437
|
+
const connectionString = dbEngine.getConnectionString(finalConfig)
|
|
366
438
|
|
|
367
439
|
console.log()
|
|
368
|
-
console.log(connectionBox(containerName, connectionString, port))
|
|
440
|
+
console.log(connectionBox(containerName, connectionString, finalConfig.port))
|
|
369
441
|
console.log()
|
|
370
|
-
console.log(chalk.gray(' Connect with:'))
|
|
371
|
-
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
372
442
|
|
|
373
|
-
|
|
374
|
-
|
|
443
|
+
if (shouldStart) {
|
|
444
|
+
console.log(chalk.gray(' Connect with:'))
|
|
445
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
446
|
+
|
|
447
|
+
// Copy connection string to clipboard
|
|
375
448
|
try {
|
|
376
449
|
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
377
450
|
const args =
|
|
@@ -397,6 +470,9 @@ export const createCommand = new Command('create')
|
|
|
397
470
|
} catch {
|
|
398
471
|
// Ignore clipboard errors
|
|
399
472
|
}
|
|
473
|
+
} else {
|
|
474
|
+
console.log(chalk.gray(' Start the container:'))
|
|
475
|
+
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
400
476
|
}
|
|
401
477
|
|
|
402
478
|
console.log()
|
|
@@ -404,23 +480,28 @@ export const createCommand = new Command('create')
|
|
|
404
480
|
} catch (err) {
|
|
405
481
|
const e = err as Error
|
|
406
482
|
|
|
407
|
-
// Check if this is a missing tool error
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
483
|
+
// Check if this is a missing tool error (PostgreSQL or MySQL)
|
|
484
|
+
const missingToolPatterns = [
|
|
485
|
+
// PostgreSQL
|
|
486
|
+
'pg_restore not found',
|
|
487
|
+
'psql not found',
|
|
488
|
+
'pg_dump not found',
|
|
489
|
+
// MySQL
|
|
490
|
+
'mysql not found',
|
|
491
|
+
'mysqldump not found',
|
|
492
|
+
'mysqld not found',
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
496
|
+
e.message.includes(p),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if (matchingPattern) {
|
|
500
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
418
501
|
const installed = await promptInstallDependencies(missingTool)
|
|
419
502
|
if (installed) {
|
|
420
503
|
console.log(
|
|
421
|
-
chalk.yellow(
|
|
422
|
-
' Please re-run your command to continue.',
|
|
423
|
-
),
|
|
504
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
424
505
|
)
|
|
425
506
|
}
|
|
426
507
|
process.exit(1)
|
package/cli/commands/delete.ts
CHANGED
|
@@ -57,7 +57,9 @@ export const deleteCommand = new Command('delete')
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Check if running
|
|
60
|
-
const running = await processManager.isRunning(containerName
|
|
60
|
+
const running = await processManager.isRunning(containerName, {
|
|
61
|
+
engine: config.engine,
|
|
62
|
+
})
|
|
61
63
|
if (running) {
|
|
62
64
|
if (options.force) {
|
|
63
65
|
// Stop the container first
|
package/cli/commands/list.ts
CHANGED
|
@@ -3,6 +3,14 @@ import chalk from 'chalk'
|
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
4
|
import { info, error } from '../ui/theme'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Engine icons for display
|
|
8
|
+
*/
|
|
9
|
+
const engineIcons: Record<string, string> = {
|
|
10
|
+
postgresql: '🐘',
|
|
11
|
+
mysql: '🐬',
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export const listCommand = new Command('list')
|
|
7
15
|
.alias('ls')
|
|
8
16
|
.description('List all containers')
|
|
@@ -26,12 +34,12 @@ export const listCommand = new Command('list')
|
|
|
26
34
|
console.log(
|
|
27
35
|
chalk.gray(' ') +
|
|
28
36
|
chalk.bold.white('NAME'.padEnd(20)) +
|
|
29
|
-
chalk.bold.white('ENGINE'.padEnd(
|
|
37
|
+
chalk.bold.white('ENGINE'.padEnd(15)) +
|
|
30
38
|
chalk.bold.white('VERSION'.padEnd(10)) +
|
|
31
39
|
chalk.bold.white('PORT'.padEnd(8)) +
|
|
32
40
|
chalk.bold.white('STATUS'),
|
|
33
41
|
)
|
|
34
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
42
|
+
console.log(chalk.gray(' ' + '─'.repeat(63)))
|
|
35
43
|
|
|
36
44
|
// Table rows
|
|
37
45
|
for (const container of containers) {
|
|
@@ -40,10 +48,13 @@ export const listCommand = new Command('list')
|
|
|
40
48
|
? chalk.green('● running')
|
|
41
49
|
: chalk.gray('○ stopped')
|
|
42
50
|
|
|
51
|
+
const engineIcon = engineIcons[container.engine] || '🗄️'
|
|
52
|
+
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
53
|
+
|
|
43
54
|
console.log(
|
|
44
55
|
chalk.gray(' ') +
|
|
45
56
|
chalk.cyan(container.name.padEnd(20)) +
|
|
46
|
-
chalk.white(
|
|
57
|
+
chalk.white(engineDisplay.padEnd(14)) +
|
|
47
58
|
chalk.yellow(container.version.padEnd(10)) +
|
|
48
59
|
chalk.green(String(container.port).padEnd(8)) +
|
|
49
60
|
statusDisplay,
|