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
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles checking, installing, and updating OS-level dependencies
|
|
5
|
+
* for database engines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exec } 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
|
+
* Execute command with timeout
|
|
177
|
+
*/
|
|
178
|
+
async function execWithTimeout(
|
|
179
|
+
command: string,
|
|
180
|
+
timeoutMs: number = 120000,
|
|
181
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const child = exec(
|
|
184
|
+
command,
|
|
185
|
+
{ timeout: timeoutMs },
|
|
186
|
+
(error, stdout, stderr) => {
|
|
187
|
+
if (error) {
|
|
188
|
+
reject(error)
|
|
189
|
+
} else {
|
|
190
|
+
resolve({ stdout, stderr })
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
child.kill('SIGTERM')
|
|
197
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`))
|
|
198
|
+
}, timeoutMs)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build install command for a dependency using a package manager
|
|
204
|
+
*/
|
|
205
|
+
export function buildInstallCommand(
|
|
206
|
+
dependency: Dependency,
|
|
207
|
+
packageManager: DetectedPackageManager,
|
|
208
|
+
): string[] {
|
|
209
|
+
const pkgDef = dependency.packages[packageManager.id]
|
|
210
|
+
if (!pkgDef) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`No package definition for ${dependency.name} with ${packageManager.name}`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const commands: string[] = []
|
|
217
|
+
|
|
218
|
+
// Pre-install commands
|
|
219
|
+
if (pkgDef.preInstall) {
|
|
220
|
+
commands.push(...pkgDef.preInstall)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Main install command
|
|
224
|
+
const installCmd = packageManager.config.installTemplate.replace(
|
|
225
|
+
'{package}',
|
|
226
|
+
pkgDef.package,
|
|
227
|
+
)
|
|
228
|
+
commands.push(installCmd)
|
|
229
|
+
|
|
230
|
+
// Post-install commands
|
|
231
|
+
if (pkgDef.postInstall) {
|
|
232
|
+
commands.push(...pkgDef.postInstall)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return commands
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Install a single dependency
|
|
240
|
+
*/
|
|
241
|
+
export async function installDependency(
|
|
242
|
+
dependency: Dependency,
|
|
243
|
+
packageManager: DetectedPackageManager,
|
|
244
|
+
): Promise<InstallResult> {
|
|
245
|
+
try {
|
|
246
|
+
const commands = buildInstallCommand(dependency, packageManager)
|
|
247
|
+
|
|
248
|
+
for (const cmd of commands) {
|
|
249
|
+
await execWithTimeout(cmd, 120000)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Verify installation
|
|
253
|
+
const status = await checkDependency(dependency)
|
|
254
|
+
if (!status.installed) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
dependency,
|
|
258
|
+
error: 'Installation completed but binary not found in PATH',
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { success: true, dependency }
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
dependency,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Install all dependencies for an engine
|
|
274
|
+
*/
|
|
275
|
+
export async function installEngineDependencies(
|
|
276
|
+
engine: string,
|
|
277
|
+
packageManager: DetectedPackageManager,
|
|
278
|
+
): Promise<InstallResult[]> {
|
|
279
|
+
const missing = await getMissingDependencies(engine)
|
|
280
|
+
if (missing.length === 0) return []
|
|
281
|
+
|
|
282
|
+
// Group by package to avoid reinstalling the same package multiple times
|
|
283
|
+
const packageGroups = new Map<string, Dependency[]>()
|
|
284
|
+
for (const dep of missing) {
|
|
285
|
+
const pkgDef = dep.packages[packageManager.id]
|
|
286
|
+
if (pkgDef) {
|
|
287
|
+
const existing = packageGroups.get(pkgDef.package) || []
|
|
288
|
+
existing.push(dep)
|
|
289
|
+
packageGroups.set(pkgDef.package, existing)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const results: InstallResult[] = []
|
|
294
|
+
|
|
295
|
+
// Install each unique package once
|
|
296
|
+
for (const [, deps] of packageGroups) {
|
|
297
|
+
// Install using the first dependency (they all use the same package)
|
|
298
|
+
const result = await installDependency(deps[0], packageManager)
|
|
299
|
+
|
|
300
|
+
// Mark all dependencies from this package with the same result
|
|
301
|
+
for (const dep of deps) {
|
|
302
|
+
results.push({ ...result, dependency: dep })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return results
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Install all missing dependencies across all engines
|
|
311
|
+
*/
|
|
312
|
+
export async function installAllDependencies(
|
|
313
|
+
packageManager: DetectedPackageManager,
|
|
314
|
+
): Promise<InstallResult[]> {
|
|
315
|
+
const missing = await getAllMissingDependencies()
|
|
316
|
+
if (missing.length === 0) return []
|
|
317
|
+
|
|
318
|
+
// Group by package
|
|
319
|
+
const packageGroups = new Map<string, Dependency[]>()
|
|
320
|
+
for (const dep of missing) {
|
|
321
|
+
const pkgDef = dep.packages[packageManager.id]
|
|
322
|
+
if (pkgDef) {
|
|
323
|
+
const existing = packageGroups.get(pkgDef.package) || []
|
|
324
|
+
existing.push(dep)
|
|
325
|
+
packageGroups.set(pkgDef.package, existing)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const results: InstallResult[] = []
|
|
330
|
+
|
|
331
|
+
for (const [, deps] of packageGroups) {
|
|
332
|
+
const result = await installDependency(deps[0], packageManager)
|
|
333
|
+
for (const dep of deps) {
|
|
334
|
+
results.push({ ...result, dependency: dep })
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return results
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// =============================================================================
|
|
342
|
+
// Manual Installation Instructions
|
|
343
|
+
// =============================================================================
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get manual installation instructions for a dependency
|
|
347
|
+
*/
|
|
348
|
+
export function getManualInstallInstructions(
|
|
349
|
+
dependency: Dependency,
|
|
350
|
+
platform: Platform = getCurrentPlatform(),
|
|
351
|
+
): string[] {
|
|
352
|
+
return dependency.manualInstall[platform] || []
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get manual installation instructions for all missing dependencies of an engine
|
|
357
|
+
*/
|
|
358
|
+
export function getEngineManualInstallInstructions(
|
|
359
|
+
engine: string,
|
|
360
|
+
missingDeps: Dependency[],
|
|
361
|
+
platform: Platform = getCurrentPlatform(),
|
|
362
|
+
): string[] {
|
|
363
|
+
// Since all deps usually come from the same package, just get instructions from the first one
|
|
364
|
+
if (missingDeps.length === 0) return []
|
|
365
|
+
|
|
366
|
+
return getManualInstallInstructions(missingDeps[0], platform)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// =============================================================================
|
|
370
|
+
// High-Level API
|
|
371
|
+
// =============================================================================
|
|
372
|
+
|
|
373
|
+
export type DependencyCheckResult = {
|
|
374
|
+
engine: string
|
|
375
|
+
allInstalled: boolean
|
|
376
|
+
installed: DependencyStatus[]
|
|
377
|
+
missing: DependencyStatus[]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get a complete dependency report for an engine
|
|
382
|
+
*/
|
|
383
|
+
export async function getDependencyReport(
|
|
384
|
+
engine: string,
|
|
385
|
+
): Promise<DependencyCheckResult> {
|
|
386
|
+
const statuses = await checkEngineDependencies(engine)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
engine,
|
|
390
|
+
allInstalled: statuses.every((s) => s.installed),
|
|
391
|
+
installed: statuses.filter((s) => s.installed),
|
|
392
|
+
missing: statuses.filter((s) => !s.installed),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get dependency reports for all engines
|
|
398
|
+
*/
|
|
399
|
+
export async function getAllDependencyReports(): Promise<
|
|
400
|
+
DependencyCheckResult[]
|
|
401
|
+
> {
|
|
402
|
+
const engines = ['postgresql', 'mysql']
|
|
403
|
+
const reports = await Promise.all(
|
|
404
|
+
engines.map((engine) => getDependencyReport(engine)),
|
|
405
|
+
)
|
|
406
|
+
return reports
|
|
407
|
+
}
|
|
@@ -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,19 +280,29 @@ 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
|
|
|
@@ -297,15 +314,25 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
297
314
|
installSpinner.start()
|
|
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
|
+
installSpinner.succeed('PostgreSQL client tools installed')
|
|
322
|
+
console.log(success('Installation completed successfully'))
|
|
323
|
+
return true
|
|
324
|
+
} else {
|
|
325
|
+
const failed = results.filter((r) => !r.success)
|
|
326
|
+
installSpinner.fail('Some installations failed')
|
|
327
|
+
for (const f of failed) {
|
|
328
|
+
console.log(themeError(`Failed to install ${f.dependency.name}: ${f.error}`))
|
|
329
|
+
}
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
304
332
|
} catch (error: unknown) {
|
|
305
333
|
installSpinner.fail('Installation failed')
|
|
306
334
|
console.log(themeError('Failed to install PostgreSQL client tools'))
|
|
307
|
-
console.log(warning('Please install manually
|
|
308
|
-
console.log(` ${packageManager.installCommand('postgresql')}`)
|
|
335
|
+
console.log(warning('Please install manually'))
|
|
309
336
|
if (error instanceof Error) {
|
|
310
337
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
311
338
|
}
|
|
@@ -429,15 +456,9 @@ export async function ensurePostgresBinary(
|
|
|
429
456
|
): Promise<{ success: boolean; info: BinaryInfo | null; action?: string }> {
|
|
430
457
|
const { autoInstall = true, autoUpdate = true } = options
|
|
431
458
|
|
|
432
|
-
console.log(
|
|
433
|
-
`[DEBUG] ensurePostgresBinary called for ${binary}, dumpPath: ${dumpPath}`,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
459
|
// Check if binary exists
|
|
437
460
|
const info = await getBinaryInfo(binary, dumpPath)
|
|
438
461
|
|
|
439
|
-
console.log(`[DEBUG] getBinaryInfo result:`, info)
|
|
440
|
-
|
|
441
462
|
if (!info) {
|
|
442
463
|
if (!autoInstall) {
|
|
443
464
|
return { success: false, info: null, action: 'install_required' }
|
|
@@ -460,10 +481,6 @@ export async function ensurePostgresBinary(
|
|
|
460
481
|
|
|
461
482
|
// Check version compatibility
|
|
462
483
|
if (dumpPath && !info.isCompatible) {
|
|
463
|
-
console.log(
|
|
464
|
-
`[DEBUG] Version incompatible: current=${info.version}, required=${info.requiredVersion}`,
|
|
465
|
-
)
|
|
466
|
-
|
|
467
484
|
if (!autoUpdate) {
|
|
468
485
|
return { success: false, info, action: 'update_required' }
|
|
469
486
|
}
|
|
@@ -487,13 +504,11 @@ export async function ensurePostgresBinary(
|
|
|
487
504
|
// Check again after update
|
|
488
505
|
const updatedInfo = await getBinaryInfo(binary, dumpPath)
|
|
489
506
|
if (!updatedInfo || !updatedInfo.isCompatible) {
|
|
490
|
-
console.log(`[DEBUG] Update failed or still incompatible:`, updatedInfo)
|
|
491
507
|
return { success: false, info: updatedInfo, action: 'update_failed' }
|
|
492
508
|
}
|
|
493
509
|
|
|
494
510
|
return { success: true, info: updatedInfo, action: 'updated' }
|
|
495
511
|
}
|
|
496
512
|
|
|
497
|
-
console.log(`[DEBUG] Binary is compatible, returning success`)
|
|
498
513
|
return { success: true, info, action: 'compatible' }
|
|
499
514
|
}
|
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.0",
|
|
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