spindb 0.5.3 → 0.5.4
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 +51 -1
- package/cli/commands/connect.ts +336 -111
- package/cli/commands/engines.ts +1 -1
- package/cli/commands/info.ts +3 -3
- package/cli/commands/list.ts +1 -1
- package/cli/commands/menu.ts +257 -15
- package/cli/commands/restore.ts +1 -1
- package/cli/ui/prompts.ts +12 -6
- package/cli/ui/theme.ts +1 -1
- package/config/os-dependencies.ts +92 -0
- package/core/binary-manager.ts +12 -19
- package/core/dependency-manager.ts +135 -0
- package/engines/postgresql/index.ts +31 -1
- package/package.json +1 -1
package/cli/commands/menu.ts
CHANGED
|
@@ -33,7 +33,19 @@ import { defaults } from '../../config/defaults'
|
|
|
33
33
|
import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
|
|
34
34
|
import type { EngineName } from '../../types'
|
|
35
35
|
import inquirer from 'inquirer'
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
getMissingDependencies,
|
|
38
|
+
isUsqlInstalled,
|
|
39
|
+
isPgcliInstalled,
|
|
40
|
+
isMycliInstalled,
|
|
41
|
+
detectPackageManager,
|
|
42
|
+
installUsql,
|
|
43
|
+
installPgcli,
|
|
44
|
+
installMycli,
|
|
45
|
+
getUsqlManualInstructions,
|
|
46
|
+
getPgcliManualInstructions,
|
|
47
|
+
getMycliManualInstructions,
|
|
48
|
+
} from '../../core/dependency-manager'
|
|
37
49
|
import {
|
|
38
50
|
getMysqldPath,
|
|
39
51
|
getMysqlVersion,
|
|
@@ -57,6 +69,19 @@ const engineIcons: Record<string, string> = {
|
|
|
57
69
|
mysql: '🐬',
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Helper to pause and wait for user to press Enter
|
|
74
|
+
*/
|
|
75
|
+
async function pressEnterToContinue(): Promise<void> {
|
|
76
|
+
await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'continue',
|
|
80
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
81
|
+
},
|
|
82
|
+
])
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
async function showMainMenu(): Promise<void> {
|
|
61
86
|
console.clear()
|
|
62
87
|
console.log(header('SpinDB - Local Database Manager'))
|
|
@@ -88,12 +113,12 @@ async function showMainMenu(): Promise<void> {
|
|
|
88
113
|
const choices: MenuChoice[] = [
|
|
89
114
|
...(hasContainers
|
|
90
115
|
? [
|
|
91
|
-
{ name: `${chalk.cyan('◉')}
|
|
116
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
92
117
|
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
93
118
|
]
|
|
94
119
|
: [
|
|
95
120
|
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
96
|
-
{ name: `${chalk.cyan('◉')}
|
|
121
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
97
122
|
]),
|
|
98
123
|
{
|
|
99
124
|
name: canStart
|
|
@@ -415,7 +440,7 @@ async function handleList(): Promise<void> {
|
|
|
415
440
|
console.log()
|
|
416
441
|
const containerChoices = [
|
|
417
442
|
...containers.map((c) => ({
|
|
418
|
-
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '
|
|
443
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
419
444
|
c.status === 'running'
|
|
420
445
|
? chalk.green('● running')
|
|
421
446
|
: chalk.gray('○ stopped')
|
|
@@ -435,6 +460,7 @@ async function handleList(): Promise<void> {
|
|
|
435
460
|
name: 'selectedContainer',
|
|
436
461
|
message: 'Select a container for more options:',
|
|
437
462
|
choices: containerChoices,
|
|
463
|
+
pageSize: 15,
|
|
438
464
|
},
|
|
439
465
|
])
|
|
440
466
|
|
|
@@ -473,7 +499,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
|
|
|
473
499
|
// Start or Stop depending on current state
|
|
474
500
|
!isRunning
|
|
475
501
|
? { name: `${chalk.green('▶')} Start container`, value: 'start' }
|
|
476
|
-
: { name: `${chalk.
|
|
502
|
+
: { name: `${chalk.red('■')} Stop container`, value: 'stop' },
|
|
477
503
|
{
|
|
478
504
|
name: isRunning
|
|
479
505
|
? `${chalk.blue('⌘')} Open shell`
|
|
@@ -496,10 +522,16 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
|
|
|
496
522
|
disabled: !isRunning ? false : 'Stop container first',
|
|
497
523
|
},
|
|
498
524
|
{ name: `${chalk.magenta('⎘')} Copy connection string`, value: 'copy' },
|
|
499
|
-
{
|
|
525
|
+
{
|
|
526
|
+
name: !isRunning
|
|
527
|
+
? `${chalk.red('✕')} Delete container`
|
|
528
|
+
: chalk.gray('✕ Delete container'),
|
|
529
|
+
value: 'delete',
|
|
530
|
+
disabled: !isRunning ? false : 'Stop container first',
|
|
531
|
+
},
|
|
500
532
|
new inquirer.Separator(),
|
|
501
|
-
{ name: `${chalk.blue('←')} Back to
|
|
502
|
-
{ name: `${chalk.blue('
|
|
533
|
+
{ name: `${chalk.blue('←')} Back to containers`, value: 'back' },
|
|
534
|
+
{ name: `${chalk.blue('⌂')} Back to main menu`, value: 'main' },
|
|
503
535
|
]
|
|
504
536
|
|
|
505
537
|
const { action } = await inquirer.prompt<{ action: string }>([
|
|
@@ -508,6 +540,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
|
|
|
508
540
|
name: 'action',
|
|
509
541
|
message: 'What would you like to do?',
|
|
510
542
|
choices: actionChoices,
|
|
543
|
+
pageSize: 15,
|
|
511
544
|
},
|
|
512
545
|
])
|
|
513
546
|
|
|
@@ -680,15 +713,222 @@ async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
680
713
|
const engine = getEngine(config.engine)
|
|
681
714
|
const connectionString = engine.getConnectionString(config)
|
|
682
715
|
|
|
716
|
+
// Check which enhanced shells are installed
|
|
717
|
+
const usqlInstalled = await isUsqlInstalled()
|
|
718
|
+
const pgcliInstalled = await isPgcliInstalled()
|
|
719
|
+
const mycliInstalled = await isMycliInstalled()
|
|
720
|
+
|
|
721
|
+
type ShellChoice =
|
|
722
|
+
| 'default'
|
|
723
|
+
| 'usql'
|
|
724
|
+
| 'install-usql'
|
|
725
|
+
| 'pgcli'
|
|
726
|
+
| 'install-pgcli'
|
|
727
|
+
| 'mycli'
|
|
728
|
+
| 'install-mycli'
|
|
729
|
+
| 'back'
|
|
730
|
+
|
|
731
|
+
const defaultShellName = config.engine === 'mysql' ? 'mysql' : 'psql'
|
|
732
|
+
const engineSpecificCli = config.engine === 'mysql' ? 'mycli' : 'pgcli'
|
|
733
|
+
const engineSpecificInstalled =
|
|
734
|
+
config.engine === 'mysql' ? mycliInstalled : pgcliInstalled
|
|
735
|
+
|
|
736
|
+
const choices: Array<{ name: string; value: ShellChoice }> = [
|
|
737
|
+
{
|
|
738
|
+
name: `>_ Use default shell (${defaultShellName})`,
|
|
739
|
+
value: 'default',
|
|
740
|
+
},
|
|
741
|
+
]
|
|
742
|
+
|
|
743
|
+
// Engine-specific enhanced CLI (pgcli for PostgreSQL, mycli for MySQL)
|
|
744
|
+
if (engineSpecificInstalled) {
|
|
745
|
+
choices.push({
|
|
746
|
+
name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
|
|
747
|
+
value: config.engine === 'mysql' ? 'mycli' : 'pgcli',
|
|
748
|
+
})
|
|
749
|
+
} else {
|
|
750
|
+
choices.push({
|
|
751
|
+
name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
|
|
752
|
+
value: config.engine === 'mysql' ? 'install-mycli' : 'install-pgcli',
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// usql - universal option
|
|
757
|
+
if (usqlInstalled) {
|
|
758
|
+
choices.push({
|
|
759
|
+
name: '⚡ Use usql (universal SQL client)',
|
|
760
|
+
value: 'usql',
|
|
761
|
+
})
|
|
762
|
+
} else {
|
|
763
|
+
choices.push({
|
|
764
|
+
name: '↓ Install usql (universal SQL client)',
|
|
765
|
+
value: 'install-usql',
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
choices.push({
|
|
770
|
+
name: `${chalk.blue('←')} Back`,
|
|
771
|
+
value: 'back',
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
const { shellChoice } = await inquirer.prompt<{ shellChoice: ShellChoice }>([
|
|
775
|
+
{
|
|
776
|
+
type: 'list',
|
|
777
|
+
name: 'shellChoice',
|
|
778
|
+
message: 'Select shell option:',
|
|
779
|
+
choices,
|
|
780
|
+
pageSize: 10,
|
|
781
|
+
},
|
|
782
|
+
])
|
|
783
|
+
|
|
784
|
+
if (shellChoice === 'back') {
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Handle pgcli installation
|
|
789
|
+
if (shellChoice === 'install-pgcli') {
|
|
790
|
+
console.log()
|
|
791
|
+
console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
|
|
792
|
+
const pm = await detectPackageManager()
|
|
793
|
+
if (pm) {
|
|
794
|
+
const result = await installPgcli(pm)
|
|
795
|
+
if (result.success) {
|
|
796
|
+
console.log(success('pgcli installed successfully!'))
|
|
797
|
+
console.log()
|
|
798
|
+
await launchShell(containerName, config, connectionString, 'pgcli')
|
|
799
|
+
} else {
|
|
800
|
+
console.error(error(`Failed to install pgcli: ${result.error}`))
|
|
801
|
+
console.log()
|
|
802
|
+
console.log(chalk.gray('Manual installation:'))
|
|
803
|
+
for (const instruction of getPgcliManualInstructions()) {
|
|
804
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
805
|
+
}
|
|
806
|
+
console.log()
|
|
807
|
+
await pressEnterToContinue()
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
console.error(error('No supported package manager found'))
|
|
811
|
+
console.log()
|
|
812
|
+
console.log(chalk.gray('Manual installation:'))
|
|
813
|
+
for (const instruction of getPgcliManualInstructions()) {
|
|
814
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
815
|
+
}
|
|
816
|
+
console.log()
|
|
817
|
+
await pressEnterToContinue()
|
|
818
|
+
}
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Handle mycli installation
|
|
823
|
+
if (shellChoice === 'install-mycli') {
|
|
824
|
+
console.log()
|
|
825
|
+
console.log(info('Installing mycli for enhanced MySQL shell...'))
|
|
826
|
+
const pm = await detectPackageManager()
|
|
827
|
+
if (pm) {
|
|
828
|
+
const result = await installMycli(pm)
|
|
829
|
+
if (result.success) {
|
|
830
|
+
console.log(success('mycli installed successfully!'))
|
|
831
|
+
console.log()
|
|
832
|
+
await launchShell(containerName, config, connectionString, 'mycli')
|
|
833
|
+
} else {
|
|
834
|
+
console.error(error(`Failed to install mycli: ${result.error}`))
|
|
835
|
+
console.log()
|
|
836
|
+
console.log(chalk.gray('Manual installation:'))
|
|
837
|
+
for (const instruction of getMycliManualInstructions()) {
|
|
838
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
839
|
+
}
|
|
840
|
+
console.log()
|
|
841
|
+
await pressEnterToContinue()
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
console.error(error('No supported package manager found'))
|
|
845
|
+
console.log()
|
|
846
|
+
console.log(chalk.gray('Manual installation:'))
|
|
847
|
+
for (const instruction of getMycliManualInstructions()) {
|
|
848
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
849
|
+
}
|
|
850
|
+
console.log()
|
|
851
|
+
await pressEnterToContinue()
|
|
852
|
+
}
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Handle usql installation
|
|
857
|
+
if (shellChoice === 'install-usql') {
|
|
858
|
+
console.log()
|
|
859
|
+
console.log(info('Installing usql for enhanced shell experience...'))
|
|
860
|
+
const pm = await detectPackageManager()
|
|
861
|
+
if (pm) {
|
|
862
|
+
const result = await installUsql(pm)
|
|
863
|
+
if (result.success) {
|
|
864
|
+
console.log(success('usql installed successfully!'))
|
|
865
|
+
console.log()
|
|
866
|
+
await launchShell(containerName, config, connectionString, 'usql')
|
|
867
|
+
} else {
|
|
868
|
+
console.error(error(`Failed to install usql: ${result.error}`))
|
|
869
|
+
console.log()
|
|
870
|
+
console.log(chalk.gray('Manual installation:'))
|
|
871
|
+
for (const instruction of getUsqlManualInstructions()) {
|
|
872
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
873
|
+
}
|
|
874
|
+
console.log()
|
|
875
|
+
await pressEnterToContinue()
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
console.error(error('No supported package manager found'))
|
|
879
|
+
console.log()
|
|
880
|
+
console.log(chalk.gray('Manual installation:'))
|
|
881
|
+
for (const instruction of getUsqlManualInstructions()) {
|
|
882
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
883
|
+
}
|
|
884
|
+
console.log()
|
|
885
|
+
await pressEnterToContinue()
|
|
886
|
+
}
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Launch the selected shell
|
|
891
|
+
await launchShell(containerName, config, connectionString, shellChoice)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function launchShell(
|
|
895
|
+
containerName: string,
|
|
896
|
+
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
|
|
897
|
+
connectionString: string,
|
|
898
|
+
shellType: 'default' | 'usql' | 'pgcli' | 'mycli',
|
|
899
|
+
): Promise<void> {
|
|
683
900
|
console.log(info(`Connecting to ${containerName}...`))
|
|
684
901
|
console.log()
|
|
685
902
|
|
|
686
|
-
// Determine shell command based on engine
|
|
903
|
+
// Determine shell command based on engine and shell type
|
|
687
904
|
let shellCmd: string
|
|
688
905
|
let shellArgs: string[]
|
|
689
906
|
let installHint: string
|
|
690
907
|
|
|
691
|
-
if (
|
|
908
|
+
if (shellType === 'pgcli') {
|
|
909
|
+
// pgcli accepts connection strings
|
|
910
|
+
shellCmd = 'pgcli'
|
|
911
|
+
shellArgs = [connectionString]
|
|
912
|
+
installHint = 'brew install pgcli'
|
|
913
|
+
} else if (shellType === 'mycli') {
|
|
914
|
+
// mycli: mycli -h host -P port -u user database
|
|
915
|
+
shellCmd = 'mycli'
|
|
916
|
+
shellArgs = [
|
|
917
|
+
'-h',
|
|
918
|
+
'127.0.0.1',
|
|
919
|
+
'-P',
|
|
920
|
+
String(config.port),
|
|
921
|
+
'-u',
|
|
922
|
+
'root',
|
|
923
|
+
config.database,
|
|
924
|
+
]
|
|
925
|
+
installHint = 'brew install mycli'
|
|
926
|
+
} else if (shellType === 'usql') {
|
|
927
|
+
// usql accepts connection strings directly for both PostgreSQL and MySQL
|
|
928
|
+
shellCmd = 'usql'
|
|
929
|
+
shellArgs = [connectionString]
|
|
930
|
+
installHint = 'brew tap xo/xo && brew install xo/xo/usql'
|
|
931
|
+
} else if (config.engine === 'mysql') {
|
|
692
932
|
shellCmd = 'mysql'
|
|
693
933
|
// MySQL connection: mysql -u root -h 127.0.0.1 -P port database
|
|
694
934
|
shellArgs = [
|
|
@@ -719,7 +959,7 @@ async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
719
959
|
console.log(chalk.gray(' Connect manually with:'))
|
|
720
960
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
721
961
|
console.log()
|
|
722
|
-
console.log(chalk.gray(` Install ${
|
|
962
|
+
console.log(chalk.gray(` Install ${shellCmd}:`))
|
|
723
963
|
console.log(chalk.cyan(` ${installHint}`))
|
|
724
964
|
}
|
|
725
965
|
})
|
|
@@ -842,7 +1082,7 @@ async function handleRestore(): Promise<void> {
|
|
|
842
1082
|
// Build choices: running containers + create new option
|
|
843
1083
|
const choices = [
|
|
844
1084
|
...running.map((c) => ({
|
|
845
|
-
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '
|
|
1085
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
|
|
846
1086
|
value: c.name,
|
|
847
1087
|
short: c.name,
|
|
848
1088
|
})),
|
|
@@ -862,6 +1102,7 @@ async function handleRestore(): Promise<void> {
|
|
|
862
1102
|
name: 'selectedContainer',
|
|
863
1103
|
message: 'Select container to restore to:',
|
|
864
1104
|
choices,
|
|
1105
|
+
pageSize: 15,
|
|
865
1106
|
},
|
|
866
1107
|
])
|
|
867
1108
|
|
|
@@ -1056,7 +1297,7 @@ async function handleRestore(): Promise<void> {
|
|
|
1056
1297
|
backupPath = stripQuotes(rawBackupPath)
|
|
1057
1298
|
}
|
|
1058
1299
|
|
|
1059
|
-
const databaseName = await promptDatabaseName(containerName)
|
|
1300
|
+
const databaseName = await promptDatabaseName(containerName, config.engine)
|
|
1060
1301
|
|
|
1061
1302
|
const engine = getEngine(config.engine)
|
|
1062
1303
|
|
|
@@ -1437,7 +1678,7 @@ async function handleEditContainer(
|
|
|
1437
1678
|
},
|
|
1438
1679
|
new inquirer.Separator(),
|
|
1439
1680
|
{ name: `${chalk.blue('←')} Back to container`, value: 'back' },
|
|
1440
|
-
{ name: `${chalk.blue('
|
|
1681
|
+
{ name: `${chalk.blue('⌂')} Back to main menu`, value: 'main' },
|
|
1441
1682
|
]
|
|
1442
1683
|
|
|
1443
1684
|
const { field } = await inquirer.prompt<{ field: string }>([
|
|
@@ -1446,6 +1687,7 @@ async function handleEditContainer(
|
|
|
1446
1687
|
name: 'field',
|
|
1447
1688
|
message: 'Select field to edit:',
|
|
1448
1689
|
choices: editChoices,
|
|
1690
|
+
pageSize: 10,
|
|
1449
1691
|
},
|
|
1450
1692
|
])
|
|
1451
1693
|
|
|
@@ -1809,7 +2051,7 @@ async function handleEngines(): Promise<void> {
|
|
|
1809
2051
|
|
|
1810
2052
|
// PostgreSQL rows
|
|
1811
2053
|
for (const engine of pgEngines) {
|
|
1812
|
-
const icon = engineIcons[engine.engine] || '
|
|
2054
|
+
const icon = engineIcons[engine.engine] || '▣'
|
|
1813
2055
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
1814
2056
|
|
|
1815
2057
|
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
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -53,7 +53,7 @@ export async function promptEngine(): Promise<string> {
|
|
|
53
53
|
|
|
54
54
|
// Build choices from available engines
|
|
55
55
|
const choices = engines.map((e) => ({
|
|
56
|
-
name: `${engineIcons[e.name] || '
|
|
56
|
+
name: `${engineIcons[e.name] || '▣'} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
|
|
57
57
|
value: e.name,
|
|
58
58
|
short: e.displayName,
|
|
59
59
|
}))
|
|
@@ -227,7 +227,7 @@ export async function promptContainerSelect(
|
|
|
227
227
|
name: 'container',
|
|
228
228
|
message,
|
|
229
229
|
choices: containers.map((c) => ({
|
|
230
|
-
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '
|
|
230
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
231
231
|
c.status === 'running'
|
|
232
232
|
? chalk.green('● running')
|
|
233
233
|
: chalk.gray('○ stopped')
|
|
@@ -243,19 +243,25 @@ export async function promptContainerSelect(
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* Prompt for database name
|
|
246
|
+
* @param defaultName - Default value for the database name
|
|
247
|
+
* @param engine - Database engine (mysql shows "schema" terminology)
|
|
246
248
|
*/
|
|
247
249
|
export async function promptDatabaseName(
|
|
248
250
|
defaultName?: string,
|
|
251
|
+
engine?: string,
|
|
249
252
|
): Promise<string> {
|
|
253
|
+
// MySQL uses "schema" terminology (database and schema are synonymous)
|
|
254
|
+
const label = engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
|
|
255
|
+
|
|
250
256
|
const { database } = await inquirer.prompt<{ database: string }>([
|
|
251
257
|
{
|
|
252
258
|
type: 'input',
|
|
253
259
|
name: 'database',
|
|
254
|
-
message:
|
|
260
|
+
message: label,
|
|
255
261
|
default: defaultName,
|
|
256
262
|
validate: (input: string) => {
|
|
257
263
|
if (!input) return 'Database name is required'
|
|
258
|
-
// PostgreSQL database naming rules
|
|
264
|
+
// PostgreSQL database naming rules (also valid for MySQL)
|
|
259
265
|
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(input)) {
|
|
260
266
|
return 'Database name must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens'
|
|
261
267
|
}
|
|
@@ -282,12 +288,12 @@ export type CreateOptions = {
|
|
|
282
288
|
* Full interactive create flow
|
|
283
289
|
*/
|
|
284
290
|
export async function promptCreateOptions(): Promise<CreateOptions> {
|
|
285
|
-
console.log(chalk.cyan('\n
|
|
291
|
+
console.log(chalk.cyan('\n ▣ Create New Database Container\n'))
|
|
286
292
|
|
|
287
293
|
const engine = await promptEngine()
|
|
288
294
|
const version = await promptVersion(engine)
|
|
289
295
|
const name = await promptContainerName()
|
|
290
|
-
const database = await promptDatabaseName(name) // Default to container name
|
|
296
|
+
const database = await promptDatabaseName(name, engine) // Default to container name
|
|
291
297
|
|
|
292
298
|
// Get engine-specific default port
|
|
293
299
|
const engineDefaults = getEngineDefaults(engine)
|
package/cli/ui/theme.ts
CHANGED
|
@@ -289,6 +289,98 @@ const mysqlDependencies: EngineDependencies = {
|
|
|
289
289
|
],
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// =============================================================================
|
|
293
|
+
// Optional Tools (engine-agnostic)
|
|
294
|
+
// =============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* usql - Universal SQL client
|
|
298
|
+
* Works with PostgreSQL, MySQL, SQLite, and 20+ other databases
|
|
299
|
+
* https://github.com/xo/usql
|
|
300
|
+
*/
|
|
301
|
+
export const usqlDependency: Dependency = {
|
|
302
|
+
name: 'usql',
|
|
303
|
+
binary: 'usql',
|
|
304
|
+
description:
|
|
305
|
+
'Universal SQL client with auto-completion, syntax highlighting, and multi-database support',
|
|
306
|
+
packages: {
|
|
307
|
+
brew: {
|
|
308
|
+
package: 'xo/xo/usql',
|
|
309
|
+
preInstall: ['brew tap xo/xo'],
|
|
310
|
+
},
|
|
311
|
+
// Note: usql is not in standard Linux package repos, must use manual install
|
|
312
|
+
},
|
|
313
|
+
manualInstall: {
|
|
314
|
+
darwin: [
|
|
315
|
+
'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
316
|
+
'Then run: brew tap xo/xo && brew install xo/xo/usql',
|
|
317
|
+
],
|
|
318
|
+
linux: [
|
|
319
|
+
'Download from GitHub releases: https://github.com/xo/usql/releases',
|
|
320
|
+
'Extract and move to PATH: sudo mv usql /usr/local/bin/',
|
|
321
|
+
'Or install via Go: go install github.com/xo/usql@latest',
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* pgcli - PostgreSQL CLI with auto-completion and syntax highlighting
|
|
328
|
+
* https://github.com/dbcli/pgcli
|
|
329
|
+
*/
|
|
330
|
+
export const pgcliDependency: Dependency = {
|
|
331
|
+
name: 'pgcli',
|
|
332
|
+
binary: 'pgcli',
|
|
333
|
+
description:
|
|
334
|
+
'PostgreSQL CLI with intelligent auto-completion and syntax highlighting',
|
|
335
|
+
packages: {
|
|
336
|
+
brew: { package: 'pgcli' },
|
|
337
|
+
apt: { package: 'pgcli' },
|
|
338
|
+
dnf: { package: 'pgcli' },
|
|
339
|
+
yum: { package: 'pgcli' },
|
|
340
|
+
pacman: { package: 'pgcli' },
|
|
341
|
+
},
|
|
342
|
+
manualInstall: {
|
|
343
|
+
darwin: [
|
|
344
|
+
'Install with Homebrew: brew install pgcli',
|
|
345
|
+
'Or with pip: pip install pgcli',
|
|
346
|
+
],
|
|
347
|
+
linux: [
|
|
348
|
+
'Debian/Ubuntu: sudo apt install pgcli',
|
|
349
|
+
'Fedora: sudo dnf install pgcli',
|
|
350
|
+
'Or with pip: pip install pgcli',
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* mycli - MySQL CLI with auto-completion and syntax highlighting
|
|
357
|
+
* https://github.com/dbcli/mycli
|
|
358
|
+
*/
|
|
359
|
+
export const mycliDependency: Dependency = {
|
|
360
|
+
name: 'mycli',
|
|
361
|
+
binary: 'mycli',
|
|
362
|
+
description:
|
|
363
|
+
'MySQL/MariaDB CLI with intelligent auto-completion and syntax highlighting',
|
|
364
|
+
packages: {
|
|
365
|
+
brew: { package: 'mycli' },
|
|
366
|
+
apt: { package: 'mycli' },
|
|
367
|
+
dnf: { package: 'mycli' },
|
|
368
|
+
yum: { package: 'mycli' },
|
|
369
|
+
pacman: { package: 'mycli' },
|
|
370
|
+
},
|
|
371
|
+
manualInstall: {
|
|
372
|
+
darwin: [
|
|
373
|
+
'Install with Homebrew: brew install mycli',
|
|
374
|
+
'Or with pip: pip install mycli',
|
|
375
|
+
],
|
|
376
|
+
linux: [
|
|
377
|
+
'Debian/Ubuntu: sudo apt install mycli',
|
|
378
|
+
'Fedora: sudo dnf install mycli',
|
|
379
|
+
'Or with pip: pip install mycli',
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
|
|
292
384
|
// =============================================================================
|
|
293
385
|
// Registry
|
|
294
386
|
// =============================================================================
|
package/core/binary-manager.ts
CHANGED
|
@@ -54,26 +54,19 @@ export class BinaryManager {
|
|
|
54
54
|
return version
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
* Get major version from any version string (e.g., "17.7.0" -> "17", "16" -> "16")
|
|
59
|
-
* Used for directory naming to ensure one directory per major version.
|
|
60
|
-
*/
|
|
61
|
-
getMajorVersion(version: string): string {
|
|
62
|
-
return version.split('.')[0]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
57
|
/**
|
|
66
58
|
* Check if binaries for a specific version are already installed
|
|
59
|
+
* Uses full version for directory naming (e.g., postgresql-17.7.0-darwin-arm64)
|
|
67
60
|
*/
|
|
68
61
|
async isInstalled(
|
|
69
62
|
version: string,
|
|
70
63
|
platform: string,
|
|
71
64
|
arch: string,
|
|
72
65
|
): Promise<boolean> {
|
|
73
|
-
const
|
|
66
|
+
const fullVersion = this.getFullVersion(version)
|
|
74
67
|
const binPath = paths.getBinaryPath({
|
|
75
68
|
engine: 'postgresql',
|
|
76
|
-
version:
|
|
69
|
+
version: fullVersion,
|
|
77
70
|
platform,
|
|
78
71
|
arch,
|
|
79
72
|
})
|
|
@@ -122,15 +115,15 @@ export class BinaryManager {
|
|
|
122
115
|
arch: string,
|
|
123
116
|
onProgress?: ProgressCallback,
|
|
124
117
|
): Promise<string> {
|
|
125
|
-
const
|
|
118
|
+
const fullVersion = this.getFullVersion(version)
|
|
126
119
|
const url = this.getDownloadUrl(version, platform, arch)
|
|
127
120
|
const binPath = paths.getBinaryPath({
|
|
128
121
|
engine: 'postgresql',
|
|
129
|
-
version:
|
|
122
|
+
version: fullVersion,
|
|
130
123
|
platform,
|
|
131
124
|
arch,
|
|
132
125
|
})
|
|
133
|
-
const tempDir = join(paths.bin, `temp-${
|
|
126
|
+
const tempDir = join(paths.bin, `temp-${fullVersion}-${platform}-${arch}`)
|
|
134
127
|
const jarFile = join(tempDir, 'postgres.jar')
|
|
135
128
|
|
|
136
129
|
// Ensure directories exist
|
|
@@ -210,10 +203,10 @@ export class BinaryManager {
|
|
|
210
203
|
platform: string,
|
|
211
204
|
arch: string,
|
|
212
205
|
): Promise<boolean> {
|
|
213
|
-
const
|
|
206
|
+
const fullVersion = this.getFullVersion(version)
|
|
214
207
|
const binPath = paths.getBinaryPath({
|
|
215
208
|
engine: 'postgresql',
|
|
216
|
-
version:
|
|
209
|
+
version: fullVersion,
|
|
217
210
|
platform,
|
|
218
211
|
arch,
|
|
219
212
|
})
|
|
@@ -267,10 +260,10 @@ export class BinaryManager {
|
|
|
267
260
|
arch: string,
|
|
268
261
|
binary: string,
|
|
269
262
|
): string {
|
|
270
|
-
const
|
|
263
|
+
const fullVersion = this.getFullVersion(version)
|
|
271
264
|
const binPath = paths.getBinaryPath({
|
|
272
265
|
engine: 'postgresql',
|
|
273
|
-
version:
|
|
266
|
+
version: fullVersion,
|
|
274
267
|
platform,
|
|
275
268
|
arch,
|
|
276
269
|
})
|
|
@@ -286,7 +279,7 @@ export class BinaryManager {
|
|
|
286
279
|
arch: string,
|
|
287
280
|
onProgress?: ProgressCallback,
|
|
288
281
|
): Promise<string> {
|
|
289
|
-
const
|
|
282
|
+
const fullVersion = this.getFullVersion(version)
|
|
290
283
|
if (await this.isInstalled(version, platform, arch)) {
|
|
291
284
|
onProgress?.({
|
|
292
285
|
stage: 'cached',
|
|
@@ -294,7 +287,7 @@ export class BinaryManager {
|
|
|
294
287
|
})
|
|
295
288
|
return paths.getBinaryPath({
|
|
296
289
|
engine: 'postgresql',
|
|
297
|
-
version:
|
|
290
|
+
version: fullVersion,
|
|
298
291
|
platform,
|
|
299
292
|
arch,
|
|
300
293
|
})
|