spindb 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -8
- package/cli/commands/create.ts +203 -1
- package/cli/commands/deps.ts +326 -0
- package/cli/commands/menu.ts +277 -28
- package/cli/commands/restore.ts +108 -18
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +133 -0
- package/config/os-dependencies.ts +358 -0
- package/core/dependency-manager.ts +407 -0
- package/core/postgres-binary-manager.ts +38 -23
- package/engines/base-engine.ts +9 -0
- package/engines/postgresql/index.ts +53 -0
- package/package.json +2 -2
- package/types/index.ts +7 -0
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ spindb connect mydb
|
|
|
51
51
|
| `spindb delete [name]` | Delete a container |
|
|
52
52
|
| `spindb config show` | Show configuration |
|
|
53
53
|
| `spindb config detect` | Auto-detect PostgreSQL tools |
|
|
54
|
+
| `spindb deps check` | Check status of client tools |
|
|
55
|
+
| `spindb deps install` | Install missing client tools |
|
|
54
56
|
|
|
55
57
|
## How It Works
|
|
56
58
|
|
|
@@ -71,15 +73,50 @@ Data is stored in `~/.spindb/`:
|
|
|
71
73
|
|
|
72
74
|
## PostgreSQL Client Tools
|
|
73
75
|
|
|
74
|
-
SpinDB bundles the PostgreSQL **server** (postgres, pg_ctl, initdb) but not client tools (psql, pg_dump, pg_restore). For `connect` and `restore` commands, you need PostgreSQL client tools installed
|
|
76
|
+
SpinDB bundles the PostgreSQL **server** (postgres, pg_ctl, initdb) but not client tools (psql, pg_dump, pg_restore). For `connect` and `restore` commands, you need PostgreSQL client tools installed.
|
|
77
|
+
|
|
78
|
+
### Automatic Installation
|
|
79
|
+
|
|
80
|
+
SpinDB can check and install client tools automatically:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Check status of all client tools
|
|
84
|
+
spindb deps check
|
|
85
|
+
|
|
86
|
+
# Install missing tools (uses Homebrew, apt, yum, dnf, or pacman)
|
|
87
|
+
spindb deps install
|
|
88
|
+
|
|
89
|
+
# Install for a specific engine
|
|
90
|
+
spindb deps install --engine postgresql
|
|
91
|
+
spindb deps install --engine mysql
|
|
92
|
+
|
|
93
|
+
# Install all missing dependencies for all engines
|
|
94
|
+
spindb deps install --all
|
|
95
|
+
|
|
96
|
+
# List all supported dependencies
|
|
97
|
+
spindb deps list
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Manual Installation
|
|
101
|
+
|
|
102
|
+
If automatic installation doesn't work, install manually:
|
|
75
103
|
|
|
76
104
|
```bash
|
|
77
105
|
# macOS (Homebrew)
|
|
78
|
-
brew install
|
|
79
|
-
brew link --
|
|
106
|
+
brew install postgresql@17
|
|
107
|
+
brew link --overwrite postgresql@17
|
|
80
108
|
|
|
81
109
|
# Ubuntu/Debian
|
|
82
|
-
apt install postgresql-client
|
|
110
|
+
sudo apt install postgresql-client
|
|
111
|
+
|
|
112
|
+
# CentOS/RHEL
|
|
113
|
+
sudo yum install postgresql
|
|
114
|
+
|
|
115
|
+
# Fedora
|
|
116
|
+
sudo dnf install postgresql
|
|
117
|
+
|
|
118
|
+
# Arch
|
|
119
|
+
sudo pacman -S postgresql-libs
|
|
83
120
|
|
|
84
121
|
# Or use Postgres.app (macOS)
|
|
85
122
|
# Client tools are automatically detected
|
|
@@ -123,16 +160,33 @@ spindb create mydb --database my_app_db
|
|
|
123
160
|
# Connection string: postgresql://postgres@localhost:5432/my_app_db
|
|
124
161
|
```
|
|
125
162
|
|
|
126
|
-
###
|
|
163
|
+
### Create and restore in one command
|
|
127
164
|
|
|
128
165
|
```bash
|
|
129
|
-
#
|
|
130
|
-
spindb
|
|
166
|
+
# Create a container and restore from a dump file
|
|
167
|
+
spindb create mydb --from ./backup.dump
|
|
168
|
+
|
|
169
|
+
# Create a container and pull from a remote database
|
|
170
|
+
spindb create mydb --from "postgresql://user:pass@remote-host:5432/production_db"
|
|
171
|
+
|
|
172
|
+
# With specific version and database name
|
|
173
|
+
spindb create mydb --pg-version 17 --database myapp --from ./backup.dump
|
|
174
|
+
```
|
|
131
175
|
|
|
132
|
-
|
|
176
|
+
The `--from` option auto-detects whether the location is a file path or connection string.
|
|
177
|
+
|
|
178
|
+
### Restore to an existing container
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Restore from a dump file (supports .sql, custom format, and tar format)
|
|
133
182
|
spindb restore mydb ./backup.dump -d myapp
|
|
183
|
+
|
|
184
|
+
# Or pull directly from a remote database
|
|
185
|
+
spindb restore mydb --from-url "postgresql://user:pass@remote-host:5432/production_db" -d myapp
|
|
134
186
|
```
|
|
135
187
|
|
|
188
|
+
The interactive menu (`spindb` → "Restore backup") also offers an option to create a new container as part of the restore flow.
|
|
189
|
+
|
|
136
190
|
### Clone for testing
|
|
137
191
|
|
|
138
192
|
```bash
|
package/cli/commands/create.ts
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { rm } from 'fs/promises'
|
|
2
4
|
import chalk from 'chalk'
|
|
3
5
|
import { containerManager } from '../../core/container-manager'
|
|
4
6
|
import { portManager } from '../../core/port-manager'
|
|
5
7
|
import { getEngine } from '../../engines'
|
|
6
8
|
import { defaults } from '../../config/defaults'
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
promptCreateOptions,
|
|
11
|
+
promptInstallDependencies,
|
|
12
|
+
} from '../ui/prompts'
|
|
8
13
|
import { createSpinner } from '../ui/spinner'
|
|
9
14
|
import { header, error, connectionBox } from '../ui/theme'
|
|
15
|
+
import { tmpdir } from 'os'
|
|
16
|
+
import { join } from 'path'
|
|
17
|
+
import { spawn } from 'child_process'
|
|
18
|
+
import { platform } from 'os'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect if a location string is a connection string or a file path
|
|
22
|
+
*/
|
|
23
|
+
function detectLocationType(
|
|
24
|
+
location: string,
|
|
25
|
+
): 'connection' | 'file' | 'not_found' {
|
|
26
|
+
// Check if it's a connection string
|
|
27
|
+
if (
|
|
28
|
+
location.startsWith('postgresql://') ||
|
|
29
|
+
location.startsWith('postgres://')
|
|
30
|
+
) {
|
|
31
|
+
return 'connection'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if file exists
|
|
35
|
+
if (existsSync(location)) {
|
|
36
|
+
return 'file'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'not_found'
|
|
40
|
+
}
|
|
10
41
|
|
|
11
42
|
export const createCommand = new Command('create')
|
|
12
43
|
.description('Create a new database container')
|
|
@@ -20,6 +51,10 @@ export const createCommand = new Command('create')
|
|
|
20
51
|
.option('-d, --database <database>', 'Database name')
|
|
21
52
|
.option('-p, --port <port>', 'Port number')
|
|
22
53
|
.option('--no-start', 'Do not start the container after creation')
|
|
54
|
+
.option(
|
|
55
|
+
'--from <location>',
|
|
56
|
+
'Restore from a dump file or connection string after creation',
|
|
57
|
+
)
|
|
23
58
|
.action(
|
|
24
59
|
async (
|
|
25
60
|
name: string | undefined,
|
|
@@ -29,8 +64,11 @@ export const createCommand = new Command('create')
|
|
|
29
64
|
database?: string
|
|
30
65
|
port?: string
|
|
31
66
|
start: boolean
|
|
67
|
+
from?: string
|
|
32
68
|
},
|
|
33
69
|
) => {
|
|
70
|
+
let tempDumpPath: string | null = null
|
|
71
|
+
|
|
34
72
|
try {
|
|
35
73
|
let containerName = name
|
|
36
74
|
let engine = options.engine
|
|
@@ -49,6 +87,37 @@ export const createCommand = new Command('create')
|
|
|
49
87
|
// Default database name to container name if not specified
|
|
50
88
|
database = database ?? containerName
|
|
51
89
|
|
|
90
|
+
// Validate --from location if provided
|
|
91
|
+
let restoreLocation: string | null = null
|
|
92
|
+
let restoreType: 'connection' | 'file' | null = null
|
|
93
|
+
|
|
94
|
+
if (options.from) {
|
|
95
|
+
const locationType = detectLocationType(options.from)
|
|
96
|
+
|
|
97
|
+
if (locationType === 'not_found') {
|
|
98
|
+
console.error(error(`Location not found: ${options.from}`))
|
|
99
|
+
console.log(
|
|
100
|
+
chalk.gray(
|
|
101
|
+
' Provide a valid file path or connection string (postgresql://...)',
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
restoreLocation = options.from
|
|
108
|
+
restoreType = locationType
|
|
109
|
+
|
|
110
|
+
// If using --from, we must start the container
|
|
111
|
+
if (options.start === false) {
|
|
112
|
+
console.error(
|
|
113
|
+
error(
|
|
114
|
+
'Cannot use --no-start with --from (restore requires running container)',
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
52
121
|
console.log(header('Creating Database Container'))
|
|
53
122
|
console.log()
|
|
54
123
|
|
|
@@ -145,6 +214,84 @@ export const createCommand = new Command('create')
|
|
|
145
214
|
|
|
146
215
|
dbSpinner.succeed(`Database "${database}" created`)
|
|
147
216
|
}
|
|
217
|
+
|
|
218
|
+
// Handle --from restore if specified
|
|
219
|
+
if (restoreLocation && restoreType && config) {
|
|
220
|
+
let backupPath: string
|
|
221
|
+
|
|
222
|
+
if (restoreType === 'connection') {
|
|
223
|
+
// Create dump from remote database
|
|
224
|
+
const timestamp = Date.now()
|
|
225
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
226
|
+
|
|
227
|
+
const dumpSpinner = createSpinner(
|
|
228
|
+
'Creating dump from remote database...',
|
|
229
|
+
)
|
|
230
|
+
dumpSpinner.start()
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await dbEngine.dumpFromConnectionString(
|
|
234
|
+
restoreLocation,
|
|
235
|
+
tempDumpPath,
|
|
236
|
+
)
|
|
237
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
238
|
+
backupPath = tempDumpPath
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const e = err as Error
|
|
241
|
+
dumpSpinner.fail('Failed to create dump')
|
|
242
|
+
|
|
243
|
+
// Check if this is a missing tool error
|
|
244
|
+
if (
|
|
245
|
+
e.message.includes('pg_dump not found') ||
|
|
246
|
+
e.message.includes('ENOENT')
|
|
247
|
+
) {
|
|
248
|
+
await promptInstallDependencies('pg_dump')
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log()
|
|
253
|
+
console.error(error('pg_dump error:'))
|
|
254
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
255
|
+
process.exit(1)
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
backupPath = restoreLocation
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Detect backup format
|
|
262
|
+
const detectSpinner = createSpinner('Detecting backup format...')
|
|
263
|
+
detectSpinner.start()
|
|
264
|
+
|
|
265
|
+
const format = await dbEngine.detectBackupFormat(backupPath)
|
|
266
|
+
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
267
|
+
|
|
268
|
+
// Restore backup
|
|
269
|
+
const restoreSpinner = createSpinner('Restoring backup...')
|
|
270
|
+
restoreSpinner.start()
|
|
271
|
+
|
|
272
|
+
const result = await dbEngine.restore(config, backupPath, {
|
|
273
|
+
database,
|
|
274
|
+
createDatabase: false, // Already created above
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
if (result.code === 0 || !result.stderr) {
|
|
278
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
279
|
+
} else {
|
|
280
|
+
restoreSpinner.warn('Restore completed with warnings')
|
|
281
|
+
if (result.stderr) {
|
|
282
|
+
console.log(chalk.yellow('\n Warnings:'))
|
|
283
|
+
const lines = result.stderr.split('\n').slice(0, 5)
|
|
284
|
+
lines.forEach((line) => {
|
|
285
|
+
if (line.trim()) {
|
|
286
|
+
console.log(chalk.gray(` ${line}`))
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
if (result.stderr.split('\n').length > 5) {
|
|
290
|
+
console.log(chalk.gray(' ...'))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
148
295
|
}
|
|
149
296
|
|
|
150
297
|
// Show success message
|
|
@@ -157,12 +304,67 @@ export const createCommand = new Command('create')
|
|
|
157
304
|
console.log()
|
|
158
305
|
console.log(chalk.gray(' Connect with:'))
|
|
159
306
|
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
307
|
+
|
|
308
|
+
// Copy connection string to clipboard
|
|
309
|
+
if (options.start !== false) {
|
|
310
|
+
try {
|
|
311
|
+
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
312
|
+
const args =
|
|
313
|
+
platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
314
|
+
|
|
315
|
+
await new Promise<void>((resolve, reject) => {
|
|
316
|
+
const proc = spawn(cmd, args, {
|
|
317
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
318
|
+
})
|
|
319
|
+
proc.stdin?.write(connectionString)
|
|
320
|
+
proc.stdin?.end()
|
|
321
|
+
proc.on('close', (code) => {
|
|
322
|
+
if (code === 0) resolve()
|
|
323
|
+
else
|
|
324
|
+
reject(
|
|
325
|
+
new Error(`Clipboard command exited with code ${code}`),
|
|
326
|
+
)
|
|
327
|
+
})
|
|
328
|
+
proc.on('error', reject)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
332
|
+
} catch {
|
|
333
|
+
// Ignore clipboard errors
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
160
337
|
console.log()
|
|
161
338
|
}
|
|
162
339
|
} catch (err) {
|
|
163
340
|
const e = err as Error
|
|
341
|
+
|
|
342
|
+
// Check if this is a missing tool error
|
|
343
|
+
if (
|
|
344
|
+
e.message.includes('pg_restore not found') ||
|
|
345
|
+
e.message.includes('psql not found') ||
|
|
346
|
+
e.message.includes('pg_dump not found')
|
|
347
|
+
) {
|
|
348
|
+
const missingTool = e.message.includes('pg_restore')
|
|
349
|
+
? 'pg_restore'
|
|
350
|
+
: e.message.includes('pg_dump')
|
|
351
|
+
? 'pg_dump'
|
|
352
|
+
: 'psql'
|
|
353
|
+
await promptInstallDependencies(missingTool)
|
|
354
|
+
process.exit(1)
|
|
355
|
+
}
|
|
356
|
+
|
|
164
357
|
console.error(error(e.message))
|
|
165
358
|
process.exit(1)
|
|
359
|
+
} finally {
|
|
360
|
+
// Clean up temp file if we created one
|
|
361
|
+
if (tempDumpPath) {
|
|
362
|
+
try {
|
|
363
|
+
await rm(tempDumpPath, { force: true })
|
|
364
|
+
} catch {
|
|
365
|
+
// Ignore cleanup errors
|
|
366
|
+
}
|
|
367
|
+
}
|
|
166
368
|
}
|
|
167
369
|
},
|
|
168
370
|
)
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { header, success, warning, error } from '../ui/theme'
|
|
4
|
+
import { createSpinner } from '../ui/spinner'
|
|
5
|
+
import {
|
|
6
|
+
detectPackageManager,
|
|
7
|
+
checkEngineDependencies,
|
|
8
|
+
getMissingDependencies,
|
|
9
|
+
getAllMissingDependencies,
|
|
10
|
+
installEngineDependencies,
|
|
11
|
+
installAllDependencies,
|
|
12
|
+
getManualInstallInstructions,
|
|
13
|
+
getCurrentPlatform,
|
|
14
|
+
type DependencyStatus,
|
|
15
|
+
} from '../../core/dependency-manager'
|
|
16
|
+
import {
|
|
17
|
+
engineDependencies,
|
|
18
|
+
getEngineDependencies,
|
|
19
|
+
} from '../../config/os-dependencies'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format dependency status for display
|
|
23
|
+
*/
|
|
24
|
+
function formatStatus(status: DependencyStatus): string {
|
|
25
|
+
const { dependency, installed, path, version } = status
|
|
26
|
+
|
|
27
|
+
if (installed) {
|
|
28
|
+
const versionStr = version ? ` (${version})` : ''
|
|
29
|
+
const pathStr = path ? chalk.gray(` → ${path}`) : ''
|
|
30
|
+
return ` ${chalk.green('✓')} ${dependency.name}${versionStr}${pathStr}`
|
|
31
|
+
} else {
|
|
32
|
+
return ` ${chalk.red('✗')} ${dependency.name} ${chalk.gray('- not installed')}`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const depsCommand = new Command('deps').description(
|
|
37
|
+
'Manage OS-level database client dependencies',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// deps check
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
depsCommand
|
|
45
|
+
.command('check')
|
|
46
|
+
.description('Check status of database client tools')
|
|
47
|
+
.option('-e, --engine <engine>', 'Check dependencies for a specific engine')
|
|
48
|
+
.option('-a, --all', 'Check all dependencies for all engines')
|
|
49
|
+
.action(async (options: { engine?: string; all?: boolean }) => {
|
|
50
|
+
console.log(header('Dependency Status'))
|
|
51
|
+
console.log()
|
|
52
|
+
|
|
53
|
+
// Detect package manager
|
|
54
|
+
const packageManager = await detectPackageManager()
|
|
55
|
+
if (packageManager) {
|
|
56
|
+
console.log(` Package Manager: ${chalk.cyan(packageManager.name)}`)
|
|
57
|
+
} else {
|
|
58
|
+
console.log(` Package Manager: ${chalk.yellow('Not detected')}`)
|
|
59
|
+
}
|
|
60
|
+
console.log()
|
|
61
|
+
|
|
62
|
+
if (options.all || (!options.engine && !options.all)) {
|
|
63
|
+
// Check all engines
|
|
64
|
+
for (const engineConfig of engineDependencies) {
|
|
65
|
+
console.log(chalk.bold(`${engineConfig.displayName}:`))
|
|
66
|
+
|
|
67
|
+
const statuses = await checkEngineDependencies(engineConfig.engine)
|
|
68
|
+
for (const status of statuses) {
|
|
69
|
+
console.log(formatStatus(status))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const installed = statuses.filter((s) => s.installed).length
|
|
73
|
+
const total = statuses.length
|
|
74
|
+
if (installed === total) {
|
|
75
|
+
console.log(chalk.green(` All ${total} dependencies installed`))
|
|
76
|
+
} else {
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.yellow(` ${installed}/${total} dependencies installed`),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
console.log()
|
|
82
|
+
}
|
|
83
|
+
} else if (options.engine) {
|
|
84
|
+
// Check specific engine
|
|
85
|
+
const engineConfig = getEngineDependencies(options.engine)
|
|
86
|
+
if (!engineConfig) {
|
|
87
|
+
console.error(error(`Unknown engine: ${options.engine}`))
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.gray(
|
|
90
|
+
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.bold(`${engineConfig.displayName}:`))
|
|
97
|
+
|
|
98
|
+
const statuses = await checkEngineDependencies(options.engine)
|
|
99
|
+
for (const status of statuses) {
|
|
100
|
+
console.log(formatStatus(status))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const installed = statuses.filter((s) => s.installed).length
|
|
104
|
+
const total = statuses.length
|
|
105
|
+
console.log()
|
|
106
|
+
if (installed === total) {
|
|
107
|
+
console.log(success(`All ${total} dependencies installed`))
|
|
108
|
+
} else {
|
|
109
|
+
console.log(warning(`${installed}/${total} dependencies installed`))
|
|
110
|
+
console.log()
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.gray(` Run: spindb deps install --engine ${options.engine}`),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// deps install
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
depsCommand
|
|
123
|
+
.command('install')
|
|
124
|
+
.description('Install missing database client tools')
|
|
125
|
+
.option(
|
|
126
|
+
'-e, --engine <engine>',
|
|
127
|
+
'Install dependencies for a specific engine (e.g., postgresql, mysql)',
|
|
128
|
+
)
|
|
129
|
+
.option('-a, --all', 'Install all missing dependencies for all engines')
|
|
130
|
+
.action(async (options: { engine?: string; all?: boolean }) => {
|
|
131
|
+
// Detect package manager first
|
|
132
|
+
const packageManager = await detectPackageManager()
|
|
133
|
+
|
|
134
|
+
if (!packageManager) {
|
|
135
|
+
console.log(error('No supported package manager detected'))
|
|
136
|
+
console.log()
|
|
137
|
+
|
|
138
|
+
const platform = getCurrentPlatform()
|
|
139
|
+
if (platform === 'darwin') {
|
|
140
|
+
console.log(chalk.gray(' macOS: Install Homebrew first:'))
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.cyan(
|
|
143
|
+
' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
} else {
|
|
147
|
+
console.log(
|
|
148
|
+
chalk.gray(' Supported package managers: apt, yum, dnf, pacman'),
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(header('Installing Dependencies'))
|
|
155
|
+
console.log()
|
|
156
|
+
console.log(` Using: ${chalk.cyan(packageManager.name)}`)
|
|
157
|
+
console.log()
|
|
158
|
+
|
|
159
|
+
if (options.all) {
|
|
160
|
+
// Install all missing dependencies
|
|
161
|
+
const missing = await getAllMissingDependencies()
|
|
162
|
+
|
|
163
|
+
if (missing.length === 0) {
|
|
164
|
+
console.log(success('All dependencies are already installed'))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(` Missing: ${missing.map((d) => d.name).join(', ')}`)
|
|
169
|
+
console.log()
|
|
170
|
+
|
|
171
|
+
const spinner = createSpinner('Installing dependencies...')
|
|
172
|
+
spinner.start()
|
|
173
|
+
|
|
174
|
+
const results = await installAllDependencies(packageManager)
|
|
175
|
+
|
|
176
|
+
const succeeded = results.filter((r) => r.success)
|
|
177
|
+
const failed = results.filter((r) => !r.success)
|
|
178
|
+
|
|
179
|
+
if (failed.length === 0) {
|
|
180
|
+
spinner.succeed('All dependencies installed successfully')
|
|
181
|
+
} else {
|
|
182
|
+
spinner.warn('Some dependencies failed to install')
|
|
183
|
+
console.log()
|
|
184
|
+
for (const f of failed) {
|
|
185
|
+
console.log(error(` ${f.dependency.name}: ${f.error}`))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (succeeded.length > 0) {
|
|
190
|
+
console.log()
|
|
191
|
+
console.log(success(`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`))
|
|
192
|
+
}
|
|
193
|
+
} else if (options.engine) {
|
|
194
|
+
// Install dependencies for specific engine
|
|
195
|
+
const engineConfig = getEngineDependencies(options.engine)
|
|
196
|
+
if (!engineConfig) {
|
|
197
|
+
console.error(error(`Unknown engine: ${options.engine}`))
|
|
198
|
+
console.log(
|
|
199
|
+
chalk.gray(
|
|
200
|
+
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const missing = await getMissingDependencies(options.engine)
|
|
207
|
+
|
|
208
|
+
if (missing.length === 0) {
|
|
209
|
+
console.log(
|
|
210
|
+
success(`All ${engineConfig.displayName} dependencies are installed`),
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(` Engine: ${chalk.cyan(engineConfig.displayName)}`)
|
|
216
|
+
console.log(` Missing: ${missing.map((d) => d.name).join(', ')}`)
|
|
217
|
+
console.log()
|
|
218
|
+
|
|
219
|
+
const spinner = createSpinner(
|
|
220
|
+
`Installing ${engineConfig.displayName} dependencies...`,
|
|
221
|
+
)
|
|
222
|
+
spinner.start()
|
|
223
|
+
|
|
224
|
+
const results = await installEngineDependencies(
|
|
225
|
+
options.engine,
|
|
226
|
+
packageManager,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const succeeded = results.filter((r) => r.success)
|
|
230
|
+
const failed = results.filter((r) => !r.success)
|
|
231
|
+
|
|
232
|
+
if (failed.length === 0) {
|
|
233
|
+
spinner.succeed(
|
|
234
|
+
`${engineConfig.displayName} dependencies installed successfully`,
|
|
235
|
+
)
|
|
236
|
+
} else {
|
|
237
|
+
spinner.warn('Some dependencies failed to install')
|
|
238
|
+
console.log()
|
|
239
|
+
for (const f of failed) {
|
|
240
|
+
console.log(error(` ${f.dependency.name}: ${f.error}`))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Show manual instructions
|
|
244
|
+
console.log()
|
|
245
|
+
console.log(chalk.gray(' Manual installation:'))
|
|
246
|
+
const instructions = getManualInstallInstructions(
|
|
247
|
+
missing[0],
|
|
248
|
+
getCurrentPlatform(),
|
|
249
|
+
)
|
|
250
|
+
for (const instruction of instructions) {
|
|
251
|
+
console.log(chalk.gray(` ${instruction}`))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (succeeded.length > 0) {
|
|
256
|
+
console.log()
|
|
257
|
+
console.log(success(`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`))
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// Default: install PostgreSQL dependencies (most common use case)
|
|
261
|
+
console.log(
|
|
262
|
+
chalk.gray(
|
|
263
|
+
' No engine specified, defaulting to PostgreSQL. Use --all for all engines.',
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
console.log()
|
|
267
|
+
|
|
268
|
+
const missing = await getMissingDependencies('postgresql')
|
|
269
|
+
|
|
270
|
+
if (missing.length === 0) {
|
|
271
|
+
console.log(success('All PostgreSQL dependencies are installed'))
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(` Missing: ${missing.map((d) => d.name).join(', ')}`)
|
|
276
|
+
console.log()
|
|
277
|
+
|
|
278
|
+
const spinner = createSpinner('Installing PostgreSQL dependencies...')
|
|
279
|
+
spinner.start()
|
|
280
|
+
|
|
281
|
+
const results = await installEngineDependencies('postgresql', packageManager)
|
|
282
|
+
|
|
283
|
+
const succeeded = results.filter((r) => r.success)
|
|
284
|
+
const failed = results.filter((r) => !r.success)
|
|
285
|
+
|
|
286
|
+
if (failed.length === 0) {
|
|
287
|
+
spinner.succeed('PostgreSQL dependencies installed successfully')
|
|
288
|
+
} else {
|
|
289
|
+
spinner.warn('Some dependencies failed to install')
|
|
290
|
+
console.log()
|
|
291
|
+
for (const f of failed) {
|
|
292
|
+
console.log(error(` ${f.dependency.name}: ${f.error}`))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (succeeded.length > 0) {
|
|
297
|
+
console.log()
|
|
298
|
+
console.log(success(`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// deps list
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
depsCommand
|
|
308
|
+
.command('list')
|
|
309
|
+
.description('List all supported dependencies')
|
|
310
|
+
.action(async () => {
|
|
311
|
+
console.log(header('Supported Dependencies'))
|
|
312
|
+
console.log()
|
|
313
|
+
|
|
314
|
+
for (const engineConfig of engineDependencies) {
|
|
315
|
+
console.log(chalk.bold(`${engineConfig.displayName}:`))
|
|
316
|
+
|
|
317
|
+
for (const dep of engineConfig.dependencies) {
|
|
318
|
+
console.log(` ${chalk.cyan(dep.name)} - ${dep.description}`)
|
|
319
|
+
}
|
|
320
|
+
console.log()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(chalk.gray('Use: spindb deps check'))
|
|
324
|
+
console.log(chalk.gray(' spindb deps install --engine <engine>'))
|
|
325
|
+
console.log(chalk.gray(' spindb deps install --all'))
|
|
326
|
+
})
|