spindb 0.4.1 → 0.5.3

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