spindb 0.5.2 → 0.5.4
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 +188 -9
- package/cli/commands/connect.ts +334 -105
- 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/list.ts +1 -1
- package/cli/commands/menu.ts +664 -167
- package/cli/commands/restore.ts +11 -25
- 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 +20 -12
- package/cli/ui/theme.ts +1 -1
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +151 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +12 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +144 -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 +35 -4
- 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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start with Retry
|
|
3
|
+
*
|
|
4
|
+
* Handles port race conditions by automatically retrying with a new port
|
|
5
|
+
* when the original port becomes unavailable between check and bind.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { portManager } from './port-manager'
|
|
9
|
+
import { containerManager } from './container-manager'
|
|
10
|
+
import { logWarning, logDebug } from './error-handler'
|
|
11
|
+
import type { BaseEngine } from '../engines/base-engine'
|
|
12
|
+
import type { ContainerConfig } from '../types'
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export type StartWithRetryOptions = {
|
|
19
|
+
engine: BaseEngine
|
|
20
|
+
config: ContainerConfig
|
|
21
|
+
maxRetries?: number // Default: 3
|
|
22
|
+
onPortChange?: (oldPort: number, newPort: number) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type StartWithRetryResult = {
|
|
26
|
+
success: boolean
|
|
27
|
+
finalPort: number
|
|
28
|
+
retriesUsed: number
|
|
29
|
+
error?: Error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Port Error Detection
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if an error is a port-in-use error
|
|
38
|
+
*/
|
|
39
|
+
function isPortInUseError(err: unknown): boolean {
|
|
40
|
+
const message = (err as Error)?.message?.toLowerCase() || ''
|
|
41
|
+
return (
|
|
42
|
+
message.includes('address already in use') ||
|
|
43
|
+
message.includes('eaddrinuse') ||
|
|
44
|
+
(message.includes('port') && message.includes('in use')) ||
|
|
45
|
+
message.includes('could not bind') ||
|
|
46
|
+
message.includes('socket already in use')
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Start with Retry Implementation
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start a database container with automatic port retry on conflict
|
|
56
|
+
*
|
|
57
|
+
* This handles the race condition where a port is available when checked
|
|
58
|
+
* but taken by the time the database server tries to bind to it.
|
|
59
|
+
*/
|
|
60
|
+
export async function startWithRetry(
|
|
61
|
+
options: StartWithRetryOptions,
|
|
62
|
+
): Promise<StartWithRetryResult> {
|
|
63
|
+
const { engine, config, maxRetries = 3, onPortChange } = options
|
|
64
|
+
|
|
65
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
logDebug(`Starting ${engine.name} (attempt ${attempt}/${maxRetries})`, {
|
|
68
|
+
containerName: config.name,
|
|
69
|
+
port: config.port,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await engine.start(config)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
finalPort: config.port,
|
|
77
|
+
retriesUsed: attempt - 1,
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const isPortError = isPortInUseError(err)
|
|
81
|
+
|
|
82
|
+
logDebug(`Start attempt ${attempt} failed`, {
|
|
83
|
+
containerName: config.name,
|
|
84
|
+
port: config.port,
|
|
85
|
+
isPortError,
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (isPortError && attempt < maxRetries) {
|
|
90
|
+
const oldPort = config.port
|
|
91
|
+
|
|
92
|
+
// Find a new available port, excluding the one that just failed
|
|
93
|
+
const { port: newPort } = await portManager.findAvailablePort({
|
|
94
|
+
portRange: getEnginePortRange(config.engine),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Update config with new port
|
|
98
|
+
config.port = newPort
|
|
99
|
+
await containerManager.updateConfig(config.name, { port: newPort })
|
|
100
|
+
|
|
101
|
+
// Notify caller of port change
|
|
102
|
+
if (onPortChange) {
|
|
103
|
+
onPortChange(oldPort, newPort)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Log and retry
|
|
107
|
+
logWarning(
|
|
108
|
+
`Port ${oldPort} is in use, retrying with port ${newPort}...`,
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Not a port error or max retries exceeded
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
finalPort: config.port,
|
|
117
|
+
retriesUsed: attempt - 1,
|
|
118
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Should never reach here, but TypeScript needs a return
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
finalPort: config.port,
|
|
127
|
+
retriesUsed: maxRetries,
|
|
128
|
+
error: new Error('Max retries exceeded'),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the port range for an engine
|
|
134
|
+
*/
|
|
135
|
+
function getEnginePortRange(engine: string): { start: number; end: number } {
|
|
136
|
+
// Import defaults dynamically to avoid circular dependency
|
|
137
|
+
// These are the standard ranges from config/defaults.ts
|
|
138
|
+
if (engine === 'postgresql') {
|
|
139
|
+
return { start: 5432, end: 5500 }
|
|
140
|
+
}
|
|
141
|
+
if (engine === 'mysql') {
|
|
142
|
+
return { start: 3306, end: 3400 }
|
|
143
|
+
}
|
|
144
|
+
// Default fallback range
|
|
145
|
+
return { start: 5432, end: 6000 }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wrapper that simplifies the common use case
|
|
150
|
+
*/
|
|
151
|
+
export async function startContainerWithRetry(
|
|
152
|
+
engine: BaseEngine,
|
|
153
|
+
config: ContainerConfig,
|
|
154
|
+
options?: {
|
|
155
|
+
onPortChange?: (oldPort: number, newPort: number) => void
|
|
156
|
+
},
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const result = await startWithRetry({
|
|
159
|
+
engine,
|
|
160
|
+
config,
|
|
161
|
+
onPortChange: options?.onPortChange,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (!result.success && result.error) {
|
|
165
|
+
throw result.error
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides rollback support for multi-step operations like container creation.
|
|
5
|
+
* If any step fails, all previously completed steps are rolled back in reverse order.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logError, logDebug, ErrorCodes } from './error-handler'
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export type RollbackAction = {
|
|
15
|
+
description: string
|
|
16
|
+
execute: () => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Transaction Manager Implementation
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages a stack of rollback actions for transactional operations.
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const tx = new TransactionManager()
|
|
29
|
+
*
|
|
30
|
+
* try {
|
|
31
|
+
* await createDirectory()
|
|
32
|
+
* tx.addRollback({
|
|
33
|
+
* description: 'Delete directory',
|
|
34
|
+
* execute: () => deleteDirectory()
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* await initDatabase()
|
|
38
|
+
* // Directory rollback covers this too
|
|
39
|
+
*
|
|
40
|
+
* await startServer()
|
|
41
|
+
* tx.addRollback({
|
|
42
|
+
* description: 'Stop server',
|
|
43
|
+
* execute: () => stopServer()
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* tx.commit() // Success - clear rollback stack
|
|
47
|
+
* } catch (err) {
|
|
48
|
+
* await tx.rollback() // Error - undo everything
|
|
49
|
+
* throw err
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class TransactionManager {
|
|
54
|
+
private rollbackStack: RollbackAction[] = []
|
|
55
|
+
private committed = false
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a rollback action to the stack.
|
|
59
|
+
* Actions are executed in reverse order during rollback.
|
|
60
|
+
*/
|
|
61
|
+
addRollback(action: RollbackAction): void {
|
|
62
|
+
if (this.committed) {
|
|
63
|
+
throw new Error('Cannot add rollback action after commit')
|
|
64
|
+
}
|
|
65
|
+
this.rollbackStack.push(action)
|
|
66
|
+
logDebug(`Added rollback action: ${action.description}`, {
|
|
67
|
+
totalActions: this.rollbackStack.length,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute all rollbacks in reverse order.
|
|
73
|
+
* Continues even if individual rollback actions fail.
|
|
74
|
+
*/
|
|
75
|
+
async rollback(): Promise<void> {
|
|
76
|
+
if (this.committed) {
|
|
77
|
+
logDebug('Skipping rollback - transaction was committed')
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.rollbackStack.length === 0) {
|
|
82
|
+
logDebug('No rollback actions to execute')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logDebug(`Starting rollback of ${this.rollbackStack.length} actions`)
|
|
87
|
+
|
|
88
|
+
// Execute in reverse order (LIFO)
|
|
89
|
+
while (this.rollbackStack.length > 0) {
|
|
90
|
+
const action = this.rollbackStack.pop()!
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
logDebug(`Executing rollback: ${action.description}`)
|
|
94
|
+
await action.execute()
|
|
95
|
+
logDebug(`Rollback successful: ${action.description}`)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Log error but continue with other rollbacks
|
|
98
|
+
logError({
|
|
99
|
+
code: ErrorCodes.ROLLBACK_FAILED,
|
|
100
|
+
message: `Failed to rollback: ${action.description}`,
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
context: {
|
|
103
|
+
error: err instanceof Error ? err.message : String(err),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logDebug('Rollback complete')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mark the transaction as committed.
|
|
114
|
+
* Clears the rollback stack since we don't need to undo anything.
|
|
115
|
+
*/
|
|
116
|
+
commit(): void {
|
|
117
|
+
if (this.committed) {
|
|
118
|
+
return // Already committed
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
logDebug(`Committing transaction with ${this.rollbackStack.length} actions`)
|
|
122
|
+
this.rollbackStack = []
|
|
123
|
+
this.committed = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if the transaction has been committed.
|
|
128
|
+
*/
|
|
129
|
+
isCommitted(): boolean {
|
|
130
|
+
return this.committed
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the number of pending rollback actions.
|
|
135
|
+
*/
|
|
136
|
+
getPendingCount(): number {
|
|
137
|
+
return this.rollbackStack.length
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Helper function to execute an operation with automatic rollback on failure.
|
|
143
|
+
*
|
|
144
|
+
* Usage:
|
|
145
|
+
* ```typescript
|
|
146
|
+
* await withTransaction(async (tx) => {
|
|
147
|
+
* await step1()
|
|
148
|
+
* tx.addRollback({ description: 'Undo step1', execute: undoStep1 })
|
|
149
|
+
*
|
|
150
|
+
* await step2()
|
|
151
|
+
* tx.addRollback({ description: 'Undo step2', execute: undoStep2 })
|
|
152
|
+
*
|
|
153
|
+
* // If we get here without throwing, transaction commits automatically
|
|
154
|
+
* })
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export async function withTransaction<T>(
|
|
158
|
+
operation: (tx: TransactionManager) => Promise<T>,
|
|
159
|
+
): Promise<T> {
|
|
160
|
+
const tx = new TransactionManager()
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const result = await operation(tx)
|
|
164
|
+
tx.commit()
|
|
165
|
+
return result
|
|
166
|
+
} catch (err) {
|
|
167
|
+
await tx.rollback()
|
|
168
|
+
throw err
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -5,89 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { exec } from 'child_process'
|
|
7
7
|
import { promisify } from 'util'
|
|
8
|
-
import {
|
|
9
|
-
import { platform } from 'os'
|
|
8
|
+
import { platformService } from '../../core/platform-service'
|
|
10
9
|
|
|
11
10
|
const execAsync = promisify(exec)
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Find a MySQL binary by name using the platform service
|
|
15
14
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Homebrew (Apple Silicon)
|
|
19
|
-
'/opt/homebrew/bin',
|
|
20
|
-
'/opt/homebrew/opt/mysql/bin',
|
|
21
|
-
'/opt/homebrew/opt/mysql@8.0/bin',
|
|
22
|
-
'/opt/homebrew/opt/mysql@8.4/bin',
|
|
23
|
-
'/opt/homebrew/opt/mysql@5.7/bin',
|
|
24
|
-
// Homebrew (Intel)
|
|
25
|
-
'/usr/local/bin',
|
|
26
|
-
'/usr/local/opt/mysql/bin',
|
|
27
|
-
'/usr/local/opt/mysql@8.0/bin',
|
|
28
|
-
'/usr/local/opt/mysql@8.4/bin',
|
|
29
|
-
'/usr/local/opt/mysql@5.7/bin',
|
|
30
|
-
// Official MySQL installer
|
|
31
|
-
'/usr/local/mysql/bin',
|
|
32
|
-
],
|
|
33
|
-
linux: [
|
|
34
|
-
'/usr/bin',
|
|
35
|
-
'/usr/sbin',
|
|
36
|
-
'/usr/local/bin',
|
|
37
|
-
'/usr/local/mysql/bin',
|
|
38
|
-
],
|
|
39
|
-
win32: [
|
|
40
|
-
'C:\\Program Files\\MySQL\\MySQL Server 8.0\\bin',
|
|
41
|
-
'C:\\Program Files\\MySQL\\MySQL Server 8.4\\bin',
|
|
42
|
-
'C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin',
|
|
43
|
-
],
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get search paths for the current platform
|
|
48
|
-
*/
|
|
49
|
-
function getSearchPaths(): string[] {
|
|
50
|
-
const plat = platform()
|
|
51
|
-
return MYSQL_SEARCH_PATHS[plat as keyof typeof MYSQL_SEARCH_PATHS] || []
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if a binary exists at the given path
|
|
56
|
-
*/
|
|
57
|
-
function binaryExists(path: string): boolean {
|
|
58
|
-
return existsSync(path)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Find a MySQL binary by name
|
|
63
|
-
*/
|
|
64
|
-
export async function findMysqlBinary(
|
|
65
|
-
name: string,
|
|
66
|
-
): Promise<string | null> {
|
|
67
|
-
// First, try using 'which' or 'where' command
|
|
68
|
-
try {
|
|
69
|
-
const cmd = platform() === 'win32' ? 'where' : 'which'
|
|
70
|
-
const { stdout } = await execAsync(`${cmd} ${name}`)
|
|
71
|
-
const path = stdout.trim().split('\n')[0]
|
|
72
|
-
if (path && binaryExists(path)) {
|
|
73
|
-
return path
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
// Not found in PATH, continue to search common locations
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Search common installation paths
|
|
80
|
-
const searchPaths = getSearchPaths()
|
|
81
|
-
for (const dir of searchPaths) {
|
|
82
|
-
const fullPath = platform() === 'win32'
|
|
83
|
-
? `${dir}\\${name}.exe`
|
|
84
|
-
: `${dir}/${name}`
|
|
85
|
-
if (binaryExists(fullPath)) {
|
|
86
|
-
return fullPath
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return null
|
|
15
|
+
export async function findMysqlBinary(name: string): Promise<string | null> {
|
|
16
|
+
return platformService.findToolPath(name)
|
|
91
17
|
}
|
|
92
18
|
|
|
93
19
|
/**
|
|
@@ -180,6 +106,7 @@ export async function detectInstalledVersions(): Promise<
|
|
|
180
106
|
Record<string, string>
|
|
181
107
|
> {
|
|
182
108
|
const versions: Record<string, string> = {}
|
|
109
|
+
const { platform } = platformService.getPlatformInfo()
|
|
183
110
|
|
|
184
111
|
// Check default mysqld
|
|
185
112
|
const defaultMysqld = await getMysqldPath()
|
|
@@ -191,25 +118,26 @@ export async function detectInstalledVersions(): Promise<
|
|
|
191
118
|
}
|
|
192
119
|
}
|
|
193
120
|
|
|
194
|
-
// Check versioned Homebrew installations
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
121
|
+
// Check versioned Homebrew installations (macOS only)
|
|
122
|
+
if (platform === 'darwin') {
|
|
123
|
+
const homebrewPaths = [
|
|
124
|
+
'/opt/homebrew/opt/mysql@5.7/bin/mysqld',
|
|
125
|
+
'/opt/homebrew/opt/mysql@8.0/bin/mysqld',
|
|
126
|
+
'/opt/homebrew/opt/mysql@8.4/bin/mysqld',
|
|
127
|
+
'/usr/local/opt/mysql@5.7/bin/mysqld',
|
|
128
|
+
'/usr/local/opt/mysql@8.0/bin/mysqld',
|
|
129
|
+
'/usr/local/opt/mysql@8.4/bin/mysqld',
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
const { existsSync } = await import('fs')
|
|
133
|
+
for (const path of homebrewPaths) {
|
|
134
|
+
if (existsSync(path)) {
|
|
135
|
+
const version = await getMysqlVersion(path)
|
|
136
|
+
if (version) {
|
|
137
|
+
const major = getMajorVersion(version)
|
|
138
|
+
if (!versions[major]) {
|
|
139
|
+
versions[major] = version
|
|
140
|
+
}
|
|
213
141
|
}
|
|
214
142
|
}
|
|
215
143
|
}
|
|
@@ -222,9 +150,9 @@ export async function detectInstalledVersions(): Promise<
|
|
|
222
150
|
* Get install instructions for MySQL
|
|
223
151
|
*/
|
|
224
152
|
export function getInstallInstructions(): string {
|
|
225
|
-
const
|
|
153
|
+
const { platform } = platformService.getPlatformInfo()
|
|
226
154
|
|
|
227
|
-
if (
|
|
155
|
+
if (platform === 'darwin') {
|
|
228
156
|
return (
|
|
229
157
|
'MySQL server not found. Install MySQL:\n' +
|
|
230
158
|
' brew install mysql\n' +
|
|
@@ -233,7 +161,7 @@ export function getInstallInstructions(): string {
|
|
|
233
161
|
)
|
|
234
162
|
}
|
|
235
163
|
|
|
236
|
-
if (
|
|
164
|
+
if (platform === 'linux') {
|
|
237
165
|
return (
|
|
238
166
|
'MySQL server not found. Install MySQL:\n' +
|
|
239
167
|
' Ubuntu/Debian: sudo apt install mysql-server\n' +
|
|
@@ -246,3 +174,152 @@ export function getInstallInstructions(): string {
|
|
|
246
174
|
' https://dev.mysql.com/downloads/mysql/'
|
|
247
175
|
)
|
|
248
176
|
}
|
|
177
|
+
|
|
178
|
+
export type MysqlPackageManager =
|
|
179
|
+
| 'homebrew'
|
|
180
|
+
| 'apt'
|
|
181
|
+
| 'yum'
|
|
182
|
+
| 'dnf'
|
|
183
|
+
| 'pacman'
|
|
184
|
+
| 'unknown'
|
|
185
|
+
|
|
186
|
+
export type MysqlInstallInfo = {
|
|
187
|
+
packageManager: MysqlPackageManager
|
|
188
|
+
packageName: string
|
|
189
|
+
path: string
|
|
190
|
+
uninstallCommand: string
|
|
191
|
+
isMariaDB: boolean
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect which package manager installed MySQL and get uninstall info
|
|
196
|
+
*/
|
|
197
|
+
export async function getMysqlInstallInfo(
|
|
198
|
+
mysqldPath: string,
|
|
199
|
+
): Promise<MysqlInstallInfo> {
|
|
200
|
+
const { platform } = platformService.getPlatformInfo()
|
|
201
|
+
const mariadb = await isMariaDB()
|
|
202
|
+
|
|
203
|
+
// macOS: Check if path is in Homebrew directories
|
|
204
|
+
if (platform === 'darwin') {
|
|
205
|
+
if (
|
|
206
|
+
mysqldPath.includes('/opt/homebrew/') ||
|
|
207
|
+
mysqldPath.includes('/usr/local/Cellar/')
|
|
208
|
+
) {
|
|
209
|
+
// Extract package name from path
|
|
210
|
+
// e.g., /opt/homebrew/opt/mysql@8.0/bin/mysqld -> mysql@8.0
|
|
211
|
+
// e.g., /opt/homebrew/bin/mysqld -> mysql (linked)
|
|
212
|
+
let packageName = mariadb ? 'mariadb' : 'mysql'
|
|
213
|
+
|
|
214
|
+
const versionMatch = mysqldPath.match(/mysql@(\d+\.\d+)/)
|
|
215
|
+
if (versionMatch) {
|
|
216
|
+
packageName = `mysql@${versionMatch[1]}`
|
|
217
|
+
} else {
|
|
218
|
+
// Try to get from Homebrew directly
|
|
219
|
+
try {
|
|
220
|
+
const { stdout } = await execAsync('brew list --formula')
|
|
221
|
+
const packages = stdout.split('\n')
|
|
222
|
+
const mysqlPackage = packages.find(
|
|
223
|
+
(p) =>
|
|
224
|
+
p.startsWith('mysql') ||
|
|
225
|
+
p.startsWith('mariadb') ||
|
|
226
|
+
p === 'percona-server',
|
|
227
|
+
)
|
|
228
|
+
if (mysqlPackage) {
|
|
229
|
+
packageName = mysqlPackage
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore errors
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
packageManager: 'homebrew',
|
|
238
|
+
packageName,
|
|
239
|
+
path: mysqldPath,
|
|
240
|
+
uninstallCommand: `brew uninstall ${packageName}`,
|
|
241
|
+
isMariaDB: mariadb,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Linux: Detect package manager from path or check installed packages
|
|
247
|
+
if (platform === 'linux') {
|
|
248
|
+
// Check for apt (Debian/Ubuntu)
|
|
249
|
+
try {
|
|
250
|
+
const { stdout } = await execAsync('which apt 2>/dev/null')
|
|
251
|
+
if (stdout.trim()) {
|
|
252
|
+
const packageName = mariadb ? 'mariadb-server' : 'mysql-server'
|
|
253
|
+
return {
|
|
254
|
+
packageManager: 'apt',
|
|
255
|
+
packageName,
|
|
256
|
+
path: mysqldPath,
|
|
257
|
+
uninstallCommand: `sudo apt remove ${packageName}`,
|
|
258
|
+
isMariaDB: mariadb,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Not apt
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check for dnf (Fedora/RHEL 8+)
|
|
266
|
+
try {
|
|
267
|
+
const { stdout } = await execAsync('which dnf 2>/dev/null')
|
|
268
|
+
if (stdout.trim()) {
|
|
269
|
+
const packageName = mariadb ? 'mariadb-server' : 'mysql-server'
|
|
270
|
+
return {
|
|
271
|
+
packageManager: 'dnf',
|
|
272
|
+
packageName,
|
|
273
|
+
path: mysqldPath,
|
|
274
|
+
uninstallCommand: `sudo dnf remove ${packageName}`,
|
|
275
|
+
isMariaDB: mariadb,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Not dnf
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for yum (CentOS/RHEL 7)
|
|
283
|
+
try {
|
|
284
|
+
const { stdout } = await execAsync('which yum 2>/dev/null')
|
|
285
|
+
if (stdout.trim()) {
|
|
286
|
+
const packageName = mariadb ? 'mariadb-server' : 'mysql-server'
|
|
287
|
+
return {
|
|
288
|
+
packageManager: 'yum',
|
|
289
|
+
packageName,
|
|
290
|
+
path: mysqldPath,
|
|
291
|
+
uninstallCommand: `sudo yum remove ${packageName}`,
|
|
292
|
+
isMariaDB: mariadb,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
// Not yum
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check for pacman (Arch Linux)
|
|
300
|
+
try {
|
|
301
|
+
const { stdout } = await execAsync('which pacman 2>/dev/null')
|
|
302
|
+
if (stdout.trim()) {
|
|
303
|
+
const packageName = mariadb ? 'mariadb' : 'mysql'
|
|
304
|
+
return {
|
|
305
|
+
packageManager: 'pacman',
|
|
306
|
+
packageName,
|
|
307
|
+
path: mysqldPath,
|
|
308
|
+
uninstallCommand: `sudo pacman -Rs ${packageName}`,
|
|
309
|
+
isMariaDB: mariadb,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Not pacman
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Unknown package manager
|
|
318
|
+
return {
|
|
319
|
+
packageManager: 'unknown',
|
|
320
|
+
packageName: mariadb ? 'mariadb' : 'mysql',
|
|
321
|
+
path: mysqldPath,
|
|
322
|
+
uninstallCommand: 'Use your system package manager to uninstall',
|
|
323
|
+
isMariaDB: mariadb,
|
|
324
|
+
}
|
|
325
|
+
}
|