spindb 0.8.2 → 0.9.1
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 +87 -7
- package/cli/commands/clone.ts +6 -0
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +170 -8
- package/cli/commands/doctor.ts +320 -0
- package/cli/commands/edit.ts +209 -9
- package/cli/commands/engines.ts +34 -3
- package/cli/commands/info.ts +81 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/logs.ts +9 -3
- package/cli/commands/menu/backup-handlers.ts +52 -21
- package/cli/commands/menu/container-handlers.ts +433 -127
- package/cli/commands/menu/engine-handlers.ts +128 -4
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/restore.ts +83 -23
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/helpers.ts +41 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +148 -7
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +191 -32
- package/core/dependency-manager.ts +18 -0
- package/core/error-handler.ts +31 -0
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/engines/index.ts +4 -0
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +48 -5
- package/engines/postgresql/index.ts +6 -0
- package/engines/sqlite/index.ts +606 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
1
2
|
import chalk from 'chalk'
|
|
2
3
|
import inquirer from 'inquirer'
|
|
3
4
|
import { updateManager } from '../../../core/update-manager'
|
|
5
|
+
import { containerManager } from '../../../core/container-manager'
|
|
6
|
+
import { configManager } from '../../../core/config-manager'
|
|
7
|
+
import { sqliteRegistry } from '../../../engines/sqlite/registry'
|
|
8
|
+
import { paths } from '../../../config/paths'
|
|
9
|
+
import { getSupportedEngines } from '../../../config/engine-defaults'
|
|
10
|
+
import { checkEngineDependencies } from '../../../core/dependency-manager'
|
|
4
11
|
import { createSpinner } from '../../ui/spinner'
|
|
5
12
|
import { header, success, error, warning, info } from '../../ui/theme'
|
|
6
13
|
import { pressEnterToContinue } from './shared'
|
|
14
|
+
import { Engine } from '../../../types'
|
|
7
15
|
|
|
8
16
|
export async function handleCheckUpdate(): Promise<void> {
|
|
9
17
|
console.clear()
|
|
@@ -92,3 +100,273 @@ export async function handleCheckUpdate(): Promise<void> {
|
|
|
92
100
|
await pressEnterToContinue()
|
|
93
101
|
}
|
|
94
102
|
}
|
|
103
|
+
|
|
104
|
+
type HealthCheckResult = {
|
|
105
|
+
name: string
|
|
106
|
+
status: 'ok' | 'warning' | 'error'
|
|
107
|
+
message: string
|
|
108
|
+
details?: string[]
|
|
109
|
+
action?: {
|
|
110
|
+
label: string
|
|
111
|
+
handler: () => Promise<void>
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function checkConfiguration(): Promise<HealthCheckResult> {
|
|
116
|
+
const configPath = paths.config
|
|
117
|
+
|
|
118
|
+
if (!existsSync(configPath)) {
|
|
119
|
+
return {
|
|
120
|
+
name: 'Configuration',
|
|
121
|
+
status: 'ok',
|
|
122
|
+
message: 'No config file yet (will be created on first use)',
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const config = await configManager.load()
|
|
128
|
+
const binaryCount = Object.keys(config.binaries || {}).length
|
|
129
|
+
const isStale = await configManager.isStale()
|
|
130
|
+
|
|
131
|
+
if (isStale) {
|
|
132
|
+
return {
|
|
133
|
+
name: 'Configuration',
|
|
134
|
+
status: 'warning',
|
|
135
|
+
message: 'Binary cache is stale (>7 days old)',
|
|
136
|
+
details: [`Binary tools cached: ${binaryCount}`],
|
|
137
|
+
action: {
|
|
138
|
+
label: 'Refresh binary cache',
|
|
139
|
+
handler: async () => {
|
|
140
|
+
await configManager.refreshAllBinaries()
|
|
141
|
+
console.log(success('Binary cache refreshed'))
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name: 'Configuration',
|
|
149
|
+
status: 'ok',
|
|
150
|
+
message: 'Configuration valid',
|
|
151
|
+
details: [`Binary tools cached: ${binaryCount}`],
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
name: 'Configuration',
|
|
156
|
+
status: 'error',
|
|
157
|
+
message: 'Configuration file is corrupted',
|
|
158
|
+
details: [(err as Error).message],
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function checkContainers(): Promise<HealthCheckResult> {
|
|
164
|
+
try {
|
|
165
|
+
const containers = await containerManager.list()
|
|
166
|
+
|
|
167
|
+
if (containers.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
name: 'Containers',
|
|
170
|
+
status: 'ok',
|
|
171
|
+
message: 'No containers (create one with: spindb create)',
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const byEngine: Record<string, { running: number; stopped: number }> = {}
|
|
176
|
+
|
|
177
|
+
for (const c of containers) {
|
|
178
|
+
const engineName = c.engine
|
|
179
|
+
if (!byEngine[engineName]) {
|
|
180
|
+
byEngine[engineName] = { running: 0, stopped: 0 }
|
|
181
|
+
}
|
|
182
|
+
if (c.status === 'running') {
|
|
183
|
+
byEngine[engineName].running++
|
|
184
|
+
} else {
|
|
185
|
+
byEngine[engineName].stopped++
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const details = Object.entries(byEngine).map(([engine, counts]) => {
|
|
190
|
+
if (engine === Engine.SQLite) {
|
|
191
|
+
return `${engine}: ${counts.running} exist, ${counts.stopped} missing`
|
|
192
|
+
}
|
|
193
|
+
return `${engine}: ${counts.running} running, ${counts.stopped} stopped`
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
name: 'Containers',
|
|
198
|
+
status: 'ok',
|
|
199
|
+
message: `${containers.length} container(s)`,
|
|
200
|
+
details,
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return {
|
|
204
|
+
name: 'Containers',
|
|
205
|
+
status: 'error',
|
|
206
|
+
message: 'Failed to list containers',
|
|
207
|
+
details: [(err as Error).message],
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function checkSqliteRegistry(): Promise<HealthCheckResult> {
|
|
213
|
+
try {
|
|
214
|
+
const entries = await sqliteRegistry.list()
|
|
215
|
+
|
|
216
|
+
if (entries.length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
name: 'SQLite Registry',
|
|
219
|
+
status: 'ok',
|
|
220
|
+
message: 'No SQLite databases registered',
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const orphans = await sqliteRegistry.findOrphans()
|
|
225
|
+
|
|
226
|
+
if (orphans.length > 0) {
|
|
227
|
+
return {
|
|
228
|
+
name: 'SQLite Registry',
|
|
229
|
+
status: 'warning',
|
|
230
|
+
message: `${orphans.length} orphaned entr${orphans.length === 1 ? 'y' : 'ies'} found`,
|
|
231
|
+
details: orphans.map((o) => `"${o.name}" → ${o.filePath}`),
|
|
232
|
+
action: {
|
|
233
|
+
label: 'Remove orphaned entries from registry',
|
|
234
|
+
handler: async () => {
|
|
235
|
+
const count = await sqliteRegistry.removeOrphans()
|
|
236
|
+
console.log(success(`Removed ${count} orphaned entries`))
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
name: 'SQLite Registry',
|
|
244
|
+
status: 'ok',
|
|
245
|
+
message: `${entries.length} database(s) registered, all files exist`,
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return {
|
|
249
|
+
name: 'SQLite Registry',
|
|
250
|
+
status: 'warning',
|
|
251
|
+
message: 'Could not check registry',
|
|
252
|
+
details: [(err as Error).message],
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function checkBinaries(): Promise<HealthCheckResult> {
|
|
258
|
+
try {
|
|
259
|
+
const engines = getSupportedEngines()
|
|
260
|
+
const results: string[] = []
|
|
261
|
+
let hasWarning = false
|
|
262
|
+
|
|
263
|
+
for (const engine of engines) {
|
|
264
|
+
const statuses = await checkEngineDependencies(engine)
|
|
265
|
+
const installed = statuses.filter((s) => s.installed).length
|
|
266
|
+
const total = statuses.length
|
|
267
|
+
|
|
268
|
+
if (installed < total) {
|
|
269
|
+
hasWarning = true
|
|
270
|
+
results.push(`${engine}: ${installed}/${total} tools installed`)
|
|
271
|
+
} else {
|
|
272
|
+
results.push(`${engine}: all ${total} tools available`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
name: 'Database Tools',
|
|
278
|
+
status: hasWarning ? 'warning' : 'ok',
|
|
279
|
+
message: hasWarning ? 'Some tools missing' : 'All tools available',
|
|
280
|
+
details: results,
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return {
|
|
284
|
+
name: 'Database Tools',
|
|
285
|
+
status: 'error',
|
|
286
|
+
message: 'Failed to check tools',
|
|
287
|
+
details: [(err as Error).message],
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function displayResult(result: HealthCheckResult): void {
|
|
293
|
+
const icon =
|
|
294
|
+
result.status === 'ok'
|
|
295
|
+
? chalk.green('✓')
|
|
296
|
+
: result.status === 'warning'
|
|
297
|
+
? chalk.yellow('⚠')
|
|
298
|
+
: chalk.red('✕')
|
|
299
|
+
|
|
300
|
+
console.log(`${icon} ${chalk.bold(result.name)}`)
|
|
301
|
+
console.log(` └─ ${result.message}`)
|
|
302
|
+
|
|
303
|
+
if (result.details) {
|
|
304
|
+
for (const detail of result.details) {
|
|
305
|
+
console.log(chalk.gray(` ${detail}`))
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
console.log()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function handleDoctor(): Promise<void> {
|
|
312
|
+
console.clear()
|
|
313
|
+
console.log(header('SpinDB Health Check'))
|
|
314
|
+
console.log()
|
|
315
|
+
|
|
316
|
+
const checks = [
|
|
317
|
+
await checkConfiguration(),
|
|
318
|
+
await checkContainers(),
|
|
319
|
+
await checkSqliteRegistry(),
|
|
320
|
+
await checkBinaries(),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
// Display results
|
|
324
|
+
for (const check of checks) {
|
|
325
|
+
displayResult(check)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Collect actions for warnings
|
|
329
|
+
const actionsAvailable = checks.filter((c) => c.action)
|
|
330
|
+
|
|
331
|
+
if (actionsAvailable.length > 0) {
|
|
332
|
+
type ActionChoice = {
|
|
333
|
+
name: string
|
|
334
|
+
value: string
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const choices: ActionChoice[] = [
|
|
338
|
+
...actionsAvailable.map((c) => ({
|
|
339
|
+
name: c.action!.label,
|
|
340
|
+
value: c.name,
|
|
341
|
+
})),
|
|
342
|
+
{ name: chalk.gray('Skip (do nothing)'), value: 'skip' },
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
const { selectedAction } = await inquirer.prompt<{
|
|
346
|
+
selectedAction: string
|
|
347
|
+
}>([
|
|
348
|
+
{
|
|
349
|
+
type: 'list',
|
|
350
|
+
name: 'selectedAction',
|
|
351
|
+
message: 'What would you like to do?',
|
|
352
|
+
choices,
|
|
353
|
+
},
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
if (selectedAction !== 'skip') {
|
|
357
|
+
const check = checks.find((c) => c.name === selectedAction)
|
|
358
|
+
if (check?.action) {
|
|
359
|
+
console.log()
|
|
360
|
+
await check.action.handler()
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
const hasIssues = checks.some((c) => c.status !== 'ok')
|
|
365
|
+
if (!hasIssues) {
|
|
366
|
+
console.log(chalk.green('All systems healthy! ✓'))
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.log()
|
|
371
|
+
await pressEnterToContinue()
|
|
372
|
+
}
|
package/cli/commands/restore.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { tmpdir } from 'os'
|
|
|
16
16
|
import { join } from 'path'
|
|
17
17
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
18
18
|
import { platformService } from '../../core/platform-service'
|
|
19
|
+
import { TransactionManager } from '../../core/transaction-manager'
|
|
20
|
+
import { logDebug } from '../../core/error-handler'
|
|
19
21
|
|
|
20
22
|
export const restoreCommand = new Command('restore')
|
|
21
23
|
.description('Restore a backup to a container')
|
|
@@ -245,41 +247,99 @@ export const restoreCommand = new Command('restore')
|
|
|
245
247
|
const format = await engine.detectBackupFormat(backupPath)
|
|
246
248
|
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
247
249
|
|
|
250
|
+
// Use TransactionManager to ensure database is cleaned up on restore failure
|
|
251
|
+
const tx = new TransactionManager()
|
|
252
|
+
let databaseCreated = false
|
|
253
|
+
|
|
248
254
|
const dbSpinner = createSpinner(
|
|
249
255
|
`Creating database "${databaseName}"...`,
|
|
250
256
|
)
|
|
251
257
|
dbSpinner.start()
|
|
252
258
|
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
try {
|
|
260
|
+
await engine.createDatabase(config, databaseName)
|
|
261
|
+
databaseCreated = true
|
|
262
|
+
dbSpinner.succeed(`Database "${databaseName}" ready`)
|
|
263
|
+
|
|
264
|
+
// Register rollback to drop database if restore fails
|
|
265
|
+
tx.addRollback({
|
|
266
|
+
description: `Drop database "${databaseName}"`,
|
|
267
|
+
execute: async () => {
|
|
268
|
+
try {
|
|
269
|
+
await engine.dropDatabase(config, databaseName)
|
|
270
|
+
logDebug(`Rolled back: dropped database "${databaseName}"`)
|
|
271
|
+
} catch (dropErr) {
|
|
272
|
+
logDebug(
|
|
273
|
+
`Failed to drop database during rollback: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
await containerManager.addDatabase(containerName, databaseName)
|
|
280
|
+
|
|
281
|
+
// Register rollback to remove database from container tracking
|
|
282
|
+
tx.addRollback({
|
|
283
|
+
description: `Remove "${databaseName}" from container tracking`,
|
|
284
|
+
execute: async () => {
|
|
285
|
+
try {
|
|
286
|
+
await containerManager.removeDatabase(
|
|
287
|
+
containerName,
|
|
288
|
+
databaseName,
|
|
289
|
+
)
|
|
290
|
+
logDebug(
|
|
291
|
+
`Rolled back: removed "${databaseName}" from container tracking`,
|
|
292
|
+
)
|
|
293
|
+
} catch (removeErr) {
|
|
294
|
+
logDebug(
|
|
295
|
+
`Failed to remove database from tracking during rollback: ${removeErr instanceof Error ? removeErr.message : String(removeErr)}`,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
})
|
|
255
300
|
|
|
256
|
-
|
|
301
|
+
const restoreSpinner = createSpinner('Restoring backup...')
|
|
302
|
+
restoreSpinner.start()
|
|
257
303
|
|
|
258
|
-
|
|
259
|
-
|
|
304
|
+
const result = await engine.restore(config, backupPath, {
|
|
305
|
+
database: databaseName,
|
|
306
|
+
createDatabase: false,
|
|
307
|
+
})
|
|
260
308
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
309
|
+
// Check if restore completely failed (non-zero code with no data restored)
|
|
310
|
+
if (result.code !== 0 && result.stderr?.includes('FATAL')) {
|
|
311
|
+
restoreSpinner.fail('Restore failed')
|
|
312
|
+
throw new Error(result.stderr || 'Restore failed with fatal error')
|
|
313
|
+
}
|
|
265
314
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
if (result.code === 0) {
|
|
316
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
317
|
+
} else {
|
|
318
|
+
// pg_restore often returns warnings even on success
|
|
319
|
+
restoreSpinner.warn('Restore completed with warnings')
|
|
320
|
+
if (result.stderr) {
|
|
321
|
+
console.log(chalk.yellow('\n Warnings:'))
|
|
322
|
+
const lines = result.stderr.split('\n').slice(0, 5)
|
|
323
|
+
lines.forEach((line) => {
|
|
324
|
+
if (line.trim()) {
|
|
325
|
+
console.log(chalk.gray(` ${line}`))
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
if (result.stderr.split('\n').length > 5) {
|
|
329
|
+
console.log(chalk.gray(' ...'))
|
|
277
330
|
}
|
|
278
|
-
})
|
|
279
|
-
if (result.stderr.split('\n').length > 5) {
|
|
280
|
-
console.log(chalk.gray(' ...'))
|
|
281
331
|
}
|
|
282
332
|
}
|
|
333
|
+
|
|
334
|
+
// Restore succeeded - commit transaction (clear rollback actions)
|
|
335
|
+
tx.commit()
|
|
336
|
+
} catch (restoreErr) {
|
|
337
|
+
// Restore failed - execute rollbacks to clean up created database
|
|
338
|
+
if (databaseCreated) {
|
|
339
|
+
console.log(chalk.yellow('\n Cleaning up after failed restore...'))
|
|
340
|
+
await tx.rollback()
|
|
341
|
+
}
|
|
342
|
+
throw restoreErr
|
|
283
343
|
}
|
|
284
344
|
|
|
285
345
|
const connectionString = engine.getConnectionString(
|
package/cli/commands/run.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { getEngine } from '../../engines'
|
|
|
7
7
|
import { promptInstallDependencies } from '../ui/prompts'
|
|
8
8
|
import { error, warning } from '../ui/theme'
|
|
9
9
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
10
|
+
import { Engine } from '../../types'
|
|
10
11
|
|
|
11
12
|
export const runCommand = new Command('run')
|
|
12
13
|
.description('Run SQL file or statement against a container')
|
|
@@ -31,16 +32,29 @@ export const runCommand = new Command('run')
|
|
|
31
32
|
|
|
32
33
|
const { engine: engineName } = config
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// SQLite: check file exists instead of running status
|
|
36
|
+
if (engineName === Engine.SQLite) {
|
|
37
|
+
if (!existsSync(config.database)) {
|
|
38
|
+
console.error(
|
|
39
|
+
error(
|
|
40
|
+
`SQLite database file not found: ${config.database}`,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
// Server databases need to be running
|
|
47
|
+
const running = await processManager.isRunning(containerName, {
|
|
48
|
+
engine: engineName,
|
|
49
|
+
})
|
|
50
|
+
if (!running) {
|
|
51
|
+
console.error(
|
|
52
|
+
error(
|
|
53
|
+
`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
if (file && options.sql) {
|
|
@@ -123,7 +137,9 @@ export const runCommand = new Command('run')
|
|
|
123
137
|
const missingTool = matchingPattern
|
|
124
138
|
.replace(' not found', '')
|
|
125
139
|
.replace(' client', '')
|
|
126
|
-
|
|
140
|
+
// Determine engine from the missing tool name
|
|
141
|
+
const toolEngine = missingTool === 'mysql' ? Engine.MySQL : Engine.PostgreSQL
|
|
142
|
+
const installed = await promptInstallDependencies(missingTool, toolEngine)
|
|
127
143
|
if (installed) {
|
|
128
144
|
console.log(
|
|
129
145
|
chalk.yellow(' Please re-run your command to continue.'),
|
package/cli/commands/url.ts
CHANGED
|
@@ -47,15 +47,23 @@ export const urlCommand = new Command('url')
|
|
|
47
47
|
const connectionString = engine.getConnectionString(config, databaseName)
|
|
48
48
|
|
|
49
49
|
if (options.json) {
|
|
50
|
-
const jsonOutput =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const jsonOutput =
|
|
51
|
+
config.engine === 'sqlite'
|
|
52
|
+
? {
|
|
53
|
+
connectionString,
|
|
54
|
+
path: databaseName,
|
|
55
|
+
engine: config.engine,
|
|
56
|
+
container: config.name,
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
connectionString,
|
|
60
|
+
host: '127.0.0.1',
|
|
61
|
+
port: config.port,
|
|
62
|
+
database: databaseName,
|
|
63
|
+
user: config.engine === 'postgresql' ? 'postgres' : 'root',
|
|
64
|
+
engine: config.engine,
|
|
65
|
+
container: config.name,
|
|
66
|
+
}
|
|
59
67
|
console.log(JSON.stringify(jsonOutput, null, 2))
|
|
60
68
|
return
|
|
61
69
|
}
|
package/cli/constants.ts
CHANGED
package/cli/helpers.ts
CHANGED
|
@@ -30,7 +30,17 @@ export type InstalledMysqlEngine = {
|
|
|
30
30
|
isMariaDB: boolean
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type
|
|
33
|
+
export type InstalledSqliteEngine = {
|
|
34
|
+
engine: 'sqlite'
|
|
35
|
+
version: string
|
|
36
|
+
path: string
|
|
37
|
+
source: 'system'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type InstalledEngine =
|
|
41
|
+
| InstalledPostgresEngine
|
|
42
|
+
| InstalledMysqlEngine
|
|
43
|
+
| InstalledSqliteEngine
|
|
34
44
|
|
|
35
45
|
async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
36
46
|
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
@@ -125,6 +135,31 @@ async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
|
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
async function getInstalledSqliteEngine(): Promise<InstalledSqliteEngine | null> {
|
|
139
|
+
try {
|
|
140
|
+
// TODO: Use 'where sqlite3' on Windows when adding Windows support
|
|
141
|
+
const { stdout: whichOutput } = await execAsync('which sqlite3')
|
|
142
|
+
const sqlitePath = whichOutput.trim()
|
|
143
|
+
if (!sqlitePath) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { stdout: versionOutput } = await execAsync(`"${sqlitePath}" --version`)
|
|
148
|
+
// sqlite3 --version outputs: "3.43.2 2023-10-10 12:14:04 ..."
|
|
149
|
+
const versionMatch = versionOutput.match(/^([\d.]+)/)
|
|
150
|
+
const version = versionMatch ? versionMatch[1] : 'unknown'
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
engine: 'sqlite',
|
|
154
|
+
version,
|
|
155
|
+
path: sqlitePath,
|
|
156
|
+
source: 'system',
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
export function compareVersions(a: string, b: string): number {
|
|
129
164
|
const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
|
|
130
165
|
const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
|
|
@@ -148,5 +183,10 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
|
148
183
|
engines.push(mysqlEngine)
|
|
149
184
|
}
|
|
150
185
|
|
|
186
|
+
const sqliteEngine = await getInstalledSqliteEngine()
|
|
187
|
+
if (sqliteEngine) {
|
|
188
|
+
engines.push(sqliteEngine)
|
|
189
|
+
}
|
|
190
|
+
|
|
151
191
|
return engines
|
|
152
192
|
}
|
package/cli/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { selfUpdateCommand } from './commands/self-update'
|
|
|
24
24
|
import { versionCommand } from './commands/version'
|
|
25
25
|
import { runCommand } from './commands/run'
|
|
26
26
|
import { logsCommand } from './commands/logs'
|
|
27
|
+
import { doctorCommand } from './commands/doctor'
|
|
27
28
|
import { updateManager } from '../core/update-manager'
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -123,6 +124,7 @@ export async function run(): Promise<void> {
|
|
|
123
124
|
program.addCommand(versionCommand)
|
|
124
125
|
program.addCommand(runCommand)
|
|
125
126
|
program.addCommand(logsCommand)
|
|
127
|
+
program.addCommand(doctorCommand)
|
|
126
128
|
|
|
127
129
|
// If no arguments provided, show interactive menu
|
|
128
130
|
if (process.argv.length <= 2) {
|