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
package/cli/commands/restore.ts
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
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'
|
|
18
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
12
19
|
|
|
13
20
|
export const restoreCommand = new Command('restore')
|
|
14
21
|
.description('Restore a backup to a container')
|
|
15
22
|
.argument('[name]', 'Container name')
|
|
16
|
-
.argument(
|
|
23
|
+
.argument(
|
|
24
|
+
'[backup]',
|
|
25
|
+
'Path to backup file (not required if using --from-url)',
|
|
26
|
+
)
|
|
17
27
|
.option('-d, --database <name>', 'Target database name')
|
|
28
|
+
.option(
|
|
29
|
+
'--from-url <url>',
|
|
30
|
+
'Pull data from a remote database connection string',
|
|
31
|
+
)
|
|
18
32
|
.action(
|
|
19
33
|
async (
|
|
20
34
|
name: string | undefined,
|
|
21
35
|
backup: string | undefined,
|
|
22
|
-
options: { database?: string },
|
|
36
|
+
options: { database?: string; fromUrl?: string },
|
|
23
37
|
) => {
|
|
38
|
+
let tempDumpPath: string | null = null
|
|
39
|
+
|
|
24
40
|
try {
|
|
25
41
|
let containerName = name
|
|
26
|
-
|
|
42
|
+
let backupPath = backup
|
|
27
43
|
|
|
28
44
|
// Interactive selection if no name provided
|
|
29
45
|
if (!containerName) {
|
|
@@ -71,18 +87,129 @@ export const restoreCommand = new Command('restore')
|
|
|
71
87
|
process.exit(1)
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
// Get engine
|
|
91
|
+
const engine = getEngine(config.engine)
|
|
92
|
+
|
|
93
|
+
// Check for required client tools BEFORE doing anything
|
|
94
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
95
|
+
depsSpinner.start()
|
|
96
|
+
|
|
97
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
98
|
+
if (missingDeps.length > 0) {
|
|
99
|
+
depsSpinner.warn(
|
|
100
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
79
101
|
)
|
|
80
|
-
|
|
102
|
+
|
|
103
|
+
// Offer to install
|
|
104
|
+
const installed = await promptInstallDependencies(
|
|
105
|
+
missingDeps[0].binary,
|
|
106
|
+
config.engine,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (!installed) {
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Verify installation worked
|
|
114
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
115
|
+
if (missingDeps.length > 0) {
|
|
116
|
+
console.error(
|
|
117
|
+
error(
|
|
118
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
125
|
+
console.log()
|
|
126
|
+
} else {
|
|
127
|
+
depsSpinner.succeed('Required tools available')
|
|
81
128
|
}
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
130
|
+
// Handle --from-url option
|
|
131
|
+
if (options.fromUrl) {
|
|
132
|
+
// Validate connection string
|
|
133
|
+
if (
|
|
134
|
+
!options.fromUrl.startsWith('postgresql://') &&
|
|
135
|
+
!options.fromUrl.startsWith('postgres://')
|
|
136
|
+
) {
|
|
137
|
+
console.error(
|
|
138
|
+
error(
|
|
139
|
+
'Connection string must start with postgresql:// or postgres://',
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Create temp file for the dump
|
|
146
|
+
const timestamp = Date.now()
|
|
147
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
148
|
+
|
|
149
|
+
let dumpSuccess = false
|
|
150
|
+
let attempts = 0
|
|
151
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
152
|
+
|
|
153
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
154
|
+
attempts++
|
|
155
|
+
const dumpSpinner = createSpinner(
|
|
156
|
+
'Creating dump from remote database...',
|
|
157
|
+
)
|
|
158
|
+
dumpSpinner.start()
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await engine.dumpFromConnectionString(options.fromUrl, tempDumpPath)
|
|
162
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
163
|
+
backupPath = tempDumpPath
|
|
164
|
+
dumpSuccess = true
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const e = err as Error
|
|
167
|
+
dumpSpinner.fail('Failed to create dump')
|
|
168
|
+
|
|
169
|
+
// Check if this is a missing tool error
|
|
170
|
+
if (
|
|
171
|
+
e.message.includes('pg_dump not found') ||
|
|
172
|
+
e.message.includes('ENOENT')
|
|
173
|
+
) {
|
|
174
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
175
|
+
if (!installed) {
|
|
176
|
+
process.exit(1)
|
|
177
|
+
}
|
|
178
|
+
// Loop will retry
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log()
|
|
183
|
+
console.error(error('pg_dump error:'))
|
|
184
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Safety check - should never reach here without backupPath set
|
|
190
|
+
if (!dumpSuccess) {
|
|
191
|
+
console.error(error('Failed to create dump after retries'))
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// Check backup file
|
|
196
|
+
if (!backupPath) {
|
|
197
|
+
console.error(error('Backup file path is required'))
|
|
198
|
+
console.log(
|
|
199
|
+
chalk.gray(' Usage: spindb restore <container> <backup-file>'),
|
|
200
|
+
)
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.gray(
|
|
203
|
+
' or: spindb restore <container> --from-url <connection-string>',
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
process.exit(1)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!existsSync(backupPath)) {
|
|
210
|
+
console.error(error(`Backup file not found: ${backupPath}`))
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
86
213
|
}
|
|
87
214
|
|
|
88
215
|
// Get database name
|
|
@@ -91,8 +218,11 @@ export const restoreCommand = new Command('restore')
|
|
|
91
218
|
databaseName = await promptDatabaseName(containerName)
|
|
92
219
|
}
|
|
93
220
|
|
|
94
|
-
//
|
|
95
|
-
|
|
221
|
+
// At this point backupPath is guaranteed to be set
|
|
222
|
+
if (!backupPath) {
|
|
223
|
+
console.error(error('No backup path specified'))
|
|
224
|
+
process.exit(1)
|
|
225
|
+
}
|
|
96
226
|
|
|
97
227
|
// Detect backup format
|
|
98
228
|
const detectSpinner = createSpinner('Detecting backup format...')
|
|
@@ -182,8 +312,35 @@ export const restoreCommand = new Command('restore')
|
|
|
182
312
|
console.log()
|
|
183
313
|
} catch (err) {
|
|
184
314
|
const e = err as Error
|
|
315
|
+
|
|
316
|
+
// Check if this is a missing tool error
|
|
317
|
+
if (
|
|
318
|
+
e.message.includes('pg_restore not found') ||
|
|
319
|
+
e.message.includes('psql not found')
|
|
320
|
+
) {
|
|
321
|
+
const missingTool = e.message.includes('pg_restore')
|
|
322
|
+
? 'pg_restore'
|
|
323
|
+
: 'psql'
|
|
324
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
325
|
+
if (installed) {
|
|
326
|
+
console.log(
|
|
327
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
|
|
185
333
|
console.error(error(e.message))
|
|
186
334
|
process.exit(1)
|
|
335
|
+
} finally {
|
|
336
|
+
// Clean up temp file if we created one
|
|
337
|
+
if (tempDumpPath) {
|
|
338
|
+
try {
|
|
339
|
+
await rm(tempDumpPath, { force: true })
|
|
340
|
+
} catch {
|
|
341
|
+
// Ignore cleanup errors
|
|
342
|
+
}
|
|
343
|
+
}
|
|
187
344
|
}
|
|
188
345
|
},
|
|
189
346
|
)
|
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(' Continuing with your operation...'))
|
|
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
|
+
}
|