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.
@@ -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
- const defaultShellName = config.engine === 'mysql' ? 'mysql' : 'psql'
90
- const engineSpecificCli = config.engine === 'mysql' ? 'mycli' : 'pgcli'
91
- const engineSpecificInstalled =
92
- config.engine === 'mysql' ? mycliInstalled : pgcliInstalled
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: config.engine === 'mysql' ? 'mycli' : 'pgcli',
132
+ value: engineSpecificValue,
105
133
  })
106
134
  } else {
107
135
  choices.push({
108
136
  name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
109
- value: config.engine === 'mysql' ? 'install-mycli' : 'install-pgcli',
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 both PostgreSQL and MySQL
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
- shellProcess.on('error', (err: NodeJS.ErrnoException) => {
306
- if (err.code === 'ENOENT') {
307
- console.log(warning(`${shellCmd} not found on your system.`))
308
- console.log()
309
- console.log(chalk.gray(' Connect manually with:'))
310
- console.log(chalk.cyan(` ${connectionString}`))
311
- console.log()
312
- console.log(chalk.gray(` Install ${shellCmd}:`))
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
- await new Promise<void>((resolve) => {
318
- shellProcess.on('close', () => resolve())
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
- process.on('SIGINT', () => {
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
- resolve()
175
- })
176
- child.on('close', () => resolve())
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
+ }
@@ -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
- 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)
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
- const installed = await promptInstallDependencies(missingTool)
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.'),
@@ -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
- 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
- }
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
@@ -1,6 +1,7 @@
1
1
  export const ENGINE_ICONS: Record<string, string> = {
2
2
  postgresql: '🐘',
3
3
  mysql: '🐬',
4
+ sqlite: '🪶',
4
5
  }
5
6
 
6
7
  export const DEFAULT_ENGINE_ICON = '▣'
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) {