@inspecto-dev/cli 0.2.0-alpha.5 → 0.3.0-alpha.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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +19 -20
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +93 -11
  4. package/bin/inspecto.js +5 -1
  5. package/dist/bin.d.ts +5 -1
  6. package/dist/bin.js +530 -49
  7. package/dist/chunk-FZS2TLXQ.js +3140 -0
  8. package/dist/index.d.ts +233 -2
  9. package/dist/index.js +17 -3
  10. package/package.json +3 -2
  11. package/src/bin.ts +286 -66
  12. package/src/commands/apply.ts +118 -0
  13. package/src/commands/detect.ts +59 -0
  14. package/src/commands/doctor.ts +225 -72
  15. package/src/commands/init.ts +143 -183
  16. package/src/commands/integration-install.ts +452 -0
  17. package/src/commands/onboard.ts +50 -0
  18. package/src/commands/plan.ts +41 -0
  19. package/src/detect/build-tool.ts +107 -3
  20. package/src/index.ts +17 -2
  21. package/src/inject/ast-injector.ts +17 -6
  22. package/src/inject/extension.ts +40 -22
  23. package/src/inject/gitignore.ts +10 -3
  24. package/src/instructions.ts +60 -46
  25. package/src/onboarding/apply.ts +364 -0
  26. package/src/onboarding/context.ts +36 -0
  27. package/src/onboarding/planner.ts +284 -0
  28. package/src/onboarding/session.ts +434 -0
  29. package/src/onboarding/target-resolution.ts +116 -0
  30. package/src/prompts.ts +54 -11
  31. package/src/types.ts +184 -0
  32. package/src/utils/fs.ts +2 -1
  33. package/src/utils/logger.ts +9 -0
  34. package/src/utils/output.ts +40 -0
  35. package/tests/apply.test.ts +583 -0
  36. package/tests/ast-injector.test.ts +50 -0
  37. package/tests/build-tool.test.ts +3 -5
  38. package/tests/detect.test.ts +94 -0
  39. package/tests/doctor.test.ts +224 -0
  40. package/tests/init.test.ts +364 -0
  41. package/tests/install-wrapper.test.ts +76 -0
  42. package/tests/instructions.test.ts +61 -0
  43. package/tests/integration-install.test.ts +294 -0
  44. package/tests/logger.test.ts +100 -0
  45. package/tests/onboard.test.ts +258 -0
  46. package/tests/plan.test.ts +713 -0
  47. package/tests/workspace-build-tool.test.ts +75 -0
  48. package/.turbo/turbo-test.log +0 -16
  49. package/dist/chunk-MIHQGC3L.js +0 -1720
@@ -0,0 +1,118 @@
1
+ import { applyOnboardingPlan, type ApplyOnboardingResult } from '../onboarding/apply.js'
2
+ import { buildOnboardingContext } from '../onboarding/context.js'
3
+ import { createPlanResult } from '../onboarding/planner.js'
4
+ import { log } from '../utils/logger.js'
5
+ import { writeCommandOutput } from '../utils/output.js'
6
+ import type { PlanResult } from '../types.js'
7
+
8
+ export interface ApplyCommandOptions {
9
+ json?: boolean
10
+ shared?: boolean
11
+ skipInstall?: boolean
12
+ dryRun?: boolean
13
+ noExtension?: boolean
14
+ }
15
+
16
+ export interface ApplyCommandResult extends ApplyOnboardingResult {
17
+ plan: PlanResult
18
+ }
19
+
20
+ function getProviderDefault(
21
+ providerId?: string,
22
+ preferredMode?: 'cli' | 'extension',
23
+ ): string | undefined {
24
+ if (!providerId) return undefined
25
+ const mode = preferredMode ?? (providerId === 'coco' ? 'cli' : 'extension')
26
+ return `${providerId}.${mode}`
27
+ }
28
+
29
+ function statusRank(status: ApplyCommandResult['status']): number {
30
+ switch (status) {
31
+ case 'error':
32
+ return 3
33
+ case 'blocked':
34
+ return 2
35
+ case 'warning':
36
+ return 1
37
+ case 'ok':
38
+ default:
39
+ return 0
40
+ }
41
+ }
42
+
43
+ function mergeStatus(
44
+ planStatus: PlanResult['status'],
45
+ applyStatus: ApplyOnboardingResult['status'],
46
+ ): ApplyCommandResult['status'] {
47
+ return statusRank(planStatus) >= statusRank(applyStatus) ? planStatus : applyStatus
48
+ }
49
+
50
+ function printApplyResult(result: ApplyCommandResult): void {
51
+ const manualSteps = result.postInstall.nextSteps.filter(
52
+ step => !result.plan.blockers.some(blocker => blocker.message === step),
53
+ )
54
+
55
+ log.header('Inspecto Apply')
56
+ log.info(`Status: ${result.status}`)
57
+ log.info(`Strategy: ${result.plan.strategy}`)
58
+
59
+ for (const blocker of result.plan.blockers) {
60
+ log.error(blocker.message)
61
+ }
62
+ for (const warning of result.plan.warnings) {
63
+ log.warn(warning.message)
64
+ }
65
+
66
+ if (manualSteps.length > 0 || result.plan.blockers.length > 0) {
67
+ log.blank()
68
+ log.warn('──────── Manual Steps Required ────────')
69
+ manualSteps.forEach(step => log.error(step))
70
+ return
71
+ }
72
+
73
+ if (result.plan.warnings.length > 0) {
74
+ return
75
+ }
76
+
77
+ log.ready('Ready! Inspecto is set up.')
78
+ log.info('Next:')
79
+ log.hint('1. Start or restart your dev server.')
80
+ log.hint('2. Open your app in the browser.')
81
+ log.hint('3. Hold Alt + Click any element to inspect.')
82
+ }
83
+
84
+ export async function apply(options: ApplyCommandOptions = {}): Promise<ApplyCommandResult> {
85
+ const root = process.cwd()
86
+ const context = await buildOnboardingContext(root)
87
+ const plan = createPlanResult(context)
88
+ const selectedProvider =
89
+ context.providers.find(provider => provider.id === plan.defaults.provider) ?? null
90
+ const selectedIDE =
91
+ context.ides.find(ide => ide.ide === plan.defaults.ide) ??
92
+ context.ides.find(ide => ide.supported) ??
93
+ null
94
+
95
+ const applyResult = await applyOnboardingPlan({
96
+ repoRoot: root,
97
+ projectRoot: root,
98
+ packageManager: context.packageManager,
99
+ supportedBuildTargets: context.buildTools.supported,
100
+ options: {
101
+ shared: options.shared ?? plan.defaults.shared,
102
+ skipInstall: options.skipInstall ?? false,
103
+ dryRun: options.dryRun ?? false,
104
+ noExtension: options.noExtension ?? !plan.defaults.extension,
105
+ quiet: options.json ?? false,
106
+ },
107
+ selectedIDE,
108
+ providerDefault: getProviderDefault(plan.defaults.provider, selectedProvider?.preferredMode),
109
+ plan,
110
+ })
111
+
112
+ const result: ApplyCommandResult = {
113
+ ...applyResult,
114
+ status: mergeStatus(plan.status, applyResult.status),
115
+ plan,
116
+ }
117
+ return writeCommandOutput(result, options.json ?? false, printApplyResult)
118
+ }
@@ -0,0 +1,59 @@
1
+ import { log } from '../utils/logger.js'
2
+ import { writeCommandOutput } from '../utils/output.js'
3
+ import { createDetectionResult } from '../onboarding/planner.js'
4
+ import type { DetectionResult } from '../types.js'
5
+
6
+ function printDetectionResult(result: DetectionResult): void {
7
+ const suppressedCodes = new Set([
8
+ 'unsupported-build-tool',
9
+ 'unsupported-build-tool-present',
10
+ 'unsupported-framework',
11
+ 'unsupported-framework-present',
12
+ ])
13
+
14
+ log.header('Inspecto Detect')
15
+ log.info(`Status: ${result.status}`)
16
+ log.info(`Root: ${result.project.root}`)
17
+ log.info(`Package manager: ${result.project.packageManager}`)
18
+
19
+ if (result.environment.frameworks.length > 0) {
20
+ log.success(`Supported frameworks: ${result.environment.frameworks.join(', ')}`)
21
+ }
22
+ if (result.environment.unsupportedFrameworks.length > 0) {
23
+ log.warn(`Unsupported frameworks: ${result.environment.unsupportedFrameworks.join(', ')}`)
24
+ }
25
+ if (result.environment.buildTools.length > 0) {
26
+ log.success(
27
+ `Supported build tools: ${result.environment.buildTools.map(tool => tool.label).join(', ')}`,
28
+ )
29
+ }
30
+ if (result.environment.unsupportedBuildTools.length > 0) {
31
+ log.warn(`Unsupported build tools: ${result.environment.unsupportedBuildTools.join(', ')}`)
32
+ }
33
+
34
+ const supportedIdes = result.environment.ides.filter(ide => ide.supported).map(ide => ide.ide)
35
+ if (supportedIdes.length > 0) {
36
+ log.success(`Supported IDEs: ${supportedIdes.join(', ')}`)
37
+ }
38
+
39
+ const supportedProviders = result.environment.providers
40
+ .filter(provider => provider.supported)
41
+ .map(provider => provider.label)
42
+ if (supportedProviders.length > 0) {
43
+ log.success(`Supported providers: ${supportedProviders.join(', ')}`)
44
+ }
45
+
46
+ for (const blocker of result.blockers) {
47
+ if (suppressedCodes.has(blocker.code)) continue
48
+ log.error(blocker.message)
49
+ }
50
+ for (const warning of result.warnings) {
51
+ if (suppressedCodes.has(warning.code)) continue
52
+ log.warn(warning.message)
53
+ }
54
+ }
55
+
56
+ export async function detect(json = false): Promise<DetectionResult> {
57
+ const result = await createDetectionResult(process.cwd())
58
+ return writeCommandOutput(result, json, printDetectionResult)
59
+ }
@@ -10,23 +10,83 @@ import { detectFrameworks } from '../detect/framework.js'
10
10
  import { detectIDE } from '../detect/ide.js'
11
11
  import { detectProviders } from '../detect/provider.js'
12
12
  import { isExtensionInstalled } from '../inject/extension.js'
13
+ import { writeCommandOutput } from '../utils/output.js'
14
+ import type { CommandStatus, DoctorDiagnostic, DoctorResult } from '../types.js'
13
15
 
14
- interface DiagResult {
15
- errors: number
16
- warnings: number
16
+ export interface DoctorCommandOptions {
17
+ json?: boolean
17
18
  }
18
19
 
19
- export async function doctor(): Promise<void> {
20
- const root = process.cwd()
21
- const result: DiagResult = { errors: 0, warnings: 0 }
20
+ function createDiagnostic(
21
+ code: string,
22
+ status: DoctorDiagnostic['status'],
23
+ message: string,
24
+ hints: string[] = [],
25
+ details?: Record<string, unknown>,
26
+ ): DoctorDiagnostic {
27
+ return {
28
+ code,
29
+ status,
30
+ message,
31
+ hints,
32
+ ...(details ? { details } : {}),
33
+ }
34
+ }
35
+
36
+ function doctorStatus(errors: number, warnings: number): CommandStatus {
37
+ if (errors > 0) return 'blocked'
38
+ if (warnings > 0) return 'warning'
39
+ return 'ok'
40
+ }
22
41
 
42
+ function printDoctorResult(result: DoctorResult): void {
23
43
  log.header('Inspecto Doctor')
24
44
 
45
+ for (const check of result.checks) {
46
+ if (check.status === 'ok') {
47
+ log.success(check.message)
48
+ } else if (check.status === 'warning') {
49
+ log.warn(check.message)
50
+ } else {
51
+ log.error(check.message)
52
+ }
53
+
54
+ for (const hint of check.hints) {
55
+ log.hint(hint)
56
+ }
57
+ }
58
+
59
+ log.blank()
60
+ if (result.summary.errors === 0 && result.summary.warnings === 0) {
61
+ log.success('All checks passed. Hold Alt + Click to start!')
62
+ } else {
63
+ const parts: string[] = []
64
+ if (result.summary.errors > 0) parts.push(`${result.summary.errors} error(s)`)
65
+ if (result.summary.warnings > 0) parts.push(`${result.summary.warnings} warning(s)`)
66
+ console.log(
67
+ ` ${parts.join(', ')}. ${result.summary.errors > 0 ? 'Fix the errors above to get started.' : ''}`,
68
+ )
69
+ }
70
+ log.blank()
71
+ }
72
+
73
+ export async function collectDoctorResult(root = process.cwd()): Promise<DoctorResult> {
74
+ const checks: DoctorDiagnostic[] = []
75
+
25
76
  // Check 1: package.json exists
26
77
  if (!(await exists(path.join(root, 'package.json')))) {
27
- log.error('No package.json found')
28
- log.hint('Run this command from your project root')
29
- return
78
+ const diagnostic = createDiagnostic('missing-package-json', 'error', 'No package.json found', [
79
+ 'Run this command from your project root',
80
+ ])
81
+ checks.push(diagnostic)
82
+ return {
83
+ status: 'blocked',
84
+ summary: { errors: 1, warnings: 0 },
85
+ project: { root },
86
+ errors: [diagnostic],
87
+ warnings: [],
88
+ checks,
89
+ }
30
90
  }
31
91
 
32
92
  // Run detections concurrently
@@ -42,42 +102,76 @@ export async function doctor(): Promise<void> {
42
102
 
43
103
  // Check 2: IDE
44
104
  if (ideProbe.detected.length === 0) {
45
- log.warn('IDE: not detected')
46
- result.warnings++
105
+ checks.push(createDiagnostic('ide-not-detected', 'warning', 'IDE: not detected'))
47
106
  } else {
48
107
  // If we have at least one supported IDE, it's a pass
49
108
  const hasSupported = ideProbe.detected.some(d => d.supported)
50
109
  if (hasSupported) {
51
- log.success(
52
- `IDE: ${ideProbe.detected
53
- .filter(d => d.supported)
54
- .map(d => d.ide)
55
- .join(', ')}`,
110
+ checks.push(
111
+ createDiagnostic(
112
+ 'ide-supported',
113
+ 'ok',
114
+ `IDE: ${ideProbe.detected
115
+ .filter(d => d.supported)
116
+ .map(d => d.ide)
117
+ .join(', ')}`,
118
+ [],
119
+ {
120
+ detected: ideProbe.detected,
121
+ },
122
+ ),
56
123
  )
57
124
  } else {
58
125
  const names = ideProbe.detected.map(d => d.ide).join(', ')
59
- log.warn(`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`)
60
- result.warnings++
126
+ checks.push(
127
+ createDiagnostic(
128
+ 'ide-unsupported',
129
+ 'warning',
130
+ `IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`,
131
+ [],
132
+ {
133
+ detected: ideProbe.detected,
134
+ },
135
+ ),
136
+ )
61
137
  }
62
138
  }
63
139
 
64
140
  // Check 3: Supported framework
65
141
  if (frameworkResult.supported.length > 0) {
66
- log.success(`Framework: ${frameworkResult.supported.join(', ')}`)
142
+ checks.push(
143
+ createDiagnostic(
144
+ 'framework-supported',
145
+ 'ok',
146
+ `Framework: ${frameworkResult.supported.join(', ')}`,
147
+ ),
148
+ )
67
149
  } else if (frameworkResult.unsupported.length > 0) {
68
150
  const names = frameworkResult.unsupported.map(f => f.name).join(', ')
69
- log.warn(`Framework: ${names} (not supported in v1, React/Vue only)`)
70
- result.warnings++
151
+ checks.push(
152
+ createDiagnostic(
153
+ 'framework-unsupported',
154
+ 'warning',
155
+ `Framework: ${names} (not supported in v1, React/Vue only)`,
156
+ ),
157
+ )
71
158
  } else {
72
- log.warn('Framework: not detected (React / Vue expected)')
73
- result.warnings++
159
+ checks.push(
160
+ createDiagnostic(
161
+ 'framework-not-detected',
162
+ 'warning',
163
+ 'Framework: not detected (React / Vue expected)',
164
+ ),
165
+ )
74
166
  }
75
167
 
76
168
  // Check 3.5: Providers
77
169
  if (providerProbe.detected.length === 0) {
78
- log.warn('Provider: none detected')
79
- log.hint('Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot')
80
- result.warnings++
170
+ checks.push(
171
+ createDiagnostic('provider-missing', 'warning', 'Provider: none detected', [
172
+ 'Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot',
173
+ ]),
174
+ )
81
175
  } else {
82
176
  const aiNames = providerProbe.detected
83
177
  .map(d => {
@@ -87,7 +181,7 @@ export async function doctor(): Promise<void> {
87
181
  return `${d.label} (${modeLabels.join(' & ')})`
88
182
  })
89
183
  .join(', ')
90
- log.success(`Provider: ${aiNames}`)
184
+ checks.push(createDiagnostic('provider-detected', 'ok', `Provider: ${aiNames}`))
91
185
  }
92
186
 
93
187
  // Check 4: @inspecto-dev/plugin installed
@@ -95,12 +189,17 @@ export async function doctor(): Promise<void> {
95
189
  if (await exists(pluginPath)) {
96
190
  const pkgJson = await readJSON<{ version?: string }>(path.join(pluginPath, 'package.json'))
97
191
  const version = pkgJson?.version ?? 'unknown'
98
- log.success(`@inspecto-dev/plugin@${version} installed`)
192
+ checks.push(
193
+ createDiagnostic('plugin-installed', 'ok', `@inspecto-dev/plugin@${version} installed`, [], {
194
+ version,
195
+ }),
196
+ )
99
197
  } else {
100
- log.error('@inspecto-dev/plugin not installed')
101
- const pm = await detectPackageManager(root)
102
- log.hint(`Fix: ${getInstallCommand(pm, '@inspecto-dev/plugin')}`)
103
- result.errors++
198
+ checks.push(
199
+ createDiagnostic('plugin-missing', 'error', '@inspecto-dev/plugin not installed', [
200
+ `Fix: ${getInstallCommand(pm, '@inspecto-dev/plugin')}`,
201
+ ]),
202
+ )
104
203
  }
105
204
 
106
205
  // Check 5: Plugin injected in build config
@@ -109,38 +208,64 @@ export async function doctor(): Promise<void> {
109
208
  for (const bt of buildResult.supported) {
110
209
  const content = await readFile(path.join(root, bt.configPath))
111
210
  if (content && content.includes('@inspecto-dev/plugin')) {
112
- log.success(`Plugin configured in ${bt.configPath}`)
211
+ checks.push(
212
+ createDiagnostic('plugin-configured', 'ok', `Plugin configured in ${bt.configPath}`, [], {
213
+ configPath: bt.configPath,
214
+ buildTool: bt.tool,
215
+ }),
216
+ )
113
217
  injected = true
114
218
  break
115
219
  }
116
220
  }
117
221
  if (!injected) {
118
- log.error('Plugin not configured in any build config')
119
- log.hint('Fix: npx @inspecto-dev/cli init')
120
- result.errors++
222
+ checks.push(
223
+ createDiagnostic(
224
+ 'plugin-not-configured',
225
+ 'error',
226
+ 'Plugin not configured in any build config',
227
+ ['Fix: npx @inspecto-dev/cli init'],
228
+ ),
229
+ )
121
230
  }
122
231
  } else if (buildResult.unsupported.length > 0) {
123
232
  const names = buildResult.unsupported.join(', ')
124
- log.warn(`Build tool: ${names} (not supported in v1)`)
125
- log.hint('current version supports: Vite, Webpack, Rspack, esbuild, Rollup')
126
- result.warnings++
233
+ checks.push(
234
+ createDiagnostic(
235
+ `build-tool-unsupported`,
236
+ 'warning',
237
+ `Build tool: ${names} (not supported in v1)`,
238
+ ['current version supports: Vite, Webpack, Rspack, esbuild, Rollup'],
239
+ ),
240
+ )
127
241
  } else {
128
- log.warn('No recognized build config found')
129
- result.warnings++
242
+ checks.push(
243
+ createDiagnostic('build-tool-missing', 'warning', 'No recognized build config found'),
244
+ )
130
245
  }
131
246
 
132
247
  // Check 6: VS Code extension
133
248
  if (extInstalled) {
134
- log.success('VS Code extension detected')
249
+ checks.push(createDiagnostic('extension-installed', 'ok', 'VS Code extension detected'))
135
250
  } else {
136
- const hasSupported = ideProbe.detected.some(d => d.supported)
137
- if (ideProbe.detected.length > 0 && !hasSupported) {
138
- log.warn('VS Code extension not applicable (non-VS Code IDE)')
251
+ const hasVSCode = ideProbe.detected.some(d => d.supported && d.ide === 'vscode')
252
+ const hasSupportedNonVSCode = ideProbe.detected.some(d => d.supported && d.ide !== 'vscode')
253
+
254
+ if (hasSupportedNonVSCode && !hasVSCode) {
255
+ checks.push(
256
+ createDiagnostic(
257
+ 'extension-not-applicable',
258
+ 'warning',
259
+ 'VS Code extension not applicable (non-VS Code IDE)',
260
+ ),
261
+ )
139
262
  } else {
140
- log.error('VS Code extension not found')
141
- log.hint('Fix: code --install-extension inspecto.inspecto')
142
- log.hint('Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto')
143
- result.errors++
263
+ checks.push(
264
+ createDiagnostic('extension-missing', 'error', 'VS Code extension not found', [
265
+ 'Fix: code --install-extension inspecto.inspecto',
266
+ 'Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto',
267
+ ]),
268
+ )
144
269
  }
145
270
  }
146
271
 
@@ -156,18 +281,31 @@ export async function doctor(): Promise<void> {
156
281
  const fileName = hasSettingsLocal ? 'settings.local.json' : 'settings.json'
157
282
  const settings = await readJSON(targetPath)
158
283
  if (settings) {
159
- log.success(`.inspecto/${fileName} valid`)
284
+ checks.push(createDiagnostic('settings-valid', 'ok', `.inspecto/${fileName} valid`))
160
285
  } else {
161
- log.error(`.inspecto/${fileName} has invalid JSON`)
162
- log.hint(
163
- 'Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init',
286
+ checks.push(
287
+ createDiagnostic(
288
+ 'settings-invalid-json',
289
+ 'error',
290
+ `.inspecto/${fileName} has invalid JSON`,
291
+ [
292
+ 'Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init',
293
+ ],
294
+ {
295
+ fileName,
296
+ },
297
+ ),
164
298
  )
165
- result.errors++
166
299
  }
167
300
  } else {
168
- log.warn('No .inspecto/settings.json or settings.local.json found (using defaults)')
169
- log.hint('Optional: npx @inspecto-dev/cli init')
170
- result.warnings++
301
+ checks.push(
302
+ createDiagnostic(
303
+ 'settings-missing',
304
+ 'warning',
305
+ 'No .inspecto/settings.json or settings.local.json found (using defaults)',
306
+ ['Optional: npx @inspecto-dev/cli init'],
307
+ ),
308
+ )
171
309
  }
172
310
 
173
311
  // Check 8: .gitignore status
@@ -176,23 +314,38 @@ export async function doctor(): Promise<void> {
176
314
  const hasLockIgnore =
177
315
  gitignoreContent.includes('.inspecto/install.lock') || gitignoreContent.includes('.inspecto/')
178
316
  if (!hasLockIgnore) {
179
- log.warn('.inspecto/install.lock not in .gitignore')
180
- log.hint('install.lock contains local machine state and should not be committed')
181
- result.warnings++
317
+ checks.push(
318
+ createDiagnostic(
319
+ 'gitignore-missing-install-lock',
320
+ 'warning',
321
+ '.inspecto/install.lock not in .gitignore',
322
+ ['install.lock contains local machine state and should not be committed'],
323
+ ),
324
+ )
182
325
  }
183
326
  }
184
327
 
185
- // Summary
186
- log.blank()
187
- if (result.errors === 0 && result.warnings === 0) {
188
- log.success('All checks passed. Hold Alt + Click to start!')
189
- } else {
190
- const parts: string[] = []
191
- if (result.errors > 0) parts.push(`${result.errors} error(s)`)
192
- if (result.warnings > 0) parts.push(`${result.warnings} warning(s)`)
193
- console.log(
194
- ` ${parts.join(', ')}. ${result.errors > 0 ? 'Fix the errors above to get started.' : ''}`,
195
- )
328
+ const errors = checks.filter(check => check.status === 'error')
329
+ const warnings = checks.filter(check => check.status === 'warning')
330
+
331
+ return {
332
+ status: doctorStatus(errors.length, warnings.length),
333
+ summary: {
334
+ errors: errors.length,
335
+ warnings: warnings.length,
336
+ },
337
+ project: {
338
+ root,
339
+ packageManager: pm,
340
+ },
341
+ errors,
342
+ warnings,
343
+ checks,
196
344
  }
197
- log.blank()
345
+ }
346
+
347
+ export async function doctor(options: DoctorCommandOptions | boolean = {}): Promise<DoctorResult> {
348
+ const json = typeof options === 'boolean' ? options : (options.json ?? false)
349
+ const result = await collectDoctorResult(process.cwd())
350
+ return writeCommandOutput(result, json, printDoctorResult)
198
351
  }