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.
@@ -28,6 +28,7 @@ import { join } from 'path'
28
28
  import { paths } from '../../config/paths'
29
29
  import { portManager } from '../../core/port-manager'
30
30
  import { defaults } from '../../config/defaults'
31
+ import type { EngineName } from '../../types'
31
32
  import inquirer from 'inquirer'
32
33
  import { getMissingDependencies } from '../../core/dependency-manager'
33
34
 
@@ -39,6 +40,14 @@ type MenuChoice =
39
40
  }
40
41
  | inquirer.Separator
41
42
 
43
+ /**
44
+ * Engine icons for display
45
+ */
46
+ const engineIcons: Record<string, string> = {
47
+ postgresql: '🐘',
48
+ mysql: '🐬',
49
+ }
50
+
42
51
  async function showMainMenu(): Promise<void> {
43
52
  console.clear()
44
53
  console.log(header('SpinDB - Local Database Manager'))
@@ -57,7 +66,6 @@ async function showMainMenu(): Promise<void> {
57
66
 
58
67
  const canStart = stopped > 0
59
68
  const canStop = running > 0
60
- const canConnect = running > 0
61
69
  const canRestore = running > 0
62
70
  const canClone = containers.length > 0
63
71
 
@@ -92,13 +100,6 @@ async function showMainMenu(): Promise<void> {
92
100
  value: 'stop',
93
101
  disabled: canStop ? false : 'No running containers',
94
102
  },
95
- {
96
- name: canConnect
97
- ? `${chalk.blue('⌘')} Open psql shell`
98
- : chalk.gray('⌘ Open psql shell'),
99
- value: 'connect',
100
- disabled: canConnect ? false : 'No running containers',
101
- },
102
103
  {
103
104
  name: canRestore
104
105
  ? `${chalk.magenta('↓')} Restore backup`
@@ -147,9 +148,6 @@ async function showMainMenu(): Promise<void> {
147
148
  case 'stop':
148
149
  await handleStop()
149
150
  break
150
- case 'connect':
151
- await handleConnect()
152
- break
153
151
  case 'restore':
154
152
  await handleRestore()
155
153
  break
@@ -246,7 +244,7 @@ async function handleCreate(): Promise<void> {
246
244
  createSpinnerInstance.start()
247
245
 
248
246
  await containerManager.create(containerName, {
249
- engine: dbEngine.name,
247
+ engine: dbEngine.name as EngineName,
250
248
  version,
251
249
  port,
252
250
  database,
@@ -362,6 +360,15 @@ async function handleList(): Promise<void> {
362
360
  console.log(
363
361
  info('No containers found. Create one with the "Create" option.'),
364
362
  )
363
+ console.log()
364
+
365
+ await inquirer.prompt([
366
+ {
367
+ type: 'input',
368
+ name: 'continue',
369
+ message: chalk.gray('Press Enter to return to the main menu...'),
370
+ },
371
+ ])
365
372
  return
366
373
  }
367
374
 
@@ -408,7 +415,7 @@ async function handleList(): Promise<void> {
408
415
  console.log()
409
416
  const containerChoices = [
410
417
  ...containers.map((c) => ({
411
- name: `${c.name} ${chalk.gray(`(${c.engine} ${c.version}, port ${c.port})`)} ${
418
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${
412
419
  c.status === 'running'
413
420
  ? chalk.green('● running')
414
421
  : chalk.gray('○ stopped')
@@ -447,7 +454,9 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
447
454
  }
448
455
 
449
456
  // Check actual running state
450
- const isRunning = await processManager.isRunning(containerName)
457
+ const isRunning = await processManager.isRunning(containerName, {
458
+ engine: config.engine,
459
+ })
451
460
  const status = isRunning ? 'running' : 'stopped'
452
461
 
453
462
  console.clear()
@@ -465,6 +474,13 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
465
474
  !isRunning
466
475
  ? { name: `${chalk.green('▶')} Start container`, value: 'start' }
467
476
  : { name: `${chalk.yellow('■')} Stop container`, value: 'stop' },
477
+ {
478
+ name: isRunning
479
+ ? `${chalk.blue('⌘')} Open shell`
480
+ : chalk.gray('⌘ Open shell'),
481
+ value: 'shell',
482
+ disabled: isRunning ? false : 'Start container first',
483
+ },
468
484
  {
469
485
  name: !isRunning
470
486
  ? `${chalk.white('⚙')} Edit container`
@@ -504,6 +520,10 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
504
520
  await handleStopContainer(containerName)
505
521
  await showContainerSubmenu(containerName)
506
522
  return
523
+ case 'shell':
524
+ await handleOpenShell(containerName)
525
+ await showContainerSubmenu(containerName)
526
+ return
507
527
  case 'edit': {
508
528
  const newName = await handleEditContainer(containerName)
509
529
  if (newName === null) {
@@ -674,21 +694,7 @@ async function handleCopyConnectionString(
674
694
  }
675
695
  }
676
696
 
677
- async function handleConnect(): Promise<void> {
678
- const containers = await containerManager.list()
679
- const running = containers.filter((c) => c.status === 'running')
680
-
681
- if (running.length === 0) {
682
- console.log(warning('No running containers'))
683
- return
684
- }
685
-
686
- const containerName = await promptContainerSelect(
687
- running,
688
- 'Select container to connect to:',
689
- )
690
- if (!containerName) return
691
-
697
+ async function handleOpenShell(containerName: string): Promise<void> {
692
698
  const config = await containerManager.getConfig(containerName)
693
699
  if (!config) {
694
700
  console.error(error(`Container "${containerName}" not found`))
@@ -701,25 +707,49 @@ async function handleConnect(): Promise<void> {
701
707
  console.log(info(`Connecting to ${containerName}...`))
702
708
  console.log()
703
709
 
704
- // Spawn psql
705
- const psqlProcess = spawn('psql', [connectionString], {
710
+ // Determine shell command based on engine
711
+ let shellCmd: string
712
+ let shellArgs: string[]
713
+ let installHint: string
714
+
715
+ if (config.engine === 'mysql') {
716
+ shellCmd = 'mysql'
717
+ // MySQL connection: mysql -u root -h 127.0.0.1 -P port database
718
+ shellArgs = [
719
+ '-u',
720
+ 'root',
721
+ '-h',
722
+ '127.0.0.1',
723
+ '-P',
724
+ String(config.port),
725
+ config.database,
726
+ ]
727
+ installHint = 'brew install mysql-client'
728
+ } else {
729
+ // PostgreSQL (default)
730
+ shellCmd = 'psql'
731
+ shellArgs = [connectionString]
732
+ installHint = 'brew install libpq && brew link --force libpq'
733
+ }
734
+
735
+ const shellProcess = spawn(shellCmd, shellArgs, {
706
736
  stdio: 'inherit',
707
737
  })
708
738
 
709
- psqlProcess.on('error', (err: NodeJS.ErrnoException) => {
739
+ shellProcess.on('error', (err: NodeJS.ErrnoException) => {
710
740
  if (err.code === 'ENOENT') {
711
- console.log(warning('psql not found on your system.'))
741
+ console.log(warning(`${shellCmd} not found on your system.`))
712
742
  console.log()
713
743
  console.log(chalk.gray(' Connect manually with:'))
714
744
  console.log(chalk.cyan(` ${connectionString}`))
715
745
  console.log()
716
- console.log(chalk.gray(' Install PostgreSQL client:'))
717
- console.log(chalk.cyan(' brew install libpq && brew link --force libpq'))
746
+ console.log(chalk.gray(` Install ${config.engine} client:`))
747
+ console.log(chalk.cyan(` ${installHint}`))
718
748
  }
719
749
  })
720
750
 
721
751
  await new Promise<void>((resolve) => {
722
- psqlProcess.on('close', () => resolve())
752
+ shellProcess.on('close', () => resolve())
723
753
  })
724
754
  }
725
755
 
@@ -779,7 +809,7 @@ async function handleCreateForRestore(): Promise<{
779
809
  createSpinnerInstance.start()
780
810
 
781
811
  await containerManager.create(containerName, {
782
- engine: dbEngine.name,
812
+ engine: dbEngine.name as EngineName,
783
813
  version,
784
814
  port,
785
815
  database,
@@ -836,7 +866,7 @@ async function handleRestore(): Promise<void> {
836
866
  // Build choices: running containers + create new option
837
867
  const choices = [
838
868
  ...running.map((c) => ({
839
- name: `${c.name} ${chalk.gray(`(${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
869
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
840
870
  value: c.name,
841
871
  short: c.name,
842
872
  })),
@@ -1352,6 +1382,15 @@ async function handleStartContainer(containerName: string): Promise<void> {
1352
1382
  `Port ${config.port} is in use. Stop the process using it or change this container's port.`,
1353
1383
  ),
1354
1384
  )
1385
+ console.log()
1386
+ console.log(
1387
+ info(
1388
+ 'Tip: If you installed MariaDB via apt, it may have started a system service.',
1389
+ ),
1390
+ )
1391
+ console.log(
1392
+ info('Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb'),
1393
+ )
1355
1394
  return
1356
1395
  }
1357
1396
 
@@ -1360,15 +1399,31 @@ async function handleStartContainer(containerName: string): Promise<void> {
1360
1399
  const spinner = createSpinner(`Starting ${containerName}...`)
1361
1400
  spinner.start()
1362
1401
 
1363
- await engine.start(config)
1364
- await containerManager.updateConfig(containerName, { status: 'running' })
1402
+ try {
1403
+ await engine.start(config)
1404
+ await containerManager.updateConfig(containerName, { status: 'running' })
1365
1405
 
1366
- spinner.succeed(`Container "${containerName}" started`)
1406
+ spinner.succeed(`Container "${containerName}" started`)
1367
1407
 
1368
- const connectionString = engine.getConnectionString(config)
1369
- console.log()
1370
- console.log(chalk.gray(' Connection string:'))
1371
- console.log(chalk.cyan(` ${connectionString}`))
1408
+ const connectionString = engine.getConnectionString(config)
1409
+ console.log()
1410
+ console.log(chalk.gray(' Connection string:'))
1411
+ console.log(chalk.cyan(` ${connectionString}`))
1412
+ } catch (err) {
1413
+ spinner.fail(`Failed to start "${containerName}"`)
1414
+ const e = err as Error
1415
+ console.log()
1416
+ console.log(error(e.message))
1417
+
1418
+ // Check if there's a log file with more details
1419
+ const logPath = paths.getContainerLogPath(containerName, {
1420
+ engine: config.engine,
1421
+ })
1422
+ if (existsSync(logPath)) {
1423
+ console.log()
1424
+ console.log(info(`Check the log file for details: ${logPath}`))
1425
+ }
1426
+ }
1372
1427
  }
1373
1428
 
1374
1429
  async function handleStopContainer(containerName: string): Promise<void> {
@@ -1566,7 +1621,9 @@ async function handleDelete(containerName: string): Promise<void> {
1566
1621
  return
1567
1622
  }
1568
1623
 
1569
- const isRunning = await processManager.isRunning(containerName)
1624
+ const isRunning = await processManager.isRunning(containerName, {
1625
+ engine: config.engine,
1626
+ })
1570
1627
 
1571
1628
  if (isRunning) {
1572
1629
  const stopSpinner = createSpinner(`Stopping ${containerName}...`)
@@ -76,8 +76,12 @@ export const restoreCommand = new Command('restore')
76
76
  process.exit(1)
77
77
  }
78
78
 
79
+ const { engine: engineName } = config
80
+
79
81
  // Check if running
80
- const running = await processManager.isRunning(containerName)
82
+ const running = await processManager.isRunning(containerName, {
83
+ engine: engineName,
84
+ })
81
85
  if (!running) {
82
86
  console.error(
83
87
  error(
@@ -88,7 +92,7 @@ export const restoreCommand = new Command('restore')
88
92
  }
89
93
 
90
94
  // Get engine
91
- const engine = getEngine(config.engine)
95
+ const engine = getEngine(engineName)
92
96
 
93
97
  // Check for required client tools BEFORE doing anything
94
98
  const depsSpinner = createSpinner('Checking required tools...')
@@ -129,14 +133,34 @@ export const restoreCommand = new Command('restore')
129
133
 
130
134
  // Handle --from-url option
131
135
  if (options.fromUrl) {
132
- // Validate connection string
133
- if (
134
- !options.fromUrl.startsWith('postgresql://') &&
135
- !options.fromUrl.startsWith('postgres://')
136
- ) {
136
+ // Validate connection string matches container's engine
137
+ const isPgUrl =
138
+ options.fromUrl.startsWith('postgresql://') ||
139
+ options.fromUrl.startsWith('postgres://')
140
+ const isMysqlUrl = options.fromUrl.startsWith('mysql://')
141
+
142
+ if (engineName === 'postgresql' && !isPgUrl) {
143
+ console.error(
144
+ error(
145
+ 'Connection string must start with postgresql:// or postgres:// for PostgreSQL containers',
146
+ ),
147
+ )
148
+ process.exit(1)
149
+ }
150
+
151
+ if (engineName === 'mysql' && !isMysqlUrl) {
137
152
  console.error(
138
153
  error(
139
- 'Connection string must start with postgresql:// or postgres://',
154
+ 'Connection string must start with mysql:// for MySQL containers',
155
+ ),
156
+ )
157
+ process.exit(1)
158
+ }
159
+
160
+ if (!isPgUrl && !isMysqlUrl) {
161
+ console.error(
162
+ error(
163
+ 'Connection string must start with postgresql://, postgres://, or mysql://',
140
164
  ),
141
165
  )
142
166
  process.exit(1)
@@ -167,11 +191,15 @@ export const restoreCommand = new Command('restore')
167
191
  dumpSpinner.fail('Failed to create dump')
168
192
 
169
193
  // Check if this is a missing tool error
194
+ const dumpTool = engineName === 'mysql' ? 'mysqldump' : 'pg_dump'
170
195
  if (
171
- e.message.includes('pg_dump not found') ||
196
+ e.message.includes(`${dumpTool} not found`) ||
172
197
  e.message.includes('ENOENT')
173
198
  ) {
174
- const installed = await promptInstallDependencies('pg_dump')
199
+ const installed = await promptInstallDependencies(
200
+ dumpTool,
201
+ engineName,
202
+ )
175
203
  if (!installed) {
176
204
  process.exit(1)
177
205
  }
@@ -180,7 +208,7 @@ export const restoreCommand = new Command('restore')
180
208
  }
181
209
 
182
210
  console.log()
183
- console.error(error('pg_dump error:'))
211
+ console.error(error(`${dumpTool} error:`))
184
212
  console.log(chalk.gray(` ${e.message}`))
185
213
  process.exit(1)
186
214
  }
@@ -313,14 +341,23 @@ export const restoreCommand = new Command('restore')
313
341
  } catch (err) {
314
342
  const e = err as Error
315
343
 
316
- // Check if this is a missing tool error
317
- if (
318
- e.message.includes('pg_restore not found') ||
319
- e.message.includes('psql not found')
320
- ) {
321
- const missingTool = e.message.includes('pg_restore')
322
- ? 'pg_restore'
323
- : 'psql'
344
+ // Check if this is a missing tool error (PostgreSQL or MySQL)
345
+ const missingToolPatterns = [
346
+ // PostgreSQL
347
+ 'pg_restore not found',
348
+ 'psql not found',
349
+ 'pg_dump not found',
350
+ // MySQL
351
+ 'mysql not found',
352
+ 'mysqldump not found',
353
+ ]
354
+
355
+ const matchingPattern = missingToolPatterns.find((p) =>
356
+ e.message.includes(p),
357
+ )
358
+
359
+ if (matchingPattern) {
360
+ const missingTool = matchingPattern.replace(' not found', '')
324
361
  const installed = await promptInstallDependencies(missingTool)
325
362
  if (installed) {
326
363
  console.log(
@@ -4,6 +4,7 @@ import { containerManager } from '../../core/container-manager'
4
4
  import { portManager } from '../../core/port-manager'
5
5
  import { processManager } from '../../core/process-manager'
6
6
  import { getEngine } from '../../engines'
7
+ import { getEngineDefaults } from '../../config/defaults'
7
8
  import { promptContainerSelect } from '../ui/prompts'
8
9
  import { createSpinner } from '../ui/spinner'
9
10
  import { error, warning } from '../ui/theme'
@@ -46,18 +47,27 @@ export const startCommand = new Command('start')
46
47
  process.exit(1)
47
48
  }
48
49
 
50
+ const { engine: engineName } = config
51
+
49
52
  // Check if already running
50
- const running = await processManager.isRunning(containerName)
53
+ const running = await processManager.isRunning(containerName, {
54
+ engine: engineName,
55
+ })
51
56
  if (running) {
52
57
  console.log(warning(`Container "${containerName}" is already running`))
53
58
  return
54
59
  }
55
60
 
61
+ // Get engine defaults for port range and database name
62
+ const engineDefaults = getEngineDefaults(engineName)
63
+
56
64
  // Check port availability
57
65
  const portAvailable = await portManager.isPortAvailable(config.port)
58
66
  if (!portAvailable) {
59
- // Try to find a new port
60
- const { port: newPort } = await portManager.findAvailablePort()
67
+ // Try to find a new port (using engine-specific port range)
68
+ const { port: newPort } = await portManager.findAvailablePort({
69
+ portRange: engineDefaults.portRange,
70
+ })
61
71
  console.log(
62
72
  warning(
63
73
  `Port ${config.port} is in use, switching to port ${newPort}`,
@@ -68,7 +78,7 @@ export const startCommand = new Command('start')
68
78
  }
69
79
 
70
80
  // Get engine and start
71
- const engine = getEngine(config.engine)
81
+ const engine = getEngine(engineName)
72
82
 
73
83
  const spinner = createSpinner(`Starting ${containerName}...`)
74
84
  spinner.start()
@@ -78,6 +88,22 @@ export const startCommand = new Command('start')
78
88
 
79
89
  spinner.succeed(`Container "${containerName}" started`)
80
90
 
91
+ // Ensure the user's database exists (if different from default)
92
+ const defaultDb = engineDefaults.superuser // postgres or root
93
+ if (config.database && config.database !== defaultDb) {
94
+ const dbSpinner = createSpinner(
95
+ `Ensuring database "${config.database}" exists...`,
96
+ )
97
+ dbSpinner.start()
98
+ try {
99
+ await engine.createDatabase(config, config.database)
100
+ dbSpinner.succeed(`Database "${config.database}" ready`)
101
+ } catch {
102
+ // Database might already exist, which is fine
103
+ dbSpinner.succeed(`Database "${config.database}" ready`)
104
+ }
105
+ }
106
+
81
107
  // Show connection info
82
108
  const connectionString = engine.getConnectionString(config)
83
109
  console.log()
@@ -67,7 +67,9 @@ export const stopCommand = new Command('stop')
67
67
  }
68
68
 
69
69
  // Check if running
70
- const running = await processManager.isRunning(containerName)
70
+ const running = await processManager.isRunning(containerName, {
71
+ engine: config.engine,
72
+ })
71
73
  if (!running) {
72
74
  console.log(warning(`Container "${containerName}" is not running`))
73
75
  return