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.
- package/README.md +78 -2
- package/cli/commands/backup.ts +263 -0
- package/cli/commands/config.ts +144 -66
- package/cli/commands/connect.ts +336 -111
- package/cli/commands/engines.ts +2 -10
- package/cli/commands/info.ts +3 -3
- package/cli/commands/list.ts +43 -5
- package/cli/commands/menu.ts +447 -33
- package/cli/commands/restore.ts +4 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +99 -6
- package/cli/ui/theme.ts +12 -1
- package/config/os-dependencies.ts +92 -0
- package/core/binary-manager.ts +12 -19
- package/core/config-manager.ts +133 -37
- package/core/container-manager.ts +76 -2
- package/core/dependency-manager.ts +140 -0
- package/engines/base-engine.ts +20 -0
- package/engines/mysql/backup.ts +159 -0
- package/engines/mysql/index.ts +39 -0
- package/engines/mysql/restore.ts +16 -2
- package/engines/postgresql/backup.ts +93 -0
- package/engines/postgresql/index.ts +68 -1
- package/package.json +1 -1
- package/types/index.ts +20 -0
package/cli/commands/menu.ts
CHANGED
|
@@ -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 {
|
|
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('◉')}
|
|
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('◉')}
|
|
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(
|
|
438
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)))
|
|
386
439
|
|
|
387
440
|
// Table rows
|
|
388
|
-
for (
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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.
|
|
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
|
-
{
|
|
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
|
|
502
|
-
{ name: `${chalk.blue('
|
|
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 (
|
|
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 ${
|
|
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] || '
|
|
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('
|
|
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(
|
package/cli/commands/restore.ts
CHANGED
|
@@ -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)
|