spindb 0.7.0 → 0.7.5

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 (40) hide show
  1. package/README.md +421 -294
  2. package/cli/commands/backup.ts +1 -30
  3. package/cli/commands/clone.ts +0 -6
  4. package/cli/commands/config.ts +7 -1
  5. package/cli/commands/connect.ts +1 -16
  6. package/cli/commands/create.ts +4 -55
  7. package/cli/commands/delete.ts +0 -6
  8. package/cli/commands/edit.ts +9 -25
  9. package/cli/commands/engines.ts +10 -188
  10. package/cli/commands/info.ts +7 -34
  11. package/cli/commands/list.ts +2 -18
  12. package/cli/commands/logs.ts +118 -0
  13. package/cli/commands/menu/backup-handlers.ts +749 -0
  14. package/cli/commands/menu/container-handlers.ts +825 -0
  15. package/cli/commands/menu/engine-handlers.ts +362 -0
  16. package/cli/commands/menu/index.ts +179 -0
  17. package/cli/commands/menu/shared.ts +26 -0
  18. package/cli/commands/menu/shell-handlers.ts +320 -0
  19. package/cli/commands/menu/sql-handlers.ts +194 -0
  20. package/cli/commands/menu/update-handlers.ts +94 -0
  21. package/cli/commands/restore.ts +2 -28
  22. package/cli/commands/run.ts +139 -0
  23. package/cli/commands/start.ts +2 -10
  24. package/cli/commands/stop.ts +0 -5
  25. package/cli/commands/url.ts +18 -13
  26. package/cli/constants.ts +10 -0
  27. package/cli/helpers.ts +152 -0
  28. package/cli/index.ts +5 -2
  29. package/cli/ui/prompts.ts +3 -11
  30. package/core/dependency-manager.ts +0 -163
  31. package/core/error-handler.ts +0 -26
  32. package/core/platform-service.ts +60 -40
  33. package/core/start-with-retry.ts +3 -28
  34. package/core/transaction-manager.ts +0 -8
  35. package/engines/base-engine.ts +10 -0
  36. package/engines/mysql/binary-detection.ts +1 -1
  37. package/engines/mysql/index.ts +78 -2
  38. package/engines/postgresql/index.ts +49 -0
  39. package/package.json +1 -1
  40. package/cli/commands/menu.ts +0 -2670
@@ -41,7 +41,6 @@ export const restoreCommand = new Command('restore')
41
41
  let containerName = name
42
42
  let backupPath = backup
43
43
 
44
- // Interactive selection if no name provided
45
44
  if (!containerName) {
46
45
  const containers = await containerManager.list()
47
46
  const running = containers.filter((c) => c.status === 'running')
@@ -69,7 +68,6 @@ export const restoreCommand = new Command('restore')
69
68
  containerName = selected
70
69
  }
71
70
 
72
- // Get container config
73
71
  const config = await containerManager.getConfig(containerName)
74
72
  if (!config) {
75
73
  console.error(error(`Container "${containerName}" not found`))
@@ -78,7 +76,6 @@ export const restoreCommand = new Command('restore')
78
76
 
79
77
  const { engine: engineName } = config
80
78
 
81
- // Check if running
82
79
  const running = await processManager.isRunning(containerName, {
83
80
  engine: engineName,
84
81
  })
@@ -91,10 +88,8 @@ export const restoreCommand = new Command('restore')
91
88
  process.exit(1)
92
89
  }
93
90
 
94
- // Get engine
95
91
  const engine = getEngine(engineName)
96
92
 
97
- // Check for required client tools BEFORE doing anything
98
93
  const depsSpinner = createSpinner('Checking required tools...')
99
94
  depsSpinner.start()
100
95
 
@@ -104,7 +99,6 @@ export const restoreCommand = new Command('restore')
104
99
  `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
105
100
  )
106
101
 
107
- // Offer to install
108
102
  const installed = await promptInstallDependencies(
109
103
  missingDeps[0].binary,
110
104
  config.engine,
@@ -114,7 +108,6 @@ export const restoreCommand = new Command('restore')
114
108
  process.exit(1)
115
109
  }
116
110
 
117
- // Verify installation worked
118
111
  missingDeps = await getMissingDependencies(config.engine)
119
112
  if (missingDeps.length > 0) {
120
113
  console.error(
@@ -131,9 +124,7 @@ export const restoreCommand = new Command('restore')
131
124
  depsSpinner.succeed('Required tools available')
132
125
  }
133
126
 
134
- // Handle --from-url option
135
127
  if (options.fromUrl) {
136
- // Validate connection string matches container's engine
137
128
  const isPgUrl =
138
129
  options.fromUrl.startsWith('postgresql://') ||
139
130
  options.fromUrl.startsWith('postgres://')
@@ -166,13 +157,12 @@ export const restoreCommand = new Command('restore')
166
157
  process.exit(1)
167
158
  }
168
159
 
169
- // Create temp file for the dump
170
160
  const timestamp = Date.now()
171
161
  tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
172
162
 
173
163
  let dumpSuccess = false
174
164
  let attempts = 0
175
- const maxAttempts = 2 // Allow one retry after installing deps
165
+ const maxAttempts = 2
176
166
 
177
167
  while (!dumpSuccess && attempts < maxAttempts) {
178
168
  attempts++
@@ -193,7 +183,6 @@ export const restoreCommand = new Command('restore')
193
183
  const e = err as Error
194
184
  dumpSpinner.fail('Failed to create dump')
195
185
 
196
- // Check if this is a missing tool error
197
186
  const dumpTool = engineName === 'mysql' ? 'mysqldump' : 'pg_dump'
198
187
  if (
199
188
  e.message.includes(`${dumpTool} not found`) ||
@@ -206,7 +195,6 @@ export const restoreCommand = new Command('restore')
206
195
  if (!installed) {
207
196
  process.exit(1)
208
197
  }
209
- // Loop will retry
210
198
  continue
211
199
  }
212
200
 
@@ -217,13 +205,11 @@ export const restoreCommand = new Command('restore')
217
205
  }
218
206
  }
219
207
 
220
- // Safety check - should never reach here without backupPath set
221
208
  if (!dumpSuccess) {
222
209
  console.error(error('Failed to create dump after retries'))
223
210
  process.exit(1)
224
211
  }
225
212
  } else {
226
- // Check backup file
227
213
  if (!backupPath) {
228
214
  console.error(error('Backup file path is required'))
229
215
  console.log(
@@ -243,26 +229,22 @@ export const restoreCommand = new Command('restore')
243
229
  }
244
230
  }
245
231
 
246
- // Get database name
247
232
  let databaseName = options.database
248
233
  if (!databaseName) {
249
234
  databaseName = await promptDatabaseName(containerName, engineName)
250
235
  }
251
236
 
252
- // At this point backupPath is guaranteed to be set
253
237
  if (!backupPath) {
254
238
  console.error(error('No backup path specified'))
255
239
  process.exit(1)
256
240
  }
257
241
 
258
- // Detect backup format
259
242
  const detectSpinner = createSpinner('Detecting backup format...')
260
243
  detectSpinner.start()
261
244
 
262
245
  const format = await engine.detectBackupFormat(backupPath)
263
246
  detectSpinner.succeed(`Detected: ${format.description}`)
264
247
 
265
- // Create database
266
248
  const dbSpinner = createSpinner(
267
249
  `Creating database "${databaseName}"...`,
268
250
  )
@@ -271,16 +253,14 @@ export const restoreCommand = new Command('restore')
271
253
  await engine.createDatabase(config, databaseName)
272
254
  dbSpinner.succeed(`Database "${databaseName}" ready`)
273
255
 
274
- // Add database to container's databases array
275
256
  await containerManager.addDatabase(containerName, databaseName)
276
257
 
277
- // Restore backup
278
258
  const restoreSpinner = createSpinner('Restoring backup...')
279
259
  restoreSpinner.start()
280
260
 
281
261
  const result = await engine.restore(config, backupPath, {
282
262
  database: databaseName,
283
- createDatabase: false, // Already created
263
+ createDatabase: false,
284
264
  })
285
265
 
286
266
  if (result.code === 0 || !result.stderr) {
@@ -302,7 +282,6 @@ export const restoreCommand = new Command('restore')
302
282
  }
303
283
  }
304
284
 
305
- // Show connection info
306
285
  const connectionString = engine.getConnectionString(
307
286
  config,
308
287
  databaseName,
@@ -313,7 +292,6 @@ export const restoreCommand = new Command('restore')
313
292
  console.log(chalk.gray(' Connection string:'))
314
293
  console.log(chalk.cyan(` ${connectionString}`))
315
294
 
316
- // Copy connection string to clipboard using platform service
317
295
  const copied = await platformService.copyToClipboard(connectionString)
318
296
  if (copied) {
319
297
  console.log(chalk.gray(' Connection string copied to clipboard'))
@@ -330,13 +308,10 @@ export const restoreCommand = new Command('restore')
330
308
  } catch (err) {
331
309
  const e = err as Error
332
310
 
333
- // Check if this is a missing tool error (PostgreSQL or MySQL)
334
311
  const missingToolPatterns = [
335
- // PostgreSQL
336
312
  'pg_restore not found',
337
313
  'psql not found',
338
314
  'pg_dump not found',
339
- // MySQL
340
315
  'mysql not found',
341
316
  'mysqldump not found',
342
317
  ]
@@ -359,7 +334,6 @@ export const restoreCommand = new Command('restore')
359
334
  console.error(error(e.message))
360
335
  process.exit(1)
361
336
  } finally {
362
- // Clean up temp file if we created one
363
337
  if (tempDumpPath) {
364
338
  try {
365
339
  await rm(tempDumpPath, { force: true })
@@ -0,0 +1,139 @@
1
+ import { Command } from 'commander'
2
+ import { existsSync } from 'fs'
3
+ import chalk from 'chalk'
4
+ import { containerManager } from '../../core/container-manager'
5
+ import { processManager } from '../../core/process-manager'
6
+ import { getEngine } from '../../engines'
7
+ import { promptInstallDependencies } from '../ui/prompts'
8
+ import { error, warning } from '../ui/theme'
9
+ import { getMissingDependencies } from '../../core/dependency-manager'
10
+
11
+ export const runCommand = new Command('run')
12
+ .description('Run SQL file or statement against a container')
13
+ .argument('<name>', 'Container name')
14
+ .argument('[file]', 'Path to SQL file')
15
+ .option('-d, --database <name>', 'Target database (defaults to primary)')
16
+ .option('--sql <statement>', 'SQL statement to execute (alternative to file)')
17
+ .action(
18
+ async (
19
+ name: string,
20
+ file: string | undefined,
21
+ options: { database?: string; sql?: string },
22
+ ) => {
23
+ try {
24
+ const containerName = name
25
+
26
+ const config = await containerManager.getConfig(containerName)
27
+ if (!config) {
28
+ console.error(error(`Container "${containerName}" not found`))
29
+ process.exit(1)
30
+ }
31
+
32
+ const { engine: engineName } = config
33
+
34
+ const running = await processManager.isRunning(containerName, {
35
+ engine: engineName,
36
+ })
37
+ if (!running) {
38
+ console.error(
39
+ error(
40
+ `Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`,
41
+ ),
42
+ )
43
+ process.exit(1)
44
+ }
45
+
46
+ if (file && options.sql) {
47
+ console.error(
48
+ error('Cannot specify both a file and --sql option. Choose one.'),
49
+ )
50
+ process.exit(1)
51
+ }
52
+
53
+ if (!file && !options.sql) {
54
+ console.error(error('Must provide either a SQL file or --sql option'))
55
+ console.log(
56
+ chalk.gray(' Usage: spindb run <container> <file.sql>'),
57
+ )
58
+ console.log(
59
+ chalk.gray(' or: spindb run <container> --sql "SELECT ..."'),
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ if (file && !existsSync(file)) {
65
+ console.error(error(`SQL file not found: ${file}`))
66
+ process.exit(1)
67
+ }
68
+
69
+ const engine = getEngine(engineName)
70
+
71
+ let missingDeps = await getMissingDependencies(engineName)
72
+ if (missingDeps.length > 0) {
73
+ console.log(
74
+ warning(
75
+ `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
76
+ ),
77
+ )
78
+
79
+ const installed = await promptInstallDependencies(
80
+ missingDeps[0].binary,
81
+ engineName,
82
+ )
83
+
84
+ if (!installed) {
85
+ process.exit(1)
86
+ }
87
+
88
+ missingDeps = await getMissingDependencies(engineName)
89
+ if (missingDeps.length > 0) {
90
+ console.error(
91
+ error(
92
+ `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
93
+ ),
94
+ )
95
+ process.exit(1)
96
+ }
97
+
98
+ console.log(chalk.green(' ✓ All required tools are now available'))
99
+ console.log()
100
+ }
101
+
102
+ const database = options.database || config.database
103
+
104
+ await engine.runScript(config, {
105
+ file,
106
+ sql: options.sql,
107
+ database,
108
+ })
109
+ } catch (err) {
110
+ const e = err as Error
111
+
112
+ const missingToolPatterns = [
113
+ 'psql not found',
114
+ 'mysql not found',
115
+ 'mysql client not found',
116
+ ]
117
+
118
+ const matchingPattern = missingToolPatterns.find((p) =>
119
+ e.message.toLowerCase().includes(p.toLowerCase()),
120
+ )
121
+
122
+ if (matchingPattern) {
123
+ const missingTool = matchingPattern
124
+ .replace(' not found', '')
125
+ .replace(' client', '')
126
+ const installed = await promptInstallDependencies(missingTool)
127
+ if (installed) {
128
+ console.log(
129
+ chalk.yellow(' Please re-run your command to continue.'),
130
+ )
131
+ }
132
+ process.exit(1)
133
+ }
134
+
135
+ console.error(error(e.message))
136
+ process.exit(1)
137
+ }
138
+ },
139
+ )
@@ -16,7 +16,6 @@ export const startCommand = new Command('start')
16
16
  try {
17
17
  let containerName = name
18
18
 
19
- // Interactive selection if no name provided
20
19
  if (!containerName) {
21
20
  const containers = await containerManager.list()
22
21
  const stopped = containers.filter((c) => c.status !== 'running')
@@ -40,7 +39,6 @@ export const startCommand = new Command('start')
40
39
  containerName = selected
41
40
  }
42
41
 
43
- // Get container config
44
42
  const config = await containerManager.getConfig(containerName)
45
43
  if (!config) {
46
44
  console.error(error(`Container "${containerName}" not found`))
@@ -49,7 +47,6 @@ export const startCommand = new Command('start')
49
47
 
50
48
  const { engine: engineName } = config
51
49
 
52
- // Check if already running
53
50
  const running = await processManager.isRunning(containerName, {
54
51
  engine: engineName,
55
52
  })
@@ -58,10 +55,7 @@ export const startCommand = new Command('start')
58
55
  return
59
56
  }
60
57
 
61
- // Get engine defaults for port range and database name
62
58
  const engineDefaults = getEngineDefaults(engineName)
63
-
64
- // Get engine and start with retry (handles port race conditions)
65
59
  const engine = getEngine(engineName)
66
60
 
67
61
  const spinner = createSpinner(`Starting ${containerName}...`)
@@ -93,8 +87,8 @@ export const startCommand = new Command('start')
93
87
  spinner.succeed(`Container "${containerName}" started`)
94
88
  }
95
89
 
96
- // Ensure the user's database exists (if different from default)
97
- const defaultDb = engineDefaults.superuser // postgres or root
90
+ // Database might already exist, which is fine
91
+ const defaultDb = engineDefaults.superuser
98
92
  if (config.database && config.database !== defaultDb) {
99
93
  const dbSpinner = createSpinner(
100
94
  `Ensuring database "${config.database}" exists...`,
@@ -104,12 +98,10 @@ export const startCommand = new Command('start')
104
98
  await engine.createDatabase(config, config.database)
105
99
  dbSpinner.succeed(`Database "${config.database}" ready`)
106
100
  } catch {
107
- // Database might already exist, which is fine
108
101
  dbSpinner.succeed(`Database "${config.database}" ready`)
109
102
  }
110
103
  }
111
104
 
112
- // Show connection info
113
105
  const connectionString = engine.getConnectionString(config)
114
106
  console.log()
115
107
  console.log(chalk.gray(' Connection string:'))
@@ -13,7 +13,6 @@ export const stopCommand = new Command('stop')
13
13
  .action(async (name: string | undefined, options: { all?: boolean }) => {
14
14
  try {
15
15
  if (options.all) {
16
- // Stop all running containers
17
16
  const containers = await containerManager.list()
18
17
  const running = containers.filter((c) => c.status === 'running')
19
18
 
@@ -41,7 +40,6 @@ export const stopCommand = new Command('stop')
41
40
 
42
41
  let containerName = name
43
42
 
44
- // Interactive selection if no name provided
45
43
  if (!containerName) {
46
44
  const containers = await containerManager.list()
47
45
  const running = containers.filter((c) => c.status === 'running')
@@ -59,14 +57,12 @@ export const stopCommand = new Command('stop')
59
57
  containerName = selected
60
58
  }
61
59
 
62
- // Get container config
63
60
  const config = await containerManager.getConfig(containerName)
64
61
  if (!config) {
65
62
  console.error(error(`Container "${containerName}" not found`))
66
63
  process.exit(1)
67
64
  }
68
65
 
69
- // Check if running
70
66
  const running = await processManager.isRunning(containerName, {
71
67
  engine: config.engine,
72
68
  })
@@ -75,7 +71,6 @@ export const stopCommand = new Command('stop')
75
71
  return
76
72
  }
77
73
 
78
- // Get engine and stop
79
74
  const engine = getEngine(config.engine)
80
75
 
81
76
  const spinner = createSpinner(`Stopping ${containerName}...`)
@@ -11,15 +11,15 @@ export const urlCommand = new Command('url')
11
11
  .argument('[name]', 'Container name')
12
12
  .option('-c, --copy', 'Copy to clipboard')
13
13
  .option('-d, --database <database>', 'Use different database name')
14
+ .option('--json', 'Output as JSON with additional connection info')
14
15
  .action(
15
16
  async (
16
17
  name: string | undefined,
17
- options: { copy?: boolean; database?: string },
18
+ options: { copy?: boolean; database?: string; json?: boolean },
18
19
  ) => {
19
20
  try {
20
21
  let containerName = name
21
22
 
22
- // Interactive selection if no name provided
23
23
  if (!containerName) {
24
24
  const containers = await containerManager.list()
25
25
 
@@ -36,36 +36,41 @@ export const urlCommand = new Command('url')
36
36
  containerName = selected
37
37
  }
38
38
 
39
- // Get container config
40
39
  const config = await containerManager.getConfig(containerName)
41
40
  if (!config) {
42
41
  console.error(error(`Container "${containerName}" not found`))
43
42
  process.exit(1)
44
43
  }
45
44
 
46
- // Get connection string
47
45
  const engine = getEngine(config.engine)
48
- const connectionString = engine.getConnectionString(
49
- config,
50
- options.database,
51
- )
46
+ const databaseName = options.database || config.database
47
+ const connectionString = engine.getConnectionString(config, databaseName)
48
+
49
+ if (options.json) {
50
+ const jsonOutput = {
51
+ connectionString,
52
+ host: '127.0.0.1',
53
+ port: config.port,
54
+ database: databaseName,
55
+ user: config.engine === 'postgresql' ? 'postgres' : 'root',
56
+ engine: config.engine,
57
+ container: config.name,
58
+ }
59
+ console.log(JSON.stringify(jsonOutput, null, 2))
60
+ return
61
+ }
52
62
 
53
- // Copy to clipboard if requested
54
63
  if (options.copy) {
55
64
  const copied = await platformService.copyToClipboard(connectionString)
56
65
  if (copied) {
57
- // Output the string AND confirmation
58
66
  console.log(connectionString)
59
67
  console.error(success('Copied to clipboard'))
60
68
  } else {
61
- // Output the string but warn about clipboard
62
69
  console.log(connectionString)
63
70
  console.error(warning('Could not copy to clipboard'))
64
71
  }
65
72
  } else {
66
- // Just output the connection string (no newline formatting for easy piping)
67
73
  process.stdout.write(connectionString)
68
- // Add newline if stdout is a TTY (interactive terminal)
69
74
  if (process.stdout.isTTY) {
70
75
  console.log()
71
76
  }
@@ -0,0 +1,10 @@
1
+ export const ENGINE_ICONS: Record<string, string> = {
2
+ postgresql: '🐘',
3
+ mysql: '🐬',
4
+ }
5
+
6
+ export const DEFAULT_ENGINE_ICON = '▣'
7
+
8
+ export function getEngineIcon(engine: string): string {
9
+ return ENGINE_ICONS[engine] || DEFAULT_ENGINE_ICON
10
+ }
package/cli/helpers.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { existsSync } from 'fs'
2
+ import { readdir, lstat } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { exec } from 'child_process'
5
+ import { promisify } from 'util'
6
+ import { paths } from '../config/paths'
7
+ import {
8
+ getMysqldPath,
9
+ getMysqlVersion,
10
+ isMariaDB,
11
+ } from '../engines/mysql/binary-detection'
12
+
13
+ const execAsync = promisify(exec)
14
+
15
+ export type InstalledPostgresEngine = {
16
+ engine: 'postgresql'
17
+ version: string
18
+ platform: string
19
+ arch: string
20
+ path: string
21
+ sizeBytes: number
22
+ source: 'downloaded'
23
+ }
24
+
25
+ export type InstalledMysqlEngine = {
26
+ engine: 'mysql'
27
+ version: string
28
+ path: string
29
+ source: 'system'
30
+ isMariaDB: boolean
31
+ }
32
+
33
+ export type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
34
+
35
+ async function getPostgresVersion(binPath: string): Promise<string | null> {
36
+ const postgresPath = join(binPath, 'bin', 'postgres')
37
+ if (!existsSync(postgresPath)) {
38
+ return null
39
+ }
40
+
41
+ try {
42
+ const { stdout } = await execAsync(`"${postgresPath}" --version`)
43
+ const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
44
+ return match ? match[1] : null
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ export async function getInstalledPostgresEngines(): Promise<InstalledPostgresEngine[]> {
51
+ const binDir = paths.bin
52
+
53
+ if (!existsSync(binDir)) {
54
+ return []
55
+ }
56
+
57
+ const entries = await readdir(binDir, { withFileTypes: true })
58
+ const engines: InstalledPostgresEngine[] = []
59
+
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
63
+ if (match && match[1] === 'postgresql') {
64
+ const [, , majorVersion, platform, arch] = match
65
+ const dirPath = join(binDir, entry.name)
66
+
67
+ const actualVersion =
68
+ (await getPostgresVersion(dirPath)) || majorVersion
69
+
70
+ let sizeBytes = 0
71
+ try {
72
+ const files = await readdir(dirPath, { recursive: true })
73
+ for (const file of files) {
74
+ try {
75
+ const filePath = join(dirPath, file.toString())
76
+ const fileStat = await lstat(filePath)
77
+ if (fileStat.isFile()) {
78
+ sizeBytes += fileStat.size
79
+ }
80
+ } catch {
81
+ // Skip files we can't stat
82
+ }
83
+ }
84
+ } catch {
85
+ // Skip directories we can't read
86
+ }
87
+
88
+ engines.push({
89
+ engine: 'postgresql',
90
+ version: actualVersion,
91
+ platform,
92
+ arch,
93
+ path: dirPath,
94
+ sizeBytes,
95
+ source: 'downloaded',
96
+ })
97
+ }
98
+ }
99
+ }
100
+
101
+ engines.sort((a, b) => compareVersions(b.version, a.version))
102
+
103
+ return engines
104
+ }
105
+
106
+ async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
107
+ const mysqldPath = await getMysqldPath()
108
+ if (!mysqldPath) {
109
+ return null
110
+ }
111
+
112
+ const version = await getMysqlVersion(mysqldPath)
113
+ if (!version) {
114
+ return null
115
+ }
116
+
117
+ const mariadb = await isMariaDB()
118
+
119
+ return {
120
+ engine: 'mysql',
121
+ version,
122
+ path: mysqldPath,
123
+ source: 'system',
124
+ isMariaDB: mariadb,
125
+ }
126
+ }
127
+
128
+ export function compareVersions(a: string, b: string): number {
129
+ const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
130
+ const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
131
+
132
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
133
+ const numA = partsA[i] || 0
134
+ const numB = partsB[i] || 0
135
+ if (numA !== numB) return numA - numB
136
+ }
137
+ return 0
138
+ }
139
+
140
+ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
141
+ const engines: InstalledEngine[] = []
142
+
143
+ const pgEngines = await getInstalledPostgresEngines()
144
+ engines.push(...pgEngines)
145
+
146
+ const mysqlEngine = await getInstalledMysqlEngine()
147
+ if (mysqlEngine) {
148
+ engines.push(mysqlEngine)
149
+ }
150
+
151
+ return engines
152
+ }
package/cli/index.ts CHANGED
@@ -22,6 +22,8 @@ import { urlCommand } from './commands/url'
22
22
  import { infoCommand } from './commands/info'
23
23
  import { selfUpdateCommand } from './commands/self-update'
24
24
  import { versionCommand } from './commands/version'
25
+ import { runCommand } from './commands/run'
26
+ import { logsCommand } from './commands/logs'
25
27
  import { updateManager } from '../core/update-manager'
26
28
 
27
29
  /**
@@ -119,11 +121,12 @@ export async function run(): Promise<void> {
119
121
  program.addCommand(infoCommand)
120
122
  program.addCommand(selfUpdateCommand)
121
123
  program.addCommand(versionCommand)
124
+ program.addCommand(runCommand)
125
+ program.addCommand(logsCommand)
122
126
 
123
127
  // If no arguments provided, show interactive menu
124
128
  if (process.argv.length <= 2) {
125
- const { menuCommand: menu } = await import('./commands/menu')
126
- await menu.parseAsync([])
129
+ await menuCommand.parseAsync([])
127
130
  return
128
131
  }
129
132