spindb 0.9.1 → 0.9.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 (55) hide show
  1. package/README.md +5 -8
  2. package/cli/commands/attach.ts +108 -0
  3. package/cli/commands/backup.ts +13 -11
  4. package/cli/commands/clone.ts +14 -10
  5. package/cli/commands/config.ts +29 -29
  6. package/cli/commands/connect.ts +51 -39
  7. package/cli/commands/create.ts +65 -32
  8. package/cli/commands/delete.ts +8 -8
  9. package/cli/commands/deps.ts +17 -15
  10. package/cli/commands/detach.ts +100 -0
  11. package/cli/commands/doctor.ts +27 -13
  12. package/cli/commands/edit.ts +120 -57
  13. package/cli/commands/engines.ts +17 -15
  14. package/cli/commands/info.ts +8 -6
  15. package/cli/commands/list.ts +127 -18
  16. package/cli/commands/logs.ts +15 -11
  17. package/cli/commands/menu/backup-handlers.ts +52 -47
  18. package/cli/commands/menu/container-handlers.ts +164 -79
  19. package/cli/commands/menu/engine-handlers.ts +21 -11
  20. package/cli/commands/menu/index.ts +4 -4
  21. package/cli/commands/menu/shell-handlers.ts +34 -31
  22. package/cli/commands/menu/sql-handlers.ts +22 -16
  23. package/cli/commands/menu/update-handlers.ts +19 -17
  24. package/cli/commands/restore.ts +22 -20
  25. package/cli/commands/run.ts +20 -18
  26. package/cli/commands/self-update.ts +5 -5
  27. package/cli/commands/sqlite.ts +247 -0
  28. package/cli/commands/start.ts +11 -9
  29. package/cli/commands/stop.ts +9 -9
  30. package/cli/commands/url.ts +12 -9
  31. package/cli/helpers.ts +9 -4
  32. package/cli/index.ts +6 -0
  33. package/cli/ui/prompts.ts +12 -5
  34. package/cli/ui/spinner.ts +4 -4
  35. package/cli/ui/theme.ts +4 -4
  36. package/config/paths.ts +0 -8
  37. package/core/binary-manager.ts +5 -1
  38. package/core/config-manager.ts +32 -0
  39. package/core/container-manager.ts +5 -5
  40. package/core/platform-service.ts +3 -3
  41. package/core/start-with-retry.ts +6 -6
  42. package/core/transaction-manager.ts +6 -6
  43. package/engines/mysql/backup.ts +37 -13
  44. package/engines/mysql/index.ts +11 -11
  45. package/engines/mysql/restore.ts +4 -4
  46. package/engines/mysql/version-validator.ts +2 -2
  47. package/engines/postgresql/binary-manager.ts +17 -17
  48. package/engines/postgresql/index.ts +7 -2
  49. package/engines/postgresql/restore.ts +2 -2
  50. package/engines/postgresql/version-validator.ts +2 -2
  51. package/engines/sqlite/index.ts +30 -15
  52. package/engines/sqlite/registry.ts +64 -33
  53. package/engines/sqlite/scanner.ts +99 -0
  54. package/package.json +4 -3
  55. package/types/index.ts +21 -1
@@ -1,6 +1,13 @@
1
1
  import chalk from 'chalk'
2
2
  import inquirer from 'inquirer'
3
- import { existsSync, renameSync, statSync, mkdirSync, copyFileSync, unlinkSync } from 'fs'
3
+ import {
4
+ existsSync,
5
+ renameSync,
6
+ statSync,
7
+ mkdirSync,
8
+ copyFileSync,
9
+ unlinkSync,
10
+ } from 'fs'
4
11
  import { dirname, basename, join, resolve } from 'path'
5
12
  import { homedir } from 'os'
6
13
  import { containerManager } from '../../../core/container-manager'
@@ -22,10 +29,10 @@ import {
22
29
  import { createSpinner } from '../../ui/spinner'
23
30
  import {
24
31
  header,
25
- success,
26
- error,
27
- warning,
28
- info,
32
+ uiSuccess,
33
+ uiError,
34
+ uiWarning,
35
+ uiInfo,
29
36
  connectionBox,
30
37
  formatBytes,
31
38
  } from '../../ui/theme'
@@ -69,7 +76,7 @@ export async function handleCreate(): Promise<void> {
69
76
  missingDeps = await getMissingDependencies(engine)
70
77
  if (missingDeps.length > 0) {
71
78
  console.log(
72
- error(
79
+ uiError(
73
80
  `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
74
81
  ),
75
82
  )
@@ -94,13 +101,17 @@ export async function handleCreate(): Promise<void> {
94
101
 
95
102
  const isInstalled = await dbEngine.isBinaryInstalled(version)
96
103
  if (isInstalled) {
97
- binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries ready (cached)`)
104
+ binarySpinner.succeed(
105
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
106
+ )
98
107
  } else {
99
108
  binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
100
109
  await dbEngine.ensureBinaries(version, ({ message }) => {
101
110
  binarySpinner.text = message
102
111
  })
103
- binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries downloaded`)
112
+ binarySpinner.succeed(
113
+ `${dbEngine.displayName} ${version} binaries downloaded`,
114
+ )
104
115
  }
105
116
  }
106
117
 
@@ -131,7 +142,9 @@ export async function handleCreate(): Promise<void> {
131
142
  path: sqlitePath, // SQLite file path (undefined for server databases)
132
143
  })
133
144
 
134
- initSpinner.succeed(isSQLite ? 'Database file created' : 'Database cluster initialized')
145
+ initSpinner.succeed(
146
+ isSQLite ? 'Database file created' : 'Database cluster initialized',
147
+ )
135
148
 
136
149
  // SQLite: show file path, no start needed
137
150
  if (isSQLite) {
@@ -139,13 +152,13 @@ export async function handleCreate(): Promise<void> {
139
152
  if (config) {
140
153
  const connectionString = dbEngine.getConnectionString(config)
141
154
  console.log()
142
- console.log(success('Database Created'))
155
+ console.log(uiSuccess('Database Created'))
143
156
  console.log()
144
157
  console.log(chalk.gray(` Container: ${containerName}`))
145
158
  console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
146
159
  console.log(chalk.gray(` File: ${config.database}`))
147
160
  console.log()
148
- console.log(success(`Available at ${config.database}`))
161
+ console.log(uiSuccess(`Available at ${config.database}`))
149
162
  console.log()
150
163
  console.log(chalk.gray(' Connection string:'))
151
164
  console.log(chalk.cyan(` ${connectionString}`))
@@ -187,7 +200,9 @@ export async function handleCreate(): Promise<void> {
187
200
 
188
201
  startSpinner.succeed(`${dbEngine.displayName} started`)
189
202
 
190
- if (config && database !== 'postgres') {
203
+ // Skip creating 'postgres' database for PostgreSQL - it's created by initdb
204
+ // For other engines (MySQL, SQLite), allow creating a database named 'postgres'
205
+ if (config && !(config.engine === 'postgresql' && database === 'postgres')) {
191
206
  const dbSpinner = createSpinner(`Creating database "${database}"...`)
192
207
  dbSpinner.start()
193
208
 
@@ -199,14 +214,14 @@ export async function handleCreate(): Promise<void> {
199
214
  if (config) {
200
215
  const connectionString = dbEngine.getConnectionString(config)
201
216
  console.log()
202
- console.log(success('Database Created'))
217
+ console.log(uiSuccess('Database Created'))
203
218
  console.log()
204
219
  console.log(chalk.gray(` Container: ${containerName}`))
205
220
  console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
206
221
  console.log(chalk.gray(` Database: ${database}`))
207
222
  console.log(chalk.gray(` Port: ${port}`))
208
223
  console.log()
209
- console.log(success(`Running on port ${port}`))
224
+ console.log(uiSuccess(`Running on port ${port}`))
210
225
  console.log()
211
226
  console.log(chalk.gray(' Connection string:'))
212
227
  console.log(chalk.cyan(` ${connectionString}`))
@@ -235,12 +250,12 @@ export async function handleCreate(): Promise<void> {
235
250
  } else {
236
251
  console.log()
237
252
  console.log(
238
- warning(
253
+ uiWarning(
239
254
  `Port ${port} is currently in use. Container created but not started.`,
240
255
  ),
241
256
  )
242
257
  console.log(
243
- info(
258
+ uiInfo(
244
259
  `Start it later with: ${chalk.cyan(`spindb start ${containerName}`)}`,
245
260
  ),
246
261
  )
@@ -257,7 +272,7 @@ export async function handleList(
257
272
 
258
273
  if (containers.length === 0) {
259
274
  console.log(
260
- info('No containers found. Create one with the "Create" option.'),
275
+ uiInfo('No containers found. Create one with the "Create" option.'),
261
276
  )
262
277
  console.log()
263
278
 
@@ -302,19 +317,20 @@ export async function handleList(
302
317
 
303
318
  // SQLite uses available/missing, server databases use running/stopped
304
319
  const statusDisplay = isSQLite
305
- ? (container.status === 'running'
306
- ? chalk.blue('● available')
307
- : chalk.gray('○ missing'))
308
- : (container.status === 'running'
309
- ? chalk.green('● running')
310
- : chalk.gray('○ stopped'))
320
+ ? container.status === 'running'
321
+ ? chalk.blue('● available')
322
+ : chalk.gray('○ missing')
323
+ : container.status === 'running'
324
+ ? chalk.green('● running')
325
+ : chalk.gray('○ stopped')
311
326
 
312
327
  const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
313
328
 
314
329
  // Truncate name if too long
315
- const displayName = container.name.length > 15
316
- ? container.name.slice(0, 14) + '…'
317
- : container.name
330
+ const displayName =
331
+ container.name.length > 15
332
+ ? container.name.slice(0, 14) + '…'
333
+ : container.name
318
334
 
319
335
  // SQLite shows dash instead of port
320
336
  const portDisplay = isSQLite ? '—' : String(container.port)
@@ -338,7 +354,9 @@ export async function handleList(
338
354
 
339
355
  const running = serverContainers.filter((c) => c.status === 'running').length
340
356
  const stopped = serverContainers.filter((c) => c.status !== 'running').length
341
- const available = sqliteContainers.filter((c) => c.status === 'running').length
357
+ const available = sqliteContainers.filter(
358
+ (c) => c.status === 'running',
359
+ ).length
342
360
  const missing = sqliteContainers.filter((c) => c.status !== 'running').length
343
361
 
344
362
  const parts: string[] = []
@@ -346,13 +364,13 @@ export async function handleList(
346
364
  parts.push(`${running} running, ${stopped} stopped`)
347
365
  }
348
366
  if (sqliteContainers.length > 0) {
349
- parts.push(`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`)
367
+ parts.push(
368
+ `${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`,
369
+ )
350
370
  }
351
371
 
352
372
  console.log(
353
- chalk.gray(
354
- ` ${containers.length} container(s): ${parts.join('; ')}`,
355
- ),
373
+ chalk.gray(` ${containers.length} container(s): ${parts.join('; ')}`),
356
374
  )
357
375
 
358
376
  console.log()
@@ -361,8 +379,12 @@ export async function handleList(
361
379
  // Simpler selector - table already shows details
362
380
  const statusLabel =
363
381
  c.engine === Engine.SQLite
364
- ? (c.status === 'running' ? chalk.blue('● available') : chalk.gray('○ missing'))
365
- : (c.status === 'running' ? chalk.green('● running') : chalk.gray('○ stopped'))
382
+ ? c.status === 'running'
383
+ ? chalk.blue('● available')
384
+ : chalk.gray('○ missing')
385
+ : c.status === 'running'
386
+ ? chalk.green('● running')
387
+ : chalk.gray('○ stopped')
366
388
 
367
389
  return {
368
390
  name: `${c.name} ${statusLabel}`,
@@ -400,7 +422,7 @@ export async function showContainerSubmenu(
400
422
  ): Promise<void> {
401
423
  const config = await containerManager.getConfig(containerName)
402
424
  if (!config) {
403
- console.error(error(`Container "${containerName}" not found`))
425
+ console.error(uiError(`Container "${containerName}" not found`))
404
426
  return
405
427
  }
406
428
 
@@ -458,7 +480,11 @@ export async function showContainerSubmenu(
458
480
  ? `${chalk.blue('⌘')} Open shell`
459
481
  : chalk.gray('⌘ Open shell'),
460
482
  value: 'shell',
461
- disabled: canOpenShell ? false : (isSQLite ? 'Database file missing' : 'Start container first'),
483
+ disabled: canOpenShell
484
+ ? false
485
+ : isSQLite
486
+ ? 'Database file missing'
487
+ : 'Start container first',
462
488
  })
463
489
 
464
490
  // Run SQL - always enabled for SQLite (if file exists), server databases need to be running
@@ -468,7 +494,11 @@ export async function showContainerSubmenu(
468
494
  ? `${chalk.yellow('▷')} Run SQL file`
469
495
  : chalk.gray('▷ Run SQL file'),
470
496
  value: 'run-sql',
471
- disabled: canRunSql ? false : (isSQLite ? 'Database file missing' : 'Start container first'),
497
+ disabled: canRunSql
498
+ ? false
499
+ : isSQLite
500
+ ? 'Database file missing'
501
+ : 'Start container first',
472
502
  })
473
503
 
474
504
  // Edit container - SQLite can always edit (no running state), server databases must be stopped
@@ -491,9 +521,10 @@ export async function showContainerSubmenu(
491
521
  disabled: canClone ? false : 'Stop container first',
492
522
  })
493
523
 
494
- actionChoices.push(
495
- { name: `${chalk.magenta('⎘')} Copy connection string`, value: 'copy' },
496
- )
524
+ actionChoices.push({
525
+ name: `${chalk.magenta('⎘')} Copy connection string`,
526
+ value: 'copy',
527
+ })
497
528
 
498
529
  // View logs - not available for SQLite (no log file)
499
530
  if (!isSQLite) {
@@ -503,6 +534,14 @@ export async function showContainerSubmenu(
503
534
  })
504
535
  }
505
536
 
537
+ // Detach - only for SQLite (unregisters without deleting file)
538
+ if (isSQLite) {
539
+ actionChoices.push({
540
+ name: `${chalk.yellow('⊘')} Detach from SpinDB`,
541
+ value: 'detach',
542
+ })
543
+ }
544
+
506
545
  // Delete container - SQLite can always delete, server databases must be stopped
507
546
  const canDelete = isSQLite ? true : !isRunning
508
547
  actionChoices.push({
@@ -577,6 +616,9 @@ export async function showContainerSubmenu(
577
616
  await handleCopyConnectionString(containerName)
578
617
  await showContainerSubmenu(containerName, showMainMenu)
579
618
  return
619
+ case 'detach':
620
+ await handleDetachContainer(containerName, showMainMenu)
621
+ return // Return to list after detach
580
622
  case 'delete':
581
623
  await handleDelete(containerName)
582
624
  return // Don't show submenu again after delete
@@ -596,7 +638,7 @@ export async function handleStart(): Promise<void> {
596
638
  )
597
639
 
598
640
  if (stopped.length === 0) {
599
- console.log(warning('All containers are already running'))
641
+ console.log(uiWarning('All containers are already running'))
600
642
  return
601
643
  }
602
644
 
@@ -609,7 +651,7 @@ export async function handleStart(): Promise<void> {
609
651
 
610
652
  const config = await containerManager.getConfig(containerName)
611
653
  if (!config) {
612
- console.error(error(`Container "${containerName}" not found`))
654
+ console.error(uiError(`Container "${containerName}" not found`))
613
655
  return
614
656
  }
615
657
 
@@ -617,7 +659,7 @@ export async function handleStart(): Promise<void> {
617
659
  if (!portAvailable) {
618
660
  const { port: newPort } = await portManager.findAvailablePort()
619
661
  console.log(
620
- warning(`Port ${config.port} is in use, switching to port ${newPort}`),
662
+ uiWarning(`Port ${config.port} is in use, switching to port ${newPort}`),
621
663
  )
622
664
  config.port = newPort
623
665
  await containerManager.updateConfig(containerName, { port: newPort })
@@ -647,7 +689,7 @@ export async function handleStop(): Promise<void> {
647
689
  )
648
690
 
649
691
  if (running.length === 0) {
650
- console.log(warning('No running containers'))
692
+ console.log(uiWarning('No running containers'))
651
693
  return
652
694
  }
653
695
 
@@ -660,7 +702,7 @@ export async function handleStop(): Promise<void> {
660
702
 
661
703
  const config = await containerManager.getConfig(containerName)
662
704
  if (!config) {
663
- console.error(error(`Container "${containerName}" not found`))
705
+ console.error(uiError(`Container "${containerName}" not found`))
664
706
  return
665
707
  }
666
708
 
@@ -678,25 +720,25 @@ export async function handleStop(): Promise<void> {
678
720
  async function handleStartContainer(containerName: string): Promise<void> {
679
721
  const config = await containerManager.getConfig(containerName)
680
722
  if (!config) {
681
- console.error(error(`Container "${containerName}" not found`))
723
+ console.error(uiError(`Container "${containerName}" not found`))
682
724
  return
683
725
  }
684
726
 
685
727
  const portAvailable = await portManager.isPortAvailable(config.port)
686
728
  if (!portAvailable) {
687
729
  console.log(
688
- warning(
730
+ uiWarning(
689
731
  `Port ${config.port} is in use. Stop the process using it or change this container's port.`,
690
732
  ),
691
733
  )
692
734
  console.log()
693
735
  console.log(
694
- info(
736
+ uiInfo(
695
737
  'Tip: If you installed MariaDB via apt, it may have started a system service.',
696
738
  ),
697
739
  )
698
740
  console.log(
699
- info(
741
+ uiInfo(
700
742
  'Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb',
701
743
  ),
702
744
  )
@@ -718,18 +760,18 @@ async function handleStartContainer(containerName: string): Promise<void> {
718
760
  console.log()
719
761
  console.log(chalk.gray(' Connection string:'))
720
762
  console.log(chalk.cyan(` ${connectionString}`))
721
- } catch (err) {
763
+ } catch (error) {
722
764
  spinner.fail(`Failed to start "${containerName}"`)
723
- const e = err as Error
765
+ const e = error as Error
724
766
  console.log()
725
- console.log(error(e.message))
767
+ console.log(uiError(e.message))
726
768
 
727
769
  const logPath = paths.getContainerLogPath(containerName, {
728
770
  engine: config.engine,
729
771
  })
730
772
  if (existsSync(logPath)) {
731
773
  console.log()
732
- console.log(info(`Check the log file for details: ${logPath}`))
774
+ console.log(uiInfo(`Check the log file for details: ${logPath}`))
733
775
  }
734
776
  }
735
777
  }
@@ -737,7 +779,7 @@ async function handleStartContainer(containerName: string): Promise<void> {
737
779
  async function handleStopContainer(containerName: string): Promise<void> {
738
780
  const config = await containerManager.getConfig(containerName)
739
781
  if (!config) {
740
- console.error(error(`Container "${containerName}" not found`))
782
+ console.error(uiError(`Container "${containerName}" not found`))
741
783
  return
742
784
  }
743
785
 
@@ -757,7 +799,7 @@ async function handleEditContainer(
757
799
  ): Promise<string | null> {
758
800
  const config = await containerManager.getConfig(containerName)
759
801
  if (!config) {
760
- console.error(error(`Container "${containerName}" not found`))
802
+ console.error(uiError(`Container "${containerName}" not found`))
761
803
  return null
762
804
  }
763
805
 
@@ -767,7 +809,9 @@ async function handleEditContainer(
767
809
  console.log(header(`Edit: ${containerName}`))
768
810
  console.log()
769
811
 
770
- const editChoices: Array<{ name: string; value: string } | inquirer.Separator> = [
812
+ const editChoices: Array<
813
+ { name: string; value: string } | inquirer.Separator
814
+ > = [
771
815
  {
772
816
  name: `Name: ${chalk.white(containerName)}`,
773
817
  value: 'name',
@@ -833,12 +877,12 @@ async function handleEditContainer(
833
877
  ])
834
878
 
835
879
  if (newName === containerName) {
836
- console.log(info('Name unchanged'))
880
+ console.log(uiInfo('Name unchanged'))
837
881
  return await handleEditContainer(containerName)
838
882
  }
839
883
 
840
884
  if (await containerManager.exists(newName)) {
841
- console.log(error(`Container "${newName}" already exists`))
885
+ console.log(uiError(`Container "${newName}" already exists`))
842
886
  return await handleEditContainer(containerName)
843
887
  }
844
888
 
@@ -872,21 +916,21 @@ async function handleEditContainer(
872
916
  ])
873
917
 
874
918
  if (newPort === config.port) {
875
- console.log(info('Port unchanged'))
919
+ console.log(uiInfo('Port unchanged'))
876
920
  return await handleEditContainer(containerName)
877
921
  }
878
922
 
879
923
  const portAvailable = await portManager.isPortAvailable(newPort)
880
924
  if (!portAvailable) {
881
925
  console.log(
882
- warning(
926
+ uiWarning(
883
927
  `Port ${newPort} is currently in use. You'll need to stop the process using it before starting this container.`,
884
928
  ),
885
929
  )
886
930
  }
887
931
 
888
932
  await containerManager.updateConfig(containerName, { port: newPort })
889
- console.log(success(`Changed port from ${config.port} to ${newPort}`))
933
+ console.log(uiSuccess(`Changed port from ${config.port} to ${newPort}`))
890
934
 
891
935
  // Continue editing
892
936
  return await handleEditContainer(containerName)
@@ -928,40 +972,43 @@ async function handleEditContainer(
928
972
  // - ends with /
929
973
  // - exists and is a directory
930
974
  // - doesn't have a database file extension (assume it's a directory path)
931
- const isDirectory = expandedPath.endsWith('/') ||
975
+ const isDirectory =
976
+ expandedPath.endsWith('/') ||
932
977
  (existsSync(expandedPath) && statSync(expandedPath).isDirectory()) ||
933
978
  !hasDbExtension
934
979
 
935
980
  let finalPath: string
936
981
  if (isDirectory) {
937
982
  // Remove trailing slash if present, then append filename
938
- const dirPath = expandedPath.endsWith('/') ? expandedPath.slice(0, -1) : expandedPath
983
+ const dirPath = expandedPath.endsWith('/')
984
+ ? expandedPath.slice(0, -1)
985
+ : expandedPath
939
986
  finalPath = join(dirPath, currentFileName)
940
987
  } else {
941
988
  finalPath = expandedPath
942
989
  }
943
990
 
944
991
  if (finalPath === config.database) {
945
- console.log(info('Location unchanged'))
992
+ console.log(uiInfo('Location unchanged'))
946
993
  return await handleEditContainer(containerName)
947
994
  }
948
995
 
949
996
  // Check if source file exists
950
997
  if (!existsSync(config.database)) {
951
- console.log(error(`Source file not found: ${config.database}`))
998
+ console.log(uiError(`Source file not found: ${config.database}`))
952
999
  return await handleEditContainer(containerName)
953
1000
  }
954
1001
 
955
1002
  // Check if destination already exists
956
1003
  if (existsSync(finalPath)) {
957
- console.log(error(`Destination file already exists: ${finalPath}`))
1004
+ console.log(uiError(`Destination file already exists: ${finalPath}`))
958
1005
  return await handleEditContainer(containerName)
959
1006
  }
960
1007
 
961
1008
  // Check if destination directory exists
962
1009
  const destDir = dirname(finalPath)
963
1010
  if (!existsSync(destDir)) {
964
- console.log(warning(`Directory does not exist: ${destDir}`))
1011
+ console.log(uiWarning(`Directory does not exist: ${destDir}`))
965
1012
  const { createDir } = await inquirer.prompt<{ createDir: string }>([
966
1013
  {
967
1014
  type: 'list',
@@ -980,9 +1027,13 @@ async function handleEditContainer(
980
1027
 
981
1028
  try {
982
1029
  mkdirSync(destDir, { recursive: true })
983
- console.log(success(`Created directory: ${destDir}`))
984
- } catch (err) {
985
- console.log(error(`Failed to create directory: ${(err as Error).message}`))
1030
+ console.log(uiSuccess(`Created directory: ${destDir}`))
1031
+ } catch (mkdirError) {
1032
+ console.log(
1033
+ uiError(
1034
+ `Failed to create directory: ${(mkdirError as Error).message}`,
1035
+ ),
1036
+ )
986
1037
  return await handleEditContainer(containerName)
987
1038
  }
988
1039
  }
@@ -1020,15 +1071,17 @@ async function handleEditContainer(
1020
1071
  }
1021
1072
 
1022
1073
  // Update the container config and SQLite registry
1023
- await containerManager.updateConfig(containerName, { database: finalPath })
1074
+ await containerManager.updateConfig(containerName, {
1075
+ database: finalPath,
1076
+ })
1024
1077
  await sqliteRegistry.update(containerName, { filePath: finalPath })
1025
1078
  spinner.succeed(`Moved database to ${finalPath}`)
1026
1079
 
1027
1080
  // Wait for user to see success message before refreshing
1028
1081
  await pressEnterToContinue()
1029
- } catch (err) {
1082
+ } catch (error) {
1030
1083
  spinner.fail('Failed to move database file')
1031
- console.log(error((err as Error).message))
1084
+ console.log(uiError((error as Error).message))
1032
1085
  await pressEnterToContinue()
1033
1086
  }
1034
1087
 
@@ -1045,7 +1098,7 @@ async function handleCloneFromSubmenu(
1045
1098
  ): Promise<void> {
1046
1099
  const sourceConfig = await containerManager.getConfig(sourceName)
1047
1100
  if (!sourceConfig) {
1048
- console.log(error(`Container "${sourceName}" not found`))
1101
+ console.log(uiError(`Container "${sourceName}" not found`))
1049
1102
  return
1050
1103
  }
1051
1104
 
@@ -1066,8 +1119,10 @@ async function handleCloneFromSubmenu(
1066
1119
  ])
1067
1120
 
1068
1121
  // Check if target container already exists
1069
- if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
1070
- console.log(error(`Container "${targetName}" already exists`))
1122
+ if (
1123
+ await containerManager.exists(targetName, { engine: sourceConfig.engine })
1124
+ ) {
1125
+ console.log(uiError(`Container "${targetName}" already exists`))
1071
1126
  return
1072
1127
  }
1073
1128
 
@@ -1086,17 +1141,47 @@ async function handleCloneFromSubmenu(
1086
1141
  console.log(connectionBox(targetName, connectionString, newConfig.port))
1087
1142
 
1088
1143
  await showContainerSubmenu(targetName, showMainMenu)
1089
- } catch (err) {
1144
+ } catch (error) {
1090
1145
  spinner.fail(`Failed to clone "${sourceName}"`)
1091
- console.log(error((err as Error).message))
1146
+ console.log(uiError((error as Error).message))
1092
1147
  await pressEnterToContinue()
1093
1148
  }
1094
1149
  }
1095
1150
 
1151
+ async function handleDetachContainer(
1152
+ containerName: string,
1153
+ showMainMenu: () => Promise<void>,
1154
+ ): Promise<void> {
1155
+ const confirmed = await promptConfirm(
1156
+ `Detach "${containerName}" from SpinDB? (file will be kept on disk)`,
1157
+ true,
1158
+ )
1159
+
1160
+ if (!confirmed) {
1161
+ console.log(uiWarning('Cancelled'))
1162
+ await pressEnterToContinue()
1163
+ await showContainerSubmenu(containerName, showMainMenu)
1164
+ return
1165
+ }
1166
+
1167
+ const entry = await sqliteRegistry.get(containerName)
1168
+ await sqliteRegistry.remove(containerName)
1169
+
1170
+ console.log(uiSuccess(`Detached "${containerName}" from SpinDB`))
1171
+ if (entry?.filePath) {
1172
+ console.log(chalk.gray(` File remains at: ${entry.filePath}`))
1173
+ console.log()
1174
+ console.log(chalk.gray(' Re-attach with:'))
1175
+ console.log(chalk.cyan(` spindb attach ${entry.filePath}`))
1176
+ }
1177
+ await pressEnterToContinue()
1178
+ await handleList(showMainMenu)
1179
+ }
1180
+
1096
1181
  async function handleDelete(containerName: string): Promise<void> {
1097
1182
  const config = await containerManager.getConfig(containerName)
1098
1183
  if (!config) {
1099
- console.error(error(`Container "${containerName}" not found`))
1184
+ console.error(uiError(`Container "${containerName}" not found`))
1100
1185
  return
1101
1186
  }
1102
1187
 
@@ -1106,7 +1191,7 @@ async function handleDelete(containerName: string): Promise<void> {
1106
1191
  )
1107
1192
 
1108
1193
  if (!confirmed) {
1109
- console.log(warning('Deletion cancelled'))
1194
+ console.log(uiWarning('Deletion cancelled'))
1110
1195
  return
1111
1196
  }
1112
1197