spindb 0.9.0 → 0.9.2
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 +7 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +18 -8
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +120 -43
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/doctor.ts +16 -15
- package/cli/commands/edit.ts +115 -60
- package/cli/commands/engines.ts +50 -17
- package/cli/commands/info.ts +12 -8
- package/cli/commands/list.ts +34 -19
- package/cli/commands/logs.ts +24 -14
- package/cli/commands/menu/backup-handlers.ts +72 -49
- package/cli/commands/menu/container-handlers.ts +140 -80
- package/cli/commands/menu/engine-handlers.ts +145 -11
- package/cli/commands/menu/index.ts +4 -4
- package/cli/commands/menu/shell-handlers.ts +34 -31
- package/cli/commands/menu/sql-handlers.ts +22 -16
- package/cli/commands/menu/update-handlers.ts +19 -17
- package/cli/commands/restore.ts +105 -43
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- package/cli/commands/start.ts +11 -9
- package/cli/commands/stop.ts +9 -9
- package/cli/commands/url.ts +12 -9
- package/cli/helpers.ts +49 -4
- package/cli/ui/prompts.ts +21 -8
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/core/binary-manager.ts +5 -1
- package/core/container-manager.ts +81 -30
- package/core/error-handler.ts +31 -0
- package/core/platform-service.ts +3 -3
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +59 -16
- package/engines/mysql/restore.ts +4 -4
- package/engines/mysql/version-validator.ts +2 -2
- package/engines/postgresql/binary-manager.ts +17 -17
- package/engines/postgresql/index.ts +13 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +31 -9
- package/package.json +1 -1
package/cli/commands/create.ts
CHANGED
|
@@ -13,16 +13,17 @@ import {
|
|
|
13
13
|
promptConfirm,
|
|
14
14
|
} from '../ui/prompts'
|
|
15
15
|
import { createSpinner } from '../ui/spinner'
|
|
16
|
-
import { header,
|
|
16
|
+
import { header, uiError, connectionBox } from '../ui/theme'
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
19
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
20
20
|
import { platformService } from '../../core/platform-service'
|
|
21
21
|
import { startWithRetry } from '../../core/start-with-retry'
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
|
+
import { isValidDatabaseName } from '../../core/error-handler'
|
|
24
|
+
import { resolve } from 'path'
|
|
23
25
|
import { Engine } from '../../types'
|
|
24
26
|
import type { BaseEngine } from '../../engines/base-engine'
|
|
25
|
-
import { resolve } from 'path'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Simplified SQLite container creation flow
|
|
@@ -32,9 +33,9 @@ async function createSqliteContainer(
|
|
|
32
33
|
containerName: string,
|
|
33
34
|
dbEngine: BaseEngine,
|
|
34
35
|
version: string,
|
|
35
|
-
options: { path?: string; from?: string | null },
|
|
36
|
+
options: { path?: string; from?: string | null; connect?: boolean },
|
|
36
37
|
): Promise<void> {
|
|
37
|
-
const { path: filePath, from: restoreLocation } = options
|
|
38
|
+
const { path: filePath, from: restoreLocation, connect } = options
|
|
38
39
|
|
|
39
40
|
// Check dependencies
|
|
40
41
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
@@ -42,8 +43,13 @@ async function createSqliteContainer(
|
|
|
42
43
|
|
|
43
44
|
const missingDeps = await getMissingDependencies('sqlite')
|
|
44
45
|
if (missingDeps.length > 0) {
|
|
45
|
-
depsSpinner.warn(
|
|
46
|
-
|
|
46
|
+
depsSpinner.warn(
|
|
47
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
48
|
+
)
|
|
49
|
+
const installed = await promptInstallDependencies(
|
|
50
|
+
missingDeps[0].binary,
|
|
51
|
+
'sqlite',
|
|
52
|
+
)
|
|
47
53
|
if (!installed) {
|
|
48
54
|
process.exit(1)
|
|
49
55
|
}
|
|
@@ -63,7 +69,7 @@ async function createSqliteContainer(
|
|
|
63
69
|
|
|
64
70
|
// Check if file already exists
|
|
65
71
|
if (existsSync(absolutePath)) {
|
|
66
|
-
console.error(
|
|
72
|
+
console.error(uiError(`File already exists: ${absolutePath}`))
|
|
67
73
|
process.exit(1)
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -74,9 +80,9 @@ async function createSqliteContainer(
|
|
|
74
80
|
// Initialize the SQLite database file and register in registry
|
|
75
81
|
await dbEngine.initDataDir(containerName, version, { path: absolutePath })
|
|
76
82
|
createSpinnerInstance.succeed('SQLite database created')
|
|
77
|
-
} catch (
|
|
83
|
+
} catch (error) {
|
|
78
84
|
createSpinnerInstance.fail('Failed to create SQLite database')
|
|
79
|
-
throw
|
|
85
|
+
throw error
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
// Handle --from restore
|
|
@@ -84,15 +90,17 @@ async function createSqliteContainer(
|
|
|
84
90
|
const config = await containerManager.getConfig(containerName)
|
|
85
91
|
if (config) {
|
|
86
92
|
const format = await dbEngine.detectBackupFormat(restoreLocation)
|
|
87
|
-
const restoreSpinner = createSpinner(
|
|
93
|
+
const restoreSpinner = createSpinner(
|
|
94
|
+
`Restoring from ${format.description}...`,
|
|
95
|
+
)
|
|
88
96
|
restoreSpinner.start()
|
|
89
97
|
|
|
90
98
|
try {
|
|
91
99
|
await dbEngine.restore(config, restoreLocation)
|
|
92
100
|
restoreSpinner.succeed('Backup restored successfully')
|
|
93
|
-
} catch (
|
|
101
|
+
} catch (error) {
|
|
94
102
|
restoreSpinner.fail('Failed to restore backup')
|
|
95
|
-
throw
|
|
103
|
+
throw error
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
}
|
|
@@ -107,9 +115,20 @@ async function createSqliteContainer(
|
|
|
107
115
|
console.log(chalk.gray(' Connection string:'))
|
|
108
116
|
console.log(chalk.cyan(` sqlite:///${absolutePath}`))
|
|
109
117
|
console.log()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
// Connect if requested
|
|
120
|
+
if (connect) {
|
|
121
|
+
const config = await containerManager.getConfig(containerName)
|
|
122
|
+
if (config) {
|
|
123
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
124
|
+
console.log()
|
|
125
|
+
await dbEngine.connect(config)
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
console.log(chalk.gray(' Connect with:'))
|
|
129
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
130
|
+
console.log()
|
|
131
|
+
}
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
function detectLocationType(location: string): {
|
|
@@ -132,8 +151,13 @@ function detectLocationType(location: string): {
|
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
if (existsSync(location)) {
|
|
135
|
-
// Check if it's a SQLite file
|
|
136
|
-
|
|
154
|
+
// Check if it's a SQLite file (case-insensitive)
|
|
155
|
+
const lowerLocation = location.toLowerCase()
|
|
156
|
+
if (
|
|
157
|
+
lowerLocation.endsWith('.sqlite') ||
|
|
158
|
+
lowerLocation.endsWith('.db') ||
|
|
159
|
+
lowerLocation.endsWith('.sqlite3')
|
|
160
|
+
) {
|
|
137
161
|
return { type: 'file', inferredEngine: Engine.SQLite }
|
|
138
162
|
}
|
|
139
163
|
return { type: 'file' }
|
|
@@ -145,7 +169,10 @@ function detectLocationType(location: string): {
|
|
|
145
169
|
export const createCommand = new Command('create')
|
|
146
170
|
.description('Create a new database container')
|
|
147
171
|
.argument('[name]', 'Container name')
|
|
148
|
-
.option(
|
|
172
|
+
.option(
|
|
173
|
+
'-e, --engine <engine>',
|
|
174
|
+
'Database engine (postgresql, mysql, sqlite)',
|
|
175
|
+
)
|
|
149
176
|
.option('-v, --version <version>', 'Database version')
|
|
150
177
|
.option('-d, --database <database>', 'Database name')
|
|
151
178
|
.option('-p, --port <port>', 'Port number')
|
|
@@ -157,7 +184,9 @@ export const createCommand = new Command('create')
|
|
|
157
184
|
'--max-connections <number>',
|
|
158
185
|
'Maximum number of database connections (default: 200)',
|
|
159
186
|
)
|
|
187
|
+
.option('--start', 'Start the container after creation (skip prompt)')
|
|
160
188
|
.option('--no-start', 'Do not start the container after creation')
|
|
189
|
+
.option('--connect', 'Open a shell connection after creation')
|
|
161
190
|
.option(
|
|
162
191
|
'--from <location>',
|
|
163
192
|
'Restore from a dump file or connection string after creation',
|
|
@@ -172,7 +201,8 @@ export const createCommand = new Command('create')
|
|
|
172
201
|
port?: string
|
|
173
202
|
path?: string
|
|
174
203
|
maxConnections?: string
|
|
175
|
-
start
|
|
204
|
+
start?: boolean
|
|
205
|
+
connect?: boolean
|
|
176
206
|
from?: string
|
|
177
207
|
},
|
|
178
208
|
) => {
|
|
@@ -191,7 +221,7 @@ export const createCommand = new Command('create')
|
|
|
191
221
|
const locationInfo = detectLocationType(options.from)
|
|
192
222
|
|
|
193
223
|
if (locationInfo.type === 'not_found') {
|
|
194
|
-
console.error(
|
|
224
|
+
console.error(uiError(`Location not found: ${options.from}`))
|
|
195
225
|
console.log(
|
|
196
226
|
chalk.gray(
|
|
197
227
|
' Provide a valid file path or connection string (postgresql://, mysql://)',
|
|
@@ -214,7 +244,7 @@ export const createCommand = new Command('create')
|
|
|
214
244
|
|
|
215
245
|
if (options.start === false) {
|
|
216
246
|
console.error(
|
|
217
|
-
|
|
247
|
+
uiError(
|
|
218
248
|
'Cannot use --no-start with --from (restore requires running container)',
|
|
219
249
|
),
|
|
220
250
|
)
|
|
@@ -238,6 +268,16 @@ export const createCommand = new Command('create')
|
|
|
238
268
|
|
|
239
269
|
database = database ?? containerName
|
|
240
270
|
|
|
271
|
+
// Validate database name to prevent SQL injection
|
|
272
|
+
if (!isValidDatabaseName(database)) {
|
|
273
|
+
console.error(
|
|
274
|
+
uiError(
|
|
275
|
+
'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
process.exit(1)
|
|
279
|
+
}
|
|
280
|
+
|
|
241
281
|
console.log(header('Creating Database Container'))
|
|
242
282
|
console.log()
|
|
243
283
|
|
|
@@ -248,10 +288,34 @@ export const createCommand = new Command('create')
|
|
|
248
288
|
await createSqliteContainer(containerName, dbEngine, version, {
|
|
249
289
|
path: options.path,
|
|
250
290
|
from: restoreLocation,
|
|
291
|
+
connect: options.connect,
|
|
251
292
|
})
|
|
252
293
|
return
|
|
253
294
|
}
|
|
254
295
|
|
|
296
|
+
// For server databases, validate --connect with --no-start
|
|
297
|
+
if (options.connect && options.start === false) {
|
|
298
|
+
console.error(
|
|
299
|
+
uiError(
|
|
300
|
+
'Cannot use --no-start with --connect (connection requires running container)',
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Validate --max-connections if provided
|
|
307
|
+
if (options.maxConnections) {
|
|
308
|
+
const parsed = parseInt(options.maxConnections, 10)
|
|
309
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
310
|
+
console.error(
|
|
311
|
+
uiError(
|
|
312
|
+
'Invalid --max-connections value: must be a positive integer',
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
process.exit(1)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
255
319
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
256
320
|
depsSpinner.start()
|
|
257
321
|
|
|
@@ -273,7 +337,7 @@ export const createCommand = new Command('create')
|
|
|
273
337
|
missingDeps = await getMissingDependencies(engine)
|
|
274
338
|
if (missingDeps.length > 0) {
|
|
275
339
|
console.error(
|
|
276
|
-
|
|
340
|
+
uiError(
|
|
277
341
|
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
278
342
|
),
|
|
279
343
|
)
|
|
@@ -362,9 +426,9 @@ export const createCommand = new Command('create')
|
|
|
362
426
|
})
|
|
363
427
|
|
|
364
428
|
createSpinnerInstance.succeed('Container created')
|
|
365
|
-
} catch (
|
|
429
|
+
} catch (error) {
|
|
366
430
|
createSpinnerInstance.fail('Failed to create container')
|
|
367
|
-
throw
|
|
431
|
+
throw error
|
|
368
432
|
}
|
|
369
433
|
|
|
370
434
|
const initSpinner = createSpinner('Initializing database cluster...')
|
|
@@ -378,15 +442,18 @@ export const createCommand = new Command('create')
|
|
|
378
442
|
: undefined,
|
|
379
443
|
})
|
|
380
444
|
initSpinner.succeed('Database cluster initialized')
|
|
381
|
-
} catch (
|
|
445
|
+
} catch (error) {
|
|
382
446
|
initSpinner.fail('Failed to initialize database cluster')
|
|
383
447
|
await tx.rollback()
|
|
384
|
-
throw
|
|
448
|
+
throw error
|
|
385
449
|
}
|
|
386
450
|
|
|
387
|
-
// --from requires start, --no-start skips, otherwise ask user
|
|
451
|
+
// --from requires start, --start forces start, --no-start skips, otherwise ask user
|
|
452
|
+
// --connect implies --start for server databases
|
|
388
453
|
let shouldStart = false
|
|
389
|
-
if (restoreLocation) {
|
|
454
|
+
if (restoreLocation || options.connect) {
|
|
455
|
+
shouldStart = true
|
|
456
|
+
} else if (options.start === true) {
|
|
390
457
|
shouldStart = true
|
|
391
458
|
} else if (options.start === false) {
|
|
392
459
|
shouldStart = false
|
|
@@ -444,14 +511,14 @@ export const createCommand = new Command('create')
|
|
|
444
511
|
} else {
|
|
445
512
|
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
446
513
|
}
|
|
447
|
-
} catch (
|
|
514
|
+
} catch (error) {
|
|
448
515
|
if (!startSpinner.isSpinning) {
|
|
449
516
|
// Error was already handled above
|
|
450
517
|
} else {
|
|
451
518
|
startSpinner.fail(`Failed to start ${dbEngine.displayName}`)
|
|
452
519
|
}
|
|
453
520
|
await tx.rollback()
|
|
454
|
-
throw
|
|
521
|
+
throw error
|
|
455
522
|
}
|
|
456
523
|
|
|
457
524
|
const defaultDb = engineDefaults.superuser
|
|
@@ -464,10 +531,10 @@ export const createCommand = new Command('create')
|
|
|
464
531
|
try {
|
|
465
532
|
await dbEngine.createDatabase(config, database)
|
|
466
533
|
dbSpinner.succeed(`Database "${database}" created`)
|
|
467
|
-
} catch (
|
|
534
|
+
} catch (error) {
|
|
468
535
|
dbSpinner.fail(`Failed to create database "${database}"`)
|
|
469
536
|
await tx.rollback()
|
|
470
|
-
throw
|
|
537
|
+
throw error
|
|
471
538
|
}
|
|
472
539
|
}
|
|
473
540
|
}
|
|
@@ -498,8 +565,8 @@ export const createCommand = new Command('create')
|
|
|
498
565
|
dumpSpinner.succeed('Dump created from remote database')
|
|
499
566
|
backupPath = tempDumpPath
|
|
500
567
|
dumpSuccess = true
|
|
501
|
-
} catch (
|
|
502
|
-
const e =
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const e = error as Error
|
|
503
570
|
dumpSpinner.fail('Failed to create dump')
|
|
504
571
|
|
|
505
572
|
if (
|
|
@@ -514,14 +581,14 @@ export const createCommand = new Command('create')
|
|
|
514
581
|
}
|
|
515
582
|
|
|
516
583
|
console.log()
|
|
517
|
-
console.error(
|
|
584
|
+
console.error(uiError('pg_dump error:'))
|
|
518
585
|
console.log(chalk.gray(` ${e.message}`))
|
|
519
586
|
process.exit(1)
|
|
520
587
|
}
|
|
521
588
|
}
|
|
522
589
|
|
|
523
590
|
if (!dumpSuccess) {
|
|
524
|
-
console.error(
|
|
591
|
+
console.error(uiError('Failed to create dump after retries'))
|
|
525
592
|
process.exit(1)
|
|
526
593
|
}
|
|
527
594
|
} else {
|
|
@@ -542,7 +609,7 @@ export const createCommand = new Command('create')
|
|
|
542
609
|
createDatabase: false,
|
|
543
610
|
})
|
|
544
611
|
|
|
545
|
-
if (result.code === 0
|
|
612
|
+
if (result.code === 0) {
|
|
546
613
|
restoreSpinner.succeed('Backup restored successfully')
|
|
547
614
|
} else {
|
|
548
615
|
restoreSpinner.warn('Restore completed with warnings')
|
|
@@ -573,7 +640,17 @@ export const createCommand = new Command('create')
|
|
|
573
640
|
)
|
|
574
641
|
console.log()
|
|
575
642
|
|
|
576
|
-
if (shouldStart) {
|
|
643
|
+
if (options.connect && shouldStart) {
|
|
644
|
+
// --connect flag: open shell directly
|
|
645
|
+
const copied =
|
|
646
|
+
await platformService.copyToClipboard(connectionString)
|
|
647
|
+
if (copied) {
|
|
648
|
+
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
649
|
+
}
|
|
650
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
651
|
+
console.log()
|
|
652
|
+
await dbEngine.connect(finalConfig, database)
|
|
653
|
+
} else if (shouldStart) {
|
|
577
654
|
console.log(chalk.gray(' Connect with:'))
|
|
578
655
|
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
579
656
|
|
|
@@ -582,15 +659,15 @@ export const createCommand = new Command('create')
|
|
|
582
659
|
if (copied) {
|
|
583
660
|
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
584
661
|
}
|
|
662
|
+
console.log()
|
|
585
663
|
} else {
|
|
586
664
|
console.log(chalk.gray(' Start the container:'))
|
|
587
665
|
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
666
|
+
console.log()
|
|
588
667
|
}
|
|
589
|
-
|
|
590
|
-
console.log()
|
|
591
668
|
}
|
|
592
|
-
} catch (
|
|
593
|
-
const e =
|
|
669
|
+
} catch (error) {
|
|
670
|
+
const e = error as Error
|
|
594
671
|
|
|
595
672
|
const missingToolPatterns = [
|
|
596
673
|
'pg_restore not found',
|
|
@@ -616,7 +693,7 @@ export const createCommand = new Command('create')
|
|
|
616
693
|
process.exit(1)
|
|
617
694
|
}
|
|
618
695
|
|
|
619
|
-
console.error(
|
|
696
|
+
console.error(uiError(e.message))
|
|
620
697
|
process.exit(1)
|
|
621
698
|
} finally {
|
|
622
699
|
if (tempDumpPath) {
|
package/cli/commands/delete.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { processManager } from '../../core/process-manager'
|
|
|
4
4
|
import { getEngine } from '../../engines'
|
|
5
5
|
import { promptContainerSelect, promptConfirm } from '../ui/prompts'
|
|
6
6
|
import { createSpinner } from '../ui/spinner'
|
|
7
|
-
import {
|
|
7
|
+
import { uiError, uiWarning } from '../ui/theme'
|
|
8
8
|
|
|
9
9
|
export const deleteCommand = new Command('delete')
|
|
10
10
|
.alias('rm')
|
|
@@ -24,7 +24,7 @@ export const deleteCommand = new Command('delete')
|
|
|
24
24
|
const containers = await containerManager.list()
|
|
25
25
|
|
|
26
26
|
if (containers.length === 0) {
|
|
27
|
-
console.log(
|
|
27
|
+
console.log(uiWarning('No containers found'))
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -38,7 +38,7 @@ export const deleteCommand = new Command('delete')
|
|
|
38
38
|
|
|
39
39
|
const config = await containerManager.getConfig(containerName)
|
|
40
40
|
if (!config) {
|
|
41
|
-
console.error(
|
|
41
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
42
42
|
process.exit(1)
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -48,7 +48,7 @@ export const deleteCommand = new Command('delete')
|
|
|
48
48
|
false,
|
|
49
49
|
)
|
|
50
50
|
if (!confirmed) {
|
|
51
|
-
console.log(
|
|
51
|
+
console.log(uiWarning('Deletion cancelled'))
|
|
52
52
|
return
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -67,7 +67,7 @@ export const deleteCommand = new Command('delete')
|
|
|
67
67
|
stopSpinner.succeed(`Stopped "${containerName}"`)
|
|
68
68
|
} else {
|
|
69
69
|
console.error(
|
|
70
|
-
|
|
70
|
+
uiError(
|
|
71
71
|
`Container "${containerName}" is running. Stop it first or use --force`,
|
|
72
72
|
),
|
|
73
73
|
)
|
|
@@ -81,9 +81,9 @@ export const deleteCommand = new Command('delete')
|
|
|
81
81
|
await containerManager.delete(containerName, { force: true })
|
|
82
82
|
|
|
83
83
|
deleteSpinner.succeed(`Container "${containerName}" deleted`)
|
|
84
|
-
} catch (
|
|
85
|
-
const e =
|
|
86
|
-
console.error(
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const e = error as Error
|
|
86
|
+
console.error(uiError(e.message))
|
|
87
87
|
process.exit(1)
|
|
88
88
|
}
|
|
89
89
|
},
|
package/cli/commands/deps.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import { header,
|
|
3
|
+
import { header, uiSuccess, uiWarning, uiError } from '../ui/theme'
|
|
4
4
|
import { createSpinner } from '../ui/spinner'
|
|
5
5
|
import {
|
|
6
6
|
detectPackageManager,
|
|
@@ -84,7 +84,7 @@ depsCommand
|
|
|
84
84
|
// Check specific engine
|
|
85
85
|
const engineConfig = getEngineDependencies(options.engine)
|
|
86
86
|
if (!engineConfig) {
|
|
87
|
-
console.error(
|
|
87
|
+
console.error(uiError(`Unknown engine: ${options.engine}`))
|
|
88
88
|
console.log(
|
|
89
89
|
chalk.gray(
|
|
90
90
|
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
@@ -104,9 +104,9 @@ depsCommand
|
|
|
104
104
|
const total = statuses.length
|
|
105
105
|
console.log()
|
|
106
106
|
if (installed === total) {
|
|
107
|
-
console.log(
|
|
107
|
+
console.log(uiSuccess(`All ${total} dependencies installed`))
|
|
108
108
|
} else {
|
|
109
|
-
console.log(
|
|
109
|
+
console.log(uiWarning(`${installed}/${total} dependencies installed`))
|
|
110
110
|
console.log()
|
|
111
111
|
console.log(
|
|
112
112
|
chalk.gray(` Run: spindb deps install --engine ${options.engine}`),
|
|
@@ -132,7 +132,7 @@ depsCommand
|
|
|
132
132
|
const packageManager = await detectPackageManager()
|
|
133
133
|
|
|
134
134
|
if (!packageManager) {
|
|
135
|
-
console.log(
|
|
135
|
+
console.log(uiError('No supported package manager detected'))
|
|
136
136
|
console.log()
|
|
137
137
|
|
|
138
138
|
const platform = getCurrentPlatform()
|
|
@@ -161,7 +161,7 @@ depsCommand
|
|
|
161
161
|
const missing = await getAllMissingDependencies()
|
|
162
162
|
|
|
163
163
|
if (missing.length === 0) {
|
|
164
|
-
console.log(
|
|
164
|
+
console.log(uiSuccess('All dependencies are already installed'))
|
|
165
165
|
return
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -182,14 +182,14 @@ depsCommand
|
|
|
182
182
|
spinner.warn('Some dependencies failed to install')
|
|
183
183
|
console.log()
|
|
184
184
|
for (const f of failed) {
|
|
185
|
-
console.log(
|
|
185
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (succeeded.length > 0) {
|
|
190
190
|
console.log()
|
|
191
191
|
console.log(
|
|
192
|
-
|
|
192
|
+
uiSuccess(
|
|
193
193
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
194
194
|
),
|
|
195
195
|
)
|
|
@@ -198,7 +198,7 @@ depsCommand
|
|
|
198
198
|
// Install dependencies for specific engine
|
|
199
199
|
const engineConfig = getEngineDependencies(options.engine)
|
|
200
200
|
if (!engineConfig) {
|
|
201
|
-
console.error(
|
|
201
|
+
console.error(uiError(`Unknown engine: ${options.engine}`))
|
|
202
202
|
console.log(
|
|
203
203
|
chalk.gray(
|
|
204
204
|
` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
|
|
@@ -211,7 +211,9 @@ depsCommand
|
|
|
211
211
|
|
|
212
212
|
if (missing.length === 0) {
|
|
213
213
|
console.log(
|
|
214
|
-
|
|
214
|
+
uiSuccess(
|
|
215
|
+
`All ${engineConfig.displayName} dependencies are installed`,
|
|
216
|
+
),
|
|
215
217
|
)
|
|
216
218
|
return
|
|
217
219
|
}
|
|
@@ -241,7 +243,7 @@ depsCommand
|
|
|
241
243
|
spinner.warn('Some dependencies failed to install')
|
|
242
244
|
console.log()
|
|
243
245
|
for (const f of failed) {
|
|
244
|
-
console.log(
|
|
246
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
245
247
|
}
|
|
246
248
|
|
|
247
249
|
// Show manual instructions
|
|
@@ -259,7 +261,7 @@ depsCommand
|
|
|
259
261
|
if (succeeded.length > 0) {
|
|
260
262
|
console.log()
|
|
261
263
|
console.log(
|
|
262
|
-
|
|
264
|
+
uiSuccess(
|
|
263
265
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
264
266
|
),
|
|
265
267
|
)
|
|
@@ -276,7 +278,7 @@ depsCommand
|
|
|
276
278
|
const missing = await getMissingDependencies('postgresql')
|
|
277
279
|
|
|
278
280
|
if (missing.length === 0) {
|
|
279
|
-
console.log(
|
|
281
|
+
console.log(uiSuccess('All PostgreSQL dependencies are installed'))
|
|
280
282
|
return
|
|
281
283
|
}
|
|
282
284
|
|
|
@@ -300,14 +302,14 @@ depsCommand
|
|
|
300
302
|
spinner.warn('Some dependencies failed to install')
|
|
301
303
|
console.log()
|
|
302
304
|
for (const f of failed) {
|
|
303
|
-
console.log(
|
|
305
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
if (succeeded.length > 0) {
|
|
308
310
|
console.log()
|
|
309
311
|
console.log(
|
|
310
|
-
|
|
312
|
+
uiSuccess(
|
|
311
313
|
`Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
|
|
312
314
|
),
|
|
313
315
|
)
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
|
|
|
18
18
|
import { paths } from '../../config/paths'
|
|
19
19
|
import { getSupportedEngines } from '../../config/engine-defaults'
|
|
20
20
|
import { checkEngineDependencies } from '../../core/dependency-manager'
|
|
21
|
-
import { header,
|
|
21
|
+
import { header, uiSuccess } from '../ui/theme'
|
|
22
22
|
import { Engine } from '../../types'
|
|
23
23
|
|
|
24
24
|
type HealthCheckResult = {
|
|
@@ -61,7 +61,7 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
|
|
|
61
61
|
label: 'Refresh binary cache',
|
|
62
62
|
handler: async () => {
|
|
63
63
|
await configManager.refreshAllBinaries()
|
|
64
|
-
console.log(
|
|
64
|
+
console.log(uiSuccess('Binary cache refreshed'))
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
}
|
|
@@ -73,12 +73,12 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
|
|
|
73
73
|
message: 'Configuration valid',
|
|
74
74
|
details: [`Binary tools cached: ${binaryCount}`],
|
|
75
75
|
}
|
|
76
|
-
} catch (
|
|
76
|
+
} catch (error) {
|
|
77
77
|
return {
|
|
78
78
|
name: 'Configuration',
|
|
79
79
|
status: 'error',
|
|
80
80
|
message: 'Configuration file is corrupted',
|
|
81
|
-
details: [(
|
|
81
|
+
details: [(error as Error).message],
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -125,12 +125,12 @@ async function checkContainers(): Promise<HealthCheckResult> {
|
|
|
125
125
|
message: `${containers.length} container(s)`,
|
|
126
126
|
details,
|
|
127
127
|
}
|
|
128
|
-
} catch (
|
|
128
|
+
} catch (error) {
|
|
129
129
|
return {
|
|
130
130
|
name: 'Containers',
|
|
131
131
|
status: 'error',
|
|
132
132
|
message: 'Failed to list containers',
|
|
133
|
-
details: [(
|
|
133
|
+
details: [(error as Error).message],
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -162,7 +162,7 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
|
|
|
162
162
|
label: 'Remove orphaned entries from registry',
|
|
163
163
|
handler: async () => {
|
|
164
164
|
const count = await sqliteRegistry.removeOrphans()
|
|
165
|
-
console.log(
|
|
165
|
+
console.log(uiSuccess(`Removed ${count} orphaned entries`))
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
}
|
|
@@ -173,12 +173,12 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
|
|
|
173
173
|
status: 'ok',
|
|
174
174
|
message: `${entries.length} database(s) registered, all files exist`,
|
|
175
175
|
}
|
|
176
|
-
} catch (
|
|
176
|
+
} catch (error) {
|
|
177
177
|
return {
|
|
178
178
|
name: 'SQLite Registry',
|
|
179
179
|
status: 'warning',
|
|
180
180
|
message: 'Could not check registry',
|
|
181
|
-
details: [(
|
|
181
|
+
details: [(error as Error).message],
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -211,12 +211,12 @@ async function checkBinaries(): Promise<HealthCheckResult> {
|
|
|
211
211
|
message: hasWarning ? 'Some tools missing' : 'All tools available',
|
|
212
212
|
details: results,
|
|
213
213
|
}
|
|
214
|
-
} catch (
|
|
214
|
+
} catch (error) {
|
|
215
215
|
return {
|
|
216
216
|
name: 'Database Tools',
|
|
217
217
|
status: 'error',
|
|
218
218
|
message: 'Failed to check tools',
|
|
219
|
-
details: [(
|
|
219
|
+
details: [(error as Error).message],
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
}
|
|
@@ -247,10 +247,6 @@ export const doctorCommand = new Command('doctor')
|
|
|
247
247
|
.description('Check system health and fix common issues')
|
|
248
248
|
.option('--json', 'Output as JSON')
|
|
249
249
|
.action(async (options: { json?: boolean }) => {
|
|
250
|
-
console.log()
|
|
251
|
-
console.log(header('SpinDB Health Check'))
|
|
252
|
-
console.log()
|
|
253
|
-
|
|
254
250
|
const checks = [
|
|
255
251
|
await checkConfiguration(),
|
|
256
252
|
await checkContainers(),
|
|
@@ -265,6 +261,11 @@ export const doctorCommand = new Command('doctor')
|
|
|
265
261
|
return
|
|
266
262
|
}
|
|
267
263
|
|
|
264
|
+
// Human-readable output - print header first
|
|
265
|
+
console.log()
|
|
266
|
+
console.log(header('SpinDB Health Check'))
|
|
267
|
+
console.log()
|
|
268
|
+
|
|
268
269
|
// Display results
|
|
269
270
|
for (const check of checks) {
|
|
270
271
|
displayResult(check)
|