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/edit.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
copyFileSync,
|
|
11
|
+
} from 'fs'
|
|
5
12
|
import { dirname, resolve, basename, join } from 'path'
|
|
6
13
|
import { homedir } from 'os'
|
|
7
14
|
import { containerManager } from '../../core/container-manager'
|
|
@@ -12,7 +19,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
|
|
|
12
19
|
import { paths } from '../../config/paths'
|
|
13
20
|
import { promptContainerSelect } from '../ui/prompts'
|
|
14
21
|
import { createSpinner } from '../ui/spinner'
|
|
15
|
-
import {
|
|
22
|
+
import { uiError, uiWarning, uiSuccess, uiInfo } from '../ui/theme'
|
|
16
23
|
import { Engine } from '../../types'
|
|
17
24
|
|
|
18
25
|
function isValidName(name: string): boolean {
|
|
@@ -25,9 +32,7 @@ function isValidName(name: string): boolean {
|
|
|
25
32
|
async function promptEditAction(
|
|
26
33
|
engine: string,
|
|
27
34
|
): Promise<'name' | 'port' | 'config' | 'relocate' | null> {
|
|
28
|
-
const choices = [
|
|
29
|
-
{ name: 'Rename container', value: 'name' },
|
|
30
|
-
]
|
|
35
|
+
const choices = [{ name: 'Rename container', value: 'name' }]
|
|
31
36
|
|
|
32
37
|
// SQLite: show relocate instead of port
|
|
33
38
|
if (engine === Engine.SQLite) {
|
|
@@ -37,8 +42,11 @@ async function promptEditAction(
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
// Only show config option for engines that support it
|
|
40
|
-
if (engine ===
|
|
41
|
-
choices.push({
|
|
45
|
+
if (engine === Engine.PostgreSQL) {
|
|
46
|
+
choices.push({
|
|
47
|
+
name: 'Edit database config (postgresql.conf)',
|
|
48
|
+
value: 'config',
|
|
49
|
+
})
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
choices.push({ name: chalk.gray('Cancel'), value: 'cancel' })
|
|
@@ -74,7 +82,7 @@ async function promptNewName(currentName: string): Promise<string | null> {
|
|
|
74
82
|
])
|
|
75
83
|
|
|
76
84
|
if (newName === currentName) {
|
|
77
|
-
console.log(
|
|
85
|
+
console.log(uiWarning('Name unchanged'))
|
|
78
86
|
return null
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -83,17 +91,36 @@ async function promptNewName(currentName: string): Promise<string | null> {
|
|
|
83
91
|
|
|
84
92
|
// Common PostgreSQL config settings that users might want to edit
|
|
85
93
|
const COMMON_PG_SETTINGS = [
|
|
86
|
-
{
|
|
87
|
-
|
|
94
|
+
{
|
|
95
|
+
name: 'max_connections',
|
|
96
|
+
description: 'Maximum concurrent connections',
|
|
97
|
+
default: '200',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'shared_buffers',
|
|
101
|
+
description: 'Memory for shared buffers',
|
|
102
|
+
default: '128MB',
|
|
103
|
+
},
|
|
88
104
|
{ name: 'work_mem', description: 'Memory per operation', default: '4MB' },
|
|
89
|
-
{
|
|
90
|
-
|
|
105
|
+
{
|
|
106
|
+
name: 'maintenance_work_mem',
|
|
107
|
+
description: 'Memory for maintenance ops',
|
|
108
|
+
default: '64MB',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'effective_cache_size',
|
|
112
|
+
description: 'Planner cache size estimate',
|
|
113
|
+
default: '4GB',
|
|
114
|
+
},
|
|
91
115
|
]
|
|
92
116
|
|
|
93
117
|
/**
|
|
94
118
|
* Prompt for PostgreSQL config setting to edit
|
|
95
119
|
*/
|
|
96
|
-
async function promptConfigSetting(): Promise<{
|
|
120
|
+
async function promptConfigSetting(): Promise<{
|
|
121
|
+
key: string
|
|
122
|
+
value: string
|
|
123
|
+
} | null> {
|
|
97
124
|
const choices = COMMON_PG_SETTINGS.map((s) => ({
|
|
98
125
|
name: `${s.name.padEnd(25)} ${chalk.gray(s.description)}`,
|
|
99
126
|
value: s.name,
|
|
@@ -121,7 +148,8 @@ async function promptConfigSetting(): Promise<{ key: string; value: string } | n
|
|
|
121
148
|
message: 'Setting name:',
|
|
122
149
|
validate: (input: string) => {
|
|
123
150
|
if (!input.trim()) return 'Setting name is required'
|
|
124
|
-
if (!/^[a-z_]+$/.test(input))
|
|
151
|
+
if (!/^[a-z_]+$/.test(input))
|
|
152
|
+
return 'Setting names are lowercase with underscores'
|
|
125
153
|
return true
|
|
126
154
|
},
|
|
127
155
|
},
|
|
@@ -129,7 +157,8 @@ async function promptConfigSetting(): Promise<{ key: string; value: string } | n
|
|
|
129
157
|
key = customKey
|
|
130
158
|
}
|
|
131
159
|
|
|
132
|
-
const defaultValue =
|
|
160
|
+
const defaultValue =
|
|
161
|
+
COMMON_PG_SETTINGS.find((s) => s.name === key)?.default || ''
|
|
133
162
|
const { value } = await inquirer.prompt<{ value: string }>([
|
|
134
163
|
{
|
|
135
164
|
type: 'input',
|
|
@@ -168,14 +197,14 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
|
|
|
168
197
|
])
|
|
169
198
|
|
|
170
199
|
if (newPort === currentPort) {
|
|
171
|
-
console.log(
|
|
200
|
+
console.log(uiWarning('Port unchanged'))
|
|
172
201
|
return null
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
const portAvailable = await portManager.isPortAvailable(newPort)
|
|
176
205
|
if (!portAvailable) {
|
|
177
206
|
console.log(
|
|
178
|
-
|
|
207
|
+
uiWarning(
|
|
179
208
|
`Note: Port ${newPort} is currently in use. It will be used when the container starts.`,
|
|
180
209
|
),
|
|
181
210
|
)
|
|
@@ -190,7 +219,9 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
|
|
|
190
219
|
async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
191
220
|
console.log()
|
|
192
221
|
console.log(chalk.gray(` Current location: ${currentPath}`))
|
|
193
|
-
console.log(
|
|
222
|
+
console.log(
|
|
223
|
+
chalk.gray(' Enter an absolute path or relative to current directory.'),
|
|
224
|
+
)
|
|
194
225
|
console.log()
|
|
195
226
|
|
|
196
227
|
const { newPath } = await inquirer.prompt<{ newPath: string }>([
|
|
@@ -201,8 +232,12 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
|
201
232
|
default: currentPath,
|
|
202
233
|
validate: (input: string) => {
|
|
203
234
|
if (!input.trim()) return 'Path is required'
|
|
204
|
-
const resolvedPath = resolve(input)
|
|
205
|
-
if (
|
|
235
|
+
const resolvedPath = resolve(input).toLowerCase()
|
|
236
|
+
if (
|
|
237
|
+
!resolvedPath.endsWith('.sqlite') &&
|
|
238
|
+
!resolvedPath.endsWith('.db') &&
|
|
239
|
+
!resolvedPath.endsWith('.sqlite3')
|
|
240
|
+
) {
|
|
206
241
|
return 'Path should end with .sqlite, .sqlite3, or .db'
|
|
207
242
|
}
|
|
208
243
|
return true
|
|
@@ -213,7 +248,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
|
213
248
|
const resolvedPath = resolve(newPath)
|
|
214
249
|
|
|
215
250
|
if (resolvedPath === currentPath) {
|
|
216
|
-
console.log(
|
|
251
|
+
console.log(uiWarning('Location unchanged'))
|
|
217
252
|
return null
|
|
218
253
|
}
|
|
219
254
|
|
|
@@ -228,7 +263,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
|
228
263
|
},
|
|
229
264
|
])
|
|
230
265
|
if (!overwrite) {
|
|
231
|
-
console.log(
|
|
266
|
+
console.log(uiWarning('Relocate cancelled'))
|
|
232
267
|
return null
|
|
233
268
|
}
|
|
234
269
|
}
|
|
@@ -237,7 +272,9 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
|
237
272
|
}
|
|
238
273
|
|
|
239
274
|
export const editCommand = new Command('edit')
|
|
240
|
-
.description(
|
|
275
|
+
.description(
|
|
276
|
+
'Edit container properties (rename, port, relocate, or database config)',
|
|
277
|
+
)
|
|
241
278
|
.argument('[name]', 'Container name')
|
|
242
279
|
.option('-n, --name <newName>', 'New container name')
|
|
243
280
|
.option('-p, --port <port>', 'New port number', parseInt)
|
|
@@ -256,7 +293,13 @@ export const editCommand = new Command('edit')
|
|
|
256
293
|
.action(
|
|
257
294
|
async (
|
|
258
295
|
name: string | undefined,
|
|
259
|
-
options: {
|
|
296
|
+
options: {
|
|
297
|
+
name?: string
|
|
298
|
+
port?: number
|
|
299
|
+
relocate?: string
|
|
300
|
+
overwrite?: boolean
|
|
301
|
+
setConfig?: string
|
|
302
|
+
},
|
|
260
303
|
) => {
|
|
261
304
|
try {
|
|
262
305
|
let containerName = name
|
|
@@ -265,7 +308,7 @@ export const editCommand = new Command('edit')
|
|
|
265
308
|
const containers = await containerManager.list()
|
|
266
309
|
|
|
267
310
|
if (containers.length === 0) {
|
|
268
|
-
console.log(
|
|
311
|
+
console.log(uiWarning('No containers found'))
|
|
269
312
|
return
|
|
270
313
|
}
|
|
271
314
|
|
|
@@ -279,7 +322,7 @@ export const editCommand = new Command('edit')
|
|
|
279
322
|
|
|
280
323
|
const config = await containerManager.getConfig(containerName)
|
|
281
324
|
if (!config) {
|
|
282
|
-
console.error(
|
|
325
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
283
326
|
process.exit(1)
|
|
284
327
|
}
|
|
285
328
|
|
|
@@ -327,7 +370,7 @@ export const editCommand = new Command('edit')
|
|
|
327
370
|
if (options.name) {
|
|
328
371
|
if (!isValidName(options.name)) {
|
|
329
372
|
console.error(
|
|
330
|
-
|
|
373
|
+
uiError(
|
|
331
374
|
'Name must start with a letter and contain only letters, numbers, hyphens, and underscores',
|
|
332
375
|
),
|
|
333
376
|
)
|
|
@@ -338,7 +381,7 @@ export const editCommand = new Command('edit')
|
|
|
338
381
|
engine: config.engine,
|
|
339
382
|
})
|
|
340
383
|
if (exists) {
|
|
341
|
-
console.error(
|
|
384
|
+
console.error(uiError(`Container "${options.name}" already exists`))
|
|
342
385
|
process.exit(1)
|
|
343
386
|
}
|
|
344
387
|
|
|
@@ -347,7 +390,7 @@ export const editCommand = new Command('edit')
|
|
|
347
390
|
})
|
|
348
391
|
if (running) {
|
|
349
392
|
console.error(
|
|
350
|
-
|
|
393
|
+
uiError(
|
|
351
394
|
`Container "${containerName}" is running. Stop it first to rename.`,
|
|
352
395
|
),
|
|
353
396
|
)
|
|
@@ -368,14 +411,14 @@ export const editCommand = new Command('edit')
|
|
|
368
411
|
|
|
369
412
|
if (options.port !== undefined) {
|
|
370
413
|
if (options.port < 1 || options.port > 65535) {
|
|
371
|
-
console.error(
|
|
414
|
+
console.error(uiError('Port must be between 1 and 65535'))
|
|
372
415
|
process.exit(1)
|
|
373
416
|
}
|
|
374
417
|
|
|
375
418
|
const portAvailable = await portManager.isPortAvailable(options.port)
|
|
376
419
|
if (!portAvailable) {
|
|
377
420
|
console.log(
|
|
378
|
-
|
|
421
|
+
uiWarning(
|
|
379
422
|
`Port ${options.port} is currently in use. The container will use this port on next start.`,
|
|
380
423
|
),
|
|
381
424
|
)
|
|
@@ -400,7 +443,7 @@ export const editCommand = new Command('edit')
|
|
|
400
443
|
if (options.relocate) {
|
|
401
444
|
if (config.engine !== Engine.SQLite) {
|
|
402
445
|
console.error(
|
|
403
|
-
|
|
446
|
+
uiError('Relocate is only available for SQLite containers'),
|
|
404
447
|
)
|
|
405
448
|
process.exit(1)
|
|
406
449
|
}
|
|
@@ -425,13 +468,17 @@ export const editCommand = new Command('edit')
|
|
|
425
468
|
// - ends with /
|
|
426
469
|
// - exists and is a directory
|
|
427
470
|
// - doesn't have a database file extension
|
|
428
|
-
const isDirectory =
|
|
429
|
-
|
|
471
|
+
const isDirectory =
|
|
472
|
+
expandedPath.endsWith('/') ||
|
|
473
|
+
(existsSync(expandedPath) &&
|
|
474
|
+
statSync(expandedPath).isDirectory()) ||
|
|
430
475
|
!hasDbExtension
|
|
431
476
|
|
|
432
477
|
let newPath: string
|
|
433
478
|
if (isDirectory) {
|
|
434
|
-
const dirPath = expandedPath.endsWith('/')
|
|
479
|
+
const dirPath = expandedPath.endsWith('/')
|
|
480
|
+
? expandedPath.slice(0, -1)
|
|
481
|
+
: expandedPath
|
|
435
482
|
const currentFileName = basename(config.database)
|
|
436
483
|
newPath = join(dirPath, currentFileName)
|
|
437
484
|
} else {
|
|
@@ -441,7 +488,7 @@ export const editCommand = new Command('edit')
|
|
|
441
488
|
// Check source file exists
|
|
442
489
|
if (!existsSync(config.database)) {
|
|
443
490
|
console.error(
|
|
444
|
-
|
|
491
|
+
uiError(`Source database file not found: ${config.database}`),
|
|
445
492
|
)
|
|
446
493
|
process.exit(1)
|
|
447
494
|
}
|
|
@@ -451,12 +498,14 @@ export const editCommand = new Command('edit')
|
|
|
451
498
|
if (options.overwrite) {
|
|
452
499
|
// Remove existing file before move
|
|
453
500
|
unlinkSync(newPath)
|
|
454
|
-
console.log(
|
|
501
|
+
console.log(uiWarning(`Overwriting existing file: ${newPath}`))
|
|
455
502
|
} else {
|
|
456
503
|
console.error(
|
|
457
|
-
|
|
504
|
+
uiError(`Destination file already exists: ${newPath}`),
|
|
505
|
+
)
|
|
506
|
+
console.log(
|
|
507
|
+
uiInfo('Use --overwrite to replace the existing file'),
|
|
458
508
|
)
|
|
459
|
-
console.log(info('Use --overwrite to replace the existing file'))
|
|
460
509
|
process.exit(1)
|
|
461
510
|
}
|
|
462
511
|
}
|
|
@@ -465,12 +514,10 @@ export const editCommand = new Command('edit')
|
|
|
465
514
|
const targetDir = dirname(newPath)
|
|
466
515
|
if (!existsSync(targetDir)) {
|
|
467
516
|
mkdirSync(targetDir, { recursive: true })
|
|
468
|
-
console.log(
|
|
517
|
+
console.log(uiInfo(`Created directory: ${targetDir}`))
|
|
469
518
|
}
|
|
470
519
|
|
|
471
|
-
const spinner = createSpinner(
|
|
472
|
-
`Moving database to ${newPath}...`,
|
|
473
|
-
)
|
|
520
|
+
const spinner = createSpinner(`Moving database to ${newPath}...`)
|
|
474
521
|
spinner.start()
|
|
475
522
|
|
|
476
523
|
try {
|
|
@@ -509,18 +556,20 @@ export const editCommand = new Command('edit')
|
|
|
509
556
|
await sqliteRegistry.update(containerName, { filePath: newPath })
|
|
510
557
|
|
|
511
558
|
spinner.succeed(`Database relocated to ${newPath}`)
|
|
512
|
-
} catch (
|
|
559
|
+
} catch (error) {
|
|
513
560
|
spinner.fail('Failed to relocate database')
|
|
514
|
-
throw
|
|
561
|
+
throw error
|
|
515
562
|
}
|
|
516
563
|
}
|
|
517
564
|
|
|
518
565
|
// Handle config change
|
|
519
566
|
if (options.setConfig) {
|
|
520
567
|
// Only PostgreSQL supports config editing for now
|
|
521
|
-
if (config.engine !==
|
|
568
|
+
if (config.engine !== Engine.PostgreSQL) {
|
|
522
569
|
console.error(
|
|
523
|
-
|
|
570
|
+
uiError(
|
|
571
|
+
`Config editing is only supported for PostgreSQL containers`,
|
|
572
|
+
),
|
|
524
573
|
)
|
|
525
574
|
process.exit(1)
|
|
526
575
|
}
|
|
@@ -529,7 +578,7 @@ export const editCommand = new Command('edit')
|
|
|
529
578
|
const match = options.setConfig.match(/^([a-z_]+)=(.+)$/)
|
|
530
579
|
if (!match) {
|
|
531
580
|
console.error(
|
|
532
|
-
|
|
581
|
+
uiError(
|
|
533
582
|
'Invalid config format. Use: --set-config key=value (e.g., max_connections=200)',
|
|
534
583
|
),
|
|
535
584
|
)
|
|
@@ -551,22 +600,28 @@ export const editCommand = new Command('edit')
|
|
|
551
600
|
|
|
552
601
|
// Use the PostgreSQL engine's setConfigValue method
|
|
553
602
|
if ('setConfigValue' in engine) {
|
|
554
|
-
await (
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
603
|
+
await (
|
|
604
|
+
engine as {
|
|
605
|
+
setConfigValue: (
|
|
606
|
+
dataDir: string,
|
|
607
|
+
key: string,
|
|
608
|
+
value: string,
|
|
609
|
+
) => Promise<void>
|
|
610
|
+
}
|
|
611
|
+
).setConfigValue(dataDir, configKey, configValue)
|
|
612
|
+
spinner.succeed(`Set ${configKey} = ${configValue}`)
|
|
613
|
+
} else {
|
|
614
|
+
spinner.fail('Config editing not supported for this engine')
|
|
615
|
+
process.exit(1)
|
|
559
616
|
}
|
|
560
617
|
|
|
561
|
-
spinner.succeed(`Set ${configKey} = ${configValue}`)
|
|
562
|
-
|
|
563
618
|
// Check if container is running and warn about restart
|
|
564
619
|
const running = await processManager.isRunning(containerName, {
|
|
565
620
|
engine: config.engine,
|
|
566
621
|
})
|
|
567
622
|
if (running) {
|
|
568
623
|
console.log(
|
|
569
|
-
|
|
624
|
+
uiInfo(
|
|
570
625
|
' Note: Restart the container for changes to take effect.',
|
|
571
626
|
),
|
|
572
627
|
)
|
|
@@ -585,10 +640,10 @@ export const editCommand = new Command('edit')
|
|
|
585
640
|
}
|
|
586
641
|
|
|
587
642
|
console.log()
|
|
588
|
-
console.log(
|
|
589
|
-
} catch (
|
|
590
|
-
const e =
|
|
591
|
-
console.error(
|
|
643
|
+
console.log(uiSuccess('Container updated successfully'))
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const e = error as Error
|
|
646
|
+
console.error(uiError(e.message))
|
|
592
647
|
process.exit(1)
|
|
593
648
|
}
|
|
594
649
|
},
|
package/cli/commands/engines.ts
CHANGED
|
@@ -5,15 +5,24 @@ import inquirer from 'inquirer'
|
|
|
5
5
|
import { containerManager } from '../../core/container-manager'
|
|
6
6
|
import { promptConfirm } from '../ui/prompts'
|
|
7
7
|
import { createSpinner } from '../ui/spinner'
|
|
8
|
-
import {
|
|
8
|
+
import { uiError, uiWarning, uiInfo, formatBytes } from '../ui/theme'
|
|
9
9
|
import { getEngineIcon, ENGINE_ICONS } from '../constants'
|
|
10
10
|
import {
|
|
11
11
|
getInstalledEngines,
|
|
12
12
|
getInstalledPostgresEngines,
|
|
13
13
|
type InstalledPostgresEngine,
|
|
14
14
|
type InstalledMysqlEngine,
|
|
15
|
+
type InstalledSqliteEngine,
|
|
15
16
|
} from '../helpers'
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Pad string to width, accounting for emoji taking 2 display columns
|
|
20
|
+
*/
|
|
21
|
+
function padWithEmoji(str: string, width: number): string {
|
|
22
|
+
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
23
|
+
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
24
|
+
return str.padEnd(width + emojiCount)
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* List subcommand action
|
|
@@ -27,7 +36,7 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
if (engines.length === 0) {
|
|
30
|
-
console.log(
|
|
39
|
+
console.log(uiInfo('No engines installed yet.'))
|
|
31
40
|
console.log(
|
|
32
41
|
chalk.gray(
|
|
33
42
|
' PostgreSQL engines are downloaded automatically when you create a container.',
|
|
@@ -41,13 +50,16 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
41
50
|
return
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
// Separate
|
|
53
|
+
// Separate engines by type
|
|
45
54
|
const pgEngines = engines.filter(
|
|
46
55
|
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
47
56
|
)
|
|
48
57
|
const mysqlEngine = engines.find(
|
|
49
58
|
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
50
59
|
)
|
|
60
|
+
const sqliteEngine = engines.find(
|
|
61
|
+
(e): e is InstalledSqliteEngine => e.engine === 'sqlite',
|
|
62
|
+
)
|
|
51
63
|
|
|
52
64
|
// Calculate total size for PostgreSQL
|
|
53
65
|
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
@@ -67,10 +79,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
67
79
|
for (const engine of pgEngines) {
|
|
68
80
|
const icon = getEngineIcon(engine.engine)
|
|
69
81
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
82
|
+
const engineDisplay = `${icon} ${engine.engine}`
|
|
70
83
|
|
|
71
84
|
console.log(
|
|
72
85
|
chalk.gray(' ') +
|
|
73
|
-
chalk.cyan(
|
|
86
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
74
87
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
75
88
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
76
89
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -81,16 +94,31 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
81
94
|
if (mysqlEngine) {
|
|
82
95
|
const icon = ENGINE_ICONS.mysql
|
|
83
96
|
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
97
|
+
const engineDisplay = `${icon} ${displayName}`
|
|
84
98
|
|
|
85
99
|
console.log(
|
|
86
100
|
chalk.gray(' ') +
|
|
87
|
-
chalk.cyan(
|
|
101
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
88
102
|
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
89
103
|
chalk.gray('system'.padEnd(18)) +
|
|
90
104
|
chalk.gray('(system-installed)'),
|
|
91
105
|
)
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
// SQLite row
|
|
109
|
+
if (sqliteEngine) {
|
|
110
|
+
const icon = ENGINE_ICONS.sqlite
|
|
111
|
+
const engineDisplay = `${icon} sqlite`
|
|
112
|
+
|
|
113
|
+
console.log(
|
|
114
|
+
chalk.gray(' ') +
|
|
115
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
116
|
+
chalk.yellow(sqliteEngine.version.padEnd(12)) +
|
|
117
|
+
chalk.gray('system'.padEnd(18)) +
|
|
118
|
+
chalk.gray('(system-installed)'),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
95
123
|
|
|
96
124
|
// Summary
|
|
@@ -105,6 +133,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
105
133
|
if (mysqlEngine) {
|
|
106
134
|
console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
|
|
107
135
|
}
|
|
136
|
+
if (sqliteEngine) {
|
|
137
|
+
console.log(
|
|
138
|
+
chalk.gray(` SQLite: system-installed at ${sqliteEngine.path}`),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
108
141
|
console.log()
|
|
109
142
|
}
|
|
110
143
|
|
|
@@ -120,7 +153,7 @@ async function deleteEngine(
|
|
|
120
153
|
const pgEngines = await getInstalledPostgresEngines()
|
|
121
154
|
|
|
122
155
|
if (pgEngines.length === 0) {
|
|
123
|
-
console.log(
|
|
156
|
+
console.log(uiWarning('No deletable engines found.'))
|
|
124
157
|
console.log(
|
|
125
158
|
chalk.gray(
|
|
126
159
|
' MySQL is system-installed and cannot be deleted via spindb.',
|
|
@@ -159,7 +192,7 @@ async function deleteEngine(
|
|
|
159
192
|
)
|
|
160
193
|
|
|
161
194
|
if (!targetEngine) {
|
|
162
|
-
console.error(
|
|
195
|
+
console.error(uiError(`Engine "${engineName} ${engineVersion}" not found`))
|
|
163
196
|
process.exit(1)
|
|
164
197
|
}
|
|
165
198
|
|
|
@@ -171,7 +204,7 @@ async function deleteEngine(
|
|
|
171
204
|
|
|
172
205
|
if (usingContainers.length > 0) {
|
|
173
206
|
console.error(
|
|
174
|
-
|
|
207
|
+
uiError(
|
|
175
208
|
`Cannot delete: ${usingContainers.length} container(s) are using ${engineName} ${engineVersion}`,
|
|
176
209
|
),
|
|
177
210
|
)
|
|
@@ -193,7 +226,7 @@ async function deleteEngine(
|
|
|
193
226
|
)
|
|
194
227
|
|
|
195
228
|
if (!confirmed) {
|
|
196
|
-
console.log(
|
|
229
|
+
console.log(uiWarning('Deletion cancelled'))
|
|
197
230
|
return
|
|
198
231
|
}
|
|
199
232
|
}
|
|
@@ -205,8 +238,8 @@ async function deleteEngine(
|
|
|
205
238
|
try {
|
|
206
239
|
await rm(targetEngine.path, { recursive: true, force: true })
|
|
207
240
|
spinner.succeed(`Deleted ${engineName} ${engineVersion}`)
|
|
208
|
-
} catch (
|
|
209
|
-
const e =
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const e = error as Error
|
|
210
243
|
spinner.fail(`Failed to delete: ${e.message}`)
|
|
211
244
|
process.exit(1)
|
|
212
245
|
}
|
|
@@ -219,9 +252,9 @@ export const enginesCommand = new Command('engines')
|
|
|
219
252
|
.action(async (options: { json?: boolean }) => {
|
|
220
253
|
try {
|
|
221
254
|
await listEngines(options)
|
|
222
|
-
} catch (
|
|
223
|
-
const e =
|
|
224
|
-
console.error(
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const e = error as Error
|
|
257
|
+
console.error(uiError(e.message))
|
|
225
258
|
process.exit(1)
|
|
226
259
|
}
|
|
227
260
|
})
|
|
@@ -239,9 +272,9 @@ enginesCommand
|
|
|
239
272
|
) => {
|
|
240
273
|
try {
|
|
241
274
|
await deleteEngine(engine, version, options)
|
|
242
|
-
} catch (
|
|
243
|
-
const e =
|
|
244
|
-
console.error(
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const e = error as Error
|
|
277
|
+
console.error(uiError(e.message))
|
|
245
278
|
process.exit(1)
|
|
246
279
|
}
|
|
247
280
|
},
|
package/cli/commands/info.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { Command } from 'commander'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
|
+
import { basename } from 'path'
|
|
5
6
|
import { containerManager } from '../../core/container-manager'
|
|
6
7
|
import { processManager } from '../../core/process-manager'
|
|
7
8
|
import { paths } from '../../config/paths'
|
|
8
9
|
import { getEngine } from '../../engines'
|
|
9
|
-
import {
|
|
10
|
+
import { uiError, uiInfo, header } from '../ui/theme'
|
|
10
11
|
import { getEngineIcon } from '../constants'
|
|
11
12
|
import { Engine, type ContainerConfig } from '../../types'
|
|
12
13
|
|
|
@@ -197,8 +198,9 @@ async function displayAllContainersInfo(
|
|
|
197
198
|
// Show truncated file path for SQLite instead of port
|
|
198
199
|
let portOrPath: string
|
|
199
200
|
if (isSQLite) {
|
|
200
|
-
const fileName = container.database
|
|
201
|
-
|
|
201
|
+
const fileName = basename(container.database)
|
|
202
|
+
// Truncate if longer than 8 chars to fit in 8-char column
|
|
203
|
+
portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + '…' : fileName
|
|
202
204
|
} else {
|
|
203
205
|
portOrPath = String(container.port)
|
|
204
206
|
}
|
|
@@ -253,14 +255,16 @@ export const infoCommand = new Command('info')
|
|
|
253
255
|
const containers = await containerManager.list()
|
|
254
256
|
|
|
255
257
|
if (containers.length === 0) {
|
|
256
|
-
console.log(
|
|
258
|
+
console.log(
|
|
259
|
+
uiInfo('No containers found. Create one with: spindb create'),
|
|
260
|
+
)
|
|
257
261
|
return
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
if (name) {
|
|
261
265
|
const config = await containerManager.getConfig(name)
|
|
262
266
|
if (!config) {
|
|
263
|
-
console.error(
|
|
267
|
+
console.error(uiError(`Container "${name}" not found`))
|
|
264
268
|
process.exit(1)
|
|
265
269
|
}
|
|
266
270
|
await displayContainerInfo(config, options)
|
|
@@ -297,9 +301,9 @@ export const infoCommand = new Command('info')
|
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
await displayAllContainersInfo(containers, options)
|
|
300
|
-
} catch (
|
|
301
|
-
const e =
|
|
302
|
-
console.error(
|
|
304
|
+
} catch (error) {
|
|
305
|
+
const e = error as Error
|
|
306
|
+
console.error(uiError(e.message))
|
|
303
307
|
process.exit(1)
|
|
304
308
|
}
|
|
305
309
|
})
|