spindb 0.3.6 → 0.4.0
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 +62 -8
- package/cli/commands/create.ts +203 -1
- package/cli/commands/deps.ts +326 -0
- package/cli/commands/menu.ts +277 -28
- package/cli/commands/restore.ts +108 -18
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +133 -0
- package/config/os-dependencies.ts +358 -0
- package/core/dependency-manager.ts +407 -0
- package/core/postgres-binary-manager.ts +38 -23
- package/engines/base-engine.ts +9 -0
- package/engines/postgresql/index.ts +53 -0
- package/package.json +2 -2
- package/types/index.ts +7 -0
package/cli/commands/menu.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
promptDatabaseName,
|
|
9
9
|
promptCreateOptions,
|
|
10
10
|
promptConfirm,
|
|
11
|
+
promptInstallDependencies,
|
|
11
12
|
} from '../ui/prompts'
|
|
12
13
|
import { createSpinner } from '../ui/spinner'
|
|
13
14
|
import {
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
import { existsSync } from 'fs'
|
|
22
23
|
import { readdir, rm, lstat } from 'fs/promises'
|
|
23
24
|
import { spawn } from 'child_process'
|
|
24
|
-
import { platform } from 'os'
|
|
25
|
+
import { platform, tmpdir } from 'os'
|
|
25
26
|
import { join } from 'path'
|
|
26
27
|
import { paths } from '../../config/paths'
|
|
27
28
|
import { portManager } from '../../core/port-manager'
|
|
@@ -678,47 +679,270 @@ async function handleConnect(): Promise<void> {
|
|
|
678
679
|
})
|
|
679
680
|
}
|
|
680
681
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Create a new container for the restore flow
|
|
684
|
+
* Returns the container name and config if successful, null if cancelled/error
|
|
685
|
+
*/
|
|
686
|
+
async function handleCreateForRestore(): Promise<{
|
|
687
|
+
name: string
|
|
688
|
+
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>
|
|
689
|
+
} | null> {
|
|
690
|
+
console.log()
|
|
691
|
+
const answers = await promptCreateOptions()
|
|
692
|
+
const { name: containerName, engine, version, port, database } = answers
|
|
684
693
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
694
|
+
console.log()
|
|
695
|
+
console.log(header('Creating Database Container'))
|
|
696
|
+
console.log()
|
|
697
|
+
|
|
698
|
+
const dbEngine = getEngine(engine)
|
|
699
|
+
|
|
700
|
+
// Check if port is currently in use
|
|
701
|
+
const portAvailable = await portManager.isPortAvailable(port)
|
|
702
|
+
if (!portAvailable) {
|
|
703
|
+
console.log(
|
|
704
|
+
error(`Port ${port} is in use. Please choose a different port.`),
|
|
705
|
+
)
|
|
706
|
+
return null
|
|
688
707
|
}
|
|
689
708
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
709
|
+
// Ensure binaries
|
|
710
|
+
const binarySpinner = createSpinner(
|
|
711
|
+
`Checking PostgreSQL ${version} binaries...`,
|
|
693
712
|
)
|
|
694
|
-
|
|
713
|
+
binarySpinner.start()
|
|
714
|
+
|
|
715
|
+
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
716
|
+
if (isInstalled) {
|
|
717
|
+
binarySpinner.succeed(`PostgreSQL ${version} binaries ready (cached)`)
|
|
718
|
+
} else {
|
|
719
|
+
binarySpinner.text = `Downloading PostgreSQL ${version} binaries...`
|
|
720
|
+
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
721
|
+
binarySpinner.text = message
|
|
722
|
+
})
|
|
723
|
+
binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Create container
|
|
727
|
+
const createSpinnerInstance = createSpinner('Creating container...')
|
|
728
|
+
createSpinnerInstance.start()
|
|
729
|
+
|
|
730
|
+
await containerManager.create(containerName, {
|
|
731
|
+
engine: dbEngine.name,
|
|
732
|
+
version,
|
|
733
|
+
port,
|
|
734
|
+
database,
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
createSpinnerInstance.succeed('Container created')
|
|
738
|
+
|
|
739
|
+
// Initialize database cluster
|
|
740
|
+
const initSpinner = createSpinner('Initializing database cluster...')
|
|
741
|
+
initSpinner.start()
|
|
742
|
+
|
|
743
|
+
await dbEngine.initDataDir(containerName, version, {
|
|
744
|
+
superuser: defaults.superuser,
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
initSpinner.succeed('Database cluster initialized')
|
|
748
|
+
|
|
749
|
+
// Start container
|
|
750
|
+
const startSpinner = createSpinner('Starting PostgreSQL...')
|
|
751
|
+
startSpinner.start()
|
|
695
752
|
|
|
696
753
|
const config = await containerManager.getConfig(containerName)
|
|
697
754
|
if (!config) {
|
|
698
|
-
|
|
699
|
-
return
|
|
755
|
+
startSpinner.fail('Failed to get container config')
|
|
756
|
+
return null
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
await dbEngine.start(config)
|
|
760
|
+
await containerManager.updateConfig(containerName, { status: 'running' })
|
|
761
|
+
|
|
762
|
+
startSpinner.succeed('PostgreSQL started')
|
|
763
|
+
|
|
764
|
+
// Create the user's database (if different from 'postgres')
|
|
765
|
+
if (database !== 'postgres') {
|
|
766
|
+
const dbSpinner = createSpinner(`Creating database "${database}"...`)
|
|
767
|
+
dbSpinner.start()
|
|
768
|
+
|
|
769
|
+
await dbEngine.createDatabase(config, database)
|
|
770
|
+
|
|
771
|
+
dbSpinner.succeed(`Database "${database}" created`)
|
|
700
772
|
}
|
|
701
773
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
774
|
+
console.log()
|
|
775
|
+
console.log(success('Container ready for restore'))
|
|
776
|
+
console.log()
|
|
777
|
+
|
|
778
|
+
return { name: containerName, config }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function handleRestore(): Promise<void> {
|
|
782
|
+
const containers = await containerManager.list()
|
|
783
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
784
|
+
|
|
785
|
+
// Build choices: running containers + create new option
|
|
786
|
+
const choices = [
|
|
787
|
+
...running.map((c) => ({
|
|
788
|
+
name: `${c.name} ${chalk.gray(`(${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
|
|
789
|
+
value: c.name,
|
|
790
|
+
short: c.name,
|
|
791
|
+
})),
|
|
792
|
+
new inquirer.Separator(),
|
|
793
|
+
{
|
|
794
|
+
name: `${chalk.green('➕')} Create new container`,
|
|
795
|
+
value: '__create_new__',
|
|
796
|
+
short: 'Create new',
|
|
797
|
+
},
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
const { selectedContainer } = await inquirer.prompt<{
|
|
801
|
+
selectedContainer: string
|
|
802
|
+
}>([
|
|
803
|
+
{
|
|
804
|
+
type: 'list',
|
|
805
|
+
name: 'selectedContainer',
|
|
806
|
+
message: 'Select container to restore to:',
|
|
807
|
+
choices,
|
|
808
|
+
},
|
|
809
|
+
])
|
|
810
|
+
|
|
811
|
+
let containerName: string
|
|
812
|
+
let config: Awaited<ReturnType<typeof containerManager.getConfig>>
|
|
813
|
+
|
|
814
|
+
if (selectedContainer === '__create_new__') {
|
|
815
|
+
// Run the create flow first
|
|
816
|
+
const createResult = await handleCreateForRestore()
|
|
817
|
+
if (!createResult) return // User cancelled or error
|
|
818
|
+
containerName = createResult.name
|
|
819
|
+
config = createResult.config
|
|
820
|
+
} else {
|
|
821
|
+
containerName = selectedContainer
|
|
822
|
+
config = await containerManager.getConfig(containerName)
|
|
823
|
+
if (!config) {
|
|
824
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
}
|
|
705
828
|
|
|
706
|
-
|
|
707
|
-
|
|
829
|
+
// Ask for restore source
|
|
830
|
+
const { restoreSource } = await inquirer.prompt<{
|
|
831
|
+
restoreSource: 'file' | 'connection'
|
|
708
832
|
}>([
|
|
709
833
|
{
|
|
710
|
-
type: '
|
|
711
|
-
name: '
|
|
712
|
-
message: '
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
834
|
+
type: 'list',
|
|
835
|
+
name: 'restoreSource',
|
|
836
|
+
message: 'Restore from:',
|
|
837
|
+
choices: [
|
|
838
|
+
{
|
|
839
|
+
name: `${chalk.magenta('📁')} Dump file (drag and drop or enter path)`,
|
|
840
|
+
value: 'file',
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: `${chalk.cyan('🔗')} Connection string (pull from remote database)`,
|
|
844
|
+
value: 'connection',
|
|
845
|
+
},
|
|
846
|
+
],
|
|
719
847
|
},
|
|
720
848
|
])
|
|
721
|
-
|
|
849
|
+
|
|
850
|
+
let backupPath: string
|
|
851
|
+
let isTempFile = false
|
|
852
|
+
|
|
853
|
+
if (restoreSource === 'connection') {
|
|
854
|
+
// Get connection string and create dump
|
|
855
|
+
const { connectionString } = await inquirer.prompt<{
|
|
856
|
+
connectionString: string
|
|
857
|
+
}>([
|
|
858
|
+
{
|
|
859
|
+
type: 'input',
|
|
860
|
+
name: 'connectionString',
|
|
861
|
+
message: 'Connection string (postgresql://user:pass@host:port/dbname):',
|
|
862
|
+
validate: (input: string) => {
|
|
863
|
+
if (!input) return 'Connection string is required'
|
|
864
|
+
if (
|
|
865
|
+
!input.startsWith('postgresql://') &&
|
|
866
|
+
!input.startsWith('postgres://')
|
|
867
|
+
) {
|
|
868
|
+
return 'Connection string must start with postgresql:// or postgres://'
|
|
869
|
+
}
|
|
870
|
+
return true
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
])
|
|
874
|
+
|
|
875
|
+
const engine = getEngine(config.engine)
|
|
876
|
+
|
|
877
|
+
// Create temp file for the dump
|
|
878
|
+
const timestamp = Date.now()
|
|
879
|
+
const tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
880
|
+
|
|
881
|
+
const dumpSpinner = createSpinner('Creating dump from remote database...')
|
|
882
|
+
dumpSpinner.start()
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
await engine.dumpFromConnectionString(connectionString, tempDumpPath)
|
|
886
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
887
|
+
backupPath = tempDumpPath
|
|
888
|
+
isTempFile = true
|
|
889
|
+
} catch (err) {
|
|
890
|
+
const e = err as Error
|
|
891
|
+
dumpSpinner.fail('Failed to create dump')
|
|
892
|
+
|
|
893
|
+
// Clean up temp file if it was created
|
|
894
|
+
try {
|
|
895
|
+
await rm(tempDumpPath, { force: true })
|
|
896
|
+
} catch {
|
|
897
|
+
// Ignore cleanup errors
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Check if this is a missing tool error
|
|
901
|
+
if (
|
|
902
|
+
e.message.includes('pg_dump not found') ||
|
|
903
|
+
e.message.includes('ENOENT')
|
|
904
|
+
) {
|
|
905
|
+
await promptInstallDependencies('pg_dump')
|
|
906
|
+
} else {
|
|
907
|
+
console.log()
|
|
908
|
+
console.log(error('pg_dump error:'))
|
|
909
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
910
|
+
console.log()
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Wait for user to see the error
|
|
914
|
+
await inquirer.prompt([
|
|
915
|
+
{
|
|
916
|
+
type: 'input',
|
|
917
|
+
name: 'continue',
|
|
918
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
919
|
+
},
|
|
920
|
+
])
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
} else {
|
|
924
|
+
// Get backup file path
|
|
925
|
+
// Strip quotes that terminals add when drag-and-dropping files
|
|
926
|
+
const stripQuotes = (path: string) =>
|
|
927
|
+
path.replace(/^['"]|['"]$/g, '').trim()
|
|
928
|
+
|
|
929
|
+
const { backupPath: rawBackupPath } = await inquirer.prompt<{
|
|
930
|
+
backupPath: string
|
|
931
|
+
}>([
|
|
932
|
+
{
|
|
933
|
+
type: 'input',
|
|
934
|
+
name: 'backupPath',
|
|
935
|
+
message: 'Path to backup file (drag and drop or enter path):',
|
|
936
|
+
validate: (input: string) => {
|
|
937
|
+
if (!input) return 'Backup path is required'
|
|
938
|
+
const cleanPath = stripQuotes(input)
|
|
939
|
+
if (!existsSync(cleanPath)) return 'File not found'
|
|
940
|
+
return true
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
])
|
|
944
|
+
backupPath = stripQuotes(rawBackupPath)
|
|
945
|
+
}
|
|
722
946
|
|
|
723
947
|
const databaseName = await promptDatabaseName(containerName)
|
|
724
948
|
|
|
@@ -936,6 +1160,15 @@ async function handleRestore(): Promise<void> {
|
|
|
936
1160
|
console.log()
|
|
937
1161
|
}
|
|
938
1162
|
|
|
1163
|
+
// Clean up temp file if we created one
|
|
1164
|
+
if (isTempFile) {
|
|
1165
|
+
try {
|
|
1166
|
+
await rm(backupPath, { force: true })
|
|
1167
|
+
} catch {
|
|
1168
|
+
// Ignore cleanup errors
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
939
1172
|
// Wait for user to see the result before returning to menu
|
|
940
1173
|
await inquirer.prompt([
|
|
941
1174
|
{
|
|
@@ -1482,6 +1715,22 @@ export const menuCommand = new Command('menu')
|
|
|
1482
1715
|
await showMainMenu()
|
|
1483
1716
|
} catch (err) {
|
|
1484
1717
|
const e = err as Error
|
|
1718
|
+
|
|
1719
|
+
// Check if this is a missing tool error
|
|
1720
|
+
if (
|
|
1721
|
+
e.message.includes('pg_restore not found') ||
|
|
1722
|
+
e.message.includes('psql not found') ||
|
|
1723
|
+
e.message.includes('pg_dump not found')
|
|
1724
|
+
) {
|
|
1725
|
+
const missingTool = e.message.includes('pg_restore')
|
|
1726
|
+
? 'pg_restore'
|
|
1727
|
+
: e.message.includes('pg_dump')
|
|
1728
|
+
? 'pg_dump'
|
|
1729
|
+
: 'psql'
|
|
1730
|
+
await promptInstallDependencies(missingTool)
|
|
1731
|
+
process.exit(1)
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1485
1734
|
console.error(error(e.message))
|
|
1486
1735
|
process.exit(1)
|
|
1487
1736
|
}
|
package/cli/commands/restore.ts
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
|
+
import { rm } from 'fs/promises'
|
|
3
4
|
import chalk from 'chalk'
|
|
4
5
|
import { containerManager } from '../../core/container-manager'
|
|
5
6
|
import { processManager } from '../../core/process-manager'
|
|
6
7
|
import { getEngine } from '../../engines'
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
promptContainerSelect,
|
|
10
|
+
promptDatabaseName,
|
|
11
|
+
promptInstallDependencies,
|
|
12
|
+
} from '../ui/prompts'
|
|
8
13
|
import { createSpinner } from '../ui/spinner'
|
|
9
14
|
import { success, error, warning } from '../ui/theme'
|
|
10
|
-
import { platform } from 'os'
|
|
15
|
+
import { platform, tmpdir } from 'os'
|
|
11
16
|
import { spawn } from 'child_process'
|
|
17
|
+
import { join } from 'path'
|
|
12
18
|
|
|
13
19
|
export const restoreCommand = new Command('restore')
|
|
14
20
|
.description('Restore a backup to a container')
|
|
15
21
|
.argument('[name]', 'Container name')
|
|
16
|
-
.argument(
|
|
22
|
+
.argument(
|
|
23
|
+
'[backup]',
|
|
24
|
+
'Path to backup file (not required if using --from-url)',
|
|
25
|
+
)
|
|
17
26
|
.option('-d, --database <name>', 'Target database name')
|
|
27
|
+
.option(
|
|
28
|
+
'--from-url <url>',
|
|
29
|
+
'Pull data from a remote database connection string',
|
|
30
|
+
)
|
|
18
31
|
.action(
|
|
19
32
|
async (
|
|
20
33
|
name: string | undefined,
|
|
21
34
|
backup: string | undefined,
|
|
22
|
-
options: { database?: string },
|
|
35
|
+
options: { database?: string; fromUrl?: string },
|
|
23
36
|
) => {
|
|
37
|
+
let tempDumpPath: string | null = null
|
|
38
|
+
|
|
24
39
|
try {
|
|
25
40
|
let containerName = name
|
|
26
|
-
|
|
41
|
+
let backupPath = backup
|
|
27
42
|
|
|
28
43
|
// Interactive selection if no name provided
|
|
29
44
|
if (!containerName) {
|
|
@@ -71,18 +86,74 @@ export const restoreCommand = new Command('restore')
|
|
|
71
86
|
process.exit(1)
|
|
72
87
|
}
|
|
73
88
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
// Get engine
|
|
90
|
+
const engine = getEngine(config.engine)
|
|
91
|
+
|
|
92
|
+
// Handle --from-url option
|
|
93
|
+
if (options.fromUrl) {
|
|
94
|
+
// Validate connection string
|
|
95
|
+
if (
|
|
96
|
+
!options.fromUrl.startsWith('postgresql://') &&
|
|
97
|
+
!options.fromUrl.startsWith('postgres://')
|
|
98
|
+
) {
|
|
99
|
+
console.error(
|
|
100
|
+
error(
|
|
101
|
+
'Connection string must start with postgresql:// or postgres://',
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create temp file for the dump
|
|
108
|
+
const timestamp = Date.now()
|
|
109
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
110
|
+
|
|
111
|
+
const dumpSpinner = createSpinner(
|
|
112
|
+
'Creating dump from remote database...',
|
|
79
113
|
)
|
|
80
|
-
|
|
81
|
-
}
|
|
114
|
+
dumpSpinner.start()
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
try {
|
|
117
|
+
await engine.dumpFromConnectionString(options.fromUrl, tempDumpPath)
|
|
118
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
119
|
+
backupPath = tempDumpPath
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const e = err as Error
|
|
122
|
+
dumpSpinner.fail('Failed to create dump')
|
|
123
|
+
|
|
124
|
+
// Check if this is a missing tool error
|
|
125
|
+
if (
|
|
126
|
+
e.message.includes('pg_dump not found') ||
|
|
127
|
+
e.message.includes('ENOENT')
|
|
128
|
+
) {
|
|
129
|
+
await promptInstallDependencies('pg_dump')
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log()
|
|
134
|
+
console.error(error('pg_dump error:'))
|
|
135
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// Check backup file
|
|
140
|
+
if (!backupPath) {
|
|
141
|
+
console.error(error('Backup file path is required'))
|
|
142
|
+
console.log(
|
|
143
|
+
chalk.gray(' Usage: spindb restore <container> <backup-file>'),
|
|
144
|
+
)
|
|
145
|
+
console.log(
|
|
146
|
+
chalk.gray(
|
|
147
|
+
' or: spindb restore <container> --from-url <connection-string>',
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
process.exit(1)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!existsSync(backupPath)) {
|
|
154
|
+
console.error(error(`Backup file not found: ${backupPath}`))
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
86
157
|
}
|
|
87
158
|
|
|
88
159
|
// Get database name
|
|
@@ -91,9 +162,6 @@ export const restoreCommand = new Command('restore')
|
|
|
91
162
|
databaseName = await promptDatabaseName(containerName)
|
|
92
163
|
}
|
|
93
164
|
|
|
94
|
-
// Get engine
|
|
95
|
-
const engine = getEngine(config.engine)
|
|
96
|
-
|
|
97
165
|
// Detect backup format
|
|
98
166
|
const detectSpinner = createSpinner('Detecting backup format...')
|
|
99
167
|
detectSpinner.start()
|
|
@@ -182,8 +250,30 @@ export const restoreCommand = new Command('restore')
|
|
|
182
250
|
console.log()
|
|
183
251
|
} catch (err) {
|
|
184
252
|
const e = err as Error
|
|
253
|
+
|
|
254
|
+
// Check if this is a missing tool error
|
|
255
|
+
if (
|
|
256
|
+
e.message.includes('pg_restore not found') ||
|
|
257
|
+
e.message.includes('psql not found')
|
|
258
|
+
) {
|
|
259
|
+
const missingTool = e.message.includes('pg_restore')
|
|
260
|
+
? 'pg_restore'
|
|
261
|
+
: 'psql'
|
|
262
|
+
await promptInstallDependencies(missingTool)
|
|
263
|
+
process.exit(1)
|
|
264
|
+
}
|
|
265
|
+
|
|
185
266
|
console.error(error(e.message))
|
|
186
267
|
process.exit(1)
|
|
268
|
+
} finally {
|
|
269
|
+
// Clean up temp file if we created one
|
|
270
|
+
if (tempDumpPath) {
|
|
271
|
+
try {
|
|
272
|
+
await rm(tempDumpPath, { force: true })
|
|
273
|
+
} catch {
|
|
274
|
+
// Ignore cleanup errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
187
277
|
}
|
|
188
278
|
},
|
|
189
279
|
)
|
package/cli/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { cloneCommand } from './commands/clone'
|
|
|
10
10
|
import { menuCommand } from './commands/menu'
|
|
11
11
|
import { configCommand } from './commands/config'
|
|
12
12
|
import { postgresToolsCommand } from './commands/postgres-tools'
|
|
13
|
+
import { depsCommand } from './commands/deps'
|
|
13
14
|
|
|
14
15
|
export async function run(): Promise<void> {
|
|
15
16
|
program
|
|
@@ -28,6 +29,7 @@ export async function run(): Promise<void> {
|
|
|
28
29
|
program.addCommand(menuCommand)
|
|
29
30
|
program.addCommand(configCommand)
|
|
30
31
|
program.addCommand(postgresToolsCommand)
|
|
32
|
+
program.addCommand(depsCommand)
|
|
31
33
|
|
|
32
34
|
// If no arguments provided, show interactive menu
|
|
33
35
|
if (process.argv.length <= 2) {
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -3,6 +3,13 @@ import chalk from 'chalk'
|
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import { listEngines, getEngine } from '../../engines'
|
|
5
5
|
import { defaults } from '../../config/defaults'
|
|
6
|
+
import { installPostgresBinaries } from '../../core/postgres-binary-manager'
|
|
7
|
+
import {
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
getManualInstallInstructions,
|
|
10
|
+
getCurrentPlatform,
|
|
11
|
+
} from '../../core/dependency-manager'
|
|
12
|
+
import { getEngineDependencies } from '../../config/os-dependencies'
|
|
6
13
|
import type { ContainerConfig } from '../../types'
|
|
7
14
|
|
|
8
15
|
/**
|
|
@@ -285,3 +292,129 @@ export async function promptCreateOptions(
|
|
|
285
292
|
|
|
286
293
|
return { name, engine, version, port, database }
|
|
287
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Prompt user to install missing database client tools
|
|
298
|
+
* Returns true if installation was successful or user declined, false if installation failed
|
|
299
|
+
*
|
|
300
|
+
* @param missingTool - The name of the missing tool (e.g., 'psql', 'pg_dump', 'mysql')
|
|
301
|
+
* @param engine - The database engine (defaults to 'postgresql')
|
|
302
|
+
*/
|
|
303
|
+
export async function promptInstallDependencies(
|
|
304
|
+
missingTool: string,
|
|
305
|
+
engine: string = 'postgresql',
|
|
306
|
+
): Promise<boolean> {
|
|
307
|
+
const platform = getCurrentPlatform()
|
|
308
|
+
|
|
309
|
+
console.log()
|
|
310
|
+
console.log(
|
|
311
|
+
chalk.yellow(` Database client tool "${missingTool}" is not installed.`),
|
|
312
|
+
)
|
|
313
|
+
console.log()
|
|
314
|
+
|
|
315
|
+
// Check what package manager is available
|
|
316
|
+
const packageManager = await detectPackageManager()
|
|
317
|
+
|
|
318
|
+
if (!packageManager) {
|
|
319
|
+
console.log(chalk.red(' No supported package manager found.'))
|
|
320
|
+
console.log()
|
|
321
|
+
|
|
322
|
+
// Get instructions from the dependency registry
|
|
323
|
+
const engineDeps = getEngineDependencies(engine)
|
|
324
|
+
if (engineDeps) {
|
|
325
|
+
// Find the specific dependency or use the first one for general instructions
|
|
326
|
+
const dep =
|
|
327
|
+
engineDeps.dependencies.find((d) => d.binary === missingTool) ||
|
|
328
|
+
engineDeps.dependencies[0]
|
|
329
|
+
|
|
330
|
+
if (dep) {
|
|
331
|
+
const instructions = getManualInstallInstructions(dep, platform)
|
|
332
|
+
console.log(
|
|
333
|
+
chalk.gray(` Please install ${engineDeps.displayName} client tools:`),
|
|
334
|
+
)
|
|
335
|
+
console.log()
|
|
336
|
+
for (const instruction of instructions) {
|
|
337
|
+
console.log(chalk.gray(` ${instruction}`))
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log()
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(
|
|
346
|
+
chalk.gray(` Detected package manager: ${chalk.white(packageManager.name)}`),
|
|
347
|
+
)
|
|
348
|
+
console.log()
|
|
349
|
+
|
|
350
|
+
// Get engine display name
|
|
351
|
+
const engineDeps = getEngineDependencies(engine)
|
|
352
|
+
const engineName = engineDeps?.displayName || engine
|
|
353
|
+
|
|
354
|
+
const { shouldInstall } = await inquirer.prompt<{ shouldInstall: string }>([
|
|
355
|
+
{
|
|
356
|
+
type: 'list',
|
|
357
|
+
name: 'shouldInstall',
|
|
358
|
+
message: `Would you like to install ${engineName} client tools now?`,
|
|
359
|
+
choices: [
|
|
360
|
+
{ name: 'Yes, install now', value: 'yes' },
|
|
361
|
+
{ name: 'No, I will install manually', value: 'no' },
|
|
362
|
+
],
|
|
363
|
+
default: 'yes',
|
|
364
|
+
},
|
|
365
|
+
])
|
|
366
|
+
|
|
367
|
+
if (shouldInstall === 'no') {
|
|
368
|
+
console.log()
|
|
369
|
+
console.log(chalk.gray(' To install manually, run:'))
|
|
370
|
+
|
|
371
|
+
// Get the specific dependency and build install command info
|
|
372
|
+
if (engineDeps) {
|
|
373
|
+
const dep = engineDeps.dependencies.find((d) => d.binary === missingTool)
|
|
374
|
+
if (dep) {
|
|
375
|
+
const pkgDef = dep.packages[packageManager.id]
|
|
376
|
+
if (pkgDef) {
|
|
377
|
+
const installCmd = packageManager.config.installTemplate.replace(
|
|
378
|
+
'{package}',
|
|
379
|
+
pkgDef.package,
|
|
380
|
+
)
|
|
381
|
+
console.log(chalk.cyan(` ${installCmd}`))
|
|
382
|
+
if (pkgDef.postInstall) {
|
|
383
|
+
for (const postCmd of pkgDef.postInstall) {
|
|
384
|
+
console.log(chalk.cyan(` ${postCmd}`))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
console.log()
|
|
391
|
+
return false
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log()
|
|
395
|
+
|
|
396
|
+
// For now, only PostgreSQL has full install support
|
|
397
|
+
// Future engines will need their own install functions
|
|
398
|
+
if (engine === 'postgresql') {
|
|
399
|
+
const success = await installPostgresBinaries()
|
|
400
|
+
|
|
401
|
+
if (success) {
|
|
402
|
+
console.log()
|
|
403
|
+
console.log(
|
|
404
|
+
chalk.green(` ${engineName} client tools installed successfully!`),
|
|
405
|
+
)
|
|
406
|
+
console.log(chalk.gray(' Please try your operation again.'))
|
|
407
|
+
console.log()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return success
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// For other engines, show manual instructions
|
|
414
|
+
console.log(
|
|
415
|
+
chalk.yellow(` Automatic installation for ${engineName} is not yet supported.`),
|
|
416
|
+
)
|
|
417
|
+
console.log(chalk.gray(' Please install manually.'))
|
|
418
|
+
console.log()
|
|
419
|
+
return false
|
|
420
|
+
}
|