spindb 0.5.2 → 0.5.3

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.
Files changed (36) hide show
  1. package/README.md +137 -8
  2. package/cli/commands/connect.ts +8 -4
  3. package/cli/commands/create.ts +106 -67
  4. package/cli/commands/deps.ts +19 -4
  5. package/cli/commands/edit.ts +245 -0
  6. package/cli/commands/engines.ts +434 -0
  7. package/cli/commands/info.ts +279 -0
  8. package/cli/commands/menu.ts +408 -153
  9. package/cli/commands/restore.ts +10 -24
  10. package/cli/commands/start.ts +25 -20
  11. package/cli/commands/url.ts +79 -0
  12. package/cli/index.ts +9 -3
  13. package/cli/ui/prompts.ts +8 -6
  14. package/config/engine-defaults.ts +24 -1
  15. package/config/os-dependencies.ts +59 -113
  16. package/config/paths.ts +7 -36
  17. package/core/binary-manager.ts +19 -6
  18. package/core/config-manager.ts +17 -5
  19. package/core/dependency-manager.ts +9 -15
  20. package/core/error-handler.ts +336 -0
  21. package/core/platform-service.ts +634 -0
  22. package/core/port-manager.ts +11 -3
  23. package/core/process-manager.ts +12 -2
  24. package/core/start-with-retry.ts +167 -0
  25. package/core/transaction-manager.ts +170 -0
  26. package/engines/mysql/binary-detection.ts +177 -100
  27. package/engines/mysql/index.ts +240 -131
  28. package/engines/mysql/restore.ts +257 -0
  29. package/engines/mysql/version-validator.ts +373 -0
  30. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  31. package/engines/postgresql/binary-urls.ts +5 -3
  32. package/engines/postgresql/index.ts +4 -3
  33. package/engines/postgresql/restore.ts +54 -5
  34. package/engines/postgresql/version-validator.ts +262 -0
  35. package/package.json +6 -2
  36. package/cli/commands/postgres-tools.ts +0 -216
@@ -22,15 +22,24 @@ import {
22
22
  } from '../ui/theme'
23
23
  import { existsSync } from 'fs'
24
24
  import { readdir, rm, lstat } from 'fs/promises'
25
- import { spawn } from 'child_process'
26
- import { platform, tmpdir } from 'os'
25
+ import { spawn, exec } from 'child_process'
26
+ import { promisify } from 'util'
27
+ import { tmpdir } from 'os'
27
28
  import { join } from 'path'
28
29
  import { paths } from '../../config/paths'
30
+ import { platformService } from '../../core/platform-service'
29
31
  import { portManager } from '../../core/port-manager'
30
32
  import { defaults } from '../../config/defaults'
33
+ import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
31
34
  import type { EngineName } from '../../types'
32
35
  import inquirer from 'inquirer'
33
36
  import { getMissingDependencies } from '../../core/dependency-manager'
37
+ import {
38
+ getMysqldPath,
39
+ getMysqlVersion,
40
+ isMariaDB,
41
+ getMysqlInstallInfo,
42
+ } from '../../engines/mysql/binary-detection'
34
43
 
35
44
  type MenuChoice =
36
45
  | {
@@ -95,7 +104,7 @@ async function showMainMenu(): Promise<void> {
95
104
  },
96
105
  {
97
106
  name: canStop
98
- ? `${chalk.yellow('■')} Stop a container`
107
+ ? `${chalk.red('■')} Stop a container`
99
108
  : chalk.gray('■ Stop a container'),
100
109
  value: 'stop',
101
110
  disabled: canStop ? false : 'No running containers',
@@ -202,7 +211,9 @@ async function handleCreate(): Promise<void> {
202
211
  missingDeps = await getMissingDependencies(engine)
203
212
  if (missingDeps.length > 0) {
204
213
  console.log(
205
- error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
214
+ error(
215
+ `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
216
+ ),
206
217
  )
207
218
  return
208
219
  }
@@ -301,25 +312,14 @@ async function handleCreate(): Promise<void> {
301
312
  console.log(chalk.gray(' Connection string:'))
302
313
  console.log(chalk.cyan(` ${connectionString}`))
303
314
 
304
- // Copy connection string to clipboard using platform-specific command
315
+ // Copy connection string to clipboard using platform service
305
316
  try {
306
- const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
307
- const args = platform() === 'darwin' ? [] : ['-selection', 'clipboard']
308
-
309
- await new Promise<void>((resolve, reject) => {
310
- const proc = spawn(cmd, args, {
311
- stdio: ['pipe', 'inherit', 'inherit'],
312
- })
313
- proc.stdin?.write(connectionString)
314
- proc.stdin?.end()
315
- proc.on('close', (code) => {
316
- if (code === 0) resolve()
317
- else reject(new Error(`Clipboard command exited with code ${code}`))
318
- })
319
- proc.on('error', reject)
320
- })
321
-
322
- console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
317
+ const copied = await platformService.copyToClipboard(connectionString)
318
+ if (copied) {
319
+ console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
320
+ } else {
321
+ console.log(chalk.gray(' (Could not copy to clipboard)'))
322
+ }
323
323
  } catch {
324
324
  console.log(chalk.gray(' (Could not copy to clipboard)'))
325
325
  }
@@ -648,50 +648,26 @@ async function handleCopyConnectionString(
648
648
  const engine = getEngine(config.engine)
649
649
  const connectionString = engine.getConnectionString(config)
650
650
 
651
- // Copy to clipboard using platform-specific command
652
- const { platform } = await import('os')
653
- const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
654
- const args = platform() === 'darwin' ? [] : ['-selection', 'clipboard']
651
+ // Copy to clipboard using platform service
652
+ const copied = await platformService.copyToClipboard(connectionString)
655
653
 
656
- try {
657
- await new Promise<void>((resolve, reject) => {
658
- const proc = spawn(cmd, args, { stdio: ['pipe', 'inherit', 'inherit'] })
659
- proc.stdin?.write(connectionString)
660
- proc.stdin?.end()
661
- proc.on('close', (code) => {
662
- if (code === 0) resolve()
663
- else reject(new Error(`Clipboard command exited with code ${code}`))
664
- })
665
- proc.on('error', reject)
666
- })
667
-
668
- console.log()
654
+ console.log()
655
+ if (copied) {
669
656
  console.log(success('Connection string copied to clipboard'))
670
657
  console.log(chalk.gray(` ${connectionString}`))
671
- console.log()
672
-
673
- await inquirer.prompt([
674
- {
675
- type: 'input',
676
- name: 'continue',
677
- message: chalk.gray('Press Enter to continue...'),
678
- },
679
- ])
680
- } catch {
681
- // Fallback: just display the string
682
- console.log()
658
+ } else {
683
659
  console.log(warning('Could not copy to clipboard. Connection string:'))
684
660
  console.log(chalk.cyan(` ${connectionString}`))
685
- console.log()
686
-
687
- await inquirer.prompt([
688
- {
689
- type: 'input',
690
- name: 'continue',
691
- message: chalk.gray('Press Enter to continue...'),
692
- },
693
- ])
694
661
  }
662
+ console.log()
663
+
664
+ await inquirer.prompt([
665
+ {
666
+ type: 'input',
667
+ name: 'continue',
668
+ message: chalk.gray('Press Enter to continue...'),
669
+ },
670
+ ])
695
671
  }
696
672
 
697
673
  async function handleOpenShell(containerName: string): Promise<void> {
@@ -931,7 +907,9 @@ async function handleRestore(): Promise<void> {
931
907
  missingDeps = await getMissingDependencies(config.engine)
932
908
  if (missingDeps.length > 0) {
933
909
  console.log(
934
- error(`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`),
910
+ error(
911
+ `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
912
+ ),
935
913
  )
936
914
  return
937
915
  }
@@ -1168,7 +1146,7 @@ async function handleRestore(): Promise<void> {
1168
1146
 
1169
1147
  try {
1170
1148
  const { updatePostgresClientTools } = await import(
1171
- '../../core/postgres-binary-manager'
1149
+ '../../engines/postgresql/binary-manager'
1172
1150
  )
1173
1151
  const updateSuccess = await updatePostgresClientTools()
1174
1152
 
@@ -1189,24 +1167,26 @@ async function handleRestore(): Promise<void> {
1189
1167
  console.log(
1190
1168
  error('Automatic upgrade failed. Please upgrade manually:'),
1191
1169
  )
1170
+ const pgPackage = getPostgresHomebrewPackage()
1171
+ const latestMajor = pgPackage.split('@')[1]
1192
1172
  console.log(
1193
1173
  warning(
1194
- ' macOS: brew install postgresql@17 && brew link --force postgresql@17',
1174
+ ` macOS: brew install ${pgPackage} && brew link --force ${pgPackage}`,
1195
1175
  ),
1196
1176
  )
1197
1177
  console.log(
1198
1178
  chalk.gray(
1199
- ' This installs PostgreSQL 17 client tools: pg_restore, pg_dump, psql, and libpq',
1179
+ ` This installs PostgreSQL ${latestMajor} client tools: pg_restore, pg_dump, psql, and libpq`,
1200
1180
  ),
1201
1181
  )
1202
1182
  console.log(
1203
1183
  warning(
1204
- ' Ubuntu/Debian: sudo apt update && sudo apt install postgresql-client-17',
1184
+ ` Ubuntu/Debian: sudo apt update && sudo apt install postgresql-client-${latestMajor}`,
1205
1185
  ),
1206
1186
  )
1207
1187
  console.log(
1208
1188
  chalk.gray(
1209
- ' This installs PostgreSQL 17 client tools: pg_restore, pg_dump, psql, and libpq',
1189
+ ` This installs PostgreSQL ${latestMajor} client tools: pg_restore, pg_dump, psql, and libpq`,
1210
1190
  ),
1211
1191
  )
1212
1192
  await new Promise((resolve) => {
@@ -1270,24 +1250,11 @@ async function handleRestore(): Promise<void> {
1270
1250
  console.log(chalk.gray(' Connection string:'))
1271
1251
  console.log(chalk.cyan(` ${connectionString}`))
1272
1252
 
1273
- // Copy connection string to clipboard using platform-specific command
1274
- try {
1275
- const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
1276
- const args = platform() === 'darwin' ? [] : ['-selection', 'clipboard']
1277
-
1278
- await new Promise<void>((resolve, reject) => {
1279
- const proc = spawn(cmd, args, { stdio: ['pipe', 'inherit', 'inherit'] })
1280
- proc.stdin?.write(connectionString)
1281
- proc.stdin?.end()
1282
- proc.on('close', (code) => {
1283
- if (code === 0) resolve()
1284
- else reject(new Error(`Clipboard command exited with code ${code}`))
1285
- })
1286
- proc.on('error', reject)
1287
- })
1288
-
1253
+ // Copy connection string to clipboard using platform service
1254
+ const copied = await platformService.copyToClipboard(connectionString)
1255
+ if (copied) {
1289
1256
  console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
1290
- } catch {
1257
+ } else {
1291
1258
  console.log(chalk.gray(' (Could not copy to clipboard)'))
1292
1259
  }
1293
1260
 
@@ -1389,7 +1356,9 @@ async function handleStartContainer(containerName: string): Promise<void> {
1389
1356
  ),
1390
1357
  )
1391
1358
  console.log(
1392
- info('Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb'),
1359
+ info(
1360
+ 'Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb',
1361
+ ),
1393
1362
  )
1394
1363
  return
1395
1364
  }
@@ -1643,72 +1612,135 @@ async function handleDelete(containerName: string): Promise<void> {
1643
1612
  deleteSpinner.succeed(`Container "${containerName}" deleted`)
1644
1613
  }
1645
1614
 
1646
- type InstalledEngine = {
1647
- engine: string
1615
+ type InstalledPostgresEngine = {
1616
+ engine: 'postgresql'
1648
1617
  version: string
1649
1618
  platform: string
1650
1619
  arch: string
1651
1620
  path: string
1652
1621
  sizeBytes: number
1622
+ source: 'downloaded'
1653
1623
  }
1654
1624
 
1655
- async function getInstalledEngines(): Promise<InstalledEngine[]> {
1656
- const binDir = paths.bin
1625
+ type InstalledMysqlEngine = {
1626
+ engine: 'mysql'
1627
+ version: string
1628
+ path: string
1629
+ source: 'system'
1630
+ isMariaDB: boolean
1631
+ }
1657
1632
 
1658
- if (!existsSync(binDir)) {
1659
- return []
1633
+ type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
1634
+
1635
+ const execAsync = promisify(exec)
1636
+
1637
+ /**
1638
+ * Get the actual PostgreSQL version from the binary
1639
+ */
1640
+ async function getPostgresVersionFromBinary(
1641
+ binPath: string,
1642
+ ): Promise<string | null> {
1643
+ const postgresPath = join(binPath, 'bin', 'postgres')
1644
+ if (!existsSync(postgresPath)) {
1645
+ return null
1660
1646
  }
1661
1647
 
1662
- const entries = await readdir(binDir, { withFileTypes: true })
1663
- const engines: InstalledEngine[] = []
1648
+ try {
1649
+ const { stdout } = await execAsync(`"${postgresPath}" --version`)
1650
+ // Output: postgres (PostgreSQL) 17.7
1651
+ const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
1652
+ return match ? match[1] : null
1653
+ } catch {
1654
+ return null
1655
+ }
1656
+ }
1664
1657
 
1665
- for (const entry of entries) {
1666
- if (entry.isDirectory()) {
1667
- // Parse directory name: postgresql-17-darwin-arm64
1668
- const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
1669
- if (match) {
1670
- const [, engine, version, platform, arch] = match
1671
- const dirPath = join(binDir, entry.name)
1658
+ async function getInstalledEngines(): Promise<InstalledEngine[]> {
1659
+ const engines: InstalledEngine[] = []
1672
1660
 
1673
- // Get directory size (using lstat to avoid following symlinks)
1674
- let sizeBytes = 0
1675
- try {
1676
- const files = await readdir(dirPath, { recursive: true })
1677
- for (const file of files) {
1678
- try {
1679
- const filePath = join(dirPath, file.toString())
1680
- const fileStat = await lstat(filePath)
1681
- // Only count regular files (not symlinks or directories)
1682
- if (fileStat.isFile()) {
1683
- sizeBytes += fileStat.size
1661
+ // Get PostgreSQL engines from ~/.spindb/bin/
1662
+ const binDir = paths.bin
1663
+ if (existsSync(binDir)) {
1664
+ const entries = await readdir(binDir, { withFileTypes: true })
1665
+
1666
+ for (const entry of entries) {
1667
+ if (entry.isDirectory()) {
1668
+ // Parse directory name: postgresql-17-darwin-arm64
1669
+ const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
1670
+ if (match && match[1] === 'postgresql') {
1671
+ const [, , majorVersion, platform, arch] = match
1672
+ const dirPath = join(binDir, entry.name)
1673
+
1674
+ // Get actual version from the binary
1675
+ const actualVersion =
1676
+ (await getPostgresVersionFromBinary(dirPath)) || majorVersion
1677
+
1678
+ // Get directory size (using lstat to avoid following symlinks)
1679
+ let sizeBytes = 0
1680
+ try {
1681
+ const files = await readdir(dirPath, { recursive: true })
1682
+ for (const file of files) {
1683
+ try {
1684
+ const filePath = join(dirPath, file.toString())
1685
+ const fileStat = await lstat(filePath)
1686
+ // Only count regular files (not symlinks or directories)
1687
+ if (fileStat.isFile()) {
1688
+ sizeBytes += fileStat.size
1689
+ }
1690
+ } catch {
1691
+ // Skip files we can't stat
1684
1692
  }
1685
- } catch {
1686
- // Skip files we can't stat
1687
1693
  }
1694
+ } catch {
1695
+ // Skip directories we can't read
1688
1696
  }
1689
- } catch {
1690
- // Skip directories we can't read
1691
- }
1692
1697
 
1693
- engines.push({
1694
- engine,
1695
- version,
1696
- platform,
1697
- arch,
1698
- path: dirPath,
1699
- sizeBytes,
1700
- })
1698
+ engines.push({
1699
+ engine: 'postgresql',
1700
+ version: actualVersion,
1701
+ platform,
1702
+ arch,
1703
+ path: dirPath,
1704
+ sizeBytes,
1705
+ source: 'downloaded',
1706
+ })
1707
+ }
1701
1708
  }
1702
1709
  }
1703
1710
  }
1704
1711
 
1705
- // Sort by engine name, then by version (descending)
1706
- engines.sort((a, b) => {
1707
- if (a.engine !== b.engine) return a.engine.localeCompare(b.engine)
1708
- return compareVersions(b.version, a.version)
1709
- })
1712
+ // Detect system-installed MySQL
1713
+ const mysqldPath = await getMysqldPath()
1714
+ if (mysqldPath) {
1715
+ const version = await getMysqlVersion(mysqldPath)
1716
+ if (version) {
1717
+ const mariadb = await isMariaDB()
1718
+ engines.push({
1719
+ engine: 'mysql',
1720
+ version,
1721
+ path: mysqldPath,
1722
+ source: 'system',
1723
+ isMariaDB: mariadb,
1724
+ })
1725
+ }
1726
+ }
1727
+
1728
+ // Sort PostgreSQL by version (descending), MySQL stays at end
1729
+ const pgEngines = engines.filter(
1730
+ (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
1731
+ )
1732
+ const mysqlEngine = engines.find(
1733
+ (e): e is InstalledMysqlEngine => e.engine === 'mysql',
1734
+ )
1735
+
1736
+ pgEngines.sort((a, b) => compareVersions(b.version, a.version))
1737
+
1738
+ const result: InstalledEngine[] = [...pgEngines]
1739
+ if (mysqlEngine) {
1740
+ result.push(mysqlEngine)
1741
+ }
1710
1742
 
1711
- return engines
1743
+ return result
1712
1744
  }
1713
1745
 
1714
1746
  function compareVersions(a: string, b: string): number {
@@ -1742,54 +1774,104 @@ async function handleEngines(): Promise<void> {
1742
1774
  console.log(info('No engines installed yet.'))
1743
1775
  console.log(
1744
1776
  chalk.gray(
1745
- ' Engines are downloaded automatically when you create a container.',
1777
+ ' PostgreSQL engines are downloaded automatically when you create a container.',
1778
+ ),
1779
+ )
1780
+ console.log(
1781
+ chalk.gray(
1782
+ ' MySQL requires system installation (brew install mysql or apt install mysql-server).',
1746
1783
  ),
1747
1784
  )
1748
1785
  return
1749
1786
  }
1750
1787
 
1751
- // Calculate total size
1752
- const totalSize = engines.reduce((acc, e) => acc + e.sizeBytes, 0)
1788
+ // Separate PostgreSQL and MySQL
1789
+ const pgEngines = engines.filter(
1790
+ (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
1791
+ )
1792
+ const mysqlEngine = engines.find(
1793
+ (e): e is InstalledMysqlEngine => e.engine === 'mysql',
1794
+ )
1795
+
1796
+ // Calculate total size for PostgreSQL
1797
+ const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
1753
1798
 
1754
1799
  // Table header
1755
1800
  console.log()
1756
1801
  console.log(
1757
1802
  chalk.gray(' ') +
1758
- chalk.bold.white('ENGINE'.padEnd(12)) +
1803
+ chalk.bold.white('ENGINE'.padEnd(14)) +
1759
1804
  chalk.bold.white('VERSION'.padEnd(12)) +
1760
- chalk.bold.white('PLATFORM'.padEnd(20)) +
1805
+ chalk.bold.white('SOURCE'.padEnd(18)) +
1761
1806
  chalk.bold.white('SIZE'),
1762
1807
  )
1763
1808
  console.log(chalk.gray(' ' + '─'.repeat(55)))
1764
1809
 
1765
- // Table rows
1766
- for (const engine of engines) {
1810
+ // PostgreSQL rows
1811
+ for (const engine of pgEngines) {
1812
+ const icon = engineIcons[engine.engine] || '🗄️'
1813
+ const platformInfo = `${engine.platform}-${engine.arch}`
1814
+
1767
1815
  console.log(
1768
1816
  chalk.gray(' ') +
1769
- chalk.cyan(engine.engine.padEnd(12)) +
1817
+ chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
1770
1818
  chalk.yellow(engine.version.padEnd(12)) +
1771
- chalk.gray(`${engine.platform}-${engine.arch}`.padEnd(20)) +
1819
+ chalk.gray(platformInfo.padEnd(18)) +
1772
1820
  chalk.white(formatBytes(engine.sizeBytes)),
1773
1821
  )
1774
1822
  }
1775
1823
 
1824
+ // MySQL row
1825
+ if (mysqlEngine) {
1826
+ const icon = engineIcons.mysql
1827
+ const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
1828
+
1829
+ console.log(
1830
+ chalk.gray(' ') +
1831
+ chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
1832
+ chalk.yellow(mysqlEngine.version.padEnd(12)) +
1833
+ chalk.gray('system'.padEnd(18)) +
1834
+ chalk.gray('(system-installed)'),
1835
+ )
1836
+ }
1837
+
1776
1838
  console.log(chalk.gray(' ' + '─'.repeat(55)))
1777
- console.log(
1778
- chalk.gray(' ') +
1779
- chalk.bold.white(`${engines.length} version(s)`.padEnd(44)) +
1780
- chalk.bold.white(formatBytes(totalSize)),
1781
- )
1839
+
1840
+ // Summary
1841
+ console.log()
1842
+ if (pgEngines.length > 0) {
1843
+ console.log(
1844
+ chalk.gray(
1845
+ ` PostgreSQL: ${pgEngines.length} version(s), ${formatBytes(totalPgSize)}`,
1846
+ ),
1847
+ )
1848
+ }
1849
+ if (mysqlEngine) {
1850
+ console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
1851
+ }
1782
1852
  console.log()
1783
1853
 
1784
- // Menu options
1785
- const choices: MenuChoice[] = [
1786
- ...engines.map((e) => ({
1854
+ // Menu options - only allow deletion of PostgreSQL engines
1855
+ const choices: MenuChoice[] = []
1856
+
1857
+ for (const e of pgEngines) {
1858
+ choices.push({
1787
1859
  name: `${chalk.red('✕')} Delete ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
1788
1860
  value: `delete:${e.path}:${e.engine}:${e.version}`,
1789
- })),
1790
- new inquirer.Separator(),
1791
- { name: `${chalk.blue('←')} Back to main menu`, value: 'back' },
1792
- ]
1861
+ })
1862
+ }
1863
+
1864
+ // MySQL info option (not disabled, shows info icon)
1865
+ if (mysqlEngine) {
1866
+ const displayName = mysqlEngine.isMariaDB ? 'MariaDB' : 'MySQL'
1867
+ choices.push({
1868
+ name: `${chalk.blue('ℹ')} ${displayName} ${mysqlEngine.version} ${chalk.gray('(system-installed)')}`,
1869
+ value: `mysql-info:${mysqlEngine.path}`,
1870
+ })
1871
+ }
1872
+
1873
+ choices.push(new inquirer.Separator())
1874
+ choices.push({ name: `${chalk.blue('←')} Back to main menu`, value: 'back' })
1793
1875
 
1794
1876
  const { action } = await inquirer.prompt<{ action: string }>([
1795
1877
  {
@@ -1811,6 +1893,13 @@ async function handleEngines(): Promise<void> {
1811
1893
  // Return to engines menu
1812
1894
  await handleEngines()
1813
1895
  }
1896
+
1897
+ if (action.startsWith('mysql-info:')) {
1898
+ const mysqldPath = action.replace('mysql-info:', '')
1899
+ await handleMysqlInfo(mysqldPath)
1900
+ // Return to engines menu
1901
+ await handleEngines()
1902
+ }
1814
1903
  }
1815
1904
 
1816
1905
  async function handleDeleteEngine(
@@ -1869,6 +1958,174 @@ async function handleDeleteEngine(
1869
1958
  }
1870
1959
  }
1871
1960
 
1961
+ async function handleMysqlInfo(mysqldPath: string): Promise<void> {
1962
+ console.clear()
1963
+
1964
+ // Get install info
1965
+ const installInfo = await getMysqlInstallInfo(mysqldPath)
1966
+ const displayName = installInfo.isMariaDB ? 'MariaDB' : 'MySQL'
1967
+
1968
+ // Get version
1969
+ const version = await getMysqlVersion(mysqldPath)
1970
+
1971
+ console.log(header(`${displayName} Information`))
1972
+ console.log()
1973
+
1974
+ // Check for containers using MySQL
1975
+ const containers = await containerManager.list()
1976
+ const mysqlContainers = containers.filter((c) => c.engine === 'mysql')
1977
+
1978
+ // Track running containers for uninstall instructions
1979
+ const runningContainers: string[] = []
1980
+
1981
+ if (mysqlContainers.length > 0) {
1982
+ console.log(
1983
+ warning(
1984
+ `${mysqlContainers.length} container(s) are using ${displayName}:`,
1985
+ ),
1986
+ )
1987
+ console.log()
1988
+ for (const c of mysqlContainers) {
1989
+ const isRunning = await processManager.isRunning(c.name, {
1990
+ engine: c.engine,
1991
+ })
1992
+ if (isRunning) {
1993
+ runningContainers.push(c.name)
1994
+ }
1995
+ const status = isRunning
1996
+ ? chalk.green('● running')
1997
+ : chalk.gray('○ stopped')
1998
+ console.log(chalk.gray(` • ${c.name} ${status}`))
1999
+ }
2000
+ console.log()
2001
+ console.log(
2002
+ chalk.yellow(
2003
+ ' Uninstalling will break these containers. Delete them first.',
2004
+ ),
2005
+ )
2006
+ console.log()
2007
+ }
2008
+
2009
+ // Show installation details
2010
+ console.log(chalk.white(' Installation Details:'))
2011
+ console.log(chalk.gray(' ' + '─'.repeat(50)))
2012
+ console.log(
2013
+ chalk.gray(' ') +
2014
+ chalk.white('Version:'.padEnd(18)) +
2015
+ chalk.yellow(version || 'unknown'),
2016
+ )
2017
+ console.log(
2018
+ chalk.gray(' ') +
2019
+ chalk.white('Binary Path:'.padEnd(18)) +
2020
+ chalk.gray(mysqldPath),
2021
+ )
2022
+ console.log(
2023
+ chalk.gray(' ') +
2024
+ chalk.white('Package Manager:'.padEnd(18)) +
2025
+ chalk.cyan(installInfo.packageManager),
2026
+ )
2027
+ console.log(
2028
+ chalk.gray(' ') +
2029
+ chalk.white('Package Name:'.padEnd(18)) +
2030
+ chalk.cyan(installInfo.packageName),
2031
+ )
2032
+ console.log()
2033
+
2034
+ // Uninstall instructions
2035
+ console.log(chalk.white(' To uninstall:'))
2036
+ console.log(chalk.gray(' ' + '─'.repeat(50)))
2037
+
2038
+ let stepNum = 1
2039
+
2040
+ // Step: Stop running containers first
2041
+ if (runningContainers.length > 0) {
2042
+ console.log(chalk.gray(` # ${stepNum}. Stop running SpinDB containers`))
2043
+ console.log(chalk.cyan(' spindb stop <container-name>'))
2044
+ console.log()
2045
+ stepNum++
2046
+ }
2047
+
2048
+ // Step: Delete SpinDB containers
2049
+ if (mysqlContainers.length > 0) {
2050
+ console.log(chalk.gray(` # ${stepNum}. Delete SpinDB containers`))
2051
+ console.log(chalk.cyan(' spindb delete <container-name>'))
2052
+ console.log()
2053
+ stepNum++
2054
+ }
2055
+
2056
+ if (installInfo.packageManager === 'homebrew') {
2057
+ console.log(
2058
+ chalk.gray(
2059
+ ` # ${stepNum}. Stop Homebrew service (if running separately)`,
2060
+ ),
2061
+ )
2062
+ console.log(chalk.cyan(` brew services stop ${installInfo.packageName}`))
2063
+ console.log()
2064
+ console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
2065
+ console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
2066
+ } else if (installInfo.packageManager === 'apt') {
2067
+ console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
2068
+ console.log(
2069
+ chalk.cyan(
2070
+ ` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
2071
+ ),
2072
+ )
2073
+ console.log()
2074
+ console.log(chalk.gray(` # ${stepNum + 1}. Disable auto-start on boot`))
2075
+ console.log(
2076
+ chalk.cyan(
2077
+ ` sudo systemctl disable ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
2078
+ ),
2079
+ )
2080
+ console.log()
2081
+ console.log(chalk.gray(` # ${stepNum + 2}. Uninstall the package`))
2082
+ console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
2083
+ console.log()
2084
+ console.log(chalk.gray(` # ${stepNum + 3}. Remove data files (optional)`))
2085
+ console.log(
2086
+ chalk.cyan(' sudo apt purge mysql-server mysql-client mysql-common'),
2087
+ )
2088
+ console.log(chalk.cyan(' sudo rm -rf /var/lib/mysql /etc/mysql'))
2089
+ } else if (
2090
+ installInfo.packageManager === 'yum' ||
2091
+ installInfo.packageManager === 'dnf'
2092
+ ) {
2093
+ console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
2094
+ console.log(
2095
+ chalk.cyan(
2096
+ ` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
2097
+ ),
2098
+ )
2099
+ console.log()
2100
+ console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
2101
+ console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
2102
+ } else if (installInfo.packageManager === 'pacman') {
2103
+ console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
2104
+ console.log(
2105
+ chalk.cyan(
2106
+ ` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
2107
+ ),
2108
+ )
2109
+ console.log()
2110
+ console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
2111
+ console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
2112
+ } else {
2113
+ console.log(chalk.gray(' Use your system package manager to uninstall.'))
2114
+ console.log(chalk.gray(` The binary is located at: ${mysqldPath}`))
2115
+ }
2116
+
2117
+ console.log()
2118
+
2119
+ // Wait for user
2120
+ await inquirer.prompt([
2121
+ {
2122
+ type: 'input',
2123
+ name: 'continue',
2124
+ message: chalk.gray('Press Enter to go back...'),
2125
+ },
2126
+ ])
2127
+ }
2128
+
1872
2129
  export const menuCommand = new Command('menu')
1873
2130
  .description('Interactive menu for managing containers')
1874
2131
  .action(async () => {
@@ -1890,9 +2147,7 @@ export const menuCommand = new Command('menu')
1890
2147
  : 'psql'
1891
2148
  const installed = await promptInstallDependencies(missingTool)
1892
2149
  if (installed) {
1893
- console.log(
1894
- chalk.yellow(' Please re-run spindb to continue.'),
1895
- )
2150
+ console.log(chalk.yellow(' Please re-run spindb to continue.'))
1896
2151
  }
1897
2152
  process.exit(1)
1898
2153
  }