spindb 0.5.2 → 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 +188 -9
- package/cli/commands/connect.ts +334 -105
- package/cli/commands/create.ts +106 -67
- package/cli/commands/deps.ts +19 -4
- package/cli/commands/edit.ts +245 -0
- package/cli/commands/engines.ts +434 -0
- package/cli/commands/info.ts +279 -0
- package/cli/commands/list.ts +1 -1
- package/cli/commands/menu.ts +664 -167
- package/cli/commands/restore.ts +11 -25
- package/cli/commands/start.ts +25 -20
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +20 -12
- package/cli/ui/theme.ts +1 -1
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +151 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +12 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +144 -15
- package/core/error-handler.ts +336 -0
- package/core/platform-service.ts +634 -0
- package/core/port-manager.ts +11 -3
- package/core/process-manager.ts +12 -2
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/mysql/binary-detection.ts +177 -100
- package/engines/mysql/index.ts +240 -131
- package/engines/mysql/restore.ts +257 -0
- package/engines/mysql/version-validator.ts +373 -0
- package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
- package/engines/postgresql/binary-urls.ts +5 -3
- package/engines/postgresql/index.ts +35 -4
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +6 -2
- package/cli/commands/postgres-tools.ts +0 -216
package/cli/commands/menu.ts
CHANGED
|
@@ -22,15 +22,36 @@ import {
|
|
|
22
22
|
} from '../ui/theme'
|
|
23
23
|
import { existsSync } from 'fs'
|
|
24
24
|
import { readdir, rm, lstat } from 'fs/promises'
|
|
25
|
-
import { spawn } from 'child_process'
|
|
26
|
-
import {
|
|
25
|
+
import { spawn, exec } from 'child_process'
|
|
26
|
+
import { promisify } from 'util'
|
|
27
|
+
import { tmpdir } from 'os'
|
|
27
28
|
import { join } from 'path'
|
|
28
29
|
import { paths } from '../../config/paths'
|
|
30
|
+
import { platformService } from '../../core/platform-service'
|
|
29
31
|
import { portManager } from '../../core/port-manager'
|
|
30
32
|
import { defaults } from '../../config/defaults'
|
|
33
|
+
import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
|
|
31
34
|
import type { EngineName } from '../../types'
|
|
32
35
|
import inquirer from 'inquirer'
|
|
33
|
-
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'
|
|
49
|
+
import {
|
|
50
|
+
getMysqldPath,
|
|
51
|
+
getMysqlVersion,
|
|
52
|
+
isMariaDB,
|
|
53
|
+
getMysqlInstallInfo,
|
|
54
|
+
} from '../../engines/mysql/binary-detection'
|
|
34
55
|
|
|
35
56
|
type MenuChoice =
|
|
36
57
|
| {
|
|
@@ -48,6 +69,19 @@ const engineIcons: Record<string, string> = {
|
|
|
48
69
|
mysql: '🐬',
|
|
49
70
|
}
|
|
50
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
|
+
|
|
51
85
|
async function showMainMenu(): Promise<void> {
|
|
52
86
|
console.clear()
|
|
53
87
|
console.log(header('SpinDB - Local Database Manager'))
|
|
@@ -79,12 +113,12 @@ async function showMainMenu(): Promise<void> {
|
|
|
79
113
|
const choices: MenuChoice[] = [
|
|
80
114
|
...(hasContainers
|
|
81
115
|
? [
|
|
82
|
-
{ name: `${chalk.cyan('◉')}
|
|
116
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
83
117
|
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
84
118
|
]
|
|
85
119
|
: [
|
|
86
120
|
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
87
|
-
{ name: `${chalk.cyan('◉')}
|
|
121
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
88
122
|
]),
|
|
89
123
|
{
|
|
90
124
|
name: canStart
|
|
@@ -95,7 +129,7 @@ async function showMainMenu(): Promise<void> {
|
|
|
95
129
|
},
|
|
96
130
|
{
|
|
97
131
|
name: canStop
|
|
98
|
-
? `${chalk.
|
|
132
|
+
? `${chalk.red('■')} Stop a container`
|
|
99
133
|
: chalk.gray('■ Stop a container'),
|
|
100
134
|
value: 'stop',
|
|
101
135
|
disabled: canStop ? false : 'No running containers',
|
|
@@ -202,7 +236,9 @@ async function handleCreate(): Promise<void> {
|
|
|
202
236
|
missingDeps = await getMissingDependencies(engine)
|
|
203
237
|
if (missingDeps.length > 0) {
|
|
204
238
|
console.log(
|
|
205
|
-
error(
|
|
239
|
+
error(
|
|
240
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
241
|
+
),
|
|
206
242
|
)
|
|
207
243
|
return
|
|
208
244
|
}
|
|
@@ -301,25 +337,14 @@ async function handleCreate(): Promise<void> {
|
|
|
301
337
|
console.log(chalk.gray(' Connection string:'))
|
|
302
338
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
303
339
|
|
|
304
|
-
// Copy connection string to clipboard using platform
|
|
340
|
+
// Copy connection string to clipboard using platform service
|
|
305
341
|
try {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
})
|
|
313
|
-
proc.stdin?.write(connectionString)
|
|
314
|
-
proc.stdin?.end()
|
|
315
|
-
proc.on('close', (code) => {
|
|
316
|
-
if (code === 0) resolve()
|
|
317
|
-
else reject(new Error(`Clipboard command exited with code ${code}`))
|
|
318
|
-
})
|
|
319
|
-
proc.on('error', reject)
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
|
|
342
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
343
|
+
if (copied) {
|
|
344
|
+
console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
|
|
345
|
+
} else {
|
|
346
|
+
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
347
|
+
}
|
|
323
348
|
} catch {
|
|
324
349
|
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
325
350
|
}
|
|
@@ -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
|
|
|
@@ -648,50 +681,26 @@ async function handleCopyConnectionString(
|
|
|
648
681
|
const engine = getEngine(config.engine)
|
|
649
682
|
const connectionString = engine.getConnectionString(config)
|
|
650
683
|
|
|
651
|
-
// Copy to clipboard using platform
|
|
652
|
-
const
|
|
653
|
-
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
654
|
-
const args = platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
await new Promise<void>((resolve, reject) => {
|
|
658
|
-
const proc = spawn(cmd, args, { stdio: ['pipe', 'inherit', 'inherit'] })
|
|
659
|
-
proc.stdin?.write(connectionString)
|
|
660
|
-
proc.stdin?.end()
|
|
661
|
-
proc.on('close', (code) => {
|
|
662
|
-
if (code === 0) resolve()
|
|
663
|
-
else reject(new Error(`Clipboard command exited with code ${code}`))
|
|
664
|
-
})
|
|
665
|
-
proc.on('error', reject)
|
|
666
|
-
})
|
|
684
|
+
// Copy to clipboard using platform service
|
|
685
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
667
686
|
|
|
668
|
-
|
|
687
|
+
console.log()
|
|
688
|
+
if (copied) {
|
|
669
689
|
console.log(success('Connection string copied to clipboard'))
|
|
670
690
|
console.log(chalk.gray(` ${connectionString}`))
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
await inquirer.prompt([
|
|
674
|
-
{
|
|
675
|
-
type: 'input',
|
|
676
|
-
name: 'continue',
|
|
677
|
-
message: chalk.gray('Press Enter to continue...'),
|
|
678
|
-
},
|
|
679
|
-
])
|
|
680
|
-
} catch {
|
|
681
|
-
// Fallback: just display the string
|
|
682
|
-
console.log()
|
|
691
|
+
} else {
|
|
683
692
|
console.log(warning('Could not copy to clipboard. Connection string:'))
|
|
684
693
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
685
|
-
console.log()
|
|
686
|
-
|
|
687
|
-
await inquirer.prompt([
|
|
688
|
-
{
|
|
689
|
-
type: 'input',
|
|
690
|
-
name: 'continue',
|
|
691
|
-
message: chalk.gray('Press Enter to continue...'),
|
|
692
|
-
},
|
|
693
|
-
])
|
|
694
694
|
}
|
|
695
|
+
console.log()
|
|
696
|
+
|
|
697
|
+
await inquirer.prompt([
|
|
698
|
+
{
|
|
699
|
+
type: 'input',
|
|
700
|
+
name: 'continue',
|
|
701
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
702
|
+
},
|
|
703
|
+
])
|
|
695
704
|
}
|
|
696
705
|
|
|
697
706
|
async function handleOpenShell(containerName: string): Promise<void> {
|
|
@@ -704,15 +713,222 @@ async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
704
713
|
const engine = getEngine(config.engine)
|
|
705
714
|
const connectionString = engine.getConnectionString(config)
|
|
706
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> {
|
|
707
900
|
console.log(info(`Connecting to ${containerName}...`))
|
|
708
901
|
console.log()
|
|
709
902
|
|
|
710
|
-
// Determine shell command based on engine
|
|
903
|
+
// Determine shell command based on engine and shell type
|
|
711
904
|
let shellCmd: string
|
|
712
905
|
let shellArgs: string[]
|
|
713
906
|
let installHint: string
|
|
714
907
|
|
|
715
|
-
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') {
|
|
716
932
|
shellCmd = 'mysql'
|
|
717
933
|
// MySQL connection: mysql -u root -h 127.0.0.1 -P port database
|
|
718
934
|
shellArgs = [
|
|
@@ -743,7 +959,7 @@ async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
743
959
|
console.log(chalk.gray(' Connect manually with:'))
|
|
744
960
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
745
961
|
console.log()
|
|
746
|
-
console.log(chalk.gray(` Install ${
|
|
962
|
+
console.log(chalk.gray(` Install ${shellCmd}:`))
|
|
747
963
|
console.log(chalk.cyan(` ${installHint}`))
|
|
748
964
|
}
|
|
749
965
|
})
|
|
@@ -866,7 +1082,7 @@ async function handleRestore(): Promise<void> {
|
|
|
866
1082
|
// Build choices: running containers + create new option
|
|
867
1083
|
const choices = [
|
|
868
1084
|
...running.map((c) => ({
|
|
869
|
-
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')}`,
|
|
870
1086
|
value: c.name,
|
|
871
1087
|
short: c.name,
|
|
872
1088
|
})),
|
|
@@ -886,6 +1102,7 @@ async function handleRestore(): Promise<void> {
|
|
|
886
1102
|
name: 'selectedContainer',
|
|
887
1103
|
message: 'Select container to restore to:',
|
|
888
1104
|
choices,
|
|
1105
|
+
pageSize: 15,
|
|
889
1106
|
},
|
|
890
1107
|
])
|
|
891
1108
|
|
|
@@ -931,7 +1148,9 @@ async function handleRestore(): Promise<void> {
|
|
|
931
1148
|
missingDeps = await getMissingDependencies(config.engine)
|
|
932
1149
|
if (missingDeps.length > 0) {
|
|
933
1150
|
console.log(
|
|
934
|
-
error(
|
|
1151
|
+
error(
|
|
1152
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
1153
|
+
),
|
|
935
1154
|
)
|
|
936
1155
|
return
|
|
937
1156
|
}
|
|
@@ -1078,7 +1297,7 @@ async function handleRestore(): Promise<void> {
|
|
|
1078
1297
|
backupPath = stripQuotes(rawBackupPath)
|
|
1079
1298
|
}
|
|
1080
1299
|
|
|
1081
|
-
const databaseName = await promptDatabaseName(containerName)
|
|
1300
|
+
const databaseName = await promptDatabaseName(containerName, config.engine)
|
|
1082
1301
|
|
|
1083
1302
|
const engine = getEngine(config.engine)
|
|
1084
1303
|
|
|
@@ -1168,7 +1387,7 @@ async function handleRestore(): Promise<void> {
|
|
|
1168
1387
|
|
|
1169
1388
|
try {
|
|
1170
1389
|
const { updatePostgresClientTools } = await import(
|
|
1171
|
-
'../../
|
|
1390
|
+
'../../engines/postgresql/binary-manager'
|
|
1172
1391
|
)
|
|
1173
1392
|
const updateSuccess = await updatePostgresClientTools()
|
|
1174
1393
|
|
|
@@ -1189,24 +1408,26 @@ async function handleRestore(): Promise<void> {
|
|
|
1189
1408
|
console.log(
|
|
1190
1409
|
error('Automatic upgrade failed. Please upgrade manually:'),
|
|
1191
1410
|
)
|
|
1411
|
+
const pgPackage = getPostgresHomebrewPackage()
|
|
1412
|
+
const latestMajor = pgPackage.split('@')[1]
|
|
1192
1413
|
console.log(
|
|
1193
1414
|
warning(
|
|
1194
|
-
|
|
1415
|
+
` macOS: brew install ${pgPackage} && brew link --force ${pgPackage}`,
|
|
1195
1416
|
),
|
|
1196
1417
|
)
|
|
1197
1418
|
console.log(
|
|
1198
1419
|
chalk.gray(
|
|
1199
|
-
|
|
1420
|
+
` This installs PostgreSQL ${latestMajor} client tools: pg_restore, pg_dump, psql, and libpq`,
|
|
1200
1421
|
),
|
|
1201
1422
|
)
|
|
1202
1423
|
console.log(
|
|
1203
1424
|
warning(
|
|
1204
|
-
|
|
1425
|
+
` Ubuntu/Debian: sudo apt update && sudo apt install postgresql-client-${latestMajor}`,
|
|
1205
1426
|
),
|
|
1206
1427
|
)
|
|
1207
1428
|
console.log(
|
|
1208
1429
|
chalk.gray(
|
|
1209
|
-
|
|
1430
|
+
` This installs PostgreSQL ${latestMajor} client tools: pg_restore, pg_dump, psql, and libpq`,
|
|
1210
1431
|
),
|
|
1211
1432
|
)
|
|
1212
1433
|
await new Promise((resolve) => {
|
|
@@ -1270,24 +1491,11 @@ async function handleRestore(): Promise<void> {
|
|
|
1270
1491
|
console.log(chalk.gray(' Connection string:'))
|
|
1271
1492
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
1272
1493
|
|
|
1273
|
-
// Copy connection string to clipboard using platform
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const args = platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
1277
|
-
|
|
1278
|
-
await new Promise<void>((resolve, reject) => {
|
|
1279
|
-
const proc = spawn(cmd, args, { stdio: ['pipe', 'inherit', 'inherit'] })
|
|
1280
|
-
proc.stdin?.write(connectionString)
|
|
1281
|
-
proc.stdin?.end()
|
|
1282
|
-
proc.on('close', (code) => {
|
|
1283
|
-
if (code === 0) resolve()
|
|
1284
|
-
else reject(new Error(`Clipboard command exited with code ${code}`))
|
|
1285
|
-
})
|
|
1286
|
-
proc.on('error', reject)
|
|
1287
|
-
})
|
|
1288
|
-
|
|
1494
|
+
// Copy connection string to clipboard using platform service
|
|
1495
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
1496
|
+
if (copied) {
|
|
1289
1497
|
console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
|
|
1290
|
-
}
|
|
1498
|
+
} else {
|
|
1291
1499
|
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
1292
1500
|
}
|
|
1293
1501
|
|
|
@@ -1389,7 +1597,9 @@ async function handleStartContainer(containerName: string): Promise<void> {
|
|
|
1389
1597
|
),
|
|
1390
1598
|
)
|
|
1391
1599
|
console.log(
|
|
1392
|
-
info(
|
|
1600
|
+
info(
|
|
1601
|
+
'Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb',
|
|
1602
|
+
),
|
|
1393
1603
|
)
|
|
1394
1604
|
return
|
|
1395
1605
|
}
|
|
@@ -1468,7 +1678,7 @@ async function handleEditContainer(
|
|
|
1468
1678
|
},
|
|
1469
1679
|
new inquirer.Separator(),
|
|
1470
1680
|
{ name: `${chalk.blue('←')} Back to container`, value: 'back' },
|
|
1471
|
-
{ name: `${chalk.blue('
|
|
1681
|
+
{ name: `${chalk.blue('⌂')} Back to main menu`, value: 'main' },
|
|
1472
1682
|
]
|
|
1473
1683
|
|
|
1474
1684
|
const { field } = await inquirer.prompt<{ field: string }>([
|
|
@@ -1477,6 +1687,7 @@ async function handleEditContainer(
|
|
|
1477
1687
|
name: 'field',
|
|
1478
1688
|
message: 'Select field to edit:',
|
|
1479
1689
|
choices: editChoices,
|
|
1690
|
+
pageSize: 10,
|
|
1480
1691
|
},
|
|
1481
1692
|
])
|
|
1482
1693
|
|
|
@@ -1643,72 +1854,135 @@ async function handleDelete(containerName: string): Promise<void> {
|
|
|
1643
1854
|
deleteSpinner.succeed(`Container "${containerName}" deleted`)
|
|
1644
1855
|
}
|
|
1645
1856
|
|
|
1646
|
-
type
|
|
1647
|
-
engine:
|
|
1857
|
+
type InstalledPostgresEngine = {
|
|
1858
|
+
engine: 'postgresql'
|
|
1648
1859
|
version: string
|
|
1649
1860
|
platform: string
|
|
1650
1861
|
arch: string
|
|
1651
1862
|
path: string
|
|
1652
1863
|
sizeBytes: number
|
|
1864
|
+
source: 'downloaded'
|
|
1653
1865
|
}
|
|
1654
1866
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1867
|
+
type InstalledMysqlEngine = {
|
|
1868
|
+
engine: 'mysql'
|
|
1869
|
+
version: string
|
|
1870
|
+
path: string
|
|
1871
|
+
source: 'system'
|
|
1872
|
+
isMariaDB: boolean
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
|
|
1876
|
+
|
|
1877
|
+
const execAsync = promisify(exec)
|
|
1657
1878
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1879
|
+
/**
|
|
1880
|
+
* Get the actual PostgreSQL version from the binary
|
|
1881
|
+
*/
|
|
1882
|
+
async function getPostgresVersionFromBinary(
|
|
1883
|
+
binPath: string,
|
|
1884
|
+
): Promise<string | null> {
|
|
1885
|
+
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
1886
|
+
if (!existsSync(postgresPath)) {
|
|
1887
|
+
return null
|
|
1660
1888
|
}
|
|
1661
1889
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1890
|
+
try {
|
|
1891
|
+
const { stdout } = await execAsync(`"${postgresPath}" --version`)
|
|
1892
|
+
// Output: postgres (PostgreSQL) 17.7
|
|
1893
|
+
const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
|
|
1894
|
+
return match ? match[1] : null
|
|
1895
|
+
} catch {
|
|
1896
|
+
return null
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1664
1899
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
// Parse directory name: postgresql-17-darwin-arm64
|
|
1668
|
-
const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
|
|
1669
|
-
if (match) {
|
|
1670
|
-
const [, engine, version, platform, arch] = match
|
|
1671
|
-
const dirPath = join(binDir, entry.name)
|
|
1900
|
+
async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
1901
|
+
const engines: InstalledEngine[] = []
|
|
1672
1902
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1903
|
+
// Get PostgreSQL engines from ~/.spindb/bin/
|
|
1904
|
+
const binDir = paths.bin
|
|
1905
|
+
if (existsSync(binDir)) {
|
|
1906
|
+
const entries = await readdir(binDir, { withFileTypes: true })
|
|
1907
|
+
|
|
1908
|
+
for (const entry of entries) {
|
|
1909
|
+
if (entry.isDirectory()) {
|
|
1910
|
+
// Parse directory name: postgresql-17-darwin-arm64
|
|
1911
|
+
const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
|
|
1912
|
+
if (match && match[1] === 'postgresql') {
|
|
1913
|
+
const [, , majorVersion, platform, arch] = match
|
|
1914
|
+
const dirPath = join(binDir, entry.name)
|
|
1915
|
+
|
|
1916
|
+
// Get actual version from the binary
|
|
1917
|
+
const actualVersion =
|
|
1918
|
+
(await getPostgresVersionFromBinary(dirPath)) || majorVersion
|
|
1919
|
+
|
|
1920
|
+
// Get directory size (using lstat to avoid following symlinks)
|
|
1921
|
+
let sizeBytes = 0
|
|
1922
|
+
try {
|
|
1923
|
+
const files = await readdir(dirPath, { recursive: true })
|
|
1924
|
+
for (const file of files) {
|
|
1925
|
+
try {
|
|
1926
|
+
const filePath = join(dirPath, file.toString())
|
|
1927
|
+
const fileStat = await lstat(filePath)
|
|
1928
|
+
// Only count regular files (not symlinks or directories)
|
|
1929
|
+
if (fileStat.isFile()) {
|
|
1930
|
+
sizeBytes += fileStat.size
|
|
1931
|
+
}
|
|
1932
|
+
} catch {
|
|
1933
|
+
// Skip files we can't stat
|
|
1684
1934
|
}
|
|
1685
|
-
} catch {
|
|
1686
|
-
// Skip files we can't stat
|
|
1687
1935
|
}
|
|
1936
|
+
} catch {
|
|
1937
|
+
// Skip directories we can't read
|
|
1688
1938
|
}
|
|
1689
|
-
} catch {
|
|
1690
|
-
// Skip directories we can't read
|
|
1691
|
-
}
|
|
1692
1939
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1940
|
+
engines.push({
|
|
1941
|
+
engine: 'postgresql',
|
|
1942
|
+
version: actualVersion,
|
|
1943
|
+
platform,
|
|
1944
|
+
arch,
|
|
1945
|
+
path: dirPath,
|
|
1946
|
+
sizeBytes,
|
|
1947
|
+
source: 'downloaded',
|
|
1948
|
+
})
|
|
1949
|
+
}
|
|
1701
1950
|
}
|
|
1702
1951
|
}
|
|
1703
1952
|
}
|
|
1704
1953
|
|
|
1705
|
-
//
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1954
|
+
// Detect system-installed MySQL
|
|
1955
|
+
const mysqldPath = await getMysqldPath()
|
|
1956
|
+
if (mysqldPath) {
|
|
1957
|
+
const version = await getMysqlVersion(mysqldPath)
|
|
1958
|
+
if (version) {
|
|
1959
|
+
const mariadb = await isMariaDB()
|
|
1960
|
+
engines.push({
|
|
1961
|
+
engine: 'mysql',
|
|
1962
|
+
version,
|
|
1963
|
+
path: mysqldPath,
|
|
1964
|
+
source: 'system',
|
|
1965
|
+
isMariaDB: mariadb,
|
|
1966
|
+
})
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Sort PostgreSQL by version (descending), MySQL stays at end
|
|
1971
|
+
const pgEngines = engines.filter(
|
|
1972
|
+
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
1973
|
+
)
|
|
1974
|
+
const mysqlEngine = engines.find(
|
|
1975
|
+
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
pgEngines.sort((a, b) => compareVersions(b.version, a.version))
|
|
1979
|
+
|
|
1980
|
+
const result: InstalledEngine[] = [...pgEngines]
|
|
1981
|
+
if (mysqlEngine) {
|
|
1982
|
+
result.push(mysqlEngine)
|
|
1983
|
+
}
|
|
1710
1984
|
|
|
1711
|
-
return
|
|
1985
|
+
return result
|
|
1712
1986
|
}
|
|
1713
1987
|
|
|
1714
1988
|
function compareVersions(a: string, b: string): number {
|
|
@@ -1742,54 +2016,104 @@ async function handleEngines(): Promise<void> {
|
|
|
1742
2016
|
console.log(info('No engines installed yet.'))
|
|
1743
2017
|
console.log(
|
|
1744
2018
|
chalk.gray(
|
|
1745
|
-
'
|
|
2019
|
+
' PostgreSQL engines are downloaded automatically when you create a container.',
|
|
2020
|
+
),
|
|
2021
|
+
)
|
|
2022
|
+
console.log(
|
|
2023
|
+
chalk.gray(
|
|
2024
|
+
' MySQL requires system installation (brew install mysql or apt install mysql-server).',
|
|
1746
2025
|
),
|
|
1747
2026
|
)
|
|
1748
2027
|
return
|
|
1749
2028
|
}
|
|
1750
2029
|
|
|
1751
|
-
//
|
|
1752
|
-
const
|
|
2030
|
+
// Separate PostgreSQL and MySQL
|
|
2031
|
+
const pgEngines = engines.filter(
|
|
2032
|
+
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
2033
|
+
)
|
|
2034
|
+
const mysqlEngine = engines.find(
|
|
2035
|
+
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
2036
|
+
)
|
|
2037
|
+
|
|
2038
|
+
// Calculate total size for PostgreSQL
|
|
2039
|
+
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
1753
2040
|
|
|
1754
2041
|
// Table header
|
|
1755
2042
|
console.log()
|
|
1756
2043
|
console.log(
|
|
1757
2044
|
chalk.gray(' ') +
|
|
1758
|
-
chalk.bold.white('ENGINE'.padEnd(
|
|
2045
|
+
chalk.bold.white('ENGINE'.padEnd(14)) +
|
|
1759
2046
|
chalk.bold.white('VERSION'.padEnd(12)) +
|
|
1760
|
-
chalk.bold.white('
|
|
2047
|
+
chalk.bold.white('SOURCE'.padEnd(18)) +
|
|
1761
2048
|
chalk.bold.white('SIZE'),
|
|
1762
2049
|
)
|
|
1763
2050
|
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
1764
2051
|
|
|
1765
|
-
//
|
|
1766
|
-
for (const engine of
|
|
2052
|
+
// PostgreSQL rows
|
|
2053
|
+
for (const engine of pgEngines) {
|
|
2054
|
+
const icon = engineIcons[engine.engine] || '▣'
|
|
2055
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
2056
|
+
|
|
1767
2057
|
console.log(
|
|
1768
2058
|
chalk.gray(' ') +
|
|
1769
|
-
chalk.cyan(engine.engine
|
|
2059
|
+
chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
|
|
1770
2060
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
1771
|
-
chalk.gray(
|
|
2061
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
1772
2062
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
1773
2063
|
)
|
|
1774
2064
|
}
|
|
1775
2065
|
|
|
2066
|
+
// MySQL row
|
|
2067
|
+
if (mysqlEngine) {
|
|
2068
|
+
const icon = engineIcons.mysql
|
|
2069
|
+
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
2070
|
+
|
|
2071
|
+
console.log(
|
|
2072
|
+
chalk.gray(' ') +
|
|
2073
|
+
chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
|
|
2074
|
+
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
2075
|
+
chalk.gray('system'.padEnd(18)) +
|
|
2076
|
+
chalk.gray('(system-installed)'),
|
|
2077
|
+
)
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1776
2080
|
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2081
|
+
|
|
2082
|
+
// Summary
|
|
2083
|
+
console.log()
|
|
2084
|
+
if (pgEngines.length > 0) {
|
|
2085
|
+
console.log(
|
|
2086
|
+
chalk.gray(
|
|
2087
|
+
` PostgreSQL: ${pgEngines.length} version(s), ${formatBytes(totalPgSize)}`,
|
|
2088
|
+
),
|
|
2089
|
+
)
|
|
2090
|
+
}
|
|
2091
|
+
if (mysqlEngine) {
|
|
2092
|
+
console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
|
|
2093
|
+
}
|
|
1782
2094
|
console.log()
|
|
1783
2095
|
|
|
1784
|
-
// Menu options
|
|
1785
|
-
const choices: MenuChoice[] = [
|
|
1786
|
-
|
|
2096
|
+
// Menu options - only allow deletion of PostgreSQL engines
|
|
2097
|
+
const choices: MenuChoice[] = []
|
|
2098
|
+
|
|
2099
|
+
for (const e of pgEngines) {
|
|
2100
|
+
choices.push({
|
|
1787
2101
|
name: `${chalk.red('✕')} Delete ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
|
|
1788
2102
|
value: `delete:${e.path}:${e.engine}:${e.version}`,
|
|
1789
|
-
})
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2103
|
+
})
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// MySQL info option (not disabled, shows info icon)
|
|
2107
|
+
if (mysqlEngine) {
|
|
2108
|
+
const displayName = mysqlEngine.isMariaDB ? 'MariaDB' : 'MySQL'
|
|
2109
|
+
choices.push({
|
|
2110
|
+
name: `${chalk.blue('ℹ')} ${displayName} ${mysqlEngine.version} ${chalk.gray('(system-installed)')}`,
|
|
2111
|
+
value: `mysql-info:${mysqlEngine.path}`,
|
|
2112
|
+
})
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
choices.push(new inquirer.Separator())
|
|
2116
|
+
choices.push({ name: `${chalk.blue('←')} Back to main menu`, value: 'back' })
|
|
1793
2117
|
|
|
1794
2118
|
const { action } = await inquirer.prompt<{ action: string }>([
|
|
1795
2119
|
{
|
|
@@ -1811,6 +2135,13 @@ async function handleEngines(): Promise<void> {
|
|
|
1811
2135
|
// Return to engines menu
|
|
1812
2136
|
await handleEngines()
|
|
1813
2137
|
}
|
|
2138
|
+
|
|
2139
|
+
if (action.startsWith('mysql-info:')) {
|
|
2140
|
+
const mysqldPath = action.replace('mysql-info:', '')
|
|
2141
|
+
await handleMysqlInfo(mysqldPath)
|
|
2142
|
+
// Return to engines menu
|
|
2143
|
+
await handleEngines()
|
|
2144
|
+
}
|
|
1814
2145
|
}
|
|
1815
2146
|
|
|
1816
2147
|
async function handleDeleteEngine(
|
|
@@ -1869,6 +2200,174 @@ async function handleDeleteEngine(
|
|
|
1869
2200
|
}
|
|
1870
2201
|
}
|
|
1871
2202
|
|
|
2203
|
+
async function handleMysqlInfo(mysqldPath: string): Promise<void> {
|
|
2204
|
+
console.clear()
|
|
2205
|
+
|
|
2206
|
+
// Get install info
|
|
2207
|
+
const installInfo = await getMysqlInstallInfo(mysqldPath)
|
|
2208
|
+
const displayName = installInfo.isMariaDB ? 'MariaDB' : 'MySQL'
|
|
2209
|
+
|
|
2210
|
+
// Get version
|
|
2211
|
+
const version = await getMysqlVersion(mysqldPath)
|
|
2212
|
+
|
|
2213
|
+
console.log(header(`${displayName} Information`))
|
|
2214
|
+
console.log()
|
|
2215
|
+
|
|
2216
|
+
// Check for containers using MySQL
|
|
2217
|
+
const containers = await containerManager.list()
|
|
2218
|
+
const mysqlContainers = containers.filter((c) => c.engine === 'mysql')
|
|
2219
|
+
|
|
2220
|
+
// Track running containers for uninstall instructions
|
|
2221
|
+
const runningContainers: string[] = []
|
|
2222
|
+
|
|
2223
|
+
if (mysqlContainers.length > 0) {
|
|
2224
|
+
console.log(
|
|
2225
|
+
warning(
|
|
2226
|
+
`${mysqlContainers.length} container(s) are using ${displayName}:`,
|
|
2227
|
+
),
|
|
2228
|
+
)
|
|
2229
|
+
console.log()
|
|
2230
|
+
for (const c of mysqlContainers) {
|
|
2231
|
+
const isRunning = await processManager.isRunning(c.name, {
|
|
2232
|
+
engine: c.engine,
|
|
2233
|
+
})
|
|
2234
|
+
if (isRunning) {
|
|
2235
|
+
runningContainers.push(c.name)
|
|
2236
|
+
}
|
|
2237
|
+
const status = isRunning
|
|
2238
|
+
? chalk.green('● running')
|
|
2239
|
+
: chalk.gray('○ stopped')
|
|
2240
|
+
console.log(chalk.gray(` • ${c.name} ${status}`))
|
|
2241
|
+
}
|
|
2242
|
+
console.log()
|
|
2243
|
+
console.log(
|
|
2244
|
+
chalk.yellow(
|
|
2245
|
+
' Uninstalling will break these containers. Delete them first.',
|
|
2246
|
+
),
|
|
2247
|
+
)
|
|
2248
|
+
console.log()
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// Show installation details
|
|
2252
|
+
console.log(chalk.white(' Installation Details:'))
|
|
2253
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)))
|
|
2254
|
+
console.log(
|
|
2255
|
+
chalk.gray(' ') +
|
|
2256
|
+
chalk.white('Version:'.padEnd(18)) +
|
|
2257
|
+
chalk.yellow(version || 'unknown'),
|
|
2258
|
+
)
|
|
2259
|
+
console.log(
|
|
2260
|
+
chalk.gray(' ') +
|
|
2261
|
+
chalk.white('Binary Path:'.padEnd(18)) +
|
|
2262
|
+
chalk.gray(mysqldPath),
|
|
2263
|
+
)
|
|
2264
|
+
console.log(
|
|
2265
|
+
chalk.gray(' ') +
|
|
2266
|
+
chalk.white('Package Manager:'.padEnd(18)) +
|
|
2267
|
+
chalk.cyan(installInfo.packageManager),
|
|
2268
|
+
)
|
|
2269
|
+
console.log(
|
|
2270
|
+
chalk.gray(' ') +
|
|
2271
|
+
chalk.white('Package Name:'.padEnd(18)) +
|
|
2272
|
+
chalk.cyan(installInfo.packageName),
|
|
2273
|
+
)
|
|
2274
|
+
console.log()
|
|
2275
|
+
|
|
2276
|
+
// Uninstall instructions
|
|
2277
|
+
console.log(chalk.white(' To uninstall:'))
|
|
2278
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)))
|
|
2279
|
+
|
|
2280
|
+
let stepNum = 1
|
|
2281
|
+
|
|
2282
|
+
// Step: Stop running containers first
|
|
2283
|
+
if (runningContainers.length > 0) {
|
|
2284
|
+
console.log(chalk.gray(` # ${stepNum}. Stop running SpinDB containers`))
|
|
2285
|
+
console.log(chalk.cyan(' spindb stop <container-name>'))
|
|
2286
|
+
console.log()
|
|
2287
|
+
stepNum++
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// Step: Delete SpinDB containers
|
|
2291
|
+
if (mysqlContainers.length > 0) {
|
|
2292
|
+
console.log(chalk.gray(` # ${stepNum}. Delete SpinDB containers`))
|
|
2293
|
+
console.log(chalk.cyan(' spindb delete <container-name>'))
|
|
2294
|
+
console.log()
|
|
2295
|
+
stepNum++
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if (installInfo.packageManager === 'homebrew') {
|
|
2299
|
+
console.log(
|
|
2300
|
+
chalk.gray(
|
|
2301
|
+
` # ${stepNum}. Stop Homebrew service (if running separately)`,
|
|
2302
|
+
),
|
|
2303
|
+
)
|
|
2304
|
+
console.log(chalk.cyan(` brew services stop ${installInfo.packageName}`))
|
|
2305
|
+
console.log()
|
|
2306
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
2307
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
2308
|
+
} else if (installInfo.packageManager === 'apt') {
|
|
2309
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
2310
|
+
console.log(
|
|
2311
|
+
chalk.cyan(
|
|
2312
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
|
|
2313
|
+
),
|
|
2314
|
+
)
|
|
2315
|
+
console.log()
|
|
2316
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Disable auto-start on boot`))
|
|
2317
|
+
console.log(
|
|
2318
|
+
chalk.cyan(
|
|
2319
|
+
` sudo systemctl disable ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
|
|
2320
|
+
),
|
|
2321
|
+
)
|
|
2322
|
+
console.log()
|
|
2323
|
+
console.log(chalk.gray(` # ${stepNum + 2}. Uninstall the package`))
|
|
2324
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
2325
|
+
console.log()
|
|
2326
|
+
console.log(chalk.gray(` # ${stepNum + 3}. Remove data files (optional)`))
|
|
2327
|
+
console.log(
|
|
2328
|
+
chalk.cyan(' sudo apt purge mysql-server mysql-client mysql-common'),
|
|
2329
|
+
)
|
|
2330
|
+
console.log(chalk.cyan(' sudo rm -rf /var/lib/mysql /etc/mysql'))
|
|
2331
|
+
} else if (
|
|
2332
|
+
installInfo.packageManager === 'yum' ||
|
|
2333
|
+
installInfo.packageManager === 'dnf'
|
|
2334
|
+
) {
|
|
2335
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
2336
|
+
console.log(
|
|
2337
|
+
chalk.cyan(
|
|
2338
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
|
|
2339
|
+
),
|
|
2340
|
+
)
|
|
2341
|
+
console.log()
|
|
2342
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
2343
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
2344
|
+
} else if (installInfo.packageManager === 'pacman') {
|
|
2345
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
2346
|
+
console.log(
|
|
2347
|
+
chalk.cyan(
|
|
2348
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
|
|
2349
|
+
),
|
|
2350
|
+
)
|
|
2351
|
+
console.log()
|
|
2352
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
2353
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
2354
|
+
} else {
|
|
2355
|
+
console.log(chalk.gray(' Use your system package manager to uninstall.'))
|
|
2356
|
+
console.log(chalk.gray(` The binary is located at: ${mysqldPath}`))
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
console.log()
|
|
2360
|
+
|
|
2361
|
+
// Wait for user
|
|
2362
|
+
await inquirer.prompt([
|
|
2363
|
+
{
|
|
2364
|
+
type: 'input',
|
|
2365
|
+
name: 'continue',
|
|
2366
|
+
message: chalk.gray('Press Enter to go back...'),
|
|
2367
|
+
},
|
|
2368
|
+
])
|
|
2369
|
+
}
|
|
2370
|
+
|
|
1872
2371
|
export const menuCommand = new Command('menu')
|
|
1873
2372
|
.description('Interactive menu for managing containers')
|
|
1874
2373
|
.action(async () => {
|
|
@@ -1890,9 +2389,7 @@ export const menuCommand = new Command('menu')
|
|
|
1890
2389
|
: 'psql'
|
|
1891
2390
|
const installed = await promptInstallDependencies(missingTool)
|
|
1892
2391
|
if (installed) {
|
|
1893
|
-
console.log(
|
|
1894
|
-
chalk.yellow(' Please re-run spindb to continue.'),
|
|
1895
|
-
)
|
|
2392
|
+
console.log(chalk.yellow(' Please re-run spindb to continue.'))
|
|
1896
2393
|
}
|
|
1897
2394
|
process.exit(1)
|
|
1898
2395
|
}
|