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.
Files changed (44) hide show
  1. package/README.md +207 -101
  2. package/cli/commands/clone.ts +3 -1
  3. package/cli/commands/connect.ts +54 -24
  4. package/cli/commands/create.ts +309 -189
  5. package/cli/commands/delete.ts +3 -1
  6. package/cli/commands/deps.ts +19 -4
  7. package/cli/commands/edit.ts +245 -0
  8. package/cli/commands/engines.ts +434 -0
  9. package/cli/commands/info.ts +279 -0
  10. package/cli/commands/list.ts +14 -3
  11. package/cli/commands/menu.ts +510 -198
  12. package/cli/commands/restore.ts +66 -43
  13. package/cli/commands/start.ts +50 -19
  14. package/cli/commands/stop.ts +3 -1
  15. package/cli/commands/url.ts +79 -0
  16. package/cli/index.ts +9 -3
  17. package/cli/ui/prompts.ts +99 -34
  18. package/config/defaults.ts +40 -15
  19. package/config/engine-defaults.ts +107 -0
  20. package/config/os-dependencies.ts +119 -124
  21. package/config/paths.ts +82 -56
  22. package/core/binary-manager.ts +44 -6
  23. package/core/config-manager.ts +17 -5
  24. package/core/container-manager.ts +124 -60
  25. package/core/dependency-manager.ts +9 -15
  26. package/core/error-handler.ts +336 -0
  27. package/core/platform-service.ts +634 -0
  28. package/core/port-manager.ts +51 -32
  29. package/core/process-manager.ts +26 -8
  30. package/core/start-with-retry.ts +167 -0
  31. package/core/transaction-manager.ts +170 -0
  32. package/engines/index.ts +7 -2
  33. package/engines/mysql/binary-detection.ts +325 -0
  34. package/engines/mysql/index.ts +808 -0
  35. package/engines/mysql/restore.ts +257 -0
  36. package/engines/mysql/version-validator.ts +373 -0
  37. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  38. package/engines/postgresql/binary-urls.ts +5 -3
  39. package/engines/postgresql/index.ts +17 -9
  40. package/engines/postgresql/restore.ts +54 -5
  41. package/engines/postgresql/version-validator.ts +262 -0
  42. package/package.json +9 -3
  43. package/types/index.ts +29 -5
  44. package/cli/commands/postgres-tools.ts +0 -216
@@ -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 { platform, tmpdir } from 'os'
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(config.engine)
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
- if (
134
- !options.fromUrl.startsWith('postgresql://') &&
135
- !options.fromUrl.startsWith('postgres://')
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(options.fromUrl, tempDumpPath)
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('pg_dump not found') ||
199
+ e.message.includes(`${dumpTool} not found`) ||
172
200
  e.message.includes('ENOENT')
173
201
  ) {
174
- const installed = await promptInstallDependencies('pg_dump')
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('pg_dump 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-specific command
283
- try {
284
- const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
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
- } catch {
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
- if (
318
- e.message.includes('pg_restore not found') ||
319
- e.message.includes('psql not found')
320
- ) {
321
- const missingTool = e.message.includes('pg_restore')
322
- ? 'pg_restore'
323
- : 'psql'
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(
@@ -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
- // Check port availability
57
- const portAvailable = await portManager.isPortAvailable(config.port)
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(config.engine)
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 engine.start(config)
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
- spinner.succeed(`Container "${containerName}" started`)
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)
@@ -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 '../../core/postgres-binary-manager'
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 plus coming soon engines
46
- const choices = [
47
- ...engines.map((e) => ({
48
- name: `🐘 ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
49
- value: e.name,
50
- short: e.displayName,
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 PostgreSQL version
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
- ? `PostgreSQL ${major} ${countLabel} ${chalk.green('← latest')}`
116
- : `PostgreSQL ${major} ${countLabel}`
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: `PostgreSQL ${major}`,
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 PostgreSQL ${majorVersion} version:`,
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
- const port = await promptPort(defaultPort)
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(` Please install ${engineDeps.displayName} client tools:`),
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(` Detected package manager: ${chalk.white(packageManager.name)}`),
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
- // For now, only PostgreSQL has full install support
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, show manual instructions
419
+ // For other engines (MySQL, etc.), use the generic installer
414
420
  console.log(
415
- chalk.yellow(` Automatic installation for ${engineName} is not yet supported.`),
421
+ chalk.cyan(` Installing ${engineName} with ${packageManager.name}...`),
416
422
  )
417
- console.log(chalk.gray(' Please install manually.'))
423
+ console.log(chalk.gray(' You may be prompted for your password.'))
418
424
  console.log()
419
- return false
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
  }