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.
- package/README.md +137 -8
- package/cli/commands/connect.ts +8 -4
- package/cli/commands/create.ts +106 -67
- package/cli/commands/deps.ts +19 -4
- package/cli/commands/edit.ts +245 -0
- package/cli/commands/engines.ts +434 -0
- package/cli/commands/info.ts +279 -0
- package/cli/commands/menu.ts +408 -153
- package/cli/commands/restore.ts +10 -24
- package/cli/commands/start.ts +25 -20
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +8 -6
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +59 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +19 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +9 -15
- package/core/error-handler.ts +336 -0
- package/core/platform-service.ts +634 -0
- package/core/port-manager.ts +11 -3
- package/core/process-manager.ts +12 -2
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/mysql/binary-detection.ts +177 -100
- package/engines/mysql/index.ts +240 -131
- package/engines/mysql/restore.ts +257 -0
- package/engines/mysql/version-validator.ts +373 -0
- package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
- package/engines/postgresql/binary-urls.ts +5 -3
- package/engines/postgresql/index.ts +4 -3
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +6 -2
- package/cli/commands/postgres-tools.ts +0 -216
package/cli/commands/menu.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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(
|
|
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
|
|
315
|
+
// Copy connection string to clipboard using platform service
|
|
305
316
|
try {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
652
|
-
const
|
|
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
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
'../../
|
|
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
|
-
|
|
1174
|
+
` macOS: brew install ${pgPackage} && brew link --force ${pgPackage}`,
|
|
1195
1175
|
),
|
|
1196
1176
|
)
|
|
1197
1177
|
console.log(
|
|
1198
1178
|
chalk.gray(
|
|
1199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
-
}
|
|
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(
|
|
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
|
|
1647
|
-
engine:
|
|
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
|
-
|
|
1656
|
-
|
|
1625
|
+
type InstalledMysqlEngine = {
|
|
1626
|
+
engine: 'mysql'
|
|
1627
|
+
version: string
|
|
1628
|
+
path: string
|
|
1629
|
+
source: 'system'
|
|
1630
|
+
isMariaDB: boolean
|
|
1631
|
+
}
|
|
1657
1632
|
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
//
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
|
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
|
-
'
|
|
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
|
-
//
|
|
1752
|
-
const
|
|
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(
|
|
1803
|
+
chalk.bold.white('ENGINE'.padEnd(14)) +
|
|
1759
1804
|
chalk.bold.white('VERSION'.padEnd(12)) +
|
|
1760
|
-
chalk.bold.white('
|
|
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
|
-
//
|
|
1766
|
-
for (const engine of
|
|
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
|
|
1817
|
+
chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
|
|
1770
1818
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
1771
|
-
chalk.gray(
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
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
|
}
|