spindb 0.5.4 → 0.5.5
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 +27 -1
- package/cli/commands/backup.ts +263 -0
- package/cli/commands/config.ts +144 -66
- package/cli/commands/engines.ts +1 -9
- package/cli/commands/list.ts +42 -4
- package/cli/commands/menu.ts +191 -19
- package/cli/commands/restore.ts +3 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +87 -0
- package/cli/ui/theme.ts +11 -0
- package/core/config-manager.ts +133 -37
- package/core/container-manager.ts +76 -2
- package/core/dependency-manager.ts +5 -0
- package/engines/base-engine.ts +20 -0
- package/engines/mysql/backup.ts +159 -0
- package/engines/mysql/index.ts +39 -0
- package/engines/mysql/restore.ts +16 -2
- package/engines/postgresql/backup.ts +93 -0
- package/engines/postgresql/index.ts +37 -0
- package/package.json +1 -1
- package/types/index.ts +20 -0
package/cli/commands/list.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
|
-
import {
|
|
4
|
+
import { getEngine } from '../../engines'
|
|
5
|
+
import { info, error, formatBytes } from '../ui/theme'
|
|
6
|
+
import type { ContainerConfig } from '../../types'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Engine icons for display
|
|
@@ -11,6 +13,23 @@ const engineIcons: Record<string, string> = {
|
|
|
11
13
|
mysql: '🐬',
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Get database size for a container (only if running)
|
|
18
|
+
*/
|
|
19
|
+
async function getContainerSize(
|
|
20
|
+
container: ContainerConfig,
|
|
21
|
+
): Promise<number | null> {
|
|
22
|
+
if (container.status !== 'running') {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const engine = getEngine(container.engine)
|
|
27
|
+
return await engine.getDatabaseSize(container)
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
export const listCommand = new Command('list')
|
|
15
34
|
.alias('ls')
|
|
16
35
|
.description('List all containers')
|
|
@@ -20,7 +39,14 @@ export const listCommand = new Command('list')
|
|
|
20
39
|
const containers = await containerManager.list()
|
|
21
40
|
|
|
22
41
|
if (options.json) {
|
|
23
|
-
|
|
42
|
+
// Include sizes in JSON output
|
|
43
|
+
const containersWithSize = await Promise.all(
|
|
44
|
+
containers.map(async (container) => ({
|
|
45
|
+
...container,
|
|
46
|
+
sizeBytes: await getContainerSize(container),
|
|
47
|
+
})),
|
|
48
|
+
)
|
|
49
|
+
console.log(JSON.stringify(containersWithSize, null, 2))
|
|
24
50
|
return
|
|
25
51
|
}
|
|
26
52
|
|
|
@@ -29,6 +55,9 @@ export const listCommand = new Command('list')
|
|
|
29
55
|
return
|
|
30
56
|
}
|
|
31
57
|
|
|
58
|
+
// Fetch sizes for running containers in parallel
|
|
59
|
+
const sizes = await Promise.all(containers.map(getContainerSize))
|
|
60
|
+
|
|
32
61
|
// Table header
|
|
33
62
|
console.log()
|
|
34
63
|
console.log(
|
|
@@ -37,12 +66,16 @@ export const listCommand = new Command('list')
|
|
|
37
66
|
chalk.bold.white('ENGINE'.padEnd(15)) +
|
|
38
67
|
chalk.bold.white('VERSION'.padEnd(10)) +
|
|
39
68
|
chalk.bold.white('PORT'.padEnd(8)) +
|
|
69
|
+
chalk.bold.white('SIZE'.padEnd(10)) +
|
|
40
70
|
chalk.bold.white('STATUS'),
|
|
41
71
|
)
|
|
42
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
72
|
+
console.log(chalk.gray(' ' + '─'.repeat(73)))
|
|
43
73
|
|
|
44
74
|
// Table rows
|
|
45
|
-
for (
|
|
75
|
+
for (let i = 0; i < containers.length; i++) {
|
|
76
|
+
const container = containers[i]
|
|
77
|
+
const size = sizes[i]
|
|
78
|
+
|
|
46
79
|
const statusDisplay =
|
|
47
80
|
container.status === 'running'
|
|
48
81
|
? chalk.green('● running')
|
|
@@ -51,12 +84,17 @@ export const listCommand = new Command('list')
|
|
|
51
84
|
const engineIcon = engineIcons[container.engine] || '▣'
|
|
52
85
|
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
53
86
|
|
|
87
|
+
// Format size: show value if running, dash if stopped
|
|
88
|
+
const sizeDisplay =
|
|
89
|
+
size !== null ? formatBytes(size) : chalk.gray('—')
|
|
90
|
+
|
|
54
91
|
console.log(
|
|
55
92
|
chalk.gray(' ') +
|
|
56
93
|
chalk.cyan(container.name.padEnd(20)) +
|
|
57
94
|
chalk.white(engineDisplay.padEnd(14)) +
|
|
58
95
|
chalk.yellow(container.version.padEnd(10)) +
|
|
59
96
|
chalk.green(String(container.port).padEnd(8)) +
|
|
97
|
+
chalk.magenta(sizeDisplay.padEnd(10)) +
|
|
60
98
|
statusDisplay,
|
|
61
99
|
)
|
|
62
100
|
}
|
package/cli/commands/menu.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
promptContainerSelect,
|
|
8
8
|
promptContainerName,
|
|
9
9
|
promptDatabaseName,
|
|
10
|
+
promptDatabaseSelect,
|
|
11
|
+
promptBackupFormat,
|
|
12
|
+
promptBackupFilename,
|
|
10
13
|
promptCreateOptions,
|
|
11
14
|
promptConfirm,
|
|
12
15
|
promptInstallDependencies,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
warning,
|
|
20
23
|
info,
|
|
21
24
|
connectionBox,
|
|
25
|
+
formatBytes,
|
|
22
26
|
} from '../ui/theme'
|
|
23
27
|
import { existsSync } from 'fs'
|
|
24
28
|
import { readdir, rm, lstat } from 'fs/promises'
|
|
@@ -141,6 +145,13 @@ async function showMainMenu(): Promise<void> {
|
|
|
141
145
|
value: 'restore',
|
|
142
146
|
disabled: canRestore ? false : 'No running containers',
|
|
143
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: canRestore
|
|
150
|
+
? `${chalk.magenta('↑')} Backup database`
|
|
151
|
+
: chalk.gray('↑ Backup database'),
|
|
152
|
+
value: 'backup',
|
|
153
|
+
disabled: canRestore ? false : 'No running containers',
|
|
154
|
+
},
|
|
144
155
|
{
|
|
145
156
|
name: canClone
|
|
146
157
|
? `${chalk.cyan('⧉')} Clone a container`
|
|
@@ -185,6 +196,9 @@ async function showMainMenu(): Promise<void> {
|
|
|
185
196
|
case 'restore':
|
|
186
197
|
await handleRestore()
|
|
187
198
|
break
|
|
199
|
+
case 'backup':
|
|
200
|
+
await handleBackup()
|
|
201
|
+
break
|
|
188
202
|
case 'clone':
|
|
189
203
|
await handleClone()
|
|
190
204
|
break
|
|
@@ -397,6 +411,19 @@ async function handleList(): Promise<void> {
|
|
|
397
411
|
return
|
|
398
412
|
}
|
|
399
413
|
|
|
414
|
+
// Fetch sizes for running containers in parallel
|
|
415
|
+
const sizes = await Promise.all(
|
|
416
|
+
containers.map(async (container) => {
|
|
417
|
+
if (container.status !== 'running') return null
|
|
418
|
+
try {
|
|
419
|
+
const engine = getEngine(container.engine)
|
|
420
|
+
return await engine.getDatabaseSize(container)
|
|
421
|
+
} catch {
|
|
422
|
+
return null
|
|
423
|
+
}
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
|
|
400
427
|
// Table header
|
|
401
428
|
console.log()
|
|
402
429
|
console.log(
|
|
@@ -405,23 +432,30 @@ async function handleList(): Promise<void> {
|
|
|
405
432
|
chalk.bold.white('ENGINE'.padEnd(12)) +
|
|
406
433
|
chalk.bold.white('VERSION'.padEnd(10)) +
|
|
407
434
|
chalk.bold.white('PORT'.padEnd(8)) +
|
|
435
|
+
chalk.bold.white('SIZE'.padEnd(10)) +
|
|
408
436
|
chalk.bold.white('STATUS'),
|
|
409
437
|
)
|
|
410
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
438
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)))
|
|
411
439
|
|
|
412
440
|
// Table rows
|
|
413
|
-
for (
|
|
441
|
+
for (let i = 0; i < containers.length; i++) {
|
|
442
|
+
const container = containers[i]
|
|
443
|
+
const size = sizes[i]
|
|
444
|
+
|
|
414
445
|
const statusDisplay =
|
|
415
446
|
container.status === 'running'
|
|
416
447
|
? chalk.green('● running')
|
|
417
448
|
: chalk.gray('○ stopped')
|
|
418
449
|
|
|
450
|
+
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
451
|
+
|
|
419
452
|
console.log(
|
|
420
453
|
chalk.gray(' ') +
|
|
421
454
|
chalk.cyan(container.name.padEnd(20)) +
|
|
422
455
|
chalk.white(container.engine.padEnd(12)) +
|
|
423
456
|
chalk.yellow(container.version.padEnd(10)) +
|
|
424
457
|
chalk.green(String(container.port).padEnd(8)) +
|
|
458
|
+
chalk.magenta(sizeDisplay.padEnd(10)) +
|
|
425
459
|
statusDisplay,
|
|
426
460
|
)
|
|
427
461
|
}
|
|
@@ -439,15 +473,19 @@ async function handleList(): Promise<void> {
|
|
|
439
473
|
// Container selection with submenu
|
|
440
474
|
console.log()
|
|
441
475
|
const containerChoices = [
|
|
442
|
-
...containers.map((c) =>
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
476
|
+
...containers.map((c, i) => {
|
|
477
|
+
const size = sizes[i]
|
|
478
|
+
const sizeLabel = size !== null ? `, ${formatBytes(size)}` : ''
|
|
479
|
+
return {
|
|
480
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port}${sizeLabel})`)} ${
|
|
481
|
+
c.status === 'running'
|
|
482
|
+
? chalk.green('● running')
|
|
483
|
+
: chalk.gray('○ stopped')
|
|
484
|
+
}`,
|
|
485
|
+
value: c.name,
|
|
486
|
+
short: c.name,
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
451
489
|
new inquirer.Separator(),
|
|
452
490
|
{ name: `${chalk.blue('←')} Back to main menu`, value: 'back' },
|
|
453
491
|
]
|
|
@@ -1521,6 +1559,148 @@ async function handleRestore(): Promise<void> {
|
|
|
1521
1559
|
])
|
|
1522
1560
|
}
|
|
1523
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* Generate a timestamp string for backup filenames
|
|
1564
|
+
*/
|
|
1565
|
+
function generateBackupTimestamp(): string {
|
|
1566
|
+
const now = new Date()
|
|
1567
|
+
return now.toISOString().replace(/:/g, '').split('.')[0]
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Get file extension for backup format
|
|
1572
|
+
*/
|
|
1573
|
+
function getBackupExtension(format: 'sql' | 'dump', engine: string): string {
|
|
1574
|
+
if (format === 'sql') {
|
|
1575
|
+
return '.sql'
|
|
1576
|
+
}
|
|
1577
|
+
return engine === 'mysql' ? '.sql.gz' : '.dump'
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function handleBackup(): Promise<void> {
|
|
1581
|
+
const containers = await containerManager.list()
|
|
1582
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
1583
|
+
|
|
1584
|
+
if (running.length === 0) {
|
|
1585
|
+
console.log(warning('No running containers. Start a container first.'))
|
|
1586
|
+
await inquirer.prompt([
|
|
1587
|
+
{
|
|
1588
|
+
type: 'input',
|
|
1589
|
+
name: 'continue',
|
|
1590
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
1591
|
+
},
|
|
1592
|
+
])
|
|
1593
|
+
return
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Select container
|
|
1597
|
+
const containerName = await promptContainerSelect(
|
|
1598
|
+
running,
|
|
1599
|
+
'Select container to backup:',
|
|
1600
|
+
)
|
|
1601
|
+
if (!containerName) return
|
|
1602
|
+
|
|
1603
|
+
const config = await containerManager.getConfig(containerName)
|
|
1604
|
+
if (!config) {
|
|
1605
|
+
console.log(error(`Container "${containerName}" not found`))
|
|
1606
|
+
return
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const engine = getEngine(config.engine)
|
|
1610
|
+
|
|
1611
|
+
// Check for required tools
|
|
1612
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
1613
|
+
depsSpinner.start()
|
|
1614
|
+
|
|
1615
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
1616
|
+
if (missingDeps.length > 0) {
|
|
1617
|
+
depsSpinner.warn(
|
|
1618
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
const installed = await promptInstallDependencies(
|
|
1622
|
+
missingDeps[0].binary,
|
|
1623
|
+
config.engine,
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
if (!installed) {
|
|
1627
|
+
return
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
1631
|
+
if (missingDeps.length > 0) {
|
|
1632
|
+
console.log(
|
|
1633
|
+
error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
|
|
1634
|
+
)
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
1639
|
+
console.log()
|
|
1640
|
+
} else {
|
|
1641
|
+
depsSpinner.succeed('Required tools available')
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Select database
|
|
1645
|
+
const databases = config.databases || [config.database]
|
|
1646
|
+
let databaseName: string
|
|
1647
|
+
|
|
1648
|
+
if (databases.length > 1) {
|
|
1649
|
+
databaseName = await promptDatabaseSelect(databases, 'Select database to backup:')
|
|
1650
|
+
} else {
|
|
1651
|
+
databaseName = databases[0]
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Select format
|
|
1655
|
+
const format = await promptBackupFormat(config.engine)
|
|
1656
|
+
|
|
1657
|
+
// Get filename
|
|
1658
|
+
const defaultFilename = `${containerName}-${databaseName}-backup-${generateBackupTimestamp()}`
|
|
1659
|
+
const filename = await promptBackupFilename(defaultFilename)
|
|
1660
|
+
|
|
1661
|
+
// Build output path
|
|
1662
|
+
const extension = getBackupExtension(format, config.engine)
|
|
1663
|
+
const outputPath = join(process.cwd(), `${filename}${extension}`)
|
|
1664
|
+
|
|
1665
|
+
// Create backup
|
|
1666
|
+
const backupSpinner = createSpinner(
|
|
1667
|
+
`Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
|
|
1668
|
+
)
|
|
1669
|
+
backupSpinner.start()
|
|
1670
|
+
|
|
1671
|
+
try {
|
|
1672
|
+
const result = await engine.backup(config, outputPath, {
|
|
1673
|
+
database: databaseName,
|
|
1674
|
+
format,
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
backupSpinner.succeed('Backup created successfully')
|
|
1678
|
+
|
|
1679
|
+
console.log()
|
|
1680
|
+
console.log(success('Backup complete'))
|
|
1681
|
+
console.log()
|
|
1682
|
+
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
1683
|
+
console.log(chalk.gray(' Size:'), chalk.white(formatBytes(result.size)))
|
|
1684
|
+
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
1685
|
+
console.log()
|
|
1686
|
+
} catch (err) {
|
|
1687
|
+
const e = err as Error
|
|
1688
|
+
backupSpinner.fail('Backup failed')
|
|
1689
|
+
console.log()
|
|
1690
|
+
console.log(error(e.message))
|
|
1691
|
+
console.log()
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Wait for user to see the result
|
|
1695
|
+
await inquirer.prompt([
|
|
1696
|
+
{
|
|
1697
|
+
type: 'input',
|
|
1698
|
+
name: 'continue',
|
|
1699
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
1700
|
+
},
|
|
1701
|
+
])
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1524
1704
|
async function handleClone(): Promise<void> {
|
|
1525
1705
|
const containers = await containerManager.list()
|
|
1526
1706
|
const stopped = containers.filter((c) => c.status !== 'running')
|
|
@@ -1997,14 +2177,6 @@ function compareVersions(a: string, b: string): number {
|
|
|
1997
2177
|
return 0
|
|
1998
2178
|
}
|
|
1999
2179
|
|
|
2000
|
-
function formatBytes(bytes: number): string {
|
|
2001
|
-
if (bytes === 0) return '0 B'
|
|
2002
|
-
const k = 1024
|
|
2003
|
-
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
2004
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
2005
|
-
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
2180
|
async function handleEngines(): Promise<void> {
|
|
2009
2181
|
console.clear()
|
|
2010
2182
|
console.log(header('Installed Engines'))
|
package/cli/commands/restore.ts
CHANGED
|
@@ -271,6 +271,9 @@ export const restoreCommand = new Command('restore')
|
|
|
271
271
|
await engine.createDatabase(config, databaseName)
|
|
272
272
|
dbSpinner.succeed(`Database "${databaseName}" ready`)
|
|
273
273
|
|
|
274
|
+
// Add database to container's databases array
|
|
275
|
+
await containerManager.addDatabase(containerName, databaseName)
|
|
276
|
+
|
|
274
277
|
// Restore backup
|
|
275
278
|
const restoreSpinner = createSpinner('Restoring backup...')
|
|
276
279
|
restoreSpinner.start()
|
package/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { startCommand } from './commands/start'
|
|
|
5
5
|
import { stopCommand } from './commands/stop'
|
|
6
6
|
import { deleteCommand } from './commands/delete'
|
|
7
7
|
import { restoreCommand } from './commands/restore'
|
|
8
|
+
import { backupCommand } from './commands/backup'
|
|
8
9
|
import { connectCommand } from './commands/connect'
|
|
9
10
|
import { cloneCommand } from './commands/clone'
|
|
10
11
|
import { menuCommand } from './commands/menu'
|
|
@@ -27,6 +28,7 @@ export async function run(): Promise<void> {
|
|
|
27
28
|
program.addCommand(stopCommand)
|
|
28
29
|
program.addCommand(deleteCommand)
|
|
29
30
|
program.addCommand(restoreCommand)
|
|
31
|
+
program.addCommand(backupCommand)
|
|
30
32
|
program.addCommand(connectCommand)
|
|
31
33
|
program.addCommand(cloneCommand)
|
|
32
34
|
program.addCommand(menuCommand)
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -276,6 +276,93 @@ export async function promptDatabaseName(
|
|
|
276
276
|
return database
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Prompt to select a database from a list of databases in a container
|
|
281
|
+
*/
|
|
282
|
+
export async function promptDatabaseSelect(
|
|
283
|
+
databases: string[],
|
|
284
|
+
message: string = 'Select database:',
|
|
285
|
+
): Promise<string> {
|
|
286
|
+
if (databases.length === 0) {
|
|
287
|
+
throw new Error('No databases available to select')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (databases.length === 1) {
|
|
291
|
+
return databases[0]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { database } = await inquirer.prompt<{ database: string }>([
|
|
295
|
+
{
|
|
296
|
+
type: 'list',
|
|
297
|
+
name: 'database',
|
|
298
|
+
message,
|
|
299
|
+
choices: databases.map((db, index) => ({
|
|
300
|
+
name: index === 0 ? `${db} ${chalk.gray('(primary)')}` : db,
|
|
301
|
+
value: db,
|
|
302
|
+
short: db,
|
|
303
|
+
})),
|
|
304
|
+
},
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
return database
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Prompt for backup format selection
|
|
312
|
+
*/
|
|
313
|
+
export async function promptBackupFormat(
|
|
314
|
+
engine: string,
|
|
315
|
+
): Promise<'sql' | 'dump'> {
|
|
316
|
+
const sqlDescription =
|
|
317
|
+
engine === 'mysql'
|
|
318
|
+
? 'Plain SQL - human-readable, larger file'
|
|
319
|
+
: 'Plain SQL - human-readable, larger file'
|
|
320
|
+
const dumpDescription =
|
|
321
|
+
engine === 'mysql'
|
|
322
|
+
? 'Compressed SQL (.sql.gz) - smaller file'
|
|
323
|
+
: 'Custom format - smaller file, faster restore'
|
|
324
|
+
|
|
325
|
+
const { format } = await inquirer.prompt<{ format: 'sql' | 'dump' }>([
|
|
326
|
+
{
|
|
327
|
+
type: 'list',
|
|
328
|
+
name: 'format',
|
|
329
|
+
message: 'Select backup format:',
|
|
330
|
+
choices: [
|
|
331
|
+
{ name: `.sql ${chalk.gray(`- ${sqlDescription}`)}`, value: 'sql' },
|
|
332
|
+
{ name: `.dump ${chalk.gray(`- ${dumpDescription}`)}`, value: 'dump' },
|
|
333
|
+
],
|
|
334
|
+
default: 'sql',
|
|
335
|
+
},
|
|
336
|
+
])
|
|
337
|
+
|
|
338
|
+
return format
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Prompt for backup filename
|
|
343
|
+
*/
|
|
344
|
+
export async function promptBackupFilename(
|
|
345
|
+
defaultName: string,
|
|
346
|
+
): Promise<string> {
|
|
347
|
+
const { filename } = await inquirer.prompt<{ filename: string }>([
|
|
348
|
+
{
|
|
349
|
+
type: 'input',
|
|
350
|
+
name: 'filename',
|
|
351
|
+
message: 'Backup filename (without extension):',
|
|
352
|
+
default: defaultName,
|
|
353
|
+
validate: (input: string) => {
|
|
354
|
+
if (!input) return 'Filename is required'
|
|
355
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
|
|
356
|
+
return 'Filename must contain only letters, numbers, underscores, and hyphens'
|
|
357
|
+
}
|
|
358
|
+
return true
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
return filename
|
|
364
|
+
}
|
|
365
|
+
|
|
279
366
|
export type CreateOptions = {
|
|
280
367
|
name: string
|
|
281
368
|
engine: string
|
package/cli/ui/theme.ts
CHANGED
|
@@ -155,3 +155,14 @@ export function connectionBox(
|
|
|
155
155
|
|
|
156
156
|
return box(lines)
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Format bytes into human-readable format (B, KB, MB, GB)
|
|
161
|
+
*/
|
|
162
|
+
export function formatBytes(bytes: number): string {
|
|
163
|
+
if (bytes === 0) return '0 B'
|
|
164
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
165
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
|
166
|
+
const value = bytes / Math.pow(1024, i)
|
|
167
|
+
return `${value.toFixed(1)} ${units[i]}`
|
|
168
|
+
}
|