spindb 0.5.4 → 0.6.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 +46 -4
- package/cli/commands/backup.ts +269 -0
- package/cli/commands/config.ts +200 -67
- package/cli/commands/connect.ts +29 -9
- package/cli/commands/engines.ts +1 -9
- package/cli/commands/list.ts +41 -4
- package/cli/commands/menu.ts +289 -19
- package/cli/commands/restore.ts +3 -0
- package/cli/commands/self-update.ts +109 -0
- package/cli/commands/version.ts +55 -0
- package/cli/index.ts +84 -1
- package/cli/ui/prompts.ts +89 -1
- package/cli/ui/theme.ts +11 -0
- package/core/config-manager.ts +123 -37
- package/core/container-manager.ts +78 -2
- package/core/dependency-manager.ts +5 -0
- package/core/update-manager.ts +194 -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 +26 -0
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'
|
|
@@ -52,6 +56,7 @@ import {
|
|
|
52
56
|
isMariaDB,
|
|
53
57
|
getMysqlInstallInfo,
|
|
54
58
|
} from '../../engines/mysql/binary-detection'
|
|
59
|
+
import { updateManager } from '../../core/update-manager'
|
|
55
60
|
|
|
56
61
|
type MenuChoice =
|
|
57
62
|
| {
|
|
@@ -141,6 +146,13 @@ async function showMainMenu(): Promise<void> {
|
|
|
141
146
|
value: 'restore',
|
|
142
147
|
disabled: canRestore ? false : 'No running containers',
|
|
143
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: canRestore
|
|
151
|
+
? `${chalk.magenta('↑')} Backup database`
|
|
152
|
+
: chalk.gray('↑ Backup database'),
|
|
153
|
+
value: 'backup',
|
|
154
|
+
disabled: canRestore ? false : 'No running containers',
|
|
155
|
+
},
|
|
144
156
|
{
|
|
145
157
|
name: canClone
|
|
146
158
|
? `${chalk.cyan('⧉')} Clone a container`
|
|
@@ -156,6 +168,7 @@ async function showMainMenu(): Promise<void> {
|
|
|
156
168
|
disabled: hasEngines ? false : 'No engines installed',
|
|
157
169
|
},
|
|
158
170
|
new inquirer.Separator(),
|
|
171
|
+
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
159
172
|
{ name: `${chalk.gray('⏻')} Exit`, value: 'exit' },
|
|
160
173
|
]
|
|
161
174
|
|
|
@@ -185,12 +198,18 @@ async function showMainMenu(): Promise<void> {
|
|
|
185
198
|
case 'restore':
|
|
186
199
|
await handleRestore()
|
|
187
200
|
break
|
|
201
|
+
case 'backup':
|
|
202
|
+
await handleBackup()
|
|
203
|
+
break
|
|
188
204
|
case 'clone':
|
|
189
205
|
await handleClone()
|
|
190
206
|
break
|
|
191
207
|
case 'engines':
|
|
192
208
|
await handleEngines()
|
|
193
209
|
break
|
|
210
|
+
case 'check-update':
|
|
211
|
+
await handleCheckUpdate()
|
|
212
|
+
break
|
|
194
213
|
case 'exit':
|
|
195
214
|
console.log(chalk.gray('\n Goodbye!\n'))
|
|
196
215
|
process.exit(0)
|
|
@@ -200,6 +219,94 @@ async function showMainMenu(): Promise<void> {
|
|
|
200
219
|
await showMainMenu()
|
|
201
220
|
}
|
|
202
221
|
|
|
222
|
+
async function handleCheckUpdate(): Promise<void> {
|
|
223
|
+
console.clear()
|
|
224
|
+
console.log(header('Check for Updates'))
|
|
225
|
+
console.log()
|
|
226
|
+
|
|
227
|
+
const spinner = createSpinner('Checking for updates...')
|
|
228
|
+
spinner.start()
|
|
229
|
+
|
|
230
|
+
const result = await updateManager.checkForUpdate(true)
|
|
231
|
+
|
|
232
|
+
if (!result) {
|
|
233
|
+
spinner.fail('Could not reach npm registry')
|
|
234
|
+
console.log()
|
|
235
|
+
console.log(info('Check your internet connection and try again.'))
|
|
236
|
+
console.log(chalk.gray(' Manual update: npm install -g spindb@latest'))
|
|
237
|
+
console.log()
|
|
238
|
+
await pressEnterToContinue()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (result.updateAvailable) {
|
|
243
|
+
spinner.succeed('Update available')
|
|
244
|
+
console.log()
|
|
245
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
246
|
+
console.log(
|
|
247
|
+
chalk.gray(` Latest version: ${chalk.green(result.latestVersion)}`),
|
|
248
|
+
)
|
|
249
|
+
console.log()
|
|
250
|
+
|
|
251
|
+
const { action } = await inquirer.prompt<{ action: string }>([
|
|
252
|
+
{
|
|
253
|
+
type: 'list',
|
|
254
|
+
name: 'action',
|
|
255
|
+
message: 'What would you like to do?',
|
|
256
|
+
choices: [
|
|
257
|
+
{ name: 'Update now', value: 'update' },
|
|
258
|
+
{ name: 'Remind me later', value: 'later' },
|
|
259
|
+
{ name: "Don't check for updates on startup", value: 'disable' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
if (action === 'update') {
|
|
265
|
+
console.log()
|
|
266
|
+
const updateSpinner = createSpinner('Updating spindb...')
|
|
267
|
+
updateSpinner.start()
|
|
268
|
+
|
|
269
|
+
const updateResult = await updateManager.performUpdate()
|
|
270
|
+
|
|
271
|
+
if (updateResult.success) {
|
|
272
|
+
updateSpinner.succeed('Update complete')
|
|
273
|
+
console.log()
|
|
274
|
+
console.log(
|
|
275
|
+
success(
|
|
276
|
+
`Updated from ${updateResult.previousVersion} to ${updateResult.newVersion}`,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
console.log()
|
|
280
|
+
if (updateResult.previousVersion !== updateResult.newVersion) {
|
|
281
|
+
console.log(warning('Please restart spindb to use the new version.'))
|
|
282
|
+
console.log()
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
updateSpinner.fail('Update failed')
|
|
286
|
+
console.log()
|
|
287
|
+
console.log(error(updateResult.error || 'Unknown error'))
|
|
288
|
+
console.log()
|
|
289
|
+
console.log(info('Manual update: npm install -g spindb@latest'))
|
|
290
|
+
}
|
|
291
|
+
await pressEnterToContinue()
|
|
292
|
+
} else if (action === 'disable') {
|
|
293
|
+
await updateManager.setAutoCheckEnabled(false)
|
|
294
|
+
console.log()
|
|
295
|
+
console.log(info('Update checks disabled on startup.'))
|
|
296
|
+
console.log(chalk.gray(' Re-enable with: spindb config update-check on'))
|
|
297
|
+
console.log()
|
|
298
|
+
await pressEnterToContinue()
|
|
299
|
+
}
|
|
300
|
+
// 'later' just returns to menu
|
|
301
|
+
} else {
|
|
302
|
+
spinner.succeed('You are on the latest version')
|
|
303
|
+
console.log()
|
|
304
|
+
console.log(chalk.gray(` Version: ${result.currentVersion}`))
|
|
305
|
+
console.log()
|
|
306
|
+
await pressEnterToContinue()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
203
310
|
async function handleCreate(): Promise<void> {
|
|
204
311
|
console.log()
|
|
205
312
|
const answers = await promptCreateOptions()
|
|
@@ -397,6 +504,19 @@ async function handleList(): Promise<void> {
|
|
|
397
504
|
return
|
|
398
505
|
}
|
|
399
506
|
|
|
507
|
+
// Fetch sizes for running containers in parallel
|
|
508
|
+
const sizes = await Promise.all(
|
|
509
|
+
containers.map(async (container) => {
|
|
510
|
+
if (container.status !== 'running') return null
|
|
511
|
+
try {
|
|
512
|
+
const engine = getEngine(container.engine)
|
|
513
|
+
return await engine.getDatabaseSize(container)
|
|
514
|
+
} catch {
|
|
515
|
+
return null
|
|
516
|
+
}
|
|
517
|
+
}),
|
|
518
|
+
)
|
|
519
|
+
|
|
400
520
|
// Table header
|
|
401
521
|
console.log()
|
|
402
522
|
console.log(
|
|
@@ -405,23 +525,30 @@ async function handleList(): Promise<void> {
|
|
|
405
525
|
chalk.bold.white('ENGINE'.padEnd(12)) +
|
|
406
526
|
chalk.bold.white('VERSION'.padEnd(10)) +
|
|
407
527
|
chalk.bold.white('PORT'.padEnd(8)) +
|
|
528
|
+
chalk.bold.white('SIZE'.padEnd(10)) +
|
|
408
529
|
chalk.bold.white('STATUS'),
|
|
409
530
|
)
|
|
410
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
531
|
+
console.log(chalk.gray(' ' + '─'.repeat(70)))
|
|
411
532
|
|
|
412
533
|
// Table rows
|
|
413
|
-
for (
|
|
534
|
+
for (let i = 0; i < containers.length; i++) {
|
|
535
|
+
const container = containers[i]
|
|
536
|
+
const size = sizes[i]
|
|
537
|
+
|
|
414
538
|
const statusDisplay =
|
|
415
539
|
container.status === 'running'
|
|
416
540
|
? chalk.green('● running')
|
|
417
541
|
: chalk.gray('○ stopped')
|
|
418
542
|
|
|
543
|
+
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
544
|
+
|
|
419
545
|
console.log(
|
|
420
546
|
chalk.gray(' ') +
|
|
421
547
|
chalk.cyan(container.name.padEnd(20)) +
|
|
422
548
|
chalk.white(container.engine.padEnd(12)) +
|
|
423
549
|
chalk.yellow(container.version.padEnd(10)) +
|
|
424
550
|
chalk.green(String(container.port).padEnd(8)) +
|
|
551
|
+
chalk.magenta(sizeDisplay.padEnd(10)) +
|
|
425
552
|
statusDisplay,
|
|
426
553
|
)
|
|
427
554
|
}
|
|
@@ -439,15 +566,19 @@ async function handleList(): Promise<void> {
|
|
|
439
566
|
// Container selection with submenu
|
|
440
567
|
console.log()
|
|
441
568
|
const containerChoices = [
|
|
442
|
-
...containers.map((c) =>
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
569
|
+
...containers.map((c, i) => {
|
|
570
|
+
const size = sizes[i]
|
|
571
|
+
const sizeLabel = size !== null ? `, ${formatBytes(size)}` : ''
|
|
572
|
+
return {
|
|
573
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port}${sizeLabel})`)} ${
|
|
574
|
+
c.status === 'running'
|
|
575
|
+
? chalk.green('● running')
|
|
576
|
+
: chalk.gray('○ stopped')
|
|
577
|
+
}`,
|
|
578
|
+
value: c.name,
|
|
579
|
+
short: c.name,
|
|
580
|
+
}
|
|
581
|
+
}),
|
|
451
582
|
new inquirer.Separator(),
|
|
452
583
|
{ name: `${chalk.blue('←')} Back to main menu`, value: 'back' },
|
|
453
584
|
]
|
|
@@ -1521,6 +1652,153 @@ async function handleRestore(): Promise<void> {
|
|
|
1521
1652
|
])
|
|
1522
1653
|
}
|
|
1523
1654
|
|
|
1655
|
+
/**
|
|
1656
|
+
* Generate a timestamp string for backup filenames
|
|
1657
|
+
*/
|
|
1658
|
+
function generateBackupTimestamp(): string {
|
|
1659
|
+
const now = new Date()
|
|
1660
|
+
return now.toISOString().replace(/:/g, '').split('.')[0]
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Get file extension for backup format
|
|
1665
|
+
*/
|
|
1666
|
+
function getBackupExtension(format: 'sql' | 'dump', engine: string): string {
|
|
1667
|
+
if (format === 'sql') {
|
|
1668
|
+
return '.sql'
|
|
1669
|
+
}
|
|
1670
|
+
return engine === 'mysql' ? '.sql.gz' : '.dump'
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
async function handleBackup(): Promise<void> {
|
|
1674
|
+
const containers = await containerManager.list()
|
|
1675
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
1676
|
+
|
|
1677
|
+
if (running.length === 0) {
|
|
1678
|
+
console.log(warning('No running containers. Start a container first.'))
|
|
1679
|
+
await inquirer.prompt([
|
|
1680
|
+
{
|
|
1681
|
+
type: 'input',
|
|
1682
|
+
name: 'continue',
|
|
1683
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
1684
|
+
},
|
|
1685
|
+
])
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Select container
|
|
1690
|
+
const containerName = await promptContainerSelect(
|
|
1691
|
+
running,
|
|
1692
|
+
'Select container to backup:',
|
|
1693
|
+
)
|
|
1694
|
+
if (!containerName) return
|
|
1695
|
+
|
|
1696
|
+
const config = await containerManager.getConfig(containerName)
|
|
1697
|
+
if (!config) {
|
|
1698
|
+
console.log(error(`Container "${containerName}" not found`))
|
|
1699
|
+
return
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const engine = getEngine(config.engine)
|
|
1703
|
+
|
|
1704
|
+
// Check for required tools
|
|
1705
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
1706
|
+
depsSpinner.start()
|
|
1707
|
+
|
|
1708
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
1709
|
+
if (missingDeps.length > 0) {
|
|
1710
|
+
depsSpinner.warn(
|
|
1711
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
1712
|
+
)
|
|
1713
|
+
|
|
1714
|
+
const installed = await promptInstallDependencies(
|
|
1715
|
+
missingDeps[0].binary,
|
|
1716
|
+
config.engine,
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
if (!installed) {
|
|
1720
|
+
return
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
1724
|
+
if (missingDeps.length > 0) {
|
|
1725
|
+
console.log(
|
|
1726
|
+
error(
|
|
1727
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
1728
|
+
),
|
|
1729
|
+
)
|
|
1730
|
+
return
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
1734
|
+
console.log()
|
|
1735
|
+
} else {
|
|
1736
|
+
depsSpinner.succeed('Required tools available')
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Select database
|
|
1740
|
+
const databases = config.databases || [config.database]
|
|
1741
|
+
let databaseName: string
|
|
1742
|
+
|
|
1743
|
+
if (databases.length > 1) {
|
|
1744
|
+
databaseName = await promptDatabaseSelect(
|
|
1745
|
+
databases,
|
|
1746
|
+
'Select database to backup:',
|
|
1747
|
+
)
|
|
1748
|
+
} else {
|
|
1749
|
+
databaseName = databases[0]
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Select format
|
|
1753
|
+
const format = await promptBackupFormat(config.engine)
|
|
1754
|
+
|
|
1755
|
+
// Get filename
|
|
1756
|
+
const defaultFilename = `${containerName}-${databaseName}-backup-${generateBackupTimestamp()}`
|
|
1757
|
+
const filename = await promptBackupFilename(defaultFilename)
|
|
1758
|
+
|
|
1759
|
+
// Build output path
|
|
1760
|
+
const extension = getBackupExtension(format, config.engine)
|
|
1761
|
+
const outputPath = join(process.cwd(), `${filename}${extension}`)
|
|
1762
|
+
|
|
1763
|
+
// Create backup
|
|
1764
|
+
const backupSpinner = createSpinner(
|
|
1765
|
+
`Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
|
|
1766
|
+
)
|
|
1767
|
+
backupSpinner.start()
|
|
1768
|
+
|
|
1769
|
+
try {
|
|
1770
|
+
const result = await engine.backup(config, outputPath, {
|
|
1771
|
+
database: databaseName,
|
|
1772
|
+
format,
|
|
1773
|
+
})
|
|
1774
|
+
|
|
1775
|
+
backupSpinner.succeed('Backup created successfully')
|
|
1776
|
+
|
|
1777
|
+
console.log()
|
|
1778
|
+
console.log(success('Backup complete'))
|
|
1779
|
+
console.log()
|
|
1780
|
+
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
1781
|
+
console.log(chalk.gray(' Size:'), chalk.white(formatBytes(result.size)))
|
|
1782
|
+
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
1783
|
+
console.log()
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
const e = err as Error
|
|
1786
|
+
backupSpinner.fail('Backup failed')
|
|
1787
|
+
console.log()
|
|
1788
|
+
console.log(error(e.message))
|
|
1789
|
+
console.log()
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Wait for user to see the result
|
|
1793
|
+
await inquirer.prompt([
|
|
1794
|
+
{
|
|
1795
|
+
type: 'input',
|
|
1796
|
+
name: 'continue',
|
|
1797
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
1798
|
+
},
|
|
1799
|
+
])
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1524
1802
|
async function handleClone(): Promise<void> {
|
|
1525
1803
|
const containers = await containerManager.list()
|
|
1526
1804
|
const stopped = containers.filter((c) => c.status !== 'running')
|
|
@@ -1997,14 +2275,6 @@ function compareVersions(a: string, b: string): number {
|
|
|
1997
2275
|
return 0
|
|
1998
2276
|
}
|
|
1999
2277
|
|
|
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
2278
|
async function handleEngines(): Promise<void> {
|
|
2009
2279
|
console.clear()
|
|
2010
2280
|
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()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { updateManager } from '../../core/update-manager'
|
|
5
|
+
import { createSpinner } from '../ui/spinner'
|
|
6
|
+
import { success, error, info, header } from '../ui/theme'
|
|
7
|
+
|
|
8
|
+
export const selfUpdateCommand = new Command('self-update')
|
|
9
|
+
.alias('update')
|
|
10
|
+
.description('Update spindb to the latest version')
|
|
11
|
+
.option('-f, --force', 'Update even if already on latest version')
|
|
12
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
13
|
+
.action(
|
|
14
|
+
async (options: { force?: boolean; yes?: boolean }): Promise<void> => {
|
|
15
|
+
console.log()
|
|
16
|
+
console.log(header('SpinDB Self-Update'))
|
|
17
|
+
console.log()
|
|
18
|
+
|
|
19
|
+
const checkSpinner = createSpinner('Checking for updates...')
|
|
20
|
+
checkSpinner.start()
|
|
21
|
+
|
|
22
|
+
const result = await updateManager.checkForUpdate(true)
|
|
23
|
+
|
|
24
|
+
if (!result) {
|
|
25
|
+
checkSpinner.fail('Could not reach npm registry')
|
|
26
|
+
console.log()
|
|
27
|
+
console.log(info('Check your internet connection and try again.'))
|
|
28
|
+
console.log(chalk.gray(' Manual update: npm install -g spindb@latest'))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!result.updateAvailable && !options.force) {
|
|
33
|
+
checkSpinner.succeed('Already on latest version')
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
36
|
+
console.log(chalk.gray(` Latest version: ${result.latestVersion}`))
|
|
37
|
+
console.log()
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.updateAvailable) {
|
|
42
|
+
checkSpinner.succeed('Update available')
|
|
43
|
+
} else {
|
|
44
|
+
checkSpinner.succeed('Version check complete')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log()
|
|
48
|
+
console.log(chalk.gray(` Current version: ${result.currentVersion}`))
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.gray(
|
|
51
|
+
` Latest version: ${result.updateAvailable ? chalk.green(result.latestVersion) : result.latestVersion}`,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
console.log()
|
|
55
|
+
|
|
56
|
+
// Confirm unless --yes
|
|
57
|
+
if (!options.yes) {
|
|
58
|
+
const message = result.updateAvailable
|
|
59
|
+
? `Update spindb from ${result.currentVersion} to ${result.latestVersion}?`
|
|
60
|
+
: `Reinstall spindb ${result.currentVersion}?`
|
|
61
|
+
|
|
62
|
+
const { confirm } = await inquirer.prompt<{ confirm: boolean }>([
|
|
63
|
+
{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'confirm',
|
|
66
|
+
message,
|
|
67
|
+
default: true,
|
|
68
|
+
},
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
if (!confirm) {
|
|
72
|
+
console.log(chalk.yellow('Update cancelled'))
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log()
|
|
78
|
+
const updateSpinner = createSpinner('Updating spindb...')
|
|
79
|
+
updateSpinner.start()
|
|
80
|
+
|
|
81
|
+
const updateResult = await updateManager.performUpdate()
|
|
82
|
+
|
|
83
|
+
if (updateResult.success) {
|
|
84
|
+
updateSpinner.succeed('Update complete')
|
|
85
|
+
console.log()
|
|
86
|
+
console.log(
|
|
87
|
+
success(
|
|
88
|
+
`Updated from ${updateResult.previousVersion} to ${updateResult.newVersion}`,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
console.log()
|
|
92
|
+
if (updateResult.previousVersion !== updateResult.newVersion) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.gray(
|
|
95
|
+
' Please restart your terminal to use the new version.',
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
console.log()
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
updateSpinner.fail('Update failed')
|
|
102
|
+
console.log()
|
|
103
|
+
console.log(error(updateResult.error || 'Unknown error'))
|
|
104
|
+
console.log()
|
|
105
|
+
console.log(info('Manual update: npm install -g spindb@latest'))
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { updateManager } from '../../core/update-manager'
|
|
4
|
+
import { createSpinner } from '../ui/spinner'
|
|
5
|
+
|
|
6
|
+
export const versionCommand = new Command('version')
|
|
7
|
+
.description('Show version information and check for updates')
|
|
8
|
+
.option('-c, --check', 'Check for available updates')
|
|
9
|
+
.option('-j, --json', 'Output as JSON')
|
|
10
|
+
.action(
|
|
11
|
+
async (options: { check?: boolean; json?: boolean }): Promise<void> => {
|
|
12
|
+
const currentVersion = updateManager.getCurrentVersion()
|
|
13
|
+
|
|
14
|
+
if (options.check) {
|
|
15
|
+
const spinner = createSpinner('Checking for updates...')
|
|
16
|
+
if (!options.json) spinner.start()
|
|
17
|
+
|
|
18
|
+
const result = await updateManager.checkForUpdate(true)
|
|
19
|
+
|
|
20
|
+
if (!options.json) spinner.stop()
|
|
21
|
+
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
current: currentVersion,
|
|
26
|
+
latest: result?.latestVersion || null,
|
|
27
|
+
updateAvailable: result?.updateAvailable || false,
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
} else {
|
|
31
|
+
console.log()
|
|
32
|
+
console.log(`SpinDB v${currentVersion}`)
|
|
33
|
+
if (result) {
|
|
34
|
+
if (result.updateAvailable) {
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.yellow(`Update available: v${result.latestVersion}`),
|
|
37
|
+
)
|
|
38
|
+
console.log(chalk.gray("Run 'spindb self-update' to update."))
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.green('You are on the latest version.'))
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log(chalk.gray('Could not check for updates (offline?)'))
|
|
44
|
+
}
|
|
45
|
+
console.log()
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify({ current: currentVersion }))
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`SpinDB v${currentVersion}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
package/cli/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { program } from 'commander'
|
|
2
|
+
import { createRequire } from 'module'
|
|
3
|
+
import chalk from 'chalk'
|
|
2
4
|
import { createCommand } from './commands/create'
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
const pkg = require('../package.json') as { version: string }
|
|
3
8
|
import { listCommand } from './commands/list'
|
|
4
9
|
import { startCommand } from './commands/start'
|
|
5
10
|
import { stopCommand } from './commands/stop'
|
|
6
11
|
import { deleteCommand } from './commands/delete'
|
|
7
12
|
import { restoreCommand } from './commands/restore'
|
|
13
|
+
import { backupCommand } from './commands/backup'
|
|
8
14
|
import { connectCommand } from './commands/connect'
|
|
9
15
|
import { cloneCommand } from './commands/clone'
|
|
10
16
|
import { menuCommand } from './commands/menu'
|
|
@@ -14,12 +20,86 @@ import { enginesCommand } from './commands/engines'
|
|
|
14
20
|
import { editCommand } from './commands/edit'
|
|
15
21
|
import { urlCommand } from './commands/url'
|
|
16
22
|
import { infoCommand } from './commands/info'
|
|
23
|
+
import { selfUpdateCommand } from './commands/self-update'
|
|
24
|
+
import { versionCommand } from './commands/version'
|
|
25
|
+
import { updateManager } from '../core/update-manager'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Show update notification banner if an update is available (from cached data)
|
|
29
|
+
* This shows on every run until the user updates or disables checks
|
|
30
|
+
*/
|
|
31
|
+
async function showUpdateNotificationIfAvailable(): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
const cached = await updateManager.getCachedUpdateInfo()
|
|
34
|
+
|
|
35
|
+
// Skip if auto-check is disabled or no cached version
|
|
36
|
+
if (!cached.autoCheckEnabled || !cached.latestVersion) return
|
|
37
|
+
|
|
38
|
+
const currentVersion = updateManager.getCurrentVersion()
|
|
39
|
+
const latestVersion = cached.latestVersion
|
|
40
|
+
|
|
41
|
+
// Skip if no update available
|
|
42
|
+
if (updateManager.compareVersions(latestVersion, currentVersion) <= 0)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
// Show notification banner
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(chalk.cyan('┌' + '─'.repeat(52) + '┐'))
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.cyan('│') +
|
|
50
|
+
chalk.yellow(' Update available! ') +
|
|
51
|
+
chalk.gray(`${currentVersion} -> `) +
|
|
52
|
+
chalk.green(latestVersion) +
|
|
53
|
+
' '.repeat(
|
|
54
|
+
Math.max(
|
|
55
|
+
0,
|
|
56
|
+
52 - 21 - currentVersion.length - 4 - latestVersion.length,
|
|
57
|
+
),
|
|
58
|
+
) +
|
|
59
|
+
chalk.cyan('│'),
|
|
60
|
+
)
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.cyan('│') +
|
|
63
|
+
chalk.gray(' Run: ') +
|
|
64
|
+
chalk.cyan('spindb self-update') +
|
|
65
|
+
' '.repeat(28) +
|
|
66
|
+
chalk.cyan('│'),
|
|
67
|
+
)
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.cyan('│') +
|
|
70
|
+
chalk.gray(' To disable: ') +
|
|
71
|
+
chalk.gray('spindb config update-check off') +
|
|
72
|
+
' '.repeat(8) +
|
|
73
|
+
chalk.cyan('│'),
|
|
74
|
+
)
|
|
75
|
+
console.log(chalk.cyan('└' + '─'.repeat(52) + '┘'))
|
|
76
|
+
console.log()
|
|
77
|
+
} catch {
|
|
78
|
+
// Silently ignore errors - update notification is not critical
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Trigger background update check (fire and forget)
|
|
84
|
+
* This updates the cache for the next run's notification
|
|
85
|
+
*/
|
|
86
|
+
function triggerBackgroundUpdateCheck(): void {
|
|
87
|
+
updateManager.checkForUpdate(false).catch(() => {
|
|
88
|
+
// Silently ignore - background check is best-effort
|
|
89
|
+
})
|
|
90
|
+
}
|
|
17
91
|
|
|
18
92
|
export async function run(): Promise<void> {
|
|
93
|
+
// Trigger background update check (non-blocking, updates cache for next run)
|
|
94
|
+
triggerBackgroundUpdateCheck()
|
|
95
|
+
|
|
96
|
+
// Show update notification if an update is available (from cached data)
|
|
97
|
+
await showUpdateNotificationIfAvailable()
|
|
98
|
+
|
|
19
99
|
program
|
|
20
100
|
.name('spindb')
|
|
21
101
|
.description('Spin up local database containers without Docker')
|
|
22
|
-
.version(
|
|
102
|
+
.version(pkg.version, '-v, --version', 'output the version number')
|
|
23
103
|
|
|
24
104
|
program.addCommand(createCommand)
|
|
25
105
|
program.addCommand(listCommand)
|
|
@@ -27,6 +107,7 @@ export async function run(): Promise<void> {
|
|
|
27
107
|
program.addCommand(stopCommand)
|
|
28
108
|
program.addCommand(deleteCommand)
|
|
29
109
|
program.addCommand(restoreCommand)
|
|
110
|
+
program.addCommand(backupCommand)
|
|
30
111
|
program.addCommand(connectCommand)
|
|
31
112
|
program.addCommand(cloneCommand)
|
|
32
113
|
program.addCommand(menuCommand)
|
|
@@ -36,6 +117,8 @@ export async function run(): Promise<void> {
|
|
|
36
117
|
program.addCommand(editCommand)
|
|
37
118
|
program.addCommand(urlCommand)
|
|
38
119
|
program.addCommand(infoCommand)
|
|
120
|
+
program.addCommand(selfUpdateCommand)
|
|
121
|
+
program.addCommand(versionCommand)
|
|
39
122
|
|
|
40
123
|
// If no arguments provided, show interactive menu
|
|
41
124
|
if (process.argv.length <= 2) {
|