spindb 0.8.2 → 0.9.0
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 +80 -7
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +113 -1
- package/cli/commands/doctor.ts +319 -0
- package/cli/commands/edit.ts +203 -5
- package/cli/commands/info.ts +79 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/menu/backup-handlers.ts +26 -13
- package/cli/commands/menu/container-handlers.ts +408 -120
- 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/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +140 -5
- 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 +119 -11
- package/core/dependency-manager.ts +18 -0
- package/engines/index.ts +4 -0
- package/engines/sqlite/index.ts +597 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from './container-handlers'
|
|
14
14
|
import { handleBackup, handleRestore, handleClone } from './backup-handlers'
|
|
15
15
|
import { handleEngines } from './engine-handlers'
|
|
16
|
-
import { handleCheckUpdate } from './update-handlers'
|
|
16
|
+
import { handleCheckUpdate, handleDoctor } from './update-handlers'
|
|
17
17
|
import { type MenuChoice } from './shared'
|
|
18
18
|
|
|
19
19
|
async function showMainMenu(): Promise<void> {
|
|
@@ -97,6 +97,7 @@ async function showMainMenu(): Promise<void> {
|
|
|
97
97
|
disabled: hasEngines ? false : 'No engines installed',
|
|
98
98
|
},
|
|
99
99
|
new inquirer.Separator(),
|
|
100
|
+
{ name: `${chalk.bgRed.white('+')} System health check`, value: 'doctor' },
|
|
100
101
|
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
101
102
|
{ name: `${chalk.gray('⏻')} Exit`, value: 'exit' },
|
|
102
103
|
]
|
|
@@ -136,6 +137,9 @@ async function showMainMenu(): Promise<void> {
|
|
|
136
137
|
case 'engines':
|
|
137
138
|
await handleEngines()
|
|
138
139
|
break
|
|
140
|
+
case 'doctor':
|
|
141
|
+
await handleDoctor()
|
|
142
|
+
break
|
|
139
143
|
case 'check-update':
|
|
140
144
|
await handleCheckUpdate()
|
|
141
145
|
break
|
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
isUsqlInstalled,
|
|
7
7
|
isPgcliInstalled,
|
|
8
8
|
isMycliInstalled,
|
|
9
|
+
isLitecliInstalled,
|
|
9
10
|
detectPackageManager,
|
|
10
11
|
installUsql,
|
|
11
12
|
installPgcli,
|
|
12
13
|
installMycli,
|
|
14
|
+
installLitecli,
|
|
13
15
|
getUsqlManualInstructions,
|
|
14
16
|
getPgcliManualInstructions,
|
|
15
17
|
getMycliManualInstructions,
|
|
18
|
+
getLitecliManualInstructions,
|
|
16
19
|
} from '../../../core/dependency-manager'
|
|
17
20
|
import { platformService } from '../../../core/platform-service'
|
|
18
21
|
import { getEngine } from '../../../engines'
|
|
@@ -66,10 +69,11 @@ export async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
66
69
|
const shellCheckSpinner = createSpinner('Checking available shells...')
|
|
67
70
|
shellCheckSpinner.start()
|
|
68
71
|
|
|
69
|
-
const [usqlInstalled, pgcliInstalled, mycliInstalled] = await Promise.all([
|
|
72
|
+
const [usqlInstalled, pgcliInstalled, mycliInstalled, litecliInstalled] = await Promise.all([
|
|
70
73
|
isUsqlInstalled(),
|
|
71
74
|
isPgcliInstalled(),
|
|
72
75
|
isMycliInstalled(),
|
|
76
|
+
isLitecliInstalled(),
|
|
73
77
|
])
|
|
74
78
|
|
|
75
79
|
shellCheckSpinner.stop()
|
|
@@ -84,12 +88,36 @@ export async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
84
88
|
| 'install-pgcli'
|
|
85
89
|
| 'mycli'
|
|
86
90
|
| 'install-mycli'
|
|
91
|
+
| 'litecli'
|
|
92
|
+
| 'install-litecli'
|
|
87
93
|
| 'back'
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
// Engine-specific shell names
|
|
96
|
+
let defaultShellName: string
|
|
97
|
+
let engineSpecificCli: string
|
|
98
|
+
let engineSpecificInstalled: boolean
|
|
99
|
+
let engineSpecificValue: ShellChoice
|
|
100
|
+
let engineSpecificInstallValue: ShellChoice
|
|
101
|
+
|
|
102
|
+
if (config.engine === 'sqlite') {
|
|
103
|
+
defaultShellName = 'sqlite3'
|
|
104
|
+
engineSpecificCli = 'litecli'
|
|
105
|
+
engineSpecificInstalled = litecliInstalled
|
|
106
|
+
engineSpecificValue = 'litecli'
|
|
107
|
+
engineSpecificInstallValue = 'install-litecli'
|
|
108
|
+
} else if (config.engine === 'mysql') {
|
|
109
|
+
defaultShellName = 'mysql'
|
|
110
|
+
engineSpecificCli = 'mycli'
|
|
111
|
+
engineSpecificInstalled = mycliInstalled
|
|
112
|
+
engineSpecificValue = 'mycli'
|
|
113
|
+
engineSpecificInstallValue = 'install-mycli'
|
|
114
|
+
} else {
|
|
115
|
+
defaultShellName = 'psql'
|
|
116
|
+
engineSpecificCli = 'pgcli'
|
|
117
|
+
engineSpecificInstalled = pgcliInstalled
|
|
118
|
+
engineSpecificValue = 'pgcli'
|
|
119
|
+
engineSpecificInstallValue = 'install-pgcli'
|
|
120
|
+
}
|
|
93
121
|
|
|
94
122
|
const choices: Array<{ name: string; value: ShellChoice } | inquirer.Separator> = [
|
|
95
123
|
{
|
|
@@ -101,15 +129,16 @@ export async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
101
129
|
if (engineSpecificInstalled) {
|
|
102
130
|
choices.push({
|
|
103
131
|
name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
|
|
104
|
-
value:
|
|
132
|
+
value: engineSpecificValue,
|
|
105
133
|
})
|
|
106
134
|
} else {
|
|
107
135
|
choices.push({
|
|
108
136
|
name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
|
|
109
|
-
value:
|
|
137
|
+
value: engineSpecificInstallValue,
|
|
110
138
|
})
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
// usql supports SQLite too
|
|
113
142
|
if (usqlInstalled) {
|
|
114
143
|
choices.push({
|
|
115
144
|
name: '⚡ Use usql (universal SQL client)',
|
|
@@ -241,6 +270,39 @@ export async function handleOpenShell(containerName: string): Promise<void> {
|
|
|
241
270
|
return
|
|
242
271
|
}
|
|
243
272
|
|
|
273
|
+
if (shellChoice === 'install-litecli') {
|
|
274
|
+
console.log()
|
|
275
|
+
console.log(info('Installing litecli for enhanced SQLite shell...'))
|
|
276
|
+
const pm = await detectPackageManager()
|
|
277
|
+
if (pm) {
|
|
278
|
+
const result = await installLitecli(pm)
|
|
279
|
+
if (result.success) {
|
|
280
|
+
console.log(success('litecli installed successfully!'))
|
|
281
|
+
console.log()
|
|
282
|
+
await launchShell(containerName, config, connectionString, 'litecli')
|
|
283
|
+
} else {
|
|
284
|
+
console.error(error(`Failed to install litecli: ${result.error}`))
|
|
285
|
+
console.log()
|
|
286
|
+
console.log(chalk.gray('Manual installation:'))
|
|
287
|
+
for (const instruction of getLitecliManualInstructions()) {
|
|
288
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
289
|
+
}
|
|
290
|
+
console.log()
|
|
291
|
+
await pressEnterToContinue()
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
console.error(error('No supported package manager found'))
|
|
295
|
+
console.log()
|
|
296
|
+
console.log(chalk.gray('Manual installation:'))
|
|
297
|
+
for (const instruction of getLitecliManualInstructions()) {
|
|
298
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
299
|
+
}
|
|
300
|
+
console.log()
|
|
301
|
+
await pressEnterToContinue()
|
|
302
|
+
}
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
244
306
|
await launchShell(containerName, config, connectionString, shellChoice)
|
|
245
307
|
}
|
|
246
308
|
|
|
@@ -248,7 +310,7 @@ async function launchShell(
|
|
|
248
310
|
containerName: string,
|
|
249
311
|
config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
|
|
250
312
|
connectionString: string,
|
|
251
|
-
shellType: 'default' | 'usql' | 'pgcli' | 'mycli',
|
|
313
|
+
shellType: 'default' | 'usql' | 'pgcli' | 'mycli' | 'litecli',
|
|
252
314
|
): Promise<void> {
|
|
253
315
|
console.log(info(`Connecting to ${containerName}...`))
|
|
254
316
|
console.log()
|
|
@@ -275,11 +337,21 @@ async function launchShell(
|
|
|
275
337
|
config.database,
|
|
276
338
|
]
|
|
277
339
|
installHint = 'brew install mycli'
|
|
340
|
+
} else if (shellType === 'litecli') {
|
|
341
|
+
// litecli takes the database file path directly
|
|
342
|
+
shellCmd = 'litecli'
|
|
343
|
+
shellArgs = [config.database]
|
|
344
|
+
installHint = 'brew install litecli'
|
|
278
345
|
} else if (shellType === 'usql') {
|
|
279
|
-
// usql accepts connection strings directly for
|
|
346
|
+
// usql accepts connection strings directly for PostgreSQL, MySQL, and SQLite
|
|
280
347
|
shellCmd = 'usql'
|
|
281
348
|
shellArgs = [connectionString]
|
|
282
349
|
installHint = 'brew tap xo/xo && brew install xo/xo/usql'
|
|
350
|
+
} else if (config.engine === 'sqlite') {
|
|
351
|
+
// Default SQLite shell
|
|
352
|
+
shellCmd = 'sqlite3'
|
|
353
|
+
shellArgs = [config.database]
|
|
354
|
+
installHint = 'brew install sqlite3'
|
|
283
355
|
} else if (config.engine === 'mysql') {
|
|
284
356
|
shellCmd = 'mysql'
|
|
285
357
|
shellArgs = [
|
|
@@ -302,19 +374,31 @@ async function launchShell(
|
|
|
302
374
|
stdio: 'inherit',
|
|
303
375
|
})
|
|
304
376
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(chalk.cyan(` ${installHint}`))
|
|
377
|
+
await new Promise<void>((resolve) => {
|
|
378
|
+
let settled = false
|
|
379
|
+
|
|
380
|
+
const settle = () => {
|
|
381
|
+
if (!settled) {
|
|
382
|
+
settled = true
|
|
383
|
+
resolve()
|
|
384
|
+
}
|
|
314
385
|
}
|
|
315
|
-
})
|
|
316
386
|
|
|
317
|
-
|
|
318
|
-
|
|
387
|
+
shellProcess.on('error', (err: NodeJS.ErrnoException) => {
|
|
388
|
+
if (err.code === 'ENOENT') {
|
|
389
|
+
console.log(warning(`${shellCmd} not found on your system.`))
|
|
390
|
+
console.log()
|
|
391
|
+
console.log(chalk.gray(' Connect manually with:'))
|
|
392
|
+
console.log(chalk.cyan(` ${connectionString}`))
|
|
393
|
+
console.log()
|
|
394
|
+
console.log(chalk.gray(` Install ${shellCmd}:`))
|
|
395
|
+
console.log(chalk.cyan(` ${installHint}`))
|
|
396
|
+
} else {
|
|
397
|
+
console.log(error(`Failed to start ${shellCmd}: ${err.message}`))
|
|
398
|
+
}
|
|
399
|
+
settle()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
shellProcess.on('close', settle)
|
|
319
403
|
})
|
|
320
404
|
}
|
|
@@ -169,11 +169,23 @@ export async function handleViewLogs(containerName: string): Promise<void> {
|
|
|
169
169
|
stdio: 'inherit',
|
|
170
170
|
})
|
|
171
171
|
await new Promise<void>((resolve) => {
|
|
172
|
-
|
|
172
|
+
let settled = false
|
|
173
|
+
|
|
174
|
+
const cleanup = () => {
|
|
175
|
+
if (!settled) {
|
|
176
|
+
settled = true
|
|
177
|
+
process.off('SIGINT', handleSigint)
|
|
178
|
+
resolve()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handleSigint = () => {
|
|
173
183
|
child.kill('SIGTERM')
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
184
|
+
cleanup()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
process.on('SIGINT', handleSigint)
|
|
188
|
+
child.on('close', cleanup)
|
|
177
189
|
})
|
|
178
190
|
return
|
|
179
191
|
}
|
|
@@ -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/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/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) {
|