spindb 0.5.3 → 0.5.5

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.
@@ -7,6 +7,9 @@ import {
7
7
  promptContainerSelect,
8
8
  promptContainerName,
9
9
  promptDatabaseName,
10
+ promptDatabaseSelect,
11
+ promptBackupFormat,
12
+ promptBackupFilename,
10
13
  promptCreateOptions,
11
14
  promptConfirm,
12
15
  promptInstallDependencies,
@@ -19,6 +22,7 @@ import {
19
22
  warning,
20
23
  info,
21
24
  connectionBox,
25
+ formatBytes,
22
26
  } from '../ui/theme'
23
27
  import { existsSync } from 'fs'
24
28
  import { readdir, rm, lstat } from 'fs/promises'
@@ -33,7 +37,19 @@ import { defaults } from '../../config/defaults'
33
37
  import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
34
38
  import type { EngineName } from '../../types'
35
39
  import inquirer from 'inquirer'
36
- import { getMissingDependencies } from '../../core/dependency-manager'
40
+ import {
41
+ getMissingDependencies,
42
+ isUsqlInstalled,
43
+ isPgcliInstalled,
44
+ isMycliInstalled,
45
+ detectPackageManager,
46
+ installUsql,
47
+ installPgcli,
48
+ installMycli,
49
+ getUsqlManualInstructions,
50
+ getPgcliManualInstructions,
51
+ getMycliManualInstructions,
52
+ } from '../../core/dependency-manager'
37
53
  import {
38
54
  getMysqldPath,
39
55
  getMysqlVersion,
@@ -57,6 +73,19 @@ const engineIcons: Record<string, string> = {
57
73
  mysql: '🐬',
58
74
  }
59
75
 
76
+ /**
77
+ * Helper to pause and wait for user to press Enter
78
+ */
79
+ async function pressEnterToContinue(): Promise<void> {
80
+ await inquirer.prompt([
81
+ {
82
+ type: 'input',
83
+ name: 'continue',
84
+ message: chalk.gray('Press Enter to continue...'),
85
+ },
86
+ ])
87
+ }
88
+
60
89
  async function showMainMenu(): Promise<void> {
61
90
  console.clear()
62
91
  console.log(header('SpinDB - Local Database Manager'))
@@ -88,12 +117,12 @@ async function showMainMenu(): Promise<void> {
88
117
  const choices: MenuChoice[] = [
89
118
  ...(hasContainers
90
119
  ? [
91
- { name: `${chalk.cyan('◉')} List containers`, value: 'list' },
120
+ { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
92
121
  { name: `${chalk.green('+')} Create new container`, value: 'create' },
93
122
  ]
94
123
  : [
95
124
  { name: `${chalk.green('+')} Create new container`, value: 'create' },
96
- { name: `${chalk.cyan('◉')} List containers`, value: 'list' },
125
+ { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
97
126
  ]),
98
127
  {
99
128
  name: canStart
@@ -116,6 +145,13 @@ async function showMainMenu(): Promise<void> {
116
145
  value: 'restore',
117
146
  disabled: canRestore ? false : 'No running containers',
118
147
  },
148
+ {
149
+ name: canRestore
150
+ ? `${chalk.magenta('↑')} Backup database`
151
+ : chalk.gray('↑ Backup database'),
152
+ value: 'backup',
153
+ disabled: canRestore ? false : 'No running containers',
154
+ },
119
155
  {
120
156
  name: canClone
121
157
  ? `${chalk.cyan('⧉')} Clone a container`
@@ -160,6 +196,9 @@ async function showMainMenu(): Promise<void> {
160
196
  case 'restore':
161
197
  await handleRestore()
162
198
  break
199
+ case 'backup':
200
+ await handleBackup()
201
+ break
163
202
  case 'clone':
164
203
  await handleClone()
165
204
  break
@@ -372,6 +411,19 @@ async function handleList(): Promise<void> {
372
411
  return
373
412
  }
374
413
 
414
+ // Fetch sizes for running containers in parallel
415
+ const sizes = await Promise.all(
416
+ containers.map(async (container) => {
417
+ if (container.status !== 'running') return null
418
+ try {
419
+ const engine = getEngine(container.engine)
420
+ return await engine.getDatabaseSize(container)
421
+ } catch {
422
+ return null
423
+ }
424
+ }),
425
+ )
426
+
375
427
  // Table header
376
428
  console.log()
377
429
  console.log(
@@ -380,23 +432,30 @@ async function handleList(): Promise<void> {
380
432
  chalk.bold.white('ENGINE'.padEnd(12)) +
381
433
  chalk.bold.white('VERSION'.padEnd(10)) +
382
434
  chalk.bold.white('PORT'.padEnd(8)) +
435
+ chalk.bold.white('SIZE'.padEnd(10)) +
383
436
  chalk.bold.white('STATUS'),
384
437
  )
385
- console.log(chalk.gray(' ' + '─'.repeat(60)))
438
+ console.log(chalk.gray(' ' + '─'.repeat(70)))
386
439
 
387
440
  // Table rows
388
- for (const container of containers) {
441
+ for (let i = 0; i < containers.length; i++) {
442
+ const container = containers[i]
443
+ const size = sizes[i]
444
+
389
445
  const statusDisplay =
390
446
  container.status === 'running'
391
447
  ? chalk.green('● running')
392
448
  : chalk.gray('○ stopped')
393
449
 
450
+ const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
451
+
394
452
  console.log(
395
453
  chalk.gray(' ') +
396
454
  chalk.cyan(container.name.padEnd(20)) +
397
455
  chalk.white(container.engine.padEnd(12)) +
398
456
  chalk.yellow(container.version.padEnd(10)) +
399
457
  chalk.green(String(container.port).padEnd(8)) +
458
+ chalk.magenta(sizeDisplay.padEnd(10)) +
400
459
  statusDisplay,
401
460
  )
402
461
  }
@@ -414,15 +473,19 @@ async function handleList(): Promise<void> {
414
473
  // Container selection with submenu
415
474
  console.log()
416
475
  const containerChoices = [
417
- ...containers.map((c) => ({
418
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${
419
- c.status === 'running'
420
- ? chalk.green('● running')
421
- : chalk.gray(' stopped')
422
- }`,
423
- value: c.name,
424
- short: c.name,
425
- })),
476
+ ...containers.map((c, i) => {
477
+ const size = sizes[i]
478
+ const sizeLabel = size !== null ? `, ${formatBytes(size)}` : ''
479
+ return {
480
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port}${sizeLabel})`)} ${
481
+ c.status === 'running'
482
+ ? chalk.green('● running')
483
+ : chalk.gray('○ stopped')
484
+ }`,
485
+ value: c.name,
486
+ short: c.name,
487
+ }
488
+ }),
426
489
  new inquirer.Separator(),
427
490
  { name: `${chalk.blue('←')} Back to main menu`, value: 'back' },
428
491
  ]
@@ -435,6 +498,7 @@ async function handleList(): Promise<void> {
435
498
  name: 'selectedContainer',
436
499
  message: 'Select a container for more options:',
437
500
  choices: containerChoices,
501
+ pageSize: 15,
438
502
  },
439
503
  ])
440
504
 
@@ -473,7 +537,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
473
537
  // Start or Stop depending on current state
474
538
  !isRunning
475
539
  ? { name: `${chalk.green('▶')} Start container`, value: 'start' }
476
- : { name: `${chalk.yellow('■')} Stop container`, value: 'stop' },
540
+ : { name: `${chalk.red('■')} Stop container`, value: 'stop' },
477
541
  {
478
542
  name: isRunning
479
543
  ? `${chalk.blue('⌘')} Open shell`
@@ -496,10 +560,16 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
496
560
  disabled: !isRunning ? false : 'Stop container first',
497
561
  },
498
562
  { name: `${chalk.magenta('⎘')} Copy connection string`, value: 'copy' },
499
- { name: `${chalk.red('✕')} Delete container`, value: 'delete' },
563
+ {
564
+ name: !isRunning
565
+ ? `${chalk.red('✕')} Delete container`
566
+ : chalk.gray('✕ Delete container'),
567
+ value: 'delete',
568
+ disabled: !isRunning ? false : 'Stop container first',
569
+ },
500
570
  new inquirer.Separator(),
501
- { name: `${chalk.blue('←')} Back to container list`, value: 'back' },
502
- { name: `${chalk.blue('🏠')} Back to main menu`, value: 'main' },
571
+ { name: `${chalk.blue('←')} Back to containers`, value: 'back' },
572
+ { name: `${chalk.blue('')} Back to main menu`, value: 'main' },
503
573
  ]
504
574
 
505
575
  const { action } = await inquirer.prompt<{ action: string }>([
@@ -508,6 +578,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
508
578
  name: 'action',
509
579
  message: 'What would you like to do?',
510
580
  choices: actionChoices,
581
+ pageSize: 15,
511
582
  },
512
583
  ])
513
584
 
@@ -680,15 +751,222 @@ async function handleOpenShell(containerName: string): Promise<void> {
680
751
  const engine = getEngine(config.engine)
681
752
  const connectionString = engine.getConnectionString(config)
682
753
 
754
+ // Check which enhanced shells are installed
755
+ const usqlInstalled = await isUsqlInstalled()
756
+ const pgcliInstalled = await isPgcliInstalled()
757
+ const mycliInstalled = await isMycliInstalled()
758
+
759
+ type ShellChoice =
760
+ | 'default'
761
+ | 'usql'
762
+ | 'install-usql'
763
+ | 'pgcli'
764
+ | 'install-pgcli'
765
+ | 'mycli'
766
+ | 'install-mycli'
767
+ | 'back'
768
+
769
+ const defaultShellName = config.engine === 'mysql' ? 'mysql' : 'psql'
770
+ const engineSpecificCli = config.engine === 'mysql' ? 'mycli' : 'pgcli'
771
+ const engineSpecificInstalled =
772
+ config.engine === 'mysql' ? mycliInstalled : pgcliInstalled
773
+
774
+ const choices: Array<{ name: string; value: ShellChoice }> = [
775
+ {
776
+ name: `>_ Use default shell (${defaultShellName})`,
777
+ value: 'default',
778
+ },
779
+ ]
780
+
781
+ // Engine-specific enhanced CLI (pgcli for PostgreSQL, mycli for MySQL)
782
+ if (engineSpecificInstalled) {
783
+ choices.push({
784
+ name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
785
+ value: config.engine === 'mysql' ? 'mycli' : 'pgcli',
786
+ })
787
+ } else {
788
+ choices.push({
789
+ name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
790
+ value: config.engine === 'mysql' ? 'install-mycli' : 'install-pgcli',
791
+ })
792
+ }
793
+
794
+ // usql - universal option
795
+ if (usqlInstalled) {
796
+ choices.push({
797
+ name: '⚡ Use usql (universal SQL client)',
798
+ value: 'usql',
799
+ })
800
+ } else {
801
+ choices.push({
802
+ name: '↓ Install usql (universal SQL client)',
803
+ value: 'install-usql',
804
+ })
805
+ }
806
+
807
+ choices.push({
808
+ name: `${chalk.blue('←')} Back`,
809
+ value: 'back',
810
+ })
811
+
812
+ const { shellChoice } = await inquirer.prompt<{ shellChoice: ShellChoice }>([
813
+ {
814
+ type: 'list',
815
+ name: 'shellChoice',
816
+ message: 'Select shell option:',
817
+ choices,
818
+ pageSize: 10,
819
+ },
820
+ ])
821
+
822
+ if (shellChoice === 'back') {
823
+ return
824
+ }
825
+
826
+ // Handle pgcli installation
827
+ if (shellChoice === 'install-pgcli') {
828
+ console.log()
829
+ console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
830
+ const pm = await detectPackageManager()
831
+ if (pm) {
832
+ const result = await installPgcli(pm)
833
+ if (result.success) {
834
+ console.log(success('pgcli installed successfully!'))
835
+ console.log()
836
+ await launchShell(containerName, config, connectionString, 'pgcli')
837
+ } else {
838
+ console.error(error(`Failed to install pgcli: ${result.error}`))
839
+ console.log()
840
+ console.log(chalk.gray('Manual installation:'))
841
+ for (const instruction of getPgcliManualInstructions()) {
842
+ console.log(chalk.cyan(` ${instruction}`))
843
+ }
844
+ console.log()
845
+ await pressEnterToContinue()
846
+ }
847
+ } else {
848
+ console.error(error('No supported package manager found'))
849
+ console.log()
850
+ console.log(chalk.gray('Manual installation:'))
851
+ for (const instruction of getPgcliManualInstructions()) {
852
+ console.log(chalk.cyan(` ${instruction}`))
853
+ }
854
+ console.log()
855
+ await pressEnterToContinue()
856
+ }
857
+ return
858
+ }
859
+
860
+ // Handle mycli installation
861
+ if (shellChoice === 'install-mycli') {
862
+ console.log()
863
+ console.log(info('Installing mycli for enhanced MySQL shell...'))
864
+ const pm = await detectPackageManager()
865
+ if (pm) {
866
+ const result = await installMycli(pm)
867
+ if (result.success) {
868
+ console.log(success('mycli installed successfully!'))
869
+ console.log()
870
+ await launchShell(containerName, config, connectionString, 'mycli')
871
+ } else {
872
+ console.error(error(`Failed to install mycli: ${result.error}`))
873
+ console.log()
874
+ console.log(chalk.gray('Manual installation:'))
875
+ for (const instruction of getMycliManualInstructions()) {
876
+ console.log(chalk.cyan(` ${instruction}`))
877
+ }
878
+ console.log()
879
+ await pressEnterToContinue()
880
+ }
881
+ } else {
882
+ console.error(error('No supported package manager found'))
883
+ console.log()
884
+ console.log(chalk.gray('Manual installation:'))
885
+ for (const instruction of getMycliManualInstructions()) {
886
+ console.log(chalk.cyan(` ${instruction}`))
887
+ }
888
+ console.log()
889
+ await pressEnterToContinue()
890
+ }
891
+ return
892
+ }
893
+
894
+ // Handle usql installation
895
+ if (shellChoice === 'install-usql') {
896
+ console.log()
897
+ console.log(info('Installing usql for enhanced shell experience...'))
898
+ const pm = await detectPackageManager()
899
+ if (pm) {
900
+ const result = await installUsql(pm)
901
+ if (result.success) {
902
+ console.log(success('usql installed successfully!'))
903
+ console.log()
904
+ await launchShell(containerName, config, connectionString, 'usql')
905
+ } else {
906
+ console.error(error(`Failed to install usql: ${result.error}`))
907
+ console.log()
908
+ console.log(chalk.gray('Manual installation:'))
909
+ for (const instruction of getUsqlManualInstructions()) {
910
+ console.log(chalk.cyan(` ${instruction}`))
911
+ }
912
+ console.log()
913
+ await pressEnterToContinue()
914
+ }
915
+ } else {
916
+ console.error(error('No supported package manager found'))
917
+ console.log()
918
+ console.log(chalk.gray('Manual installation:'))
919
+ for (const instruction of getUsqlManualInstructions()) {
920
+ console.log(chalk.cyan(` ${instruction}`))
921
+ }
922
+ console.log()
923
+ await pressEnterToContinue()
924
+ }
925
+ return
926
+ }
927
+
928
+ // Launch the selected shell
929
+ await launchShell(containerName, config, connectionString, shellChoice)
930
+ }
931
+
932
+ async function launchShell(
933
+ containerName: string,
934
+ config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
935
+ connectionString: string,
936
+ shellType: 'default' | 'usql' | 'pgcli' | 'mycli',
937
+ ): Promise<void> {
683
938
  console.log(info(`Connecting to ${containerName}...`))
684
939
  console.log()
685
940
 
686
- // Determine shell command based on engine
941
+ // Determine shell command based on engine and shell type
687
942
  let shellCmd: string
688
943
  let shellArgs: string[]
689
944
  let installHint: string
690
945
 
691
- if (config.engine === 'mysql') {
946
+ if (shellType === 'pgcli') {
947
+ // pgcli accepts connection strings
948
+ shellCmd = 'pgcli'
949
+ shellArgs = [connectionString]
950
+ installHint = 'brew install pgcli'
951
+ } else if (shellType === 'mycli') {
952
+ // mycli: mycli -h host -P port -u user database
953
+ shellCmd = 'mycli'
954
+ shellArgs = [
955
+ '-h',
956
+ '127.0.0.1',
957
+ '-P',
958
+ String(config.port),
959
+ '-u',
960
+ 'root',
961
+ config.database,
962
+ ]
963
+ installHint = 'brew install mycli'
964
+ } else if (shellType === 'usql') {
965
+ // usql accepts connection strings directly for both PostgreSQL and MySQL
966
+ shellCmd = 'usql'
967
+ shellArgs = [connectionString]
968
+ installHint = 'brew tap xo/xo && brew install xo/xo/usql'
969
+ } else if (config.engine === 'mysql') {
692
970
  shellCmd = 'mysql'
693
971
  // MySQL connection: mysql -u root -h 127.0.0.1 -P port database
694
972
  shellArgs = [
@@ -719,7 +997,7 @@ async function handleOpenShell(containerName: string): Promise<void> {
719
997
  console.log(chalk.gray(' Connect manually with:'))
720
998
  console.log(chalk.cyan(` ${connectionString}`))
721
999
  console.log()
722
- console.log(chalk.gray(` Install ${config.engine} client:`))
1000
+ console.log(chalk.gray(` Install ${shellCmd}:`))
723
1001
  console.log(chalk.cyan(` ${installHint}`))
724
1002
  }
725
1003
  })
@@ -842,7 +1120,7 @@ async function handleRestore(): Promise<void> {
842
1120
  // Build choices: running containers + create new option
843
1121
  const choices = [
844
1122
  ...running.map((c) => ({
845
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
1123
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || ''} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
846
1124
  value: c.name,
847
1125
  short: c.name,
848
1126
  })),
@@ -862,6 +1140,7 @@ async function handleRestore(): Promise<void> {
862
1140
  name: 'selectedContainer',
863
1141
  message: 'Select container to restore to:',
864
1142
  choices,
1143
+ pageSize: 15,
865
1144
  },
866
1145
  ])
867
1146
 
@@ -1056,7 +1335,7 @@ async function handleRestore(): Promise<void> {
1056
1335
  backupPath = stripQuotes(rawBackupPath)
1057
1336
  }
1058
1337
 
1059
- const databaseName = await promptDatabaseName(containerName)
1338
+ const databaseName = await promptDatabaseName(containerName, config.engine)
1060
1339
 
1061
1340
  const engine = getEngine(config.engine)
1062
1341
 
@@ -1280,6 +1559,148 @@ async function handleRestore(): Promise<void> {
1280
1559
  ])
1281
1560
  }
1282
1561
 
1562
+ /**
1563
+ * Generate a timestamp string for backup filenames
1564
+ */
1565
+ function generateBackupTimestamp(): string {
1566
+ const now = new Date()
1567
+ return now.toISOString().replace(/:/g, '').split('.')[0]
1568
+ }
1569
+
1570
+ /**
1571
+ * Get file extension for backup format
1572
+ */
1573
+ function getBackupExtension(format: 'sql' | 'dump', engine: string): string {
1574
+ if (format === 'sql') {
1575
+ return '.sql'
1576
+ }
1577
+ return engine === 'mysql' ? '.sql.gz' : '.dump'
1578
+ }
1579
+
1580
+ async function handleBackup(): Promise<void> {
1581
+ const containers = await containerManager.list()
1582
+ const running = containers.filter((c) => c.status === 'running')
1583
+
1584
+ if (running.length === 0) {
1585
+ console.log(warning('No running containers. Start a container first.'))
1586
+ await inquirer.prompt([
1587
+ {
1588
+ type: 'input',
1589
+ name: 'continue',
1590
+ message: chalk.gray('Press Enter to continue...'),
1591
+ },
1592
+ ])
1593
+ return
1594
+ }
1595
+
1596
+ // Select container
1597
+ const containerName = await promptContainerSelect(
1598
+ running,
1599
+ 'Select container to backup:',
1600
+ )
1601
+ if (!containerName) return
1602
+
1603
+ const config = await containerManager.getConfig(containerName)
1604
+ if (!config) {
1605
+ console.log(error(`Container "${containerName}" not found`))
1606
+ return
1607
+ }
1608
+
1609
+ const engine = getEngine(config.engine)
1610
+
1611
+ // Check for required tools
1612
+ const depsSpinner = createSpinner('Checking required tools...')
1613
+ depsSpinner.start()
1614
+
1615
+ let missingDeps = await getMissingDependencies(config.engine)
1616
+ if (missingDeps.length > 0) {
1617
+ depsSpinner.warn(
1618
+ `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
1619
+ )
1620
+
1621
+ const installed = await promptInstallDependencies(
1622
+ missingDeps[0].binary,
1623
+ config.engine,
1624
+ )
1625
+
1626
+ if (!installed) {
1627
+ return
1628
+ }
1629
+
1630
+ missingDeps = await getMissingDependencies(config.engine)
1631
+ if (missingDeps.length > 0) {
1632
+ console.log(
1633
+ error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
1634
+ )
1635
+ return
1636
+ }
1637
+
1638
+ console.log(chalk.green(' ✓ All required tools are now available'))
1639
+ console.log()
1640
+ } else {
1641
+ depsSpinner.succeed('Required tools available')
1642
+ }
1643
+
1644
+ // Select database
1645
+ const databases = config.databases || [config.database]
1646
+ let databaseName: string
1647
+
1648
+ if (databases.length > 1) {
1649
+ databaseName = await promptDatabaseSelect(databases, 'Select database to backup:')
1650
+ } else {
1651
+ databaseName = databases[0]
1652
+ }
1653
+
1654
+ // Select format
1655
+ const format = await promptBackupFormat(config.engine)
1656
+
1657
+ // Get filename
1658
+ const defaultFilename = `${containerName}-${databaseName}-backup-${generateBackupTimestamp()}`
1659
+ const filename = await promptBackupFilename(defaultFilename)
1660
+
1661
+ // Build output path
1662
+ const extension = getBackupExtension(format, config.engine)
1663
+ const outputPath = join(process.cwd(), `${filename}${extension}`)
1664
+
1665
+ // Create backup
1666
+ const backupSpinner = createSpinner(
1667
+ `Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
1668
+ )
1669
+ backupSpinner.start()
1670
+
1671
+ try {
1672
+ const result = await engine.backup(config, outputPath, {
1673
+ database: databaseName,
1674
+ format,
1675
+ })
1676
+
1677
+ backupSpinner.succeed('Backup created successfully')
1678
+
1679
+ console.log()
1680
+ console.log(success('Backup complete'))
1681
+ console.log()
1682
+ console.log(chalk.gray(' File:'), chalk.cyan(result.path))
1683
+ console.log(chalk.gray(' Size:'), chalk.white(formatBytes(result.size)))
1684
+ console.log(chalk.gray(' Format:'), chalk.white(result.format))
1685
+ console.log()
1686
+ } catch (err) {
1687
+ const e = err as Error
1688
+ backupSpinner.fail('Backup failed')
1689
+ console.log()
1690
+ console.log(error(e.message))
1691
+ console.log()
1692
+ }
1693
+
1694
+ // Wait for user to see the result
1695
+ await inquirer.prompt([
1696
+ {
1697
+ type: 'input',
1698
+ name: 'continue',
1699
+ message: chalk.gray('Press Enter to continue...'),
1700
+ },
1701
+ ])
1702
+ }
1703
+
1283
1704
  async function handleClone(): Promise<void> {
1284
1705
  const containers = await containerManager.list()
1285
1706
  const stopped = containers.filter((c) => c.status !== 'running')
@@ -1437,7 +1858,7 @@ async function handleEditContainer(
1437
1858
  },
1438
1859
  new inquirer.Separator(),
1439
1860
  { name: `${chalk.blue('←')} Back to container`, value: 'back' },
1440
- { name: `${chalk.blue('🏠')} Back to main menu`, value: 'main' },
1861
+ { name: `${chalk.blue('')} Back to main menu`, value: 'main' },
1441
1862
  ]
1442
1863
 
1443
1864
  const { field } = await inquirer.prompt<{ field: string }>([
@@ -1446,6 +1867,7 @@ async function handleEditContainer(
1446
1867
  name: 'field',
1447
1868
  message: 'Select field to edit:',
1448
1869
  choices: editChoices,
1870
+ pageSize: 10,
1449
1871
  },
1450
1872
  ])
1451
1873
 
@@ -1755,14 +2177,6 @@ function compareVersions(a: string, b: string): number {
1755
2177
  return 0
1756
2178
  }
1757
2179
 
1758
- function formatBytes(bytes: number): string {
1759
- if (bytes === 0) return '0 B'
1760
- const k = 1024
1761
- const sizes = ['B', 'KB', 'MB', 'GB']
1762
- const i = Math.floor(Math.log(bytes) / Math.log(k))
1763
- return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
1764
- }
1765
-
1766
2180
  async function handleEngines(): Promise<void> {
1767
2181
  console.clear()
1768
2182
  console.log(header('Installed Engines'))
@@ -1809,7 +2223,7 @@ async function handleEngines(): Promise<void> {
1809
2223
 
1810
2224
  // PostgreSQL rows
1811
2225
  for (const engine of pgEngines) {
1812
- const icon = engineIcons[engine.engine] || '🗄️'
2226
+ const icon = engineIcons[engine.engine] || ''
1813
2227
  const platformInfo = `${engine.platform}-${engine.arch}`
1814
2228
 
1815
2229
  console.log(
@@ -246,7 +246,7 @@ export const restoreCommand = new Command('restore')
246
246
  // Get database name
247
247
  let databaseName = options.database
248
248
  if (!databaseName) {
249
- databaseName = await promptDatabaseName(containerName)
249
+ databaseName = await promptDatabaseName(containerName, engineName)
250
250
  }
251
251
 
252
252
  // At this point backupPath is guaranteed to be set
@@ -271,6 +271,9 @@ export const restoreCommand = new Command('restore')
271
271
  await engine.createDatabase(config, databaseName)
272
272
  dbSpinner.succeed(`Database "${databaseName}" ready`)
273
273
 
274
+ // Add database to container's databases array
275
+ await containerManager.addDatabase(containerName, databaseName)
276
+
274
277
  // Restore backup
275
278
  const restoreSpinner = createSpinner('Restoring backup...')
276
279
  restoreSpinner.start()
package/cli/index.ts CHANGED
@@ -5,6 +5,7 @@ import { startCommand } from './commands/start'
5
5
  import { stopCommand } from './commands/stop'
6
6
  import { deleteCommand } from './commands/delete'
7
7
  import { restoreCommand } from './commands/restore'
8
+ import { backupCommand } from './commands/backup'
8
9
  import { connectCommand } from './commands/connect'
9
10
  import { cloneCommand } from './commands/clone'
10
11
  import { menuCommand } from './commands/menu'
@@ -27,6 +28,7 @@ export async function run(): Promise<void> {
27
28
  program.addCommand(stopCommand)
28
29
  program.addCommand(deleteCommand)
29
30
  program.addCommand(restoreCommand)
31
+ program.addCommand(backupCommand)
30
32
  program.addCommand(connectCommand)
31
33
  program.addCommand(cloneCommand)
32
34
  program.addCommand(menuCommand)