spindb 0.9.0 → 0.9.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.
Files changed (49) hide show
  1. package/README.md +7 -0
  2. package/cli/commands/backup.ts +13 -11
  3. package/cli/commands/clone.ts +18 -8
  4. package/cli/commands/config.ts +29 -29
  5. package/cli/commands/connect.ts +51 -39
  6. package/cli/commands/create.ts +120 -43
  7. package/cli/commands/delete.ts +8 -8
  8. package/cli/commands/deps.ts +17 -15
  9. package/cli/commands/doctor.ts +16 -15
  10. package/cli/commands/edit.ts +115 -60
  11. package/cli/commands/engines.ts +50 -17
  12. package/cli/commands/info.ts +12 -8
  13. package/cli/commands/list.ts +34 -19
  14. package/cli/commands/logs.ts +24 -14
  15. package/cli/commands/menu/backup-handlers.ts +72 -49
  16. package/cli/commands/menu/container-handlers.ts +140 -80
  17. package/cli/commands/menu/engine-handlers.ts +145 -11
  18. package/cli/commands/menu/index.ts +4 -4
  19. package/cli/commands/menu/shell-handlers.ts +34 -31
  20. package/cli/commands/menu/sql-handlers.ts +22 -16
  21. package/cli/commands/menu/update-handlers.ts +19 -17
  22. package/cli/commands/restore.ts +105 -43
  23. package/cli/commands/run.ts +20 -18
  24. package/cli/commands/self-update.ts +5 -5
  25. package/cli/commands/start.ts +11 -9
  26. package/cli/commands/stop.ts +9 -9
  27. package/cli/commands/url.ts +12 -9
  28. package/cli/helpers.ts +49 -4
  29. package/cli/ui/prompts.ts +21 -8
  30. package/cli/ui/spinner.ts +4 -4
  31. package/cli/ui/theme.ts +4 -4
  32. package/core/binary-manager.ts +5 -1
  33. package/core/container-manager.ts +81 -30
  34. package/core/error-handler.ts +31 -0
  35. package/core/platform-service.ts +3 -3
  36. package/core/port-manager.ts +2 -0
  37. package/core/process-manager.ts +25 -3
  38. package/core/start-with-retry.ts +6 -6
  39. package/core/transaction-manager.ts +6 -6
  40. package/engines/mysql/backup.ts +53 -36
  41. package/engines/mysql/index.ts +59 -16
  42. package/engines/mysql/restore.ts +4 -4
  43. package/engines/mysql/version-validator.ts +2 -2
  44. package/engines/postgresql/binary-manager.ts +17 -17
  45. package/engines/postgresql/index.ts +13 -2
  46. package/engines/postgresql/restore.ts +2 -2
  47. package/engines/postgresql/version-validator.ts +2 -2
  48. package/engines/sqlite/index.ts +31 -9
  49. package/package.json +1 -1
@@ -13,16 +13,17 @@ import {
13
13
  promptConfirm,
14
14
  } from '../ui/prompts'
15
15
  import { createSpinner } from '../ui/spinner'
16
- import { header, error, connectionBox } from '../ui/theme'
16
+ import { header, uiError, connectionBox } from '../ui/theme'
17
17
  import { tmpdir } from 'os'
18
18
  import { join } from 'path'
19
19
  import { getMissingDependencies } from '../../core/dependency-manager'
20
20
  import { platformService } from '../../core/platform-service'
21
21
  import { startWithRetry } from '../../core/start-with-retry'
22
22
  import { TransactionManager } from '../../core/transaction-manager'
23
+ import { isValidDatabaseName } from '../../core/error-handler'
24
+ import { resolve } from 'path'
23
25
  import { Engine } from '../../types'
24
26
  import type { BaseEngine } from '../../engines/base-engine'
25
- import { resolve } from 'path'
26
27
 
27
28
  /**
28
29
  * Simplified SQLite container creation flow
@@ -32,9 +33,9 @@ async function createSqliteContainer(
32
33
  containerName: string,
33
34
  dbEngine: BaseEngine,
34
35
  version: string,
35
- options: { path?: string; from?: string | null },
36
+ options: { path?: string; from?: string | null; connect?: boolean },
36
37
  ): Promise<void> {
37
- const { path: filePath, from: restoreLocation } = options
38
+ const { path: filePath, from: restoreLocation, connect } = options
38
39
 
39
40
  // Check dependencies
40
41
  const depsSpinner = createSpinner('Checking required tools...')
@@ -42,8 +43,13 @@ async function createSqliteContainer(
42
43
 
43
44
  const missingDeps = await getMissingDependencies('sqlite')
44
45
  if (missingDeps.length > 0) {
45
- depsSpinner.warn(`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`)
46
- const installed = await promptInstallDependencies(missingDeps[0].binary, 'sqlite')
46
+ depsSpinner.warn(
47
+ `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
48
+ )
49
+ const installed = await promptInstallDependencies(
50
+ missingDeps[0].binary,
51
+ 'sqlite',
52
+ )
47
53
  if (!installed) {
48
54
  process.exit(1)
49
55
  }
@@ -63,7 +69,7 @@ async function createSqliteContainer(
63
69
 
64
70
  // Check if file already exists
65
71
  if (existsSync(absolutePath)) {
66
- console.error(error(`File already exists: ${absolutePath}`))
72
+ console.error(uiError(`File already exists: ${absolutePath}`))
67
73
  process.exit(1)
68
74
  }
69
75
 
@@ -74,9 +80,9 @@ async function createSqliteContainer(
74
80
  // Initialize the SQLite database file and register in registry
75
81
  await dbEngine.initDataDir(containerName, version, { path: absolutePath })
76
82
  createSpinnerInstance.succeed('SQLite database created')
77
- } catch (err) {
83
+ } catch (error) {
78
84
  createSpinnerInstance.fail('Failed to create SQLite database')
79
- throw err
85
+ throw error
80
86
  }
81
87
 
82
88
  // Handle --from restore
@@ -84,15 +90,17 @@ async function createSqliteContainer(
84
90
  const config = await containerManager.getConfig(containerName)
85
91
  if (config) {
86
92
  const format = await dbEngine.detectBackupFormat(restoreLocation)
87
- const restoreSpinner = createSpinner(`Restoring from ${format.description}...`)
93
+ const restoreSpinner = createSpinner(
94
+ `Restoring from ${format.description}...`,
95
+ )
88
96
  restoreSpinner.start()
89
97
 
90
98
  try {
91
99
  await dbEngine.restore(config, restoreLocation)
92
100
  restoreSpinner.succeed('Backup restored successfully')
93
- } catch (err) {
101
+ } catch (error) {
94
102
  restoreSpinner.fail('Failed to restore backup')
95
- throw err
103
+ throw error
96
104
  }
97
105
  }
98
106
  }
@@ -107,9 +115,20 @@ async function createSqliteContainer(
107
115
  console.log(chalk.gray(' Connection string:'))
108
116
  console.log(chalk.cyan(` sqlite:///${absolutePath}`))
109
117
  console.log()
110
- console.log(chalk.gray(' Connect with:'))
111
- console.log(chalk.cyan(` spindb connect ${containerName}`))
112
- console.log()
118
+
119
+ // Connect if requested
120
+ if (connect) {
121
+ const config = await containerManager.getConfig(containerName)
122
+ if (config) {
123
+ console.log(chalk.gray(' Opening shell...'))
124
+ console.log()
125
+ await dbEngine.connect(config)
126
+ }
127
+ } else {
128
+ console.log(chalk.gray(' Connect with:'))
129
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
130
+ console.log()
131
+ }
113
132
  }
114
133
 
115
134
  function detectLocationType(location: string): {
@@ -132,8 +151,13 @@ function detectLocationType(location: string): {
132
151
  }
133
152
 
134
153
  if (existsSync(location)) {
135
- // Check if it's a SQLite file
136
- if (location.endsWith('.sqlite') || location.endsWith('.db') || location.endsWith('.sqlite3')) {
154
+ // Check if it's a SQLite file (case-insensitive)
155
+ const lowerLocation = location.toLowerCase()
156
+ if (
157
+ lowerLocation.endsWith('.sqlite') ||
158
+ lowerLocation.endsWith('.db') ||
159
+ lowerLocation.endsWith('.sqlite3')
160
+ ) {
137
161
  return { type: 'file', inferredEngine: Engine.SQLite }
138
162
  }
139
163
  return { type: 'file' }
@@ -145,7 +169,10 @@ function detectLocationType(location: string): {
145
169
  export const createCommand = new Command('create')
146
170
  .description('Create a new database container')
147
171
  .argument('[name]', 'Container name')
148
- .option('-e, --engine <engine>', 'Database engine (postgresql, mysql, sqlite)')
172
+ .option(
173
+ '-e, --engine <engine>',
174
+ 'Database engine (postgresql, mysql, sqlite)',
175
+ )
149
176
  .option('-v, --version <version>', 'Database version')
150
177
  .option('-d, --database <database>', 'Database name')
151
178
  .option('-p, --port <port>', 'Port number')
@@ -157,7 +184,9 @@ export const createCommand = new Command('create')
157
184
  '--max-connections <number>',
158
185
  'Maximum number of database connections (default: 200)',
159
186
  )
187
+ .option('--start', 'Start the container after creation (skip prompt)')
160
188
  .option('--no-start', 'Do not start the container after creation')
189
+ .option('--connect', 'Open a shell connection after creation')
161
190
  .option(
162
191
  '--from <location>',
163
192
  'Restore from a dump file or connection string after creation',
@@ -172,7 +201,8 @@ export const createCommand = new Command('create')
172
201
  port?: string
173
202
  path?: string
174
203
  maxConnections?: string
175
- start: boolean
204
+ start?: boolean
205
+ connect?: boolean
176
206
  from?: string
177
207
  },
178
208
  ) => {
@@ -191,7 +221,7 @@ export const createCommand = new Command('create')
191
221
  const locationInfo = detectLocationType(options.from)
192
222
 
193
223
  if (locationInfo.type === 'not_found') {
194
- console.error(error(`Location not found: ${options.from}`))
224
+ console.error(uiError(`Location not found: ${options.from}`))
195
225
  console.log(
196
226
  chalk.gray(
197
227
  ' Provide a valid file path or connection string (postgresql://, mysql://)',
@@ -214,7 +244,7 @@ export const createCommand = new Command('create')
214
244
 
215
245
  if (options.start === false) {
216
246
  console.error(
217
- error(
247
+ uiError(
218
248
  'Cannot use --no-start with --from (restore requires running container)',
219
249
  ),
220
250
  )
@@ -238,6 +268,16 @@ export const createCommand = new Command('create')
238
268
 
239
269
  database = database ?? containerName
240
270
 
271
+ // Validate database name to prevent SQL injection
272
+ if (!isValidDatabaseName(database)) {
273
+ console.error(
274
+ uiError(
275
+ 'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
276
+ ),
277
+ )
278
+ process.exit(1)
279
+ }
280
+
241
281
  console.log(header('Creating Database Container'))
242
282
  console.log()
243
283
 
@@ -248,10 +288,34 @@ export const createCommand = new Command('create')
248
288
  await createSqliteContainer(containerName, dbEngine, version, {
249
289
  path: options.path,
250
290
  from: restoreLocation,
291
+ connect: options.connect,
251
292
  })
252
293
  return
253
294
  }
254
295
 
296
+ // For server databases, validate --connect with --no-start
297
+ if (options.connect && options.start === false) {
298
+ console.error(
299
+ uiError(
300
+ 'Cannot use --no-start with --connect (connection requires running container)',
301
+ ),
302
+ )
303
+ process.exit(1)
304
+ }
305
+
306
+ // Validate --max-connections if provided
307
+ if (options.maxConnections) {
308
+ const parsed = parseInt(options.maxConnections, 10)
309
+ if (!Number.isFinite(parsed) || parsed <= 0) {
310
+ console.error(
311
+ uiError(
312
+ 'Invalid --max-connections value: must be a positive integer',
313
+ ),
314
+ )
315
+ process.exit(1)
316
+ }
317
+ }
318
+
255
319
  const depsSpinner = createSpinner('Checking required tools...')
256
320
  depsSpinner.start()
257
321
 
@@ -273,7 +337,7 @@ export const createCommand = new Command('create')
273
337
  missingDeps = await getMissingDependencies(engine)
274
338
  if (missingDeps.length > 0) {
275
339
  console.error(
276
- error(
340
+ uiError(
277
341
  `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
278
342
  ),
279
343
  )
@@ -362,9 +426,9 @@ export const createCommand = new Command('create')
362
426
  })
363
427
 
364
428
  createSpinnerInstance.succeed('Container created')
365
- } catch (err) {
429
+ } catch (error) {
366
430
  createSpinnerInstance.fail('Failed to create container')
367
- throw err
431
+ throw error
368
432
  }
369
433
 
370
434
  const initSpinner = createSpinner('Initializing database cluster...')
@@ -378,15 +442,18 @@ export const createCommand = new Command('create')
378
442
  : undefined,
379
443
  })
380
444
  initSpinner.succeed('Database cluster initialized')
381
- } catch (err) {
445
+ } catch (error) {
382
446
  initSpinner.fail('Failed to initialize database cluster')
383
447
  await tx.rollback()
384
- throw err
448
+ throw error
385
449
  }
386
450
 
387
- // --from requires start, --no-start skips, otherwise ask user
451
+ // --from requires start, --start forces start, --no-start skips, otherwise ask user
452
+ // --connect implies --start for server databases
388
453
  let shouldStart = false
389
- if (restoreLocation) {
454
+ if (restoreLocation || options.connect) {
455
+ shouldStart = true
456
+ } else if (options.start === true) {
390
457
  shouldStart = true
391
458
  } else if (options.start === false) {
392
459
  shouldStart = false
@@ -444,14 +511,14 @@ export const createCommand = new Command('create')
444
511
  } else {
445
512
  startSpinner.succeed(`${dbEngine.displayName} started`)
446
513
  }
447
- } catch (err) {
514
+ } catch (error) {
448
515
  if (!startSpinner.isSpinning) {
449
516
  // Error was already handled above
450
517
  } else {
451
518
  startSpinner.fail(`Failed to start ${dbEngine.displayName}`)
452
519
  }
453
520
  await tx.rollback()
454
- throw err
521
+ throw error
455
522
  }
456
523
 
457
524
  const defaultDb = engineDefaults.superuser
@@ -464,10 +531,10 @@ export const createCommand = new Command('create')
464
531
  try {
465
532
  await dbEngine.createDatabase(config, database)
466
533
  dbSpinner.succeed(`Database "${database}" created`)
467
- } catch (err) {
534
+ } catch (error) {
468
535
  dbSpinner.fail(`Failed to create database "${database}"`)
469
536
  await tx.rollback()
470
- throw err
537
+ throw error
471
538
  }
472
539
  }
473
540
  }
@@ -498,8 +565,8 @@ export const createCommand = new Command('create')
498
565
  dumpSpinner.succeed('Dump created from remote database')
499
566
  backupPath = tempDumpPath
500
567
  dumpSuccess = true
501
- } catch (err) {
502
- const e = err as Error
568
+ } catch (error) {
569
+ const e = error as Error
503
570
  dumpSpinner.fail('Failed to create dump')
504
571
 
505
572
  if (
@@ -514,14 +581,14 @@ export const createCommand = new Command('create')
514
581
  }
515
582
 
516
583
  console.log()
517
- console.error(error('pg_dump error:'))
584
+ console.error(uiError('pg_dump error:'))
518
585
  console.log(chalk.gray(` ${e.message}`))
519
586
  process.exit(1)
520
587
  }
521
588
  }
522
589
 
523
590
  if (!dumpSuccess) {
524
- console.error(error('Failed to create dump after retries'))
591
+ console.error(uiError('Failed to create dump after retries'))
525
592
  process.exit(1)
526
593
  }
527
594
  } else {
@@ -542,7 +609,7 @@ export const createCommand = new Command('create')
542
609
  createDatabase: false,
543
610
  })
544
611
 
545
- if (result.code === 0 || !result.stderr) {
612
+ if (result.code === 0) {
546
613
  restoreSpinner.succeed('Backup restored successfully')
547
614
  } else {
548
615
  restoreSpinner.warn('Restore completed with warnings')
@@ -573,7 +640,17 @@ export const createCommand = new Command('create')
573
640
  )
574
641
  console.log()
575
642
 
576
- if (shouldStart) {
643
+ if (options.connect && shouldStart) {
644
+ // --connect flag: open shell directly
645
+ const copied =
646
+ await platformService.copyToClipboard(connectionString)
647
+ if (copied) {
648
+ console.log(chalk.gray(' Connection string copied to clipboard'))
649
+ }
650
+ console.log(chalk.gray(' Opening shell...'))
651
+ console.log()
652
+ await dbEngine.connect(finalConfig, database)
653
+ } else if (shouldStart) {
577
654
  console.log(chalk.gray(' Connect with:'))
578
655
  console.log(chalk.cyan(` spindb connect ${containerName}`))
579
656
 
@@ -582,15 +659,15 @@ export const createCommand = new Command('create')
582
659
  if (copied) {
583
660
  console.log(chalk.gray(' Connection string copied to clipboard'))
584
661
  }
662
+ console.log()
585
663
  } else {
586
664
  console.log(chalk.gray(' Start the container:'))
587
665
  console.log(chalk.cyan(` spindb start ${containerName}`))
666
+ console.log()
588
667
  }
589
-
590
- console.log()
591
668
  }
592
- } catch (err) {
593
- const e = err as Error
669
+ } catch (error) {
670
+ const e = error as Error
594
671
 
595
672
  const missingToolPatterns = [
596
673
  'pg_restore not found',
@@ -616,7 +693,7 @@ export const createCommand = new Command('create')
616
693
  process.exit(1)
617
694
  }
618
695
 
619
- console.error(error(e.message))
696
+ console.error(uiError(e.message))
620
697
  process.exit(1)
621
698
  } finally {
622
699
  if (tempDumpPath) {
@@ -4,7 +4,7 @@ import { processManager } from '../../core/process-manager'
4
4
  import { getEngine } from '../../engines'
5
5
  import { promptContainerSelect, promptConfirm } from '../ui/prompts'
6
6
  import { createSpinner } from '../ui/spinner'
7
- import { error, warning } from '../ui/theme'
7
+ import { uiError, uiWarning } from '../ui/theme'
8
8
 
9
9
  export const deleteCommand = new Command('delete')
10
10
  .alias('rm')
@@ -24,7 +24,7 @@ export const deleteCommand = new Command('delete')
24
24
  const containers = await containerManager.list()
25
25
 
26
26
  if (containers.length === 0) {
27
- console.log(warning('No containers found'))
27
+ console.log(uiWarning('No containers found'))
28
28
  return
29
29
  }
30
30
 
@@ -38,7 +38,7 @@ export const deleteCommand = new Command('delete')
38
38
 
39
39
  const config = await containerManager.getConfig(containerName)
40
40
  if (!config) {
41
- console.error(error(`Container "${containerName}" not found`))
41
+ console.error(uiError(`Container "${containerName}" not found`))
42
42
  process.exit(1)
43
43
  }
44
44
 
@@ -48,7 +48,7 @@ export const deleteCommand = new Command('delete')
48
48
  false,
49
49
  )
50
50
  if (!confirmed) {
51
- console.log(warning('Deletion cancelled'))
51
+ console.log(uiWarning('Deletion cancelled'))
52
52
  return
53
53
  }
54
54
  }
@@ -67,7 +67,7 @@ export const deleteCommand = new Command('delete')
67
67
  stopSpinner.succeed(`Stopped "${containerName}"`)
68
68
  } else {
69
69
  console.error(
70
- error(
70
+ uiError(
71
71
  `Container "${containerName}" is running. Stop it first or use --force`,
72
72
  ),
73
73
  )
@@ -81,9 +81,9 @@ export const deleteCommand = new Command('delete')
81
81
  await containerManager.delete(containerName, { force: true })
82
82
 
83
83
  deleteSpinner.succeed(`Container "${containerName}" deleted`)
84
- } catch (err) {
85
- const e = err as Error
86
- console.error(error(e.message))
84
+ } catch (error) {
85
+ const e = error as Error
86
+ console.error(uiError(e.message))
87
87
  process.exit(1)
88
88
  }
89
89
  },
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
- import { header, success, warning, error } from '../ui/theme'
3
+ import { header, uiSuccess, uiWarning, uiError } from '../ui/theme'
4
4
  import { createSpinner } from '../ui/spinner'
5
5
  import {
6
6
  detectPackageManager,
@@ -84,7 +84,7 @@ depsCommand
84
84
  // Check specific engine
85
85
  const engineConfig = getEngineDependencies(options.engine)
86
86
  if (!engineConfig) {
87
- console.error(error(`Unknown engine: ${options.engine}`))
87
+ console.error(uiError(`Unknown engine: ${options.engine}`))
88
88
  console.log(
89
89
  chalk.gray(
90
90
  ` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
@@ -104,9 +104,9 @@ depsCommand
104
104
  const total = statuses.length
105
105
  console.log()
106
106
  if (installed === total) {
107
- console.log(success(`All ${total} dependencies installed`))
107
+ console.log(uiSuccess(`All ${total} dependencies installed`))
108
108
  } else {
109
- console.log(warning(`${installed}/${total} dependencies installed`))
109
+ console.log(uiWarning(`${installed}/${total} dependencies installed`))
110
110
  console.log()
111
111
  console.log(
112
112
  chalk.gray(` Run: spindb deps install --engine ${options.engine}`),
@@ -132,7 +132,7 @@ depsCommand
132
132
  const packageManager = await detectPackageManager()
133
133
 
134
134
  if (!packageManager) {
135
- console.log(error('No supported package manager detected'))
135
+ console.log(uiError('No supported package manager detected'))
136
136
  console.log()
137
137
 
138
138
  const platform = getCurrentPlatform()
@@ -161,7 +161,7 @@ depsCommand
161
161
  const missing = await getAllMissingDependencies()
162
162
 
163
163
  if (missing.length === 0) {
164
- console.log(success('All dependencies are already installed'))
164
+ console.log(uiSuccess('All dependencies are already installed'))
165
165
  return
166
166
  }
167
167
 
@@ -182,14 +182,14 @@ depsCommand
182
182
  spinner.warn('Some dependencies failed to install')
183
183
  console.log()
184
184
  for (const f of failed) {
185
- console.log(error(` ${f.dependency.name}: ${f.error}`))
185
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
186
186
  }
187
187
  }
188
188
 
189
189
  if (succeeded.length > 0) {
190
190
  console.log()
191
191
  console.log(
192
- success(
192
+ uiSuccess(
193
193
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
194
194
  ),
195
195
  )
@@ -198,7 +198,7 @@ depsCommand
198
198
  // Install dependencies for specific engine
199
199
  const engineConfig = getEngineDependencies(options.engine)
200
200
  if (!engineConfig) {
201
- console.error(error(`Unknown engine: ${options.engine}`))
201
+ console.error(uiError(`Unknown engine: ${options.engine}`))
202
202
  console.log(
203
203
  chalk.gray(
204
204
  ` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
@@ -211,7 +211,9 @@ depsCommand
211
211
 
212
212
  if (missing.length === 0) {
213
213
  console.log(
214
- success(`All ${engineConfig.displayName} dependencies are installed`),
214
+ uiSuccess(
215
+ `All ${engineConfig.displayName} dependencies are installed`,
216
+ ),
215
217
  )
216
218
  return
217
219
  }
@@ -241,7 +243,7 @@ depsCommand
241
243
  spinner.warn('Some dependencies failed to install')
242
244
  console.log()
243
245
  for (const f of failed) {
244
- console.log(error(` ${f.dependency.name}: ${f.error}`))
246
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
245
247
  }
246
248
 
247
249
  // Show manual instructions
@@ -259,7 +261,7 @@ depsCommand
259
261
  if (succeeded.length > 0) {
260
262
  console.log()
261
263
  console.log(
262
- success(
264
+ uiSuccess(
263
265
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
264
266
  ),
265
267
  )
@@ -276,7 +278,7 @@ depsCommand
276
278
  const missing = await getMissingDependencies('postgresql')
277
279
 
278
280
  if (missing.length === 0) {
279
- console.log(success('All PostgreSQL dependencies are installed'))
281
+ console.log(uiSuccess('All PostgreSQL dependencies are installed'))
280
282
  return
281
283
  }
282
284
 
@@ -300,14 +302,14 @@ depsCommand
300
302
  spinner.warn('Some dependencies failed to install')
301
303
  console.log()
302
304
  for (const f of failed) {
303
- console.log(error(` ${f.dependency.name}: ${f.error}`))
305
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
304
306
  }
305
307
  }
306
308
 
307
309
  if (succeeded.length > 0) {
308
310
  console.log()
309
311
  console.log(
310
- success(
312
+ uiSuccess(
311
313
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
312
314
  ),
313
315
  )
@@ -18,7 +18,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
18
18
  import { paths } from '../../config/paths'
19
19
  import { getSupportedEngines } from '../../config/engine-defaults'
20
20
  import { checkEngineDependencies } from '../../core/dependency-manager'
21
- import { header, success } from '../ui/theme'
21
+ import { header, uiSuccess } from '../ui/theme'
22
22
  import { Engine } from '../../types'
23
23
 
24
24
  type HealthCheckResult = {
@@ -61,7 +61,7 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
61
61
  label: 'Refresh binary cache',
62
62
  handler: async () => {
63
63
  await configManager.refreshAllBinaries()
64
- console.log(success('Binary cache refreshed'))
64
+ console.log(uiSuccess('Binary cache refreshed'))
65
65
  },
66
66
  },
67
67
  }
@@ -73,12 +73,12 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
73
73
  message: 'Configuration valid',
74
74
  details: [`Binary tools cached: ${binaryCount}`],
75
75
  }
76
- } catch (err) {
76
+ } catch (error) {
77
77
  return {
78
78
  name: 'Configuration',
79
79
  status: 'error',
80
80
  message: 'Configuration file is corrupted',
81
- details: [(err as Error).message],
81
+ details: [(error as Error).message],
82
82
  }
83
83
  }
84
84
  }
@@ -125,12 +125,12 @@ async function checkContainers(): Promise<HealthCheckResult> {
125
125
  message: `${containers.length} container(s)`,
126
126
  details,
127
127
  }
128
- } catch (err) {
128
+ } catch (error) {
129
129
  return {
130
130
  name: 'Containers',
131
131
  status: 'error',
132
132
  message: 'Failed to list containers',
133
- details: [(err as Error).message],
133
+ details: [(error as Error).message],
134
134
  }
135
135
  }
136
136
  }
@@ -162,7 +162,7 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
162
162
  label: 'Remove orphaned entries from registry',
163
163
  handler: async () => {
164
164
  const count = await sqliteRegistry.removeOrphans()
165
- console.log(success(`Removed ${count} orphaned entries`))
165
+ console.log(uiSuccess(`Removed ${count} orphaned entries`))
166
166
  },
167
167
  },
168
168
  }
@@ -173,12 +173,12 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
173
173
  status: 'ok',
174
174
  message: `${entries.length} database(s) registered, all files exist`,
175
175
  }
176
- } catch (err) {
176
+ } catch (error) {
177
177
  return {
178
178
  name: 'SQLite Registry',
179
179
  status: 'warning',
180
180
  message: 'Could not check registry',
181
- details: [(err as Error).message],
181
+ details: [(error as Error).message],
182
182
  }
183
183
  }
184
184
  }
@@ -211,12 +211,12 @@ async function checkBinaries(): Promise<HealthCheckResult> {
211
211
  message: hasWarning ? 'Some tools missing' : 'All tools available',
212
212
  details: results,
213
213
  }
214
- } catch (err) {
214
+ } catch (error) {
215
215
  return {
216
216
  name: 'Database Tools',
217
217
  status: 'error',
218
218
  message: 'Failed to check tools',
219
- details: [(err as Error).message],
219
+ details: [(error as Error).message],
220
220
  }
221
221
  }
222
222
  }
@@ -247,10 +247,6 @@ export const doctorCommand = new Command('doctor')
247
247
  .description('Check system health and fix common issues')
248
248
  .option('--json', 'Output as JSON')
249
249
  .action(async (options: { json?: boolean }) => {
250
- console.log()
251
- console.log(header('SpinDB Health Check'))
252
- console.log()
253
-
254
250
  const checks = [
255
251
  await checkConfiguration(),
256
252
  await checkContainers(),
@@ -265,6 +261,11 @@ export const doctorCommand = new Command('doctor')
265
261
  return
266
262
  }
267
263
 
264
+ // Human-readable output - print header first
265
+ console.log()
266
+ console.log(header('SpinDB Health Check'))
267
+ console.log()
268
+
268
269
  // Display results
269
270
  for (const check of checks) {
270
271
  displayResult(check)