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.
- package/README.md +421 -294
- package/cli/commands/backup.ts +1 -30
- package/cli/commands/clone.ts +0 -6
- package/cli/commands/config.ts +7 -1
- package/cli/commands/connect.ts +1 -16
- package/cli/commands/create.ts +4 -55
- package/cli/commands/delete.ts +0 -6
- package/cli/commands/edit.ts +9 -25
- package/cli/commands/engines.ts +10 -188
- package/cli/commands/info.ts +7 -34
- package/cli/commands/list.ts +2 -18
- package/cli/commands/logs.ts +118 -0
- package/cli/commands/menu/backup-handlers.ts +749 -0
- package/cli/commands/menu/container-handlers.ts +825 -0
- package/cli/commands/menu/engine-handlers.ts +362 -0
- package/cli/commands/menu/index.ts +179 -0
- package/cli/commands/menu/shared.ts +26 -0
- package/cli/commands/menu/shell-handlers.ts +320 -0
- package/cli/commands/menu/sql-handlers.ts +194 -0
- package/cli/commands/menu/update-handlers.ts +94 -0
- package/cli/commands/restore.ts +2 -28
- package/cli/commands/run.ts +139 -0
- package/cli/commands/start.ts +2 -10
- package/cli/commands/stop.ts +0 -5
- package/cli/commands/url.ts +18 -13
- package/cli/constants.ts +10 -0
- package/cli/helpers.ts +152 -0
- package/cli/index.ts +5 -2
- package/cli/ui/prompts.ts +3 -11
- package/core/dependency-manager.ts +0 -163
- package/core/error-handler.ts +0 -26
- package/core/platform-service.ts +60 -40
- package/core/start-with-retry.ts +3 -28
- package/core/transaction-manager.ts +0 -8
- package/engines/base-engine.ts +10 -0
- package/engines/mysql/binary-detection.ts +1 -1
- package/engines/mysql/index.ts +78 -2
- package/engines/postgresql/index.ts +49 -0
- package/package.json +1 -1
- package/cli/commands/menu.ts +0 -2670
package/cli/commands/restore.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
+
)
|
package/cli/commands/start.ts
CHANGED
|
@@ -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
|
-
//
|
|
97
|
-
const defaultDb = engineDefaults.superuser
|
|
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:'))
|
package/cli/commands/stop.ts
CHANGED
|
@@ -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}...`)
|
package/cli/commands/url.ts
CHANGED
|
@@ -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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
}
|
package/cli/constants.ts
ADDED
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
|
-
|
|
126
|
-
await menu.parseAsync([])
|
|
129
|
+
await menuCommand.parseAsync([])
|
|
127
130
|
return
|
|
128
131
|
}
|
|
129
132
|
|