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.
@@ -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(60)))
531
+ console.log(chalk.gray(' ' + '─'.repeat(70)))
411
532
 
412
533
  // Table rows
413
- for (const container of containers) {
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
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '▣'} ${c.engine} ${c.version}, port ${c.port})`)} ${
444
- c.status === 'running'
445
- ? chalk.green('● running')
446
- : chalk.gray(' stopped')
447
- }`,
448
- value: c.name,
449
- short: c.name,
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'))
@@ -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('0.1.0', '-v, --version', 'output the version number')
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) {