spindb 0.3.6 → 0.4.1
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 +275 -1
- package/cli/commands/deps.ts +326 -0
- package/cli/commands/menu.ts +387 -29
- package/cli/commands/restore.ts +173 -16
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +133 -0
- package/config/os-dependencies.ts +358 -0
- package/config/paths.ts +39 -1
- package/core/dependency-manager.ts +429 -0
- package/core/postgres-binary-manager.ts +44 -28
- 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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles checking, installing, and updating OS-level dependencies
|
|
5
|
+
* for database engines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exec, spawnSync } from 'child_process'
|
|
9
|
+
import { promisify } from 'util'
|
|
10
|
+
import {
|
|
11
|
+
type PackageManagerId,
|
|
12
|
+
type PackageManagerConfig,
|
|
13
|
+
type Dependency,
|
|
14
|
+
type Platform,
|
|
15
|
+
packageManagers,
|
|
16
|
+
getEngineDependencies,
|
|
17
|
+
getUniqueDependencies,
|
|
18
|
+
} from '../config/os-dependencies'
|
|
19
|
+
|
|
20
|
+
const execAsync = promisify(exec)
|
|
21
|
+
|
|
22
|
+
export type DependencyStatus = {
|
|
23
|
+
dependency: Dependency
|
|
24
|
+
installed: boolean
|
|
25
|
+
path?: string
|
|
26
|
+
version?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DetectedPackageManager = {
|
|
30
|
+
config: PackageManagerConfig
|
|
31
|
+
id: PackageManagerId
|
|
32
|
+
name: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type InstallResult = {
|
|
36
|
+
success: boolean
|
|
37
|
+
dependency: Dependency
|
|
38
|
+
error?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Package Manager Detection
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect which package manager is available on the current system
|
|
47
|
+
*/
|
|
48
|
+
export async function detectPackageManager(): Promise<DetectedPackageManager | null> {
|
|
49
|
+
const platform = process.platform as Platform
|
|
50
|
+
|
|
51
|
+
// Filter to package managers available on this platform
|
|
52
|
+
const candidates = packageManagers.filter((pm) =>
|
|
53
|
+
pm.platforms.includes(platform),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for (const pm of candidates) {
|
|
57
|
+
try {
|
|
58
|
+
await execAsync(pm.checkCommand)
|
|
59
|
+
return {
|
|
60
|
+
config: pm,
|
|
61
|
+
id: pm.id,
|
|
62
|
+
name: pm.name,
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Package manager not available
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the current platform
|
|
74
|
+
*/
|
|
75
|
+
export function getCurrentPlatform(): Platform {
|
|
76
|
+
return process.platform as Platform
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Dependency Checking
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a binary is installed and get its path
|
|
85
|
+
*/
|
|
86
|
+
export async function findBinary(
|
|
87
|
+
binary: string,
|
|
88
|
+
): Promise<{ path: string; version?: string } | null> {
|
|
89
|
+
try {
|
|
90
|
+
const command = process.platform === 'win32' ? 'where' : 'which'
|
|
91
|
+
const { stdout } = await execAsync(`${command} ${binary}`)
|
|
92
|
+
const path = stdout.trim().split('\n')[0]
|
|
93
|
+
|
|
94
|
+
if (!path) return null
|
|
95
|
+
|
|
96
|
+
// Try to get version
|
|
97
|
+
let version: string | undefined
|
|
98
|
+
try {
|
|
99
|
+
const { stdout: versionOutput } = await execAsync(`${binary} --version`)
|
|
100
|
+
const match = versionOutput.match(/(\d+\.\d+(\.\d+)?)/)
|
|
101
|
+
version = match ? match[1] : undefined
|
|
102
|
+
} catch {
|
|
103
|
+
// Version check failed, that's ok
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { path, version }
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check the status of a single dependency
|
|
114
|
+
*/
|
|
115
|
+
export async function checkDependency(
|
|
116
|
+
dependency: Dependency,
|
|
117
|
+
): Promise<DependencyStatus> {
|
|
118
|
+
const result = await findBinary(dependency.binary)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
dependency,
|
|
122
|
+
installed: result !== null,
|
|
123
|
+
path: result?.path,
|
|
124
|
+
version: result?.version,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check all dependencies for a specific engine
|
|
130
|
+
*/
|
|
131
|
+
export async function checkEngineDependencies(
|
|
132
|
+
engine: string,
|
|
133
|
+
): Promise<DependencyStatus[]> {
|
|
134
|
+
const engineDeps = getEngineDependencies(engine)
|
|
135
|
+
if (!engineDeps) return []
|
|
136
|
+
|
|
137
|
+
const results = await Promise.all(
|
|
138
|
+
engineDeps.dependencies.map((dep) => checkDependency(dep)),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return results
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check all dependencies across all engines
|
|
146
|
+
*/
|
|
147
|
+
export async function checkAllDependencies(): Promise<DependencyStatus[]> {
|
|
148
|
+
const deps = getUniqueDependencies()
|
|
149
|
+
const results = await Promise.all(deps.map((dep) => checkDependency(dep)))
|
|
150
|
+
return results
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get missing dependencies for an engine
|
|
155
|
+
*/
|
|
156
|
+
export async function getMissingDependencies(
|
|
157
|
+
engine: string,
|
|
158
|
+
): Promise<Dependency[]> {
|
|
159
|
+
const statuses = await checkEngineDependencies(engine)
|
|
160
|
+
return statuses.filter((s) => !s.installed).map((s) => s.dependency)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all missing dependencies across all engines
|
|
165
|
+
*/
|
|
166
|
+
export async function getAllMissingDependencies(): Promise<Dependency[]> {
|
|
167
|
+
const statuses = await checkAllDependencies()
|
|
168
|
+
return statuses.filter((s) => !s.installed).map((s) => s.dependency)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// Installation
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if stdin is a TTY (interactive terminal)
|
|
177
|
+
*/
|
|
178
|
+
function hasTTY(): boolean {
|
|
179
|
+
return process.stdin.isTTY === true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if running as root
|
|
184
|
+
*/
|
|
185
|
+
function isRoot(): boolean {
|
|
186
|
+
return process.getuid?.() === 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute command with inherited stdio (for TTY support with sudo)
|
|
191
|
+
* Uses spawnSync to properly connect to the terminal for password prompts
|
|
192
|
+
*/
|
|
193
|
+
function execWithInheritedStdio(command: string): void {
|
|
194
|
+
let cmdToRun = command
|
|
195
|
+
|
|
196
|
+
// If already running as root, strip sudo from the command
|
|
197
|
+
if (isRoot() && command.startsWith('sudo ')) {
|
|
198
|
+
cmdToRun = command.replace(/^sudo\s+/, '')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if we need a TTY for sudo password prompts
|
|
202
|
+
if (!hasTTY() && cmdToRun.includes('sudo')) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'Cannot run sudo commands without an interactive terminal. Please run the install command manually:\n' +
|
|
205
|
+
` ${command}`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = spawnSync(cmdToRun, [], {
|
|
210
|
+
shell: true,
|
|
211
|
+
stdio: 'inherit',
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (result.error) {
|
|
215
|
+
throw result.error
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.status !== 0) {
|
|
219
|
+
throw new Error(`Command failed with exit code ${result.status}: ${cmdToRun}`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build install command for a dependency using a package manager
|
|
225
|
+
*/
|
|
226
|
+
export function buildInstallCommand(
|
|
227
|
+
dependency: Dependency,
|
|
228
|
+
packageManager: DetectedPackageManager,
|
|
229
|
+
): string[] {
|
|
230
|
+
const pkgDef = dependency.packages[packageManager.id]
|
|
231
|
+
if (!pkgDef) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`No package definition for ${dependency.name} with ${packageManager.name}`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const commands: string[] = []
|
|
238
|
+
|
|
239
|
+
// Pre-install commands
|
|
240
|
+
if (pkgDef.preInstall) {
|
|
241
|
+
commands.push(...pkgDef.preInstall)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Main install command
|
|
245
|
+
const installCmd = packageManager.config.installTemplate.replace(
|
|
246
|
+
'{package}',
|
|
247
|
+
pkgDef.package,
|
|
248
|
+
)
|
|
249
|
+
commands.push(installCmd)
|
|
250
|
+
|
|
251
|
+
// Post-install commands
|
|
252
|
+
if (pkgDef.postInstall) {
|
|
253
|
+
commands.push(...pkgDef.postInstall)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return commands
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Install a single dependency
|
|
261
|
+
*/
|
|
262
|
+
export async function installDependency(
|
|
263
|
+
dependency: Dependency,
|
|
264
|
+
packageManager: DetectedPackageManager,
|
|
265
|
+
): Promise<InstallResult> {
|
|
266
|
+
try {
|
|
267
|
+
const commands = buildInstallCommand(dependency, packageManager)
|
|
268
|
+
|
|
269
|
+
for (const cmd of commands) {
|
|
270
|
+
// Use inherited stdio so sudo can prompt for password in terminal
|
|
271
|
+
execWithInheritedStdio(cmd)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Verify installation
|
|
275
|
+
const status = await checkDependency(dependency)
|
|
276
|
+
if (!status.installed) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
dependency,
|
|
280
|
+
error: 'Installation completed but binary not found in PATH',
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { success: true, dependency }
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
dependency,
|
|
289
|
+
error: error instanceof Error ? error.message : String(error),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Install all dependencies for an engine
|
|
296
|
+
*/
|
|
297
|
+
export async function installEngineDependencies(
|
|
298
|
+
engine: string,
|
|
299
|
+
packageManager: DetectedPackageManager,
|
|
300
|
+
): Promise<InstallResult[]> {
|
|
301
|
+
const missing = await getMissingDependencies(engine)
|
|
302
|
+
if (missing.length === 0) return []
|
|
303
|
+
|
|
304
|
+
// Group by package to avoid reinstalling the same package multiple times
|
|
305
|
+
const packageGroups = new Map<string, Dependency[]>()
|
|
306
|
+
for (const dep of missing) {
|
|
307
|
+
const pkgDef = dep.packages[packageManager.id]
|
|
308
|
+
if (pkgDef) {
|
|
309
|
+
const existing = packageGroups.get(pkgDef.package) || []
|
|
310
|
+
existing.push(dep)
|
|
311
|
+
packageGroups.set(pkgDef.package, existing)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const results: InstallResult[] = []
|
|
316
|
+
|
|
317
|
+
// Install each unique package once
|
|
318
|
+
for (const [, deps] of packageGroups) {
|
|
319
|
+
// Install using the first dependency (they all use the same package)
|
|
320
|
+
const result = await installDependency(deps[0], packageManager)
|
|
321
|
+
|
|
322
|
+
// Mark all dependencies from this package with the same result
|
|
323
|
+
for (const dep of deps) {
|
|
324
|
+
results.push({ ...result, dependency: dep })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Install all missing dependencies across all engines
|
|
333
|
+
*/
|
|
334
|
+
export async function installAllDependencies(
|
|
335
|
+
packageManager: DetectedPackageManager,
|
|
336
|
+
): Promise<InstallResult[]> {
|
|
337
|
+
const missing = await getAllMissingDependencies()
|
|
338
|
+
if (missing.length === 0) return []
|
|
339
|
+
|
|
340
|
+
// Group by package
|
|
341
|
+
const packageGroups = new Map<string, Dependency[]>()
|
|
342
|
+
for (const dep of missing) {
|
|
343
|
+
const pkgDef = dep.packages[packageManager.id]
|
|
344
|
+
if (pkgDef) {
|
|
345
|
+
const existing = packageGroups.get(pkgDef.package) || []
|
|
346
|
+
existing.push(dep)
|
|
347
|
+
packageGroups.set(pkgDef.package, existing)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const results: InstallResult[] = []
|
|
352
|
+
|
|
353
|
+
for (const [, deps] of packageGroups) {
|
|
354
|
+
const result = await installDependency(deps[0], packageManager)
|
|
355
|
+
for (const dep of deps) {
|
|
356
|
+
results.push({ ...result, dependency: dep })
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return results
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// =============================================================================
|
|
364
|
+
// Manual Installation Instructions
|
|
365
|
+
// =============================================================================
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get manual installation instructions for a dependency
|
|
369
|
+
*/
|
|
370
|
+
export function getManualInstallInstructions(
|
|
371
|
+
dependency: Dependency,
|
|
372
|
+
platform: Platform = getCurrentPlatform(),
|
|
373
|
+
): string[] {
|
|
374
|
+
return dependency.manualInstall[platform] || []
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get manual installation instructions for all missing dependencies of an engine
|
|
379
|
+
*/
|
|
380
|
+
export function getEngineManualInstallInstructions(
|
|
381
|
+
engine: string,
|
|
382
|
+
missingDeps: Dependency[],
|
|
383
|
+
platform: Platform = getCurrentPlatform(),
|
|
384
|
+
): string[] {
|
|
385
|
+
// Since all deps usually come from the same package, just get instructions from the first one
|
|
386
|
+
if (missingDeps.length === 0) return []
|
|
387
|
+
|
|
388
|
+
return getManualInstallInstructions(missingDeps[0], platform)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// =============================================================================
|
|
392
|
+
// High-Level API
|
|
393
|
+
// =============================================================================
|
|
394
|
+
|
|
395
|
+
export type DependencyCheckResult = {
|
|
396
|
+
engine: string
|
|
397
|
+
allInstalled: boolean
|
|
398
|
+
installed: DependencyStatus[]
|
|
399
|
+
missing: DependencyStatus[]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get a complete dependency report for an engine
|
|
404
|
+
*/
|
|
405
|
+
export async function getDependencyReport(
|
|
406
|
+
engine: string,
|
|
407
|
+
): Promise<DependencyCheckResult> {
|
|
408
|
+
const statuses = await checkEngineDependencies(engine)
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
engine,
|
|
412
|
+
allInstalled: statuses.every((s) => s.installed),
|
|
413
|
+
installed: statuses.filter((s) => s.installed),
|
|
414
|
+
missing: statuses.filter((s) => !s.installed),
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get dependency reports for all engines
|
|
420
|
+
*/
|
|
421
|
+
export async function getAllDependencyReports(): Promise<
|
|
422
|
+
DependencyCheckResult[]
|
|
423
|
+
> {
|
|
424
|
+
const engines = ['postgresql', 'mysql']
|
|
425
|
+
const reports = await Promise.all(
|
|
426
|
+
engines.map((engine) => getDependencyReport(engine)),
|
|
427
|
+
)
|
|
428
|
+
return reports
|
|
429
|
+
}
|
|
@@ -3,6 +3,13 @@ import { promisify } from 'util'
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { createSpinner } from '../cli/ui/spinner'
|
|
5
5
|
import { warning, error as themeError, success } from '../cli/ui/theme'
|
|
6
|
+
import {
|
|
7
|
+
detectPackageManager as detectPM,
|
|
8
|
+
installEngineDependencies,
|
|
9
|
+
getManualInstallInstructions,
|
|
10
|
+
getCurrentPlatform,
|
|
11
|
+
} from './dependency-manager'
|
|
12
|
+
import { getEngineDependencies } from '../config/os-dependencies'
|
|
6
13
|
|
|
7
14
|
const execAsync = promisify(exec)
|
|
8
15
|
|
|
@@ -273,39 +280,60 @@ export async function getBinaryInfo(
|
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
/**
|
|
276
|
-
* Install PostgreSQL client tools
|
|
283
|
+
* Install PostgreSQL client tools using the new dependency manager
|
|
277
284
|
*/
|
|
278
285
|
export async function installPostgresBinaries(): Promise<boolean> {
|
|
279
286
|
const spinner = createSpinner('Checking package manager...')
|
|
280
287
|
spinner.start()
|
|
281
288
|
|
|
282
|
-
const packageManager = await
|
|
289
|
+
const packageManager = await detectPM()
|
|
283
290
|
if (!packageManager) {
|
|
284
291
|
spinner.fail('No supported package manager found')
|
|
285
292
|
console.log(themeError('Please install PostgreSQL client tools manually:'))
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
|
|
294
|
+
// Show platform-specific instructions from the registry
|
|
295
|
+
const platform = getCurrentPlatform()
|
|
296
|
+
const pgDeps = getEngineDependencies('postgresql')
|
|
297
|
+
if (pgDeps && pgDeps.dependencies.length > 0) {
|
|
298
|
+
const instructions = getManualInstallInstructions(
|
|
299
|
+
pgDeps.dependencies[0],
|
|
300
|
+
platform,
|
|
301
|
+
)
|
|
302
|
+
for (const instruction of instructions) {
|
|
303
|
+
console.log(` ${instruction}`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
289
306
|
return false
|
|
290
307
|
}
|
|
291
308
|
|
|
292
309
|
spinner.succeed(`Found package manager: ${packageManager.name}`)
|
|
293
310
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
)
|
|
297
|
-
|
|
311
|
+
// Don't use a spinner during installation - it blocks TTY access for sudo password prompts
|
|
312
|
+
console.log(chalk.cyan(` Installing PostgreSQL client tools with ${packageManager.name}...`))
|
|
313
|
+
console.log(chalk.gray(' You may be prompted for your password.'))
|
|
314
|
+
console.log()
|
|
298
315
|
|
|
299
316
|
try {
|
|
300
|
-
await
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
317
|
+
const results = await installEngineDependencies('postgresql', packageManager)
|
|
318
|
+
const allSuccess = results.every((r) => r.success)
|
|
319
|
+
|
|
320
|
+
if (allSuccess) {
|
|
321
|
+
console.log()
|
|
322
|
+
console.log(success('PostgreSQL client tools installed successfully'))
|
|
323
|
+
return true
|
|
324
|
+
} else {
|
|
325
|
+
const failed = results.filter((r) => !r.success)
|
|
326
|
+
console.log()
|
|
327
|
+
console.log(themeError('Some installations failed:'))
|
|
328
|
+
for (const f of failed) {
|
|
329
|
+
console.log(themeError(` ${f.dependency.name}: ${f.error}`))
|
|
330
|
+
}
|
|
331
|
+
return false
|
|
332
|
+
}
|
|
304
333
|
} catch (error: unknown) {
|
|
305
|
-
|
|
334
|
+
console.log()
|
|
306
335
|
console.log(themeError('Failed to install PostgreSQL client tools'))
|
|
307
|
-
console.log(warning('Please install manually
|
|
308
|
-
console.log(` ${packageManager.installCommand('postgresql')}`)
|
|
336
|
+
console.log(warning('Please install manually'))
|
|
309
337
|
if (error instanceof Error) {
|
|
310
338
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
311
339
|
}
|
|
@@ -429,15 +457,9 @@ export async function ensurePostgresBinary(
|
|
|
429
457
|
): Promise<{ success: boolean; info: BinaryInfo | null; action?: string }> {
|
|
430
458
|
const { autoInstall = true, autoUpdate = true } = options
|
|
431
459
|
|
|
432
|
-
console.log(
|
|
433
|
-
`[DEBUG] ensurePostgresBinary called for ${binary}, dumpPath: ${dumpPath}`,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
460
|
// Check if binary exists
|
|
437
461
|
const info = await getBinaryInfo(binary, dumpPath)
|
|
438
462
|
|
|
439
|
-
console.log(`[DEBUG] getBinaryInfo result:`, info)
|
|
440
|
-
|
|
441
463
|
if (!info) {
|
|
442
464
|
if (!autoInstall) {
|
|
443
465
|
return { success: false, info: null, action: 'install_required' }
|
|
@@ -460,10 +482,6 @@ export async function ensurePostgresBinary(
|
|
|
460
482
|
|
|
461
483
|
// Check version compatibility
|
|
462
484
|
if (dumpPath && !info.isCompatible) {
|
|
463
|
-
console.log(
|
|
464
|
-
`[DEBUG] Version incompatible: current=${info.version}, required=${info.requiredVersion}`,
|
|
465
|
-
)
|
|
466
|
-
|
|
467
485
|
if (!autoUpdate) {
|
|
468
486
|
return { success: false, info, action: 'update_required' }
|
|
469
487
|
}
|
|
@@ -487,13 +505,11 @@ export async function ensurePostgresBinary(
|
|
|
487
505
|
// Check again after update
|
|
488
506
|
const updatedInfo = await getBinaryInfo(binary, dumpPath)
|
|
489
507
|
if (!updatedInfo || !updatedInfo.isCompatible) {
|
|
490
|
-
console.log(`[DEBUG] Update failed or still incompatible:`, updatedInfo)
|
|
491
508
|
return { success: false, info: updatedInfo, action: 'update_failed' }
|
|
492
509
|
}
|
|
493
510
|
|
|
494
511
|
return { success: true, info: updatedInfo, action: 'updated' }
|
|
495
512
|
}
|
|
496
513
|
|
|
497
|
-
console.log(`[DEBUG] Binary is compatible, returning success`)
|
|
498
514
|
return { success: true, info, action: 'compatible' }
|
|
499
515
|
}
|
package/engines/base-engine.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ProgressCallback,
|
|
4
4
|
BackupFormat,
|
|
5
5
|
RestoreResult,
|
|
6
|
+
DumpResult,
|
|
6
7
|
StatusResult,
|
|
7
8
|
} from '../types'
|
|
8
9
|
|
|
@@ -122,4 +123,12 @@ export abstract class BaseEngine {
|
|
|
122
123
|
}
|
|
123
124
|
return versions
|
|
124
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a dump from a remote database using a connection string
|
|
129
|
+
*/
|
|
130
|
+
abstract dumpFromConnectionString(
|
|
131
|
+
connectionString: string,
|
|
132
|
+
outputPath: string,
|
|
133
|
+
): Promise<DumpResult>
|
|
125
134
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ProgressCallback,
|
|
20
20
|
BackupFormat,
|
|
21
21
|
RestoreResult,
|
|
22
|
+
DumpResult,
|
|
22
23
|
StatusResult,
|
|
23
24
|
} from '../../types'
|
|
24
25
|
|
|
@@ -329,6 +330,58 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create a dump from a remote database using a connection string
|
|
336
|
+
* @param connectionString PostgreSQL connection string (e.g., postgresql://user:pass@host:port/dbname)
|
|
337
|
+
* @param outputPath Path where the dump file will be saved
|
|
338
|
+
* @returns DumpResult with file path and any output
|
|
339
|
+
*/
|
|
340
|
+
async dumpFromConnectionString(
|
|
341
|
+
connectionString: string,
|
|
342
|
+
outputPath: string,
|
|
343
|
+
): Promise<DumpResult> {
|
|
344
|
+
const pgDumpPath = await this.getPgDumpPath()
|
|
345
|
+
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
// Use custom format (-Fc) for best compatibility and compression
|
|
348
|
+
const args = [connectionString, '-Fc', '-f', outputPath]
|
|
349
|
+
|
|
350
|
+
const proc = spawn(pgDumpPath, args, {
|
|
351
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
let stdout = ''
|
|
355
|
+
let stderr = ''
|
|
356
|
+
|
|
357
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
358
|
+
stdout += data.toString()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
362
|
+
stderr += data.toString()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
366
|
+
reject(err)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
proc.on('close', (code) => {
|
|
370
|
+
if (code === 0) {
|
|
371
|
+
resolve({
|
|
372
|
+
filePath: outputPath,
|
|
373
|
+
stdout,
|
|
374
|
+
stderr,
|
|
375
|
+
code,
|
|
376
|
+
})
|
|
377
|
+
} else {
|
|
378
|
+
// pg_dump failed
|
|
379
|
+
const errorMessage = stderr || `pg_dump exited with code ${code}`
|
|
380
|
+
reject(new Error(errorMessage))
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
}
|
|
332
385
|
}
|
|
333
386
|
|
|
334
387
|
export const postgresqlEngine = new PostgreSQLEngine()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Spin up local database containers without Docker. A DBngin-like CLI for PostgreSQL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dev": "tsx watch cli/bin.ts",
|
|
12
12
|
"test": "tsx --test",
|
|
13
13
|
"format": "prettier --write .",
|
|
14
|
-
"lint": "eslint ."
|
|
14
|
+
"lint": "tsc --noEmit && eslint ."
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"postgres",
|
package/types/index.ts
CHANGED