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
@@ -1,7 +1,14 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import inquirer from 'inquirer'
4
- import { existsSync, renameSync, mkdirSync, statSync, unlinkSync, copyFileSync } from 'fs'
4
+ import {
5
+ existsSync,
6
+ renameSync,
7
+ mkdirSync,
8
+ statSync,
9
+ unlinkSync,
10
+ copyFileSync,
11
+ } from 'fs'
5
12
  import { dirname, resolve, basename, join } from 'path'
6
13
  import { homedir } from 'os'
7
14
  import { containerManager } from '../../core/container-manager'
@@ -12,7 +19,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
12
19
  import { paths } from '../../config/paths'
13
20
  import { promptContainerSelect } from '../ui/prompts'
14
21
  import { createSpinner } from '../ui/spinner'
15
- import { error, warning, success, info } from '../ui/theme'
22
+ import { uiError, uiWarning, uiSuccess, uiInfo } from '../ui/theme'
16
23
  import { Engine } from '../../types'
17
24
 
18
25
  function isValidName(name: string): boolean {
@@ -25,9 +32,7 @@ function isValidName(name: string): boolean {
25
32
  async function promptEditAction(
26
33
  engine: string,
27
34
  ): Promise<'name' | 'port' | 'config' | 'relocate' | null> {
28
- const choices = [
29
- { name: 'Rename container', value: 'name' },
30
- ]
35
+ const choices = [{ name: 'Rename container', value: 'name' }]
31
36
 
32
37
  // SQLite: show relocate instead of port
33
38
  if (engine === Engine.SQLite) {
@@ -37,8 +42,11 @@ async function promptEditAction(
37
42
  }
38
43
 
39
44
  // Only show config option for engines that support it
40
- if (engine === 'postgresql') {
41
- choices.push({ name: 'Edit database config (postgresql.conf)', value: 'config' })
45
+ if (engine === Engine.PostgreSQL) {
46
+ choices.push({
47
+ name: 'Edit database config (postgresql.conf)',
48
+ value: 'config',
49
+ })
42
50
  }
43
51
 
44
52
  choices.push({ name: chalk.gray('Cancel'), value: 'cancel' })
@@ -74,7 +82,7 @@ async function promptNewName(currentName: string): Promise<string | null> {
74
82
  ])
75
83
 
76
84
  if (newName === currentName) {
77
- console.log(warning('Name unchanged'))
85
+ console.log(uiWarning('Name unchanged'))
78
86
  return null
79
87
  }
80
88
 
@@ -83,17 +91,36 @@ async function promptNewName(currentName: string): Promise<string | null> {
83
91
 
84
92
  // Common PostgreSQL config settings that users might want to edit
85
93
  const COMMON_PG_SETTINGS = [
86
- { name: 'max_connections', description: 'Maximum concurrent connections', default: '200' },
87
- { name: 'shared_buffers', description: 'Memory for shared buffers', default: '128MB' },
94
+ {
95
+ name: 'max_connections',
96
+ description: 'Maximum concurrent connections',
97
+ default: '200',
98
+ },
99
+ {
100
+ name: 'shared_buffers',
101
+ description: 'Memory for shared buffers',
102
+ default: '128MB',
103
+ },
88
104
  { name: 'work_mem', description: 'Memory per operation', default: '4MB' },
89
- { name: 'maintenance_work_mem', description: 'Memory for maintenance ops', default: '64MB' },
90
- { name: 'effective_cache_size', description: 'Planner cache size estimate', default: '4GB' },
105
+ {
106
+ name: 'maintenance_work_mem',
107
+ description: 'Memory for maintenance ops',
108
+ default: '64MB',
109
+ },
110
+ {
111
+ name: 'effective_cache_size',
112
+ description: 'Planner cache size estimate',
113
+ default: '4GB',
114
+ },
91
115
  ]
92
116
 
93
117
  /**
94
118
  * Prompt for PostgreSQL config setting to edit
95
119
  */
96
- async function promptConfigSetting(): Promise<{ key: string; value: string } | null> {
120
+ async function promptConfigSetting(): Promise<{
121
+ key: string
122
+ value: string
123
+ } | null> {
97
124
  const choices = COMMON_PG_SETTINGS.map((s) => ({
98
125
  name: `${s.name.padEnd(25)} ${chalk.gray(s.description)}`,
99
126
  value: s.name,
@@ -121,7 +148,8 @@ async function promptConfigSetting(): Promise<{ key: string; value: string } | n
121
148
  message: 'Setting name:',
122
149
  validate: (input: string) => {
123
150
  if (!input.trim()) return 'Setting name is required'
124
- if (!/^[a-z_]+$/.test(input)) return 'Setting names are lowercase with underscores'
151
+ if (!/^[a-z_]+$/.test(input))
152
+ return 'Setting names are lowercase with underscores'
125
153
  return true
126
154
  },
127
155
  },
@@ -129,7 +157,8 @@ async function promptConfigSetting(): Promise<{ key: string; value: string } | n
129
157
  key = customKey
130
158
  }
131
159
 
132
- const defaultValue = COMMON_PG_SETTINGS.find((s) => s.name === key)?.default || ''
160
+ const defaultValue =
161
+ COMMON_PG_SETTINGS.find((s) => s.name === key)?.default || ''
133
162
  const { value } = await inquirer.prompt<{ value: string }>([
134
163
  {
135
164
  type: 'input',
@@ -168,14 +197,14 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
168
197
  ])
169
198
 
170
199
  if (newPort === currentPort) {
171
- console.log(warning('Port unchanged'))
200
+ console.log(uiWarning('Port unchanged'))
172
201
  return null
173
202
  }
174
203
 
175
204
  const portAvailable = await portManager.isPortAvailable(newPort)
176
205
  if (!portAvailable) {
177
206
  console.log(
178
- warning(
207
+ uiWarning(
179
208
  `Note: Port ${newPort} is currently in use. It will be used when the container starts.`,
180
209
  ),
181
210
  )
@@ -190,7 +219,9 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
190
219
  async function promptNewLocation(currentPath: string): Promise<string | null> {
191
220
  console.log()
192
221
  console.log(chalk.gray(` Current location: ${currentPath}`))
193
- console.log(chalk.gray(' Enter an absolute path or relative to current directory.'))
222
+ console.log(
223
+ chalk.gray(' Enter an absolute path or relative to current directory.'),
224
+ )
194
225
  console.log()
195
226
 
196
227
  const { newPath } = await inquirer.prompt<{ newPath: string }>([
@@ -201,8 +232,12 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
201
232
  default: currentPath,
202
233
  validate: (input: string) => {
203
234
  if (!input.trim()) return 'Path is required'
204
- const resolvedPath = resolve(input)
205
- if (!resolvedPath.endsWith('.sqlite') && !resolvedPath.endsWith('.db') && !resolvedPath.endsWith('.sqlite3')) {
235
+ const resolvedPath = resolve(input).toLowerCase()
236
+ if (
237
+ !resolvedPath.endsWith('.sqlite') &&
238
+ !resolvedPath.endsWith('.db') &&
239
+ !resolvedPath.endsWith('.sqlite3')
240
+ ) {
206
241
  return 'Path should end with .sqlite, .sqlite3, or .db'
207
242
  }
208
243
  return true
@@ -213,7 +248,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
213
248
  const resolvedPath = resolve(newPath)
214
249
 
215
250
  if (resolvedPath === currentPath) {
216
- console.log(warning('Location unchanged'))
251
+ console.log(uiWarning('Location unchanged'))
217
252
  return null
218
253
  }
219
254
 
@@ -228,7 +263,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
228
263
  },
229
264
  ])
230
265
  if (!overwrite) {
231
- console.log(warning('Relocate cancelled'))
266
+ console.log(uiWarning('Relocate cancelled'))
232
267
  return null
233
268
  }
234
269
  }
@@ -237,7 +272,9 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
237
272
  }
238
273
 
239
274
  export const editCommand = new Command('edit')
240
- .description('Edit container properties (rename, port, relocate, or database config)')
275
+ .description(
276
+ 'Edit container properties (rename, port, relocate, or database config)',
277
+ )
241
278
  .argument('[name]', 'Container name')
242
279
  .option('-n, --name <newName>', 'New container name')
243
280
  .option('-p, --port <port>', 'New port number', parseInt)
@@ -256,7 +293,13 @@ export const editCommand = new Command('edit')
256
293
  .action(
257
294
  async (
258
295
  name: string | undefined,
259
- options: { name?: string; port?: number; relocate?: string; overwrite?: boolean; setConfig?: string },
296
+ options: {
297
+ name?: string
298
+ port?: number
299
+ relocate?: string
300
+ overwrite?: boolean
301
+ setConfig?: string
302
+ },
260
303
  ) => {
261
304
  try {
262
305
  let containerName = name
@@ -265,7 +308,7 @@ export const editCommand = new Command('edit')
265
308
  const containers = await containerManager.list()
266
309
 
267
310
  if (containers.length === 0) {
268
- console.log(warning('No containers found'))
311
+ console.log(uiWarning('No containers found'))
269
312
  return
270
313
  }
271
314
 
@@ -279,7 +322,7 @@ export const editCommand = new Command('edit')
279
322
 
280
323
  const config = await containerManager.getConfig(containerName)
281
324
  if (!config) {
282
- console.error(error(`Container "${containerName}" not found`))
325
+ console.error(uiError(`Container "${containerName}" not found`))
283
326
  process.exit(1)
284
327
  }
285
328
 
@@ -327,7 +370,7 @@ export const editCommand = new Command('edit')
327
370
  if (options.name) {
328
371
  if (!isValidName(options.name)) {
329
372
  console.error(
330
- error(
373
+ uiError(
331
374
  'Name must start with a letter and contain only letters, numbers, hyphens, and underscores',
332
375
  ),
333
376
  )
@@ -338,7 +381,7 @@ export const editCommand = new Command('edit')
338
381
  engine: config.engine,
339
382
  })
340
383
  if (exists) {
341
- console.error(error(`Container "${options.name}" already exists`))
384
+ console.error(uiError(`Container "${options.name}" already exists`))
342
385
  process.exit(1)
343
386
  }
344
387
 
@@ -347,7 +390,7 @@ export const editCommand = new Command('edit')
347
390
  })
348
391
  if (running) {
349
392
  console.error(
350
- error(
393
+ uiError(
351
394
  `Container "${containerName}" is running. Stop it first to rename.`,
352
395
  ),
353
396
  )
@@ -368,14 +411,14 @@ export const editCommand = new Command('edit')
368
411
 
369
412
  if (options.port !== undefined) {
370
413
  if (options.port < 1 || options.port > 65535) {
371
- console.error(error('Port must be between 1 and 65535'))
414
+ console.error(uiError('Port must be between 1 and 65535'))
372
415
  process.exit(1)
373
416
  }
374
417
 
375
418
  const portAvailable = await portManager.isPortAvailable(options.port)
376
419
  if (!portAvailable) {
377
420
  console.log(
378
- warning(
421
+ uiWarning(
379
422
  `Port ${options.port} is currently in use. The container will use this port on next start.`,
380
423
  ),
381
424
  )
@@ -400,7 +443,7 @@ export const editCommand = new Command('edit')
400
443
  if (options.relocate) {
401
444
  if (config.engine !== Engine.SQLite) {
402
445
  console.error(
403
- error('Relocate is only available for SQLite containers'),
446
+ uiError('Relocate is only available for SQLite containers'),
404
447
  )
405
448
  process.exit(1)
406
449
  }
@@ -425,13 +468,17 @@ export const editCommand = new Command('edit')
425
468
  // - ends with /
426
469
  // - exists and is a directory
427
470
  // - doesn't have a database file extension
428
- const isDirectory = expandedPath.endsWith('/') ||
429
- (existsSync(expandedPath) && statSync(expandedPath).isDirectory()) ||
471
+ const isDirectory =
472
+ expandedPath.endsWith('/') ||
473
+ (existsSync(expandedPath) &&
474
+ statSync(expandedPath).isDirectory()) ||
430
475
  !hasDbExtension
431
476
 
432
477
  let newPath: string
433
478
  if (isDirectory) {
434
- const dirPath = expandedPath.endsWith('/') ? expandedPath.slice(0, -1) : expandedPath
479
+ const dirPath = expandedPath.endsWith('/')
480
+ ? expandedPath.slice(0, -1)
481
+ : expandedPath
435
482
  const currentFileName = basename(config.database)
436
483
  newPath = join(dirPath, currentFileName)
437
484
  } else {
@@ -441,7 +488,7 @@ export const editCommand = new Command('edit')
441
488
  // Check source file exists
442
489
  if (!existsSync(config.database)) {
443
490
  console.error(
444
- error(`Source database file not found: ${config.database}`),
491
+ uiError(`Source database file not found: ${config.database}`),
445
492
  )
446
493
  process.exit(1)
447
494
  }
@@ -451,12 +498,14 @@ export const editCommand = new Command('edit')
451
498
  if (options.overwrite) {
452
499
  // Remove existing file before move
453
500
  unlinkSync(newPath)
454
- console.log(warning(`Overwriting existing file: ${newPath}`))
501
+ console.log(uiWarning(`Overwriting existing file: ${newPath}`))
455
502
  } else {
456
503
  console.error(
457
- error(`Destination file already exists: ${newPath}`),
504
+ uiError(`Destination file already exists: ${newPath}`),
505
+ )
506
+ console.log(
507
+ uiInfo('Use --overwrite to replace the existing file'),
458
508
  )
459
- console.log(info('Use --overwrite to replace the existing file'))
460
509
  process.exit(1)
461
510
  }
462
511
  }
@@ -465,12 +514,10 @@ export const editCommand = new Command('edit')
465
514
  const targetDir = dirname(newPath)
466
515
  if (!existsSync(targetDir)) {
467
516
  mkdirSync(targetDir, { recursive: true })
468
- console.log(info(`Created directory: ${targetDir}`))
517
+ console.log(uiInfo(`Created directory: ${targetDir}`))
469
518
  }
470
519
 
471
- const spinner = createSpinner(
472
- `Moving database to ${newPath}...`,
473
- )
520
+ const spinner = createSpinner(`Moving database to ${newPath}...`)
474
521
  spinner.start()
475
522
 
476
523
  try {
@@ -509,18 +556,20 @@ export const editCommand = new Command('edit')
509
556
  await sqliteRegistry.update(containerName, { filePath: newPath })
510
557
 
511
558
  spinner.succeed(`Database relocated to ${newPath}`)
512
- } catch (err) {
559
+ } catch (error) {
513
560
  spinner.fail('Failed to relocate database')
514
- throw err
561
+ throw error
515
562
  }
516
563
  }
517
564
 
518
565
  // Handle config change
519
566
  if (options.setConfig) {
520
567
  // Only PostgreSQL supports config editing for now
521
- if (config.engine !== 'postgresql') {
568
+ if (config.engine !== Engine.PostgreSQL) {
522
569
  console.error(
523
- error(`Config editing is only supported for PostgreSQL containers`),
570
+ uiError(
571
+ `Config editing is only supported for PostgreSQL containers`,
572
+ ),
524
573
  )
525
574
  process.exit(1)
526
575
  }
@@ -529,7 +578,7 @@ export const editCommand = new Command('edit')
529
578
  const match = options.setConfig.match(/^([a-z_]+)=(.+)$/)
530
579
  if (!match) {
531
580
  console.error(
532
- error(
581
+ uiError(
533
582
  'Invalid config format. Use: --set-config key=value (e.g., max_connections=200)',
534
583
  ),
535
584
  )
@@ -551,22 +600,28 @@ export const editCommand = new Command('edit')
551
600
 
552
601
  // Use the PostgreSQL engine's setConfigValue method
553
602
  if ('setConfigValue' in engine) {
554
- await (engine as { setConfigValue: (dataDir: string, key: string, value: string) => Promise<void> }).setConfigValue(
555
- dataDir,
556
- configKey,
557
- configValue,
558
- )
603
+ await (
604
+ engine as {
605
+ setConfigValue: (
606
+ dataDir: string,
607
+ key: string,
608
+ value: string,
609
+ ) => Promise<void>
610
+ }
611
+ ).setConfigValue(dataDir, configKey, configValue)
612
+ spinner.succeed(`Set ${configKey} = ${configValue}`)
613
+ } else {
614
+ spinner.fail('Config editing not supported for this engine')
615
+ process.exit(1)
559
616
  }
560
617
 
561
- spinner.succeed(`Set ${configKey} = ${configValue}`)
562
-
563
618
  // Check if container is running and warn about restart
564
619
  const running = await processManager.isRunning(containerName, {
565
620
  engine: config.engine,
566
621
  })
567
622
  if (running) {
568
623
  console.log(
569
- info(
624
+ uiInfo(
570
625
  ' Note: Restart the container for changes to take effect.',
571
626
  ),
572
627
  )
@@ -585,10 +640,10 @@ export const editCommand = new Command('edit')
585
640
  }
586
641
 
587
642
  console.log()
588
- console.log(success('Container updated successfully'))
589
- } catch (err) {
590
- const e = err as Error
591
- console.error(error(e.message))
643
+ console.log(uiSuccess('Container updated successfully'))
644
+ } catch (error) {
645
+ const e = error as Error
646
+ console.error(uiError(e.message))
592
647
  process.exit(1)
593
648
  }
594
649
  },
@@ -5,15 +5,24 @@ import inquirer from 'inquirer'
5
5
  import { containerManager } from '../../core/container-manager'
6
6
  import { promptConfirm } from '../ui/prompts'
7
7
  import { createSpinner } from '../ui/spinner'
8
- import { error, warning, info, formatBytes } from '../ui/theme'
8
+ import { uiError, uiWarning, uiInfo, formatBytes } from '../ui/theme'
9
9
  import { getEngineIcon, ENGINE_ICONS } from '../constants'
10
10
  import {
11
11
  getInstalledEngines,
12
12
  getInstalledPostgresEngines,
13
13
  type InstalledPostgresEngine,
14
14
  type InstalledMysqlEngine,
15
+ type InstalledSqliteEngine,
15
16
  } from '../helpers'
16
17
 
18
+ /**
19
+ * Pad string to width, accounting for emoji taking 2 display columns
20
+ */
21
+ function padWithEmoji(str: string, width: number): string {
22
+ // Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
23
+ const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
24
+ return str.padEnd(width + emojiCount)
25
+ }
17
26
 
18
27
  /**
19
28
  * List subcommand action
@@ -27,7 +36,7 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
27
36
  }
28
37
 
29
38
  if (engines.length === 0) {
30
- console.log(info('No engines installed yet.'))
39
+ console.log(uiInfo('No engines installed yet.'))
31
40
  console.log(
32
41
  chalk.gray(
33
42
  ' PostgreSQL engines are downloaded automatically when you create a container.',
@@ -41,13 +50,16 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
41
50
  return
42
51
  }
43
52
 
44
- // Separate PostgreSQL and MySQL
53
+ // Separate engines by type
45
54
  const pgEngines = engines.filter(
46
55
  (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
47
56
  )
48
57
  const mysqlEngine = engines.find(
49
58
  (e): e is InstalledMysqlEngine => e.engine === 'mysql',
50
59
  )
60
+ const sqliteEngine = engines.find(
61
+ (e): e is InstalledSqliteEngine => e.engine === 'sqlite',
62
+ )
51
63
 
52
64
  // Calculate total size for PostgreSQL
53
65
  const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
@@ -67,10 +79,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
67
79
  for (const engine of pgEngines) {
68
80
  const icon = getEngineIcon(engine.engine)
69
81
  const platformInfo = `${engine.platform}-${engine.arch}`
82
+ const engineDisplay = `${icon} ${engine.engine}`
70
83
 
71
84
  console.log(
72
85
  chalk.gray(' ') +
73
- chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
86
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
74
87
  chalk.yellow(engine.version.padEnd(12)) +
75
88
  chalk.gray(platformInfo.padEnd(18)) +
76
89
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -81,16 +94,31 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
81
94
  if (mysqlEngine) {
82
95
  const icon = ENGINE_ICONS.mysql
83
96
  const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
97
+ const engineDisplay = `${icon} ${displayName}`
84
98
 
85
99
  console.log(
86
100
  chalk.gray(' ') +
87
- chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
101
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
88
102
  chalk.yellow(mysqlEngine.version.padEnd(12)) +
89
103
  chalk.gray('system'.padEnd(18)) +
90
104
  chalk.gray('(system-installed)'),
91
105
  )
92
106
  }
93
107
 
108
+ // SQLite row
109
+ if (sqliteEngine) {
110
+ const icon = ENGINE_ICONS.sqlite
111
+ const engineDisplay = `${icon} sqlite`
112
+
113
+ console.log(
114
+ chalk.gray(' ') +
115
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
116
+ chalk.yellow(sqliteEngine.version.padEnd(12)) +
117
+ chalk.gray('system'.padEnd(18)) +
118
+ chalk.gray('(system-installed)'),
119
+ )
120
+ }
121
+
94
122
  console.log(chalk.gray(' ' + '─'.repeat(55)))
95
123
 
96
124
  // Summary
@@ -105,6 +133,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
105
133
  if (mysqlEngine) {
106
134
  console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
107
135
  }
136
+ if (sqliteEngine) {
137
+ console.log(
138
+ chalk.gray(` SQLite: system-installed at ${sqliteEngine.path}`),
139
+ )
140
+ }
108
141
  console.log()
109
142
  }
110
143
 
@@ -120,7 +153,7 @@ async function deleteEngine(
120
153
  const pgEngines = await getInstalledPostgresEngines()
121
154
 
122
155
  if (pgEngines.length === 0) {
123
- console.log(warning('No deletable engines found.'))
156
+ console.log(uiWarning('No deletable engines found.'))
124
157
  console.log(
125
158
  chalk.gray(
126
159
  ' MySQL is system-installed and cannot be deleted via spindb.',
@@ -159,7 +192,7 @@ async function deleteEngine(
159
192
  )
160
193
 
161
194
  if (!targetEngine) {
162
- console.error(error(`Engine "${engineName} ${engineVersion}" not found`))
195
+ console.error(uiError(`Engine "${engineName} ${engineVersion}" not found`))
163
196
  process.exit(1)
164
197
  }
165
198
 
@@ -171,7 +204,7 @@ async function deleteEngine(
171
204
 
172
205
  if (usingContainers.length > 0) {
173
206
  console.error(
174
- error(
207
+ uiError(
175
208
  `Cannot delete: ${usingContainers.length} container(s) are using ${engineName} ${engineVersion}`,
176
209
  ),
177
210
  )
@@ -193,7 +226,7 @@ async function deleteEngine(
193
226
  )
194
227
 
195
228
  if (!confirmed) {
196
- console.log(warning('Deletion cancelled'))
229
+ console.log(uiWarning('Deletion cancelled'))
197
230
  return
198
231
  }
199
232
  }
@@ -205,8 +238,8 @@ async function deleteEngine(
205
238
  try {
206
239
  await rm(targetEngine.path, { recursive: true, force: true })
207
240
  spinner.succeed(`Deleted ${engineName} ${engineVersion}`)
208
- } catch (err) {
209
- const e = err as Error
241
+ } catch (error) {
242
+ const e = error as Error
210
243
  spinner.fail(`Failed to delete: ${e.message}`)
211
244
  process.exit(1)
212
245
  }
@@ -219,9 +252,9 @@ export const enginesCommand = new Command('engines')
219
252
  .action(async (options: { json?: boolean }) => {
220
253
  try {
221
254
  await listEngines(options)
222
- } catch (err) {
223
- const e = err as Error
224
- console.error(error(e.message))
255
+ } catch (error) {
256
+ const e = error as Error
257
+ console.error(uiError(e.message))
225
258
  process.exit(1)
226
259
  }
227
260
  })
@@ -239,9 +272,9 @@ enginesCommand
239
272
  ) => {
240
273
  try {
241
274
  await deleteEngine(engine, version, options)
242
- } catch (err) {
243
- const e = err as Error
244
- console.error(error(e.message))
275
+ } catch (error) {
276
+ const e = error as Error
277
+ console.error(uiError(e.message))
245
278
  process.exit(1)
246
279
  }
247
280
  },
@@ -2,11 +2,12 @@ import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import inquirer from 'inquirer'
4
4
  import { existsSync } from 'fs'
5
+ import { basename } from 'path'
5
6
  import { containerManager } from '../../core/container-manager'
6
7
  import { processManager } from '../../core/process-manager'
7
8
  import { paths } from '../../config/paths'
8
9
  import { getEngine } from '../../engines'
9
- import { error, info, header } from '../ui/theme'
10
+ import { uiError, uiInfo, header } from '../ui/theme'
10
11
  import { getEngineIcon } from '../constants'
11
12
  import { Engine, type ContainerConfig } from '../../types'
12
13
 
@@ -197,8 +198,9 @@ async function displayAllContainersInfo(
197
198
  // Show truncated file path for SQLite instead of port
198
199
  let portOrPath: string
199
200
  if (isSQLite) {
200
- const fileName = container.database.split('/').pop() || container.database
201
- portOrPath = fileName.length > 7 ? fileName.slice(0, 6) + '…' : fileName
201
+ const fileName = basename(container.database)
202
+ // Truncate if longer than 8 chars to fit in 8-char column
203
+ portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + '…' : fileName
202
204
  } else {
203
205
  portOrPath = String(container.port)
204
206
  }
@@ -253,14 +255,16 @@ export const infoCommand = new Command('info')
253
255
  const containers = await containerManager.list()
254
256
 
255
257
  if (containers.length === 0) {
256
- console.log(info('No containers found. Create one with: spindb create'))
258
+ console.log(
259
+ uiInfo('No containers found. Create one with: spindb create'),
260
+ )
257
261
  return
258
262
  }
259
263
 
260
264
  if (name) {
261
265
  const config = await containerManager.getConfig(name)
262
266
  if (!config) {
263
- console.error(error(`Container "${name}" not found`))
267
+ console.error(uiError(`Container "${name}" not found`))
264
268
  process.exit(1)
265
269
  }
266
270
  await displayContainerInfo(config, options)
@@ -297,9 +301,9 @@ export const infoCommand = new Command('info')
297
301
  }
298
302
 
299
303
  await displayAllContainersInfo(containers, options)
300
- } catch (err) {
301
- const e = err as Error
302
- console.error(error(e.message))
304
+ } catch (error) {
305
+ const e = error as Error
306
+ console.error(uiError(e.message))
303
307
  process.exit(1)
304
308
  }
305
309
  })