spindb 0.4.1 → 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 +207 -101
- package/cli/commands/clone.ts +3 -1
- package/cli/commands/connect.ts +54 -24
- package/cli/commands/create.ts +309 -189
- package/cli/commands/delete.ts +3 -1
- 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 +14 -3
- package/cli/commands/menu.ts +510 -198
- package/cli/commands/restore.ts +66 -43
- package/cli/commands/start.ts +50 -19
- package/cli/commands/stop.ts +3 -1
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +99 -34
- package/config/defaults.ts +40 -15
- package/config/engine-defaults.ts +107 -0
- package/config/os-dependencies.ts +119 -124
- package/config/paths.ts +82 -56
- package/core/binary-manager.ts +44 -6
- package/core/config-manager.ts +17 -5
- package/core/container-manager.ts +124 -60
- 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 +51 -32
- package/core/process-manager.ts +26 -8
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/index.ts +7 -2
- package/engines/mysql/binary-detection.ts +325 -0
- package/engines/mysql/index.ts +808 -0
- 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 +17 -9
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +9 -3
- package/types/index.ts +29 -5
- package/cli/commands/postgres-tools.ts +0 -216
package/cli/commands/restore.ts
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
} from '../ui/prompts'
|
|
13
13
|
import { createSpinner } from '../ui/spinner'
|
|
14
14
|
import { success, error, warning } from '../ui/theme'
|
|
15
|
-
import {
|
|
16
|
-
import { spawn } from 'child_process'
|
|
15
|
+
import { tmpdir } from 'os'
|
|
17
16
|
import { join } from 'path'
|
|
18
17
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
18
|
+
import { platformService } from '../../core/platform-service'
|
|
19
19
|
|
|
20
20
|
export const restoreCommand = new Command('restore')
|
|
21
21
|
.description('Restore a backup to a container')
|
|
@@ -76,8 +76,12 @@ export const restoreCommand = new Command('restore')
|
|
|
76
76
|
process.exit(1)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const { engine: engineName } = config
|
|
80
|
+
|
|
79
81
|
// Check if running
|
|
80
|
-
const running = await processManager.isRunning(containerName
|
|
82
|
+
const running = await processManager.isRunning(containerName, {
|
|
83
|
+
engine: engineName,
|
|
84
|
+
})
|
|
81
85
|
if (!running) {
|
|
82
86
|
console.error(
|
|
83
87
|
error(
|
|
@@ -88,7 +92,7 @@ export const restoreCommand = new Command('restore')
|
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
// Get engine
|
|
91
|
-
const engine = getEngine(
|
|
95
|
+
const engine = getEngine(engineName)
|
|
92
96
|
|
|
93
97
|
// Check for required client tools BEFORE doing anything
|
|
94
98
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
@@ -129,14 +133,34 @@ export const restoreCommand = new Command('restore')
|
|
|
129
133
|
|
|
130
134
|
// Handle --from-url option
|
|
131
135
|
if (options.fromUrl) {
|
|
132
|
-
// Validate connection string
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
136
|
+
// Validate connection string matches container's engine
|
|
137
|
+
const isPgUrl =
|
|
138
|
+
options.fromUrl.startsWith('postgresql://') ||
|
|
139
|
+
options.fromUrl.startsWith('postgres://')
|
|
140
|
+
const isMysqlUrl = options.fromUrl.startsWith('mysql://')
|
|
141
|
+
|
|
142
|
+
if (engineName === 'postgresql' && !isPgUrl) {
|
|
137
143
|
console.error(
|
|
138
144
|
error(
|
|
139
|
-
'Connection string must start with postgresql:// or postgres://',
|
|
145
|
+
'Connection string must start with postgresql:// or postgres:// for PostgreSQL containers',
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (engineName === 'mysql' && !isMysqlUrl) {
|
|
152
|
+
console.error(
|
|
153
|
+
error(
|
|
154
|
+
'Connection string must start with mysql:// for MySQL containers',
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!isPgUrl && !isMysqlUrl) {
|
|
161
|
+
console.error(
|
|
162
|
+
error(
|
|
163
|
+
'Connection string must start with postgresql://, postgres://, or mysql://',
|
|
140
164
|
),
|
|
141
165
|
)
|
|
142
166
|
process.exit(1)
|
|
@@ -158,7 +182,10 @@ export const restoreCommand = new Command('restore')
|
|
|
158
182
|
dumpSpinner.start()
|
|
159
183
|
|
|
160
184
|
try {
|
|
161
|
-
await engine.dumpFromConnectionString(
|
|
185
|
+
await engine.dumpFromConnectionString(
|
|
186
|
+
options.fromUrl,
|
|
187
|
+
tempDumpPath,
|
|
188
|
+
)
|
|
162
189
|
dumpSpinner.succeed('Dump created from remote database')
|
|
163
190
|
backupPath = tempDumpPath
|
|
164
191
|
dumpSuccess = true
|
|
@@ -167,11 +194,15 @@ export const restoreCommand = new Command('restore')
|
|
|
167
194
|
dumpSpinner.fail('Failed to create dump')
|
|
168
195
|
|
|
169
196
|
// Check if this is a missing tool error
|
|
197
|
+
const dumpTool = engineName === 'mysql' ? 'mysqldump' : 'pg_dump'
|
|
170
198
|
if (
|
|
171
|
-
e.message.includes(
|
|
199
|
+
e.message.includes(`${dumpTool} not found`) ||
|
|
172
200
|
e.message.includes('ENOENT')
|
|
173
201
|
) {
|
|
174
|
-
const installed = await promptInstallDependencies(
|
|
202
|
+
const installed = await promptInstallDependencies(
|
|
203
|
+
dumpTool,
|
|
204
|
+
engineName,
|
|
205
|
+
)
|
|
175
206
|
if (!installed) {
|
|
176
207
|
process.exit(1)
|
|
177
208
|
}
|
|
@@ -180,7 +211,7 @@ export const restoreCommand = new Command('restore')
|
|
|
180
211
|
}
|
|
181
212
|
|
|
182
213
|
console.log()
|
|
183
|
-
console.error(error(
|
|
214
|
+
console.error(error(`${dumpTool} error:`))
|
|
184
215
|
console.log(chalk.gray(` ${e.message}`))
|
|
185
216
|
process.exit(1)
|
|
186
217
|
}
|
|
@@ -279,28 +310,11 @@ export const restoreCommand = new Command('restore')
|
|
|
279
310
|
console.log(chalk.gray(' Connection string:'))
|
|
280
311
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
281
312
|
|
|
282
|
-
// Copy connection string to clipboard using platform
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const args =
|
|
286
|
-
platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
287
|
-
|
|
288
|
-
await new Promise<void>((resolve, reject) => {
|
|
289
|
-
const proc = spawn(cmd, args, {
|
|
290
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
291
|
-
})
|
|
292
|
-
proc.stdin?.write(connectionString)
|
|
293
|
-
proc.stdin?.end()
|
|
294
|
-
proc.on('close', (code) => {
|
|
295
|
-
if (code === 0) resolve()
|
|
296
|
-
else
|
|
297
|
-
reject(new Error(`Clipboard command exited with code ${code}`))
|
|
298
|
-
})
|
|
299
|
-
proc.on('error', reject)
|
|
300
|
-
})
|
|
301
|
-
|
|
313
|
+
// Copy connection string to clipboard using platform service
|
|
314
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
315
|
+
if (copied) {
|
|
302
316
|
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
303
|
-
}
|
|
317
|
+
} else {
|
|
304
318
|
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
305
319
|
}
|
|
306
320
|
|
|
@@ -313,14 +327,23 @@ export const restoreCommand = new Command('restore')
|
|
|
313
327
|
} catch (err) {
|
|
314
328
|
const e = err as Error
|
|
315
329
|
|
|
316
|
-
// Check if this is a missing tool error
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
330
|
+
// Check if this is a missing tool error (PostgreSQL or MySQL)
|
|
331
|
+
const missingToolPatterns = [
|
|
332
|
+
// PostgreSQL
|
|
333
|
+
'pg_restore not found',
|
|
334
|
+
'psql not found',
|
|
335
|
+
'pg_dump not found',
|
|
336
|
+
// MySQL
|
|
337
|
+
'mysql not found',
|
|
338
|
+
'mysqldump not found',
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
342
|
+
e.message.includes(p),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if (matchingPattern) {
|
|
346
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
324
347
|
const installed = await promptInstallDependencies(missingTool)
|
|
325
348
|
if (installed) {
|
|
326
349
|
console.log(
|
package/cli/commands/start.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
|
-
import { portManager } from '../../core/port-manager'
|
|
5
4
|
import { processManager } from '../../core/process-manager'
|
|
5
|
+
import { startWithRetry } from '../../core/start-with-retry'
|
|
6
6
|
import { getEngine } from '../../engines'
|
|
7
|
+
import { getEngineDefaults } from '../../config/defaults'
|
|
7
8
|
import { promptContainerSelect } from '../ui/prompts'
|
|
8
9
|
import { createSpinner } from '../ui/spinner'
|
|
9
10
|
import { error, warning } from '../ui/theme'
|
|
@@ -46,37 +47,67 @@ export const startCommand = new Command('start')
|
|
|
46
47
|
process.exit(1)
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const { engine: engineName } = config
|
|
51
|
+
|
|
49
52
|
// Check if already running
|
|
50
|
-
const running = await processManager.isRunning(containerName
|
|
53
|
+
const running = await processManager.isRunning(containerName, {
|
|
54
|
+
engine: engineName,
|
|
55
|
+
})
|
|
51
56
|
if (running) {
|
|
52
57
|
console.log(warning(`Container "${containerName}" is already running`))
|
|
53
58
|
return
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
if (!portAvailable) {
|
|
59
|
-
// Try to find a new port
|
|
60
|
-
const { port: newPort } = await portManager.findAvailablePort()
|
|
61
|
-
console.log(
|
|
62
|
-
warning(
|
|
63
|
-
`Port ${config.port} is in use, switching to port ${newPort}`,
|
|
64
|
-
),
|
|
65
|
-
)
|
|
66
|
-
config.port = newPort
|
|
67
|
-
await containerManager.updateConfig(containerName, { port: newPort })
|
|
68
|
-
}
|
|
61
|
+
// Get engine defaults for port range and database name
|
|
62
|
+
const engineDefaults = getEngineDefaults(engineName)
|
|
69
63
|
|
|
70
|
-
// Get engine and start
|
|
71
|
-
const engine = getEngine(
|
|
64
|
+
// Get engine and start with retry (handles port race conditions)
|
|
65
|
+
const engine = getEngine(engineName)
|
|
72
66
|
|
|
73
67
|
const spinner = createSpinner(`Starting ${containerName}...`)
|
|
74
68
|
spinner.start()
|
|
75
69
|
|
|
76
|
-
await
|
|
70
|
+
const result = await startWithRetry({
|
|
71
|
+
engine,
|
|
72
|
+
config,
|
|
73
|
+
onPortChange: (oldPort, newPort) => {
|
|
74
|
+
spinner.text = `Port ${oldPort} was in use, retrying with port ${newPort}...`
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
spinner.fail(`Failed to start "${containerName}"`)
|
|
80
|
+
if (result.error) {
|
|
81
|
+
console.error(error(result.error.message))
|
|
82
|
+
}
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
await containerManager.updateConfig(containerName, { status: 'running' })
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
if (result.retriesUsed > 0) {
|
|
89
|
+
spinner.warn(
|
|
90
|
+
`Container "${containerName}" started on port ${result.finalPort} (original port was in use)`,
|
|
91
|
+
)
|
|
92
|
+
} else {
|
|
93
|
+
spinner.succeed(`Container "${containerName}" started`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ensure the user's database exists (if different from default)
|
|
97
|
+
const defaultDb = engineDefaults.superuser // postgres or root
|
|
98
|
+
if (config.database && config.database !== defaultDb) {
|
|
99
|
+
const dbSpinner = createSpinner(
|
|
100
|
+
`Ensuring database "${config.database}" exists...`,
|
|
101
|
+
)
|
|
102
|
+
dbSpinner.start()
|
|
103
|
+
try {
|
|
104
|
+
await engine.createDatabase(config, config.database)
|
|
105
|
+
dbSpinner.succeed(`Database "${config.database}" ready`)
|
|
106
|
+
} catch {
|
|
107
|
+
// Database might already exist, which is fine
|
|
108
|
+
dbSpinner.succeed(`Database "${config.database}" ready`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
80
111
|
|
|
81
112
|
// Show connection info
|
|
82
113
|
const connectionString = engine.getConnectionString(config)
|
package/cli/commands/stop.ts
CHANGED
|
@@ -67,7 +67,9 @@ export const stopCommand = new Command('stop')
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Check if running
|
|
70
|
-
const running = await processManager.isRunning(containerName
|
|
70
|
+
const running = await processManager.isRunning(containerName, {
|
|
71
|
+
engine: config.engine,
|
|
72
|
+
})
|
|
71
73
|
if (!running) {
|
|
72
74
|
console.log(warning(`Container "${containerName}" is not running`))
|
|
73
75
|
return
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { containerManager } from '../../core/container-manager'
|
|
3
|
+
import { platformService } from '../../core/platform-service'
|
|
4
|
+
import { getEngine } from '../../engines'
|
|
5
|
+
import { promptContainerSelect } from '../ui/prompts'
|
|
6
|
+
import { error, warning, success } from '../ui/theme'
|
|
7
|
+
|
|
8
|
+
export const urlCommand = new Command('url')
|
|
9
|
+
.alias('connection-string')
|
|
10
|
+
.description('Output connection string for a container')
|
|
11
|
+
.argument('[name]', 'Container name')
|
|
12
|
+
.option('-c, --copy', 'Copy to clipboard')
|
|
13
|
+
.option('-d, --database <database>', 'Use different database name')
|
|
14
|
+
.action(
|
|
15
|
+
async (
|
|
16
|
+
name: string | undefined,
|
|
17
|
+
options: { copy?: boolean; database?: string },
|
|
18
|
+
) => {
|
|
19
|
+
try {
|
|
20
|
+
let containerName = name
|
|
21
|
+
|
|
22
|
+
// Interactive selection if no name provided
|
|
23
|
+
if (!containerName) {
|
|
24
|
+
const containers = await containerManager.list()
|
|
25
|
+
|
|
26
|
+
if (containers.length === 0) {
|
|
27
|
+
console.log(warning('No containers found'))
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selected = await promptContainerSelect(
|
|
32
|
+
containers,
|
|
33
|
+
'Select container:',
|
|
34
|
+
)
|
|
35
|
+
if (!selected) return
|
|
36
|
+
containerName = selected
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get container config
|
|
40
|
+
const config = await containerManager.getConfig(containerName)
|
|
41
|
+
if (!config) {
|
|
42
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get connection string
|
|
47
|
+
const engine = getEngine(config.engine)
|
|
48
|
+
const connectionString = engine.getConnectionString(
|
|
49
|
+
config,
|
|
50
|
+
options.database,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Copy to clipboard if requested
|
|
54
|
+
if (options.copy) {
|
|
55
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
56
|
+
if (copied) {
|
|
57
|
+
// Output the string AND confirmation
|
|
58
|
+
console.log(connectionString)
|
|
59
|
+
console.error(success('Copied to clipboard'))
|
|
60
|
+
} else {
|
|
61
|
+
// Output the string but warn about clipboard
|
|
62
|
+
console.log(connectionString)
|
|
63
|
+
console.error(warning('Could not copy to clipboard'))
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Just output the connection string (no newline formatting for easy piping)
|
|
67
|
+
process.stdout.write(connectionString)
|
|
68
|
+
// Add newline if stdout is a TTY (interactive terminal)
|
|
69
|
+
if (process.stdout.isTTY) {
|
|
70
|
+
console.log()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const e = err as Error
|
|
75
|
+
console.error(error(e.message))
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
)
|
package/cli/index.ts
CHANGED
|
@@ -9,14 +9,17 @@ import { connectCommand } from './commands/connect'
|
|
|
9
9
|
import { cloneCommand } from './commands/clone'
|
|
10
10
|
import { menuCommand } from './commands/menu'
|
|
11
11
|
import { configCommand } from './commands/config'
|
|
12
|
-
import { postgresToolsCommand } from './commands/postgres-tools'
|
|
13
12
|
import { depsCommand } from './commands/deps'
|
|
13
|
+
import { enginesCommand } from './commands/engines'
|
|
14
|
+
import { editCommand } from './commands/edit'
|
|
15
|
+
import { urlCommand } from './commands/url'
|
|
16
|
+
import { infoCommand } from './commands/info'
|
|
14
17
|
|
|
15
18
|
export async function run(): Promise<void> {
|
|
16
19
|
program
|
|
17
20
|
.name('spindb')
|
|
18
21
|
.description('Spin up local database containers without Docker')
|
|
19
|
-
.version('0.1.0')
|
|
22
|
+
.version('0.1.0', '-v, --version', 'output the version number')
|
|
20
23
|
|
|
21
24
|
program.addCommand(createCommand)
|
|
22
25
|
program.addCommand(listCommand)
|
|
@@ -28,8 +31,11 @@ export async function run(): Promise<void> {
|
|
|
28
31
|
program.addCommand(cloneCommand)
|
|
29
32
|
program.addCommand(menuCommand)
|
|
30
33
|
program.addCommand(configCommand)
|
|
31
|
-
program.addCommand(postgresToolsCommand)
|
|
32
34
|
program.addCommand(depsCommand)
|
|
35
|
+
program.addCommand(enginesCommand)
|
|
36
|
+
program.addCommand(editCommand)
|
|
37
|
+
program.addCommand(urlCommand)
|
|
38
|
+
program.addCommand(infoCommand)
|
|
33
39
|
|
|
34
40
|
// If no arguments provided, show interactive menu
|
|
35
41
|
if (process.argv.length <= 2) {
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -2,12 +2,13 @@ import inquirer from 'inquirer'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import { listEngines, getEngine } from '../../engines'
|
|
5
|
-
import { defaults } from '../../config/defaults'
|
|
6
|
-
import { installPostgresBinaries } from '../../
|
|
5
|
+
import { defaults, getEngineDefaults } from '../../config/defaults'
|
|
6
|
+
import { installPostgresBinaries } from '../../engines/postgresql/binary-manager'
|
|
7
7
|
import {
|
|
8
8
|
detectPackageManager,
|
|
9
9
|
getManualInstallInstructions,
|
|
10
10
|
getCurrentPlatform,
|
|
11
|
+
installEngineDependencies,
|
|
11
12
|
} from '../../core/dependency-manager'
|
|
12
13
|
import { getEngineDependencies } from '../../config/os-dependencies'
|
|
13
14
|
import type { ContainerConfig } from '../../types'
|
|
@@ -36,25 +37,26 @@ export async function promptContainerName(
|
|
|
36
37
|
return name
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Engine icons for display
|
|
42
|
+
*/
|
|
43
|
+
const engineIcons: Record<string, string> = {
|
|
44
|
+
postgresql: '🐘',
|
|
45
|
+
mysql: '🐬',
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
/**
|
|
40
49
|
* Prompt for database engine selection
|
|
41
50
|
*/
|
|
42
51
|
export async function promptEngine(): Promise<string> {
|
|
43
52
|
const engines = listEngines()
|
|
44
53
|
|
|
45
|
-
// Build choices from available engines
|
|
46
|
-
const choices =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})),
|
|
52
|
-
{
|
|
53
|
-
name: chalk.gray('🐬 MySQL (coming soon)'),
|
|
54
|
-
value: 'mysql',
|
|
55
|
-
disabled: 'Coming soon',
|
|
56
|
-
},
|
|
57
|
-
]
|
|
54
|
+
// Build choices from available engines
|
|
55
|
+
const choices = engines.map((e) => ({
|
|
56
|
+
name: `${engineIcons[e.name] || '🗄️'} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
|
|
57
|
+
value: e.name,
|
|
58
|
+
short: e.displayName,
|
|
59
|
+
}))
|
|
58
60
|
|
|
59
61
|
const { engine } = await inquirer.prompt<{ engine: string }>([
|
|
60
62
|
{
|
|
@@ -69,8 +71,8 @@ export async function promptEngine(): Promise<string> {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
* Prompt for
|
|
73
|
-
* Two-step selection: first major version, then specific minor version
|
|
74
|
+
* Prompt for database version
|
|
75
|
+
* Two-step selection: first major version, then specific minor version (if available)
|
|
74
76
|
*/
|
|
75
77
|
export async function promptVersion(engineName: string): Promise<string> {
|
|
76
78
|
const engine = getEngine(engineName)
|
|
@@ -112,13 +114,13 @@ export async function promptVersion(engineName: string): Promise<string> {
|
|
|
112
114
|
const countLabel =
|
|
113
115
|
versionCount > 0 ? chalk.gray(`(${versionCount} versions)`) : ''
|
|
114
116
|
const label = isLatestMajor
|
|
115
|
-
?
|
|
116
|
-
:
|
|
117
|
+
? `${engine.displayName} ${major} ${countLabel} ${chalk.green('← latest')}`
|
|
118
|
+
: `${engine.displayName} ${major} ${countLabel}`
|
|
117
119
|
|
|
118
120
|
majorChoices.push({
|
|
119
121
|
name: label,
|
|
120
122
|
value: major,
|
|
121
|
-
short:
|
|
123
|
+
short: `${engine.displayName} ${major}`,
|
|
122
124
|
})
|
|
123
125
|
}
|
|
124
126
|
|
|
@@ -150,7 +152,7 @@ export async function promptVersion(engineName: string): Promise<string> {
|
|
|
150
152
|
{
|
|
151
153
|
type: 'list',
|
|
152
154
|
name: 'version',
|
|
153
|
-
message: `Select
|
|
155
|
+
message: `Select ${engine.displayName} ${majorVersion} version:`,
|
|
154
156
|
choices: minorChoices,
|
|
155
157
|
default: minorVersions[0], // Default to latest
|
|
156
158
|
},
|
|
@@ -225,7 +227,7 @@ export async function promptContainerSelect(
|
|
|
225
227
|
name: 'container',
|
|
226
228
|
message,
|
|
227
229
|
choices: containers.map((c) => ({
|
|
228
|
-
name: `${c.name} ${chalk.gray(`(${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
230
|
+
name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
229
231
|
c.status === 'running'
|
|
230
232
|
? chalk.green('● running')
|
|
231
233
|
: chalk.gray('○ stopped')
|
|
@@ -279,16 +281,17 @@ export type CreateOptions = {
|
|
|
279
281
|
/**
|
|
280
282
|
* Full interactive create flow
|
|
281
283
|
*/
|
|
282
|
-
export async function promptCreateOptions(
|
|
283
|
-
defaultPort: number = defaults.port,
|
|
284
|
-
): Promise<CreateOptions> {
|
|
284
|
+
export async function promptCreateOptions(): Promise<CreateOptions> {
|
|
285
285
|
console.log(chalk.cyan('\n 🗄️ Create New Database Container\n'))
|
|
286
286
|
|
|
287
287
|
const engine = await promptEngine()
|
|
288
288
|
const version = await promptVersion(engine)
|
|
289
289
|
const name = await promptContainerName()
|
|
290
290
|
const database = await promptDatabaseName(name) // Default to container name
|
|
291
|
-
|
|
291
|
+
|
|
292
|
+
// Get engine-specific default port
|
|
293
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
294
|
+
const port = await promptPort(engineDefaults.defaultPort)
|
|
292
295
|
|
|
293
296
|
return { name, engine, version, port, database }
|
|
294
297
|
}
|
|
@@ -330,7 +333,9 @@ export async function promptInstallDependencies(
|
|
|
330
333
|
if (dep) {
|
|
331
334
|
const instructions = getManualInstallInstructions(dep, platform)
|
|
332
335
|
console.log(
|
|
333
|
-
chalk.gray(
|
|
336
|
+
chalk.gray(
|
|
337
|
+
` Please install ${engineDeps.displayName} client tools:`,
|
|
338
|
+
),
|
|
334
339
|
)
|
|
335
340
|
console.log()
|
|
336
341
|
for (const instruction of instructions) {
|
|
@@ -343,7 +348,9 @@ export async function promptInstallDependencies(
|
|
|
343
348
|
}
|
|
344
349
|
|
|
345
350
|
console.log(
|
|
346
|
-
chalk.gray(
|
|
351
|
+
chalk.gray(
|
|
352
|
+
` Detected package manager: ${chalk.white(packageManager.name)}`,
|
|
353
|
+
),
|
|
347
354
|
)
|
|
348
355
|
console.log()
|
|
349
356
|
|
|
@@ -393,8 +400,7 @@ export async function promptInstallDependencies(
|
|
|
393
400
|
|
|
394
401
|
console.log()
|
|
395
402
|
|
|
396
|
-
//
|
|
397
|
-
// Future engines will need their own install functions
|
|
403
|
+
// PostgreSQL has its own install function with extra logic
|
|
398
404
|
if (engine === 'postgresql') {
|
|
399
405
|
const success = await installPostgresBinaries()
|
|
400
406
|
|
|
@@ -410,11 +416,70 @@ export async function promptInstallDependencies(
|
|
|
410
416
|
return success
|
|
411
417
|
}
|
|
412
418
|
|
|
413
|
-
// For other engines,
|
|
419
|
+
// For other engines (MySQL, etc.), use the generic installer
|
|
414
420
|
console.log(
|
|
415
|
-
chalk.
|
|
421
|
+
chalk.cyan(` Installing ${engineName} with ${packageManager.name}...`),
|
|
416
422
|
)
|
|
417
|
-
console.log(chalk.gray('
|
|
423
|
+
console.log(chalk.gray(' You may be prompted for your password.'))
|
|
418
424
|
console.log()
|
|
419
|
-
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const results = await installEngineDependencies(engine, packageManager)
|
|
428
|
+
const allSuccess = results.every((r) => r.success)
|
|
429
|
+
|
|
430
|
+
if (allSuccess) {
|
|
431
|
+
console.log()
|
|
432
|
+
console.log(chalk.green(` ${engineName} tools installed successfully!`))
|
|
433
|
+
console.log(chalk.gray(' Continuing with your operation...'))
|
|
434
|
+
console.log()
|
|
435
|
+
return true
|
|
436
|
+
} else {
|
|
437
|
+
const failed = results.filter((r) => !r.success)
|
|
438
|
+
console.log()
|
|
439
|
+
console.log(chalk.red(' Some installations failed:'))
|
|
440
|
+
for (const f of failed) {
|
|
441
|
+
console.log(chalk.red(` ${f.dependency.name}: ${f.error}`))
|
|
442
|
+
}
|
|
443
|
+
console.log()
|
|
444
|
+
|
|
445
|
+
// Show manual install instructions
|
|
446
|
+
if (engineDeps) {
|
|
447
|
+
const instructions = getManualInstallInstructions(
|
|
448
|
+
engineDeps.dependencies[0],
|
|
449
|
+
platform,
|
|
450
|
+
)
|
|
451
|
+
if (instructions.length > 0) {
|
|
452
|
+
console.log(chalk.gray(' To install manually:'))
|
|
453
|
+
for (const instruction of instructions) {
|
|
454
|
+
console.log(chalk.gray(` ${instruction}`))
|
|
455
|
+
}
|
|
456
|
+
console.log()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
const e = err as Error
|
|
464
|
+
console.log()
|
|
465
|
+
console.log(chalk.red(` Installation failed: ${e.message}`))
|
|
466
|
+
console.log()
|
|
467
|
+
|
|
468
|
+
// Show manual install instructions on error
|
|
469
|
+
if (engineDeps) {
|
|
470
|
+
const instructions = getManualInstallInstructions(
|
|
471
|
+
engineDeps.dependencies[0],
|
|
472
|
+
platform,
|
|
473
|
+
)
|
|
474
|
+
if (instructions.length > 0) {
|
|
475
|
+
console.log(chalk.gray(' To install manually:'))
|
|
476
|
+
for (const instruction of instructions) {
|
|
477
|
+
console.log(chalk.gray(` ${instruction}`))
|
|
478
|
+
}
|
|
479
|
+
console.log()
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
420
485
|
}
|