spindb 0.1.0 → 0.2.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/.claude/settings.local.json +6 -1
- package/CLAUDE.md +42 -12
- package/README.md +24 -3
- package/TODO.md +12 -3
- package/eslint.config.js +7 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +23 -3
- package/src/cli/commands/menu.ts +955 -142
- package/src/cli/commands/postgres-tools.ts +216 -0
- package/src/cli/commands/restore.ts +28 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/ui/prompts.ts +111 -21
- package/src/cli/ui/theme.ts +54 -10
- package/src/config/defaults.ts +6 -3
- package/src/core/binary-manager.ts +42 -12
- package/src/core/container-manager.ts +53 -5
- package/src/core/port-manager.ts +76 -1
- package/src/core/postgres-binary-manager.ts +499 -0
- package/src/core/process-manager.ts +4 -4
- package/src/engines/base-engine.ts +22 -0
- package/src/engines/postgresql/binary-urls.ts +130 -12
- package/src/engines/postgresql/index.ts +40 -4
- package/src/engines/postgresql/restore.ts +20 -9
- package/src/types/index.ts +15 -13
- package/tsconfig.json +6 -3
|
@@ -5,13 +5,14 @@ import { processManager } from '@/core/process-manager'
|
|
|
5
5
|
import { portManager } from '@/core/port-manager'
|
|
6
6
|
import type { ContainerConfig } from '@/types'
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export type CreateOptions = {
|
|
9
9
|
engine: string
|
|
10
10
|
version: string
|
|
11
11
|
port: number
|
|
12
|
+
database: string
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
+
export type DeleteOptions = {
|
|
15
16
|
force?: boolean
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -20,7 +21,7 @@ export class ContainerManager {
|
|
|
20
21
|
* Create a new container
|
|
21
22
|
*/
|
|
22
23
|
async create(name: string, options: CreateOptions): Promise<ContainerConfig> {
|
|
23
|
-
const { engine, version, port } = options
|
|
24
|
+
const { engine, version, port, database } = options
|
|
24
25
|
|
|
25
26
|
// Validate container name
|
|
26
27
|
if (!this.isValidName(name)) {
|
|
@@ -47,6 +48,7 @@ export class ContainerManager {
|
|
|
47
48
|
engine,
|
|
48
49
|
version,
|
|
49
50
|
port,
|
|
51
|
+
database,
|
|
50
52
|
created: new Date().toISOString(),
|
|
51
53
|
status: 'created',
|
|
52
54
|
}
|
|
@@ -203,8 +205,8 @@ export class ContainerManager {
|
|
|
203
205
|
config.created = new Date().toISOString()
|
|
204
206
|
config.clonedFrom = sourceName
|
|
205
207
|
|
|
206
|
-
// Assign new port
|
|
207
|
-
const { port } = await portManager.
|
|
208
|
+
// Assign new port (excluding ports already used by other containers)
|
|
209
|
+
const { port } = await portManager.findAvailablePortExcludingContainers()
|
|
208
210
|
config.port = port
|
|
209
211
|
|
|
210
212
|
await this.saveConfig(targetName, config)
|
|
@@ -212,6 +214,52 @@ export class ContainerManager {
|
|
|
212
214
|
return config
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Rename a container
|
|
219
|
+
*/
|
|
220
|
+
async rename(oldName: string, newName: string): Promise<ContainerConfig> {
|
|
221
|
+
// Validate new name
|
|
222
|
+
if (!this.isValidName(newName)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
'Container name must be alphanumeric with hyphens/underscores only',
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check source exists
|
|
229
|
+
if (!(await this.exists(oldName))) {
|
|
230
|
+
throw new Error(`Container "${oldName}" not found`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check target doesn't exist
|
|
234
|
+
if (await this.exists(newName)) {
|
|
235
|
+
throw new Error(`Container "${newName}" already exists`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check container is not running
|
|
239
|
+
const running = await processManager.isRunning(oldName)
|
|
240
|
+
if (running) {
|
|
241
|
+
throw new Error(`Container "${oldName}" is running. Stop it first`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Rename directory
|
|
245
|
+
const oldPath = paths.getContainerPath(oldName)
|
|
246
|
+
const newPath = paths.getContainerPath(newName)
|
|
247
|
+
|
|
248
|
+
await cp(oldPath, newPath, { recursive: true })
|
|
249
|
+
await rm(oldPath, { recursive: true, force: true })
|
|
250
|
+
|
|
251
|
+
// Update config with new name
|
|
252
|
+
const config = await this.getConfig(newName)
|
|
253
|
+
if (!config) {
|
|
254
|
+
throw new Error('Failed to read renamed container config')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
config.name = newName
|
|
258
|
+
await this.saveConfig(newName, config)
|
|
259
|
+
|
|
260
|
+
return config
|
|
261
|
+
}
|
|
262
|
+
|
|
215
263
|
/**
|
|
216
264
|
* Validate container name
|
|
217
265
|
*/
|
package/src/core/port-manager.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import net from 'net'
|
|
2
2
|
import { exec } from 'child_process'
|
|
3
3
|
import { promisify } from 'util'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { readdir, readFile } from 'fs/promises'
|
|
4
6
|
import { defaults } from '@/config/defaults'
|
|
5
|
-
import
|
|
7
|
+
import { paths } from '@/config/paths'
|
|
8
|
+
import type { ContainerConfig, PortResult } from '@/types'
|
|
6
9
|
|
|
7
10
|
const execAsync = promisify(exec)
|
|
8
11
|
|
|
@@ -79,6 +82,78 @@ export class PortManager {
|
|
|
79
82
|
return null
|
|
80
83
|
}
|
|
81
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all ports currently assigned to containers
|
|
88
|
+
*/
|
|
89
|
+
async getContainerPorts(): Promise<number[]> {
|
|
90
|
+
const containersDir = paths.containers
|
|
91
|
+
|
|
92
|
+
if (!existsSync(containersDir)) {
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ports: number[] = []
|
|
97
|
+
const entries = await readdir(containersDir, { withFileTypes: true })
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
const configPath = `${containersDir}/${entry.name}/container.json`
|
|
102
|
+
if (existsSync(configPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const content = await readFile(configPath, 'utf8')
|
|
105
|
+
const config = JSON.parse(content) as ContainerConfig
|
|
106
|
+
ports.push(config.port)
|
|
107
|
+
} catch {
|
|
108
|
+
// Skip invalid configs
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return ports
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Find an available port that's not in use by any process AND not assigned to any container
|
|
119
|
+
*/
|
|
120
|
+
async findAvailablePortExcludingContainers(
|
|
121
|
+
preferredPort: number = defaults.port,
|
|
122
|
+
): Promise<PortResult> {
|
|
123
|
+
const containerPorts = await this.getContainerPorts()
|
|
124
|
+
|
|
125
|
+
// First try the preferred port
|
|
126
|
+
if (
|
|
127
|
+
!containerPorts.includes(preferredPort) &&
|
|
128
|
+
(await this.isPortAvailable(preferredPort))
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
port: preferredPort,
|
|
132
|
+
isDefault: preferredPort === defaults.port,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Scan for available ports in the range
|
|
137
|
+
for (
|
|
138
|
+
let port = defaults.portRange.start;
|
|
139
|
+
port <= defaults.portRange.end;
|
|
140
|
+
port++
|
|
141
|
+
) {
|
|
142
|
+
if (containerPorts.includes(port)) continue // Skip ports used by containers
|
|
143
|
+
if (port === preferredPort) continue // Already tried this one
|
|
144
|
+
|
|
145
|
+
if (await this.isPortAvailable(port)) {
|
|
146
|
+
return {
|
|
147
|
+
port,
|
|
148
|
+
isDefault: false,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error(
|
|
154
|
+
`No available ports found in range ${defaults.portRange.start}-${defaults.portRange.end}`,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
82
157
|
}
|
|
83
158
|
|
|
84
159
|
export const portManager = new PortManager()
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import { promisify } from 'util'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { createSpinner } from '@/cli/ui/spinner'
|
|
5
|
+
import { warning, error as themeError, success } from '@/cli/ui/theme'
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec)
|
|
8
|
+
|
|
9
|
+
export type BinaryInfo = {
|
|
10
|
+
command: string
|
|
11
|
+
version: string
|
|
12
|
+
path: string
|
|
13
|
+
packageManager?: string
|
|
14
|
+
isCompatible: boolean
|
|
15
|
+
requiredVersion?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PackageManager = {
|
|
19
|
+
name: string
|
|
20
|
+
checkCommand: string
|
|
21
|
+
installCommand: (binary: string) => string
|
|
22
|
+
updateCommand: (binary: string) => string
|
|
23
|
+
versionCheckCommand: (binary: string) => string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect which package manager is available on the system
|
|
28
|
+
*/
|
|
29
|
+
export async function detectPackageManager(): Promise<PackageManager | null> {
|
|
30
|
+
const managers: PackageManager[] = [
|
|
31
|
+
{
|
|
32
|
+
name: 'brew',
|
|
33
|
+
checkCommand: 'brew --version',
|
|
34
|
+
installCommand: () =>
|
|
35
|
+
'brew install postgresql@17 && brew link --overwrite postgresql@17',
|
|
36
|
+
updateCommand: () =>
|
|
37
|
+
'brew link --overwrite postgresql@17 || brew install postgresql@17 && brew link --overwrite postgresql@17',
|
|
38
|
+
versionCheckCommand: () =>
|
|
39
|
+
'brew info postgresql@17 | grep "postgresql@17:" | head -1',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'apt',
|
|
43
|
+
checkCommand: 'apt --version',
|
|
44
|
+
installCommand: () =>
|
|
45
|
+
'sudo apt update && sudo apt install -y postgresql-client',
|
|
46
|
+
updateCommand: () =>
|
|
47
|
+
'sudo apt update && sudo apt upgrade -y postgresql-client',
|
|
48
|
+
versionCheckCommand: () => 'apt show postgresql-client | grep Version',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'yum',
|
|
52
|
+
checkCommand: 'yum --version',
|
|
53
|
+
installCommand: () => 'sudo yum install -y postgresql',
|
|
54
|
+
updateCommand: () => 'sudo yum update -y postgresql',
|
|
55
|
+
versionCheckCommand: () => 'yum info postgresql | grep Version',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'dnf',
|
|
59
|
+
checkCommand: 'dnf --version',
|
|
60
|
+
installCommand: () => 'sudo dnf install -y postgresql',
|
|
61
|
+
updateCommand: () => 'sudo dnf upgrade -y postgresql',
|
|
62
|
+
versionCheckCommand: () => 'dnf info postgresql | grep Version',
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for (const manager of managers) {
|
|
67
|
+
try {
|
|
68
|
+
await execAsync(manager.checkCommand)
|
|
69
|
+
return manager
|
|
70
|
+
} catch {
|
|
71
|
+
// Manager not available
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get PostgreSQL version from pg_restore or psql
|
|
80
|
+
*/
|
|
81
|
+
export async function getPostgresVersion(
|
|
82
|
+
binary: 'pg_restore' | 'psql',
|
|
83
|
+
): Promise<string | null> {
|
|
84
|
+
try {
|
|
85
|
+
const { stdout } = await execAsync(`${binary} --version`)
|
|
86
|
+
const match = stdout.match(/(\d+\.\d+)/)
|
|
87
|
+
return match ? match[1] : null
|
|
88
|
+
} catch {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find binary path using which/where command
|
|
95
|
+
*/
|
|
96
|
+
export async function findBinaryPath(binary: string): Promise<string | null> {
|
|
97
|
+
try {
|
|
98
|
+
const command = process.platform === 'win32' ? 'where' : 'which'
|
|
99
|
+
const { stdout } = await execAsync(`${command} ${binary}`)
|
|
100
|
+
return stdout.trim().split('\n')[0] || null
|
|
101
|
+
} catch {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find binary path with fallback to refresh PATH cache
|
|
108
|
+
*/
|
|
109
|
+
export async function findBinaryPathFresh(
|
|
110
|
+
binary: string,
|
|
111
|
+
): Promise<string | null> {
|
|
112
|
+
// Try normal lookup first
|
|
113
|
+
const path = await findBinaryPath(binary)
|
|
114
|
+
if (path) return path
|
|
115
|
+
|
|
116
|
+
// If not found, try to refresh PATH cache (especially after package updates)
|
|
117
|
+
try {
|
|
118
|
+
// Force shell to re-evaluate PATH
|
|
119
|
+
const shell = process.env.SHELL || '/bin/bash'
|
|
120
|
+
const { stdout } = await execWithTimeout(
|
|
121
|
+
`${shell} -c 'source ~/.${shell.endsWith('zsh') ? 'zshrc' : 'bashrc'} && which ${binary}'`,
|
|
122
|
+
)
|
|
123
|
+
return stdout.trim().split('\n')[0] || null
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Execute command with timeout
|
|
131
|
+
*/
|
|
132
|
+
async function execWithTimeout(
|
|
133
|
+
command: string,
|
|
134
|
+
timeoutMs: number = 60000,
|
|
135
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const child = exec(
|
|
138
|
+
command,
|
|
139
|
+
{ timeout: timeoutMs },
|
|
140
|
+
(error, stdout, stderr) => {
|
|
141
|
+
if (error) {
|
|
142
|
+
reject(error)
|
|
143
|
+
} else {
|
|
144
|
+
resolve({ stdout, stderr })
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Additional timeout safety
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
child.kill('SIGTERM')
|
|
152
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`))
|
|
153
|
+
}, timeoutMs)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse dump file to get required PostgreSQL version
|
|
159
|
+
*/
|
|
160
|
+
export async function getDumpRequiredVersion(
|
|
161
|
+
dumpPath: string,
|
|
162
|
+
): Promise<string | null> {
|
|
163
|
+
try {
|
|
164
|
+
// Try to read pg_dump custom format header
|
|
165
|
+
const { stdout } = await execAsync(`file "${dumpPath}"`)
|
|
166
|
+
if (stdout.includes('PostgreSQL custom database dump')) {
|
|
167
|
+
// For custom format, we need to check the version in the dump
|
|
168
|
+
try {
|
|
169
|
+
const { stdout: hexdump } = await execAsync(
|
|
170
|
+
`hexdump -C "${dumpPath}" | head -5`,
|
|
171
|
+
)
|
|
172
|
+
// Look for version info in the header (simplified approach)
|
|
173
|
+
const versionMatch = hexdump.match(/(\d+)\.(\d+)/)
|
|
174
|
+
if (versionMatch) {
|
|
175
|
+
// If it's a recent dump, assume it needs the latest PostgreSQL
|
|
176
|
+
const majorVersion = parseInt(versionMatch[1])
|
|
177
|
+
if (majorVersion >= 15) {
|
|
178
|
+
return '15.0' // Minimum version for recent dumps
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// If hexdump fails, fall back to checking error patterns
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fallback: if we can't determine, assume it needs a recent version
|
|
187
|
+
return '15.0'
|
|
188
|
+
} catch {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if a PostgreSQL version is compatible with a dump
|
|
195
|
+
*/
|
|
196
|
+
export function isVersionCompatible(
|
|
197
|
+
currentVersion: string,
|
|
198
|
+
requiredVersion: string,
|
|
199
|
+
): boolean {
|
|
200
|
+
const current = parseFloat(currentVersion)
|
|
201
|
+
const required = parseFloat(requiredVersion)
|
|
202
|
+
|
|
203
|
+
// Current version should be >= required version
|
|
204
|
+
// But not too far ahead (major version compatibility)
|
|
205
|
+
return current >= required && Math.floor(current) === Math.floor(required)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get binary information including version and compatibility
|
|
210
|
+
*/
|
|
211
|
+
export async function getBinaryInfo(
|
|
212
|
+
binary: 'pg_restore' | 'psql',
|
|
213
|
+
dumpPath?: string,
|
|
214
|
+
): Promise<BinaryInfo | null> {
|
|
215
|
+
const path = await findBinaryPath(binary)
|
|
216
|
+
if (!path) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const version = await getPostgresVersion(binary)
|
|
221
|
+
if (!version) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let requiredVersion: string | undefined
|
|
226
|
+
let isCompatible = true
|
|
227
|
+
|
|
228
|
+
if (dumpPath) {
|
|
229
|
+
const dumpVersion = await getDumpRequiredVersion(dumpPath)
|
|
230
|
+
requiredVersion = dumpVersion || undefined
|
|
231
|
+
if (requiredVersion) {
|
|
232
|
+
isCompatible = isVersionCompatible(version, requiredVersion)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Try to detect which package manager installed this binary
|
|
237
|
+
let packageManager: string | undefined
|
|
238
|
+
try {
|
|
239
|
+
if (process.platform === 'darwin') {
|
|
240
|
+
// On macOS, check if it's from Homebrew
|
|
241
|
+
const { stdout } = await execAsync(
|
|
242
|
+
'brew list postgresql@* 2>/dev/null || brew list libpq 2>/dev/null || true',
|
|
243
|
+
)
|
|
244
|
+
if (stdout.includes('postgresql') || stdout.includes('libpq')) {
|
|
245
|
+
packageManager = 'brew'
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
// On Linux, check common package managers
|
|
249
|
+
try {
|
|
250
|
+
await execAsync('dpkg -S $(which pg_restore) 2>/dev/null')
|
|
251
|
+
packageManager = 'apt'
|
|
252
|
+
} catch {
|
|
253
|
+
try {
|
|
254
|
+
await execAsync('rpm -qf $(which pg_restore) 2>/dev/null')
|
|
255
|
+
packageManager = 'yum/dnf'
|
|
256
|
+
} catch {
|
|
257
|
+
// Could be from source or other installation method
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Could not determine package manager
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
command: binary,
|
|
267
|
+
version,
|
|
268
|
+
path,
|
|
269
|
+
packageManager,
|
|
270
|
+
isCompatible,
|
|
271
|
+
requiredVersion,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Install PostgreSQL client tools
|
|
277
|
+
*/
|
|
278
|
+
export async function installPostgresBinaries(): Promise<boolean> {
|
|
279
|
+
const spinner = createSpinner('Checking package manager...')
|
|
280
|
+
spinner.start()
|
|
281
|
+
|
|
282
|
+
const packageManager = await detectPackageManager()
|
|
283
|
+
if (!packageManager) {
|
|
284
|
+
spinner.fail('No supported package manager found')
|
|
285
|
+
console.log(themeError('Please install PostgreSQL client tools manually:'))
|
|
286
|
+
console.log(' macOS: brew install libpq')
|
|
287
|
+
console.log(' Ubuntu/Debian: sudo apt install postgresql-client')
|
|
288
|
+
console.log(' CentOS/RHEL/Fedora: sudo yum install postgresql')
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
293
|
+
|
|
294
|
+
const installSpinner = createSpinner(
|
|
295
|
+
`Installing PostgreSQL client tools with ${packageManager.name}...`,
|
|
296
|
+
)
|
|
297
|
+
installSpinner.start()
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await execWithTimeout(packageManager.installCommand('postgresql'), 120000) // 2 minute timeout
|
|
301
|
+
installSpinner.succeed('PostgreSQL client tools installed')
|
|
302
|
+
console.log(success('Installation completed successfully'))
|
|
303
|
+
return true
|
|
304
|
+
} catch (error: unknown) {
|
|
305
|
+
installSpinner.fail('Installation failed')
|
|
306
|
+
console.log(themeError('Failed to install PostgreSQL client tools'))
|
|
307
|
+
console.log(warning('Please install manually:'))
|
|
308
|
+
console.log(` ${packageManager.installCommand('postgresql')}`)
|
|
309
|
+
if (error instanceof Error) {
|
|
310
|
+
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
311
|
+
}
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Update individual PostgreSQL client tools to resolve conflicts
|
|
318
|
+
*/
|
|
319
|
+
export async function updatePostgresClientTools(): Promise<boolean> {
|
|
320
|
+
const spinner = createSpinner('Updating PostgreSQL client tools...')
|
|
321
|
+
spinner.start()
|
|
322
|
+
|
|
323
|
+
const packageManager = await detectPackageManager()
|
|
324
|
+
if (!packageManager) {
|
|
325
|
+
spinner.fail('No supported package manager found')
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
if (packageManager.name === 'brew') {
|
|
333
|
+
// Handle brew conflicts and dependency issues
|
|
334
|
+
const commands = [
|
|
335
|
+
'brew unlink postgresql@14 2>/dev/null || true', // Unlink old version if exists
|
|
336
|
+
'brew unlink postgresql@15 2>/dev/null || true', // Unlink other old versions
|
|
337
|
+
'brew unlink postgresql@16 2>/dev/null || true',
|
|
338
|
+
'brew link --overwrite postgresql@17', // Link postgresql@17 with overwrite
|
|
339
|
+
'brew upgrade icu4c 2>/dev/null || true', // Fix ICU dependency issues
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
for (const command of commands) {
|
|
343
|
+
await execWithTimeout(command, 60000)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
spinner.succeed('PostgreSQL client tools updated')
|
|
347
|
+
console.log(success('Client tools successfully linked to PostgreSQL 17'))
|
|
348
|
+
console.log(chalk.gray('ICU dependencies have been updated'))
|
|
349
|
+
return true
|
|
350
|
+
} else {
|
|
351
|
+
// For other package managers, use the standard update
|
|
352
|
+
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000)
|
|
353
|
+
spinner.succeed('PostgreSQL client tools updated')
|
|
354
|
+
console.log(success('Update completed successfully'))
|
|
355
|
+
return true
|
|
356
|
+
}
|
|
357
|
+
} catch (error: unknown) {
|
|
358
|
+
spinner.fail('Update failed')
|
|
359
|
+
console.log(themeError('Failed to update PostgreSQL client tools'))
|
|
360
|
+
console.log(warning('Please update manually:'))
|
|
361
|
+
|
|
362
|
+
if (packageManager.name === 'brew') {
|
|
363
|
+
console.log(chalk.yellow(' macOS:'))
|
|
364
|
+
console.log(
|
|
365
|
+
chalk.yellow(
|
|
366
|
+
' brew unlink postgresql@14 postgresql@15 postgresql@16',
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
console.log(chalk.yellow(' brew link --overwrite postgresql@17'))
|
|
370
|
+
console.log(
|
|
371
|
+
chalk.yellow(' brew upgrade icu4c # Fix ICU dependency issues'),
|
|
372
|
+
)
|
|
373
|
+
console.log(
|
|
374
|
+
chalk.gray(
|
|
375
|
+
' This will update: pg_restore, pg_dump, psql, and fix dependency issues',
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
} else {
|
|
379
|
+
console.log(` ${packageManager.updateCommand('postgresql')}`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (error instanceof Error) {
|
|
383
|
+
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
384
|
+
}
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export async function updatePostgresBinaries(): Promise<boolean> {
|
|
389
|
+
const spinner = createSpinner('Checking package manager...')
|
|
390
|
+
spinner.start()
|
|
391
|
+
|
|
392
|
+
const packageManager = await detectPackageManager()
|
|
393
|
+
if (!packageManager) {
|
|
394
|
+
spinner.fail('No supported package manager found')
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
399
|
+
|
|
400
|
+
const updateSpinner = createSpinner(
|
|
401
|
+
`Updating PostgreSQL client tools with ${packageManager.name}...`,
|
|
402
|
+
)
|
|
403
|
+
updateSpinner.start()
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000) // 2 minute timeout
|
|
407
|
+
updateSpinner.succeed('PostgreSQL client tools updated')
|
|
408
|
+
console.log(success('Update completed successfully'))
|
|
409
|
+
return true
|
|
410
|
+
} catch (error: unknown) {
|
|
411
|
+
updateSpinner.fail('Update failed')
|
|
412
|
+
console.log(themeError('Failed to update PostgreSQL client tools'))
|
|
413
|
+
console.log(warning('Please update manually:'))
|
|
414
|
+
console.log(` ${packageManager.updateCommand('postgresql')}`)
|
|
415
|
+
if (error instanceof Error) {
|
|
416
|
+
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
417
|
+
}
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Ensure PostgreSQL binary is available and compatible
|
|
424
|
+
*/
|
|
425
|
+
export async function ensurePostgresBinary(
|
|
426
|
+
binary: 'pg_restore' | 'psql',
|
|
427
|
+
dumpPath?: string,
|
|
428
|
+
options: { autoInstall?: boolean; autoUpdate?: boolean } = {},
|
|
429
|
+
): Promise<{ success: boolean; info: BinaryInfo | null; action?: string }> {
|
|
430
|
+
const { autoInstall = true, autoUpdate = true } = options
|
|
431
|
+
|
|
432
|
+
console.log(
|
|
433
|
+
`[DEBUG] ensurePostgresBinary called for ${binary}, dumpPath: ${dumpPath}`,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
// Check if binary exists
|
|
437
|
+
const info = await getBinaryInfo(binary, dumpPath)
|
|
438
|
+
|
|
439
|
+
console.log(`[DEBUG] getBinaryInfo result:`, info)
|
|
440
|
+
|
|
441
|
+
if (!info) {
|
|
442
|
+
if (!autoInstall) {
|
|
443
|
+
return { success: false, info: null, action: 'install_required' }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log(warning(`${binary} not found on your system`))
|
|
447
|
+
const success = await installPostgresBinaries()
|
|
448
|
+
if (!success) {
|
|
449
|
+
return { success: false, info: null, action: 'install_failed' }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Check again after installation
|
|
453
|
+
const newInfo = await getBinaryInfo(binary, dumpPath)
|
|
454
|
+
if (!newInfo) {
|
|
455
|
+
return { success: false, info: null, action: 'install_failed' }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { success: true, info: newInfo, action: 'installed' }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check version compatibility
|
|
462
|
+
if (dumpPath && !info.isCompatible) {
|
|
463
|
+
console.log(
|
|
464
|
+
`[DEBUG] Version incompatible: current=${info.version}, required=${info.requiredVersion}`,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if (!autoUpdate) {
|
|
468
|
+
return { success: false, info, action: 'update_required' }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log(
|
|
472
|
+
warning(
|
|
473
|
+
`Your ${binary} version (${info.version}) is incompatible with the dump file`,
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
if (info.requiredVersion) {
|
|
477
|
+
console.log(
|
|
478
|
+
warning(`Required version: ${info.requiredVersion} or compatible`),
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const success = await updatePostgresBinaries()
|
|
483
|
+
if (!success) {
|
|
484
|
+
return { success: false, info, action: 'update_failed' }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Check again after update
|
|
488
|
+
const updatedInfo = await getBinaryInfo(binary, dumpPath)
|
|
489
|
+
if (!updatedInfo || !updatedInfo.isCompatible) {
|
|
490
|
+
console.log(`[DEBUG] Update failed or still incompatible:`, updatedInfo)
|
|
491
|
+
return { success: false, info: updatedInfo, action: 'update_failed' }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { success: true, info: updatedInfo, action: 'updated' }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(`[DEBUG] Binary is compatible, returning success`)
|
|
498
|
+
return { success: true, info, action: 'compatible' }
|
|
499
|
+
}
|
|
@@ -7,23 +7,23 @@ import type { ProcessResult, StatusResult } from '@/types'
|
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec)
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export type InitdbOptions = {
|
|
11
11
|
superuser?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export
|
|
14
|
+
export type StartOptions = {
|
|
15
15
|
port?: number
|
|
16
16
|
logFile?: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export
|
|
19
|
+
export type PsqlOptions = {
|
|
20
20
|
port: number
|
|
21
21
|
database?: string
|
|
22
22
|
user?: string
|
|
23
23
|
command?: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export
|
|
26
|
+
export type PgRestoreOptions = {
|
|
27
27
|
port: number
|
|
28
28
|
database: string
|
|
29
29
|
user?: string
|