prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -0,0 +1,426 @@
1
+ /**
2
+ * DoctorService - System health checks and diagnostics
3
+ *
4
+ * Checks:
5
+ * - Required tools (git, node)
6
+ * - Optional tools (bun, gh, claude, gemini)
7
+ * - Project configuration
8
+ * - Generated file staleness
9
+ * - Provides actionable recommendations
10
+ */
11
+
12
+ import { execSync } from 'node:child_process'
13
+ import fs from 'node:fs/promises'
14
+ import path from 'node:path'
15
+ import chalk from 'chalk'
16
+ import configManager from '../infrastructure/config-manager'
17
+ import pathManager from '../infrastructure/path-manager'
18
+ import { VERSION } from '../utils/version'
19
+
20
+ // ============================================================================
21
+ // TYPES
22
+ // ============================================================================
23
+
24
+ interface CheckResult {
25
+ name: string
26
+ status: 'ok' | 'warn' | 'error'
27
+ version?: string
28
+ message?: string
29
+ optional?: boolean
30
+ }
31
+
32
+ interface DoctorResult {
33
+ success: boolean
34
+ tools: CheckResult[]
35
+ project: CheckResult[]
36
+ recommendations: string[]
37
+ hasErrors: boolean
38
+ hasWarnings: boolean
39
+ }
40
+
41
+ // ============================================================================
42
+ // DOCTOR SERVICE
43
+ // ============================================================================
44
+
45
+ class DoctorService {
46
+ private projectPath: string = ''
47
+ private projectId: string | null = null
48
+ private globalPath: string = ''
49
+
50
+ /**
51
+ * Run all health checks
52
+ */
53
+ async check(projectPath: string = process.cwd()): Promise<DoctorResult> {
54
+ this.projectPath = projectPath
55
+ this.projectId = await configManager.getProjectId(projectPath)
56
+ if (this.projectId) {
57
+ this.globalPath = pathManager.getGlobalProjectPath(this.projectId)
58
+ }
59
+
60
+ const tools = await this.checkTools()
61
+ const project = await this.checkProject()
62
+ const recommendations = this.generateRecommendations(tools, project)
63
+
64
+ const hasErrors = [...tools, ...project].some((c) => c.status === 'error' && !c.optional)
65
+ const hasWarnings = [...tools, ...project].some(
66
+ (c) => c.status === 'warn' || (c.status === 'error' && c.optional)
67
+ )
68
+
69
+ return {
70
+ success: !hasErrors,
71
+ tools,
72
+ project,
73
+ recommendations,
74
+ hasErrors,
75
+ hasWarnings,
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Run checks and print formatted output
81
+ */
82
+ async run(projectPath: string = process.cwd()): Promise<number> {
83
+ const result = await this.check(projectPath)
84
+
85
+ this.printHeader()
86
+ this.printSection('System Tools', result.tools)
87
+ this.printSection('Project Status', result.project)
88
+
89
+ if (result.recommendations.length > 0) {
90
+ this.printRecommendations(result.recommendations)
91
+ }
92
+
93
+ this.printSummary(result)
94
+
95
+ return result.hasErrors ? 1 : 0
96
+ }
97
+
98
+ // ==========================================================================
99
+ // TOOL CHECKS
100
+ // ==========================================================================
101
+
102
+ private async checkTools(): Promise<CheckResult[]> {
103
+ const checks: CheckResult[] = []
104
+
105
+ // Git (required)
106
+ checks.push(this.checkCommand('git', 'git --version', /git version ([\d.]+)/, false))
107
+
108
+ // Node (required)
109
+ checks.push(this.checkCommand('node', 'node --version', /v([\d.]+)/, false))
110
+
111
+ // Bun (optional)
112
+ checks.push(this.checkCommand('bun', 'bun --version', /([\d.]+)/, true))
113
+
114
+ // GitHub CLI (optional)
115
+ checks.push(
116
+ this.checkCommand('gh', 'gh --version', /gh version ([\d.]+)/, true, 'needed for PR commands')
117
+ )
118
+
119
+ // Claude Code (optional)
120
+ checks.push(
121
+ this.checkCommand(
122
+ 'claude',
123
+ 'claude --version',
124
+ /claude ([\d.]+)/,
125
+ true,
126
+ 'Anthropic Claude Code CLI'
127
+ )
128
+ )
129
+
130
+ // Gemini CLI (optional)
131
+ checks.push(
132
+ this.checkCommand('gemini', 'gemini --version', /gemini ([\d.]+)/, true, 'Google Gemini CLI')
133
+ )
134
+
135
+ return checks
136
+ }
137
+
138
+ private checkCommand(
139
+ name: string,
140
+ command: string,
141
+ versionRegex: RegExp,
142
+ optional: boolean,
143
+ description?: string
144
+ ): CheckResult {
145
+ try {
146
+ const output = execSync(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
147
+ const match = output.match(versionRegex)
148
+ const version = match ? match[1] : 'unknown'
149
+
150
+ return {
151
+ name,
152
+ status: 'ok',
153
+ version,
154
+ optional,
155
+ }
156
+ } catch {
157
+ return {
158
+ name,
159
+ status: 'error',
160
+ message: description ? `not found (${description})` : 'not found',
161
+ optional,
162
+ }
163
+ }
164
+ }
165
+
166
+ // ==========================================================================
167
+ // PROJECT CHECKS
168
+ // ==========================================================================
169
+
170
+ private async checkProject(): Promise<CheckResult[]> {
171
+ const checks: CheckResult[] = []
172
+
173
+ // prjct config
174
+ checks.push(await this.checkPrjctConfig())
175
+
176
+ // CLAUDE.md
177
+ checks.push(await this.checkClaudeMd())
178
+
179
+ // Git repo
180
+ checks.push(await this.checkGitRepo())
181
+
182
+ // State file
183
+ checks.push(await this.checkStateFile())
184
+
185
+ return checks
186
+ }
187
+
188
+ private async checkPrjctConfig(): Promise<CheckResult> {
189
+ const configPath = path.join(this.projectPath, '.prjct', 'prjct.config.json')
190
+
191
+ try {
192
+ await fs.access(configPath)
193
+ return {
194
+ name: 'prjct config',
195
+ status: 'ok',
196
+ message: 'initialized',
197
+ }
198
+ } catch {
199
+ return {
200
+ name: 'prjct config',
201
+ status: 'error',
202
+ message: 'not initialized - run "prjct init"',
203
+ }
204
+ }
205
+ }
206
+
207
+ private async checkClaudeMd(): Promise<CheckResult> {
208
+ if (!this.globalPath) {
209
+ return {
210
+ name: 'CLAUDE.md',
211
+ status: 'warn',
212
+ message: 'project not initialized',
213
+ }
214
+ }
215
+
216
+ const claudePath = path.join(this.globalPath, 'context', 'CLAUDE.md')
217
+
218
+ try {
219
+ const stat = await fs.stat(claudePath)
220
+ const ageMs = Date.now() - stat.mtimeMs
221
+ const ageHours = Math.floor(ageMs / (1000 * 60 * 60))
222
+ const ageDays = Math.floor(ageHours / 24)
223
+
224
+ let ageStr: string
225
+ if (ageDays > 0) {
226
+ ageStr = `${ageDays} day${ageDays > 1 ? 's' : ''} ago`
227
+ } else if (ageHours > 0) {
228
+ ageStr = `${ageHours} hour${ageHours > 1 ? 's' : ''} ago`
229
+ } else {
230
+ ageStr = 'recently'
231
+ }
232
+
233
+ // Warn if older than 24 hours
234
+ if (ageHours > 24) {
235
+ return {
236
+ name: 'CLAUDE.md',
237
+ status: 'warn',
238
+ message: `stale (last sync: ${ageStr})`,
239
+ }
240
+ }
241
+
242
+ return {
243
+ name: 'CLAUDE.md',
244
+ status: 'ok',
245
+ message: `synced ${ageStr}`,
246
+ }
247
+ } catch {
248
+ return {
249
+ name: 'CLAUDE.md',
250
+ status: 'error',
251
+ message: 'not found - run "prjct sync"',
252
+ }
253
+ }
254
+ }
255
+
256
+ private async checkGitRepo(): Promise<CheckResult> {
257
+ try {
258
+ execSync('git rev-parse --git-dir', {
259
+ cwd: this.projectPath,
260
+ stdio: ['pipe', 'pipe', 'pipe'],
261
+ })
262
+
263
+ // Check for uncommitted changes
264
+ const status = execSync('git status --porcelain', {
265
+ cwd: this.projectPath,
266
+ encoding: 'utf-8',
267
+ })
268
+ const hasChanges = status.trim().length > 0
269
+
270
+ if (hasChanges) {
271
+ const lines = status.trim().split('\n').filter(Boolean)
272
+ return {
273
+ name: 'git repo',
274
+ status: 'ok',
275
+ message: `${lines.length} uncommitted change${lines.length > 1 ? 's' : ''}`,
276
+ }
277
+ }
278
+
279
+ return {
280
+ name: 'git repo',
281
+ status: 'ok',
282
+ message: 'clean',
283
+ }
284
+ } catch {
285
+ return {
286
+ name: 'git repo',
287
+ status: 'warn',
288
+ message: 'not a git repository',
289
+ }
290
+ }
291
+ }
292
+
293
+ private async checkStateFile(): Promise<CheckResult> {
294
+ if (!this.globalPath) {
295
+ return {
296
+ name: 'task state',
297
+ status: 'warn',
298
+ message: 'project not initialized',
299
+ }
300
+ }
301
+
302
+ const statePath = path.join(this.globalPath, 'storage', 'state.json')
303
+
304
+ try {
305
+ const content = await fs.readFile(statePath, 'utf-8')
306
+ const state = JSON.parse(content)
307
+
308
+ if (state.currentTask) {
309
+ return {
310
+ name: 'task state',
311
+ status: 'ok',
312
+ message: `active: ${state.currentTask.description?.slice(0, 30)}...`,
313
+ }
314
+ }
315
+
316
+ return {
317
+ name: 'task state',
318
+ status: 'ok',
319
+ message: 'no active task',
320
+ }
321
+ } catch {
322
+ return {
323
+ name: 'task state',
324
+ status: 'ok',
325
+ message: 'no state file (normal for new projects)',
326
+ optional: true,
327
+ }
328
+ }
329
+ }
330
+
331
+ // ==========================================================================
332
+ // RECOMMENDATIONS
333
+ // ==========================================================================
334
+
335
+ private generateRecommendations(tools: CheckResult[], project: CheckResult[]): string[] {
336
+ const recommendations: string[] = []
337
+
338
+ // Check for missing optional tools
339
+ const missingGh = tools.find((t) => t.name === 'gh' && t.status === 'error')
340
+ if (missingGh) {
341
+ recommendations.push('Install GitHub CLI (gh) for PR commands: https://cli.github.com')
342
+ }
343
+
344
+ // Check for stale CLAUDE.md
345
+ const claudeCheck = project.find((p) => p.name === 'CLAUDE.md')
346
+ if (claudeCheck?.status === 'warn' && claudeCheck.message?.includes('stale')) {
347
+ recommendations.push('Run "prjct sync" to update context')
348
+ }
349
+
350
+ // Check for missing init
351
+ const configCheck = project.find((p) => p.name === 'prjct config')
352
+ if (configCheck?.status === 'error') {
353
+ recommendations.push('Run "prjct init" to initialize this project')
354
+ }
355
+
356
+ // Check for missing CLAUDE.md
357
+ const claudeMissing = project.find((p) => p.name === 'CLAUDE.md' && p.status === 'error')
358
+ if (claudeMissing && !configCheck?.status?.includes('error')) {
359
+ recommendations.push('Run "prjct sync" to generate context files')
360
+ }
361
+
362
+ return recommendations
363
+ }
364
+
365
+ // ==========================================================================
366
+ // OUTPUT
367
+ // ==========================================================================
368
+
369
+ private printHeader(): void {
370
+ console.log('')
371
+ console.log(chalk.bold(`prjct doctor v${VERSION}`))
372
+ console.log(chalk.dim('─'.repeat(40)))
373
+ }
374
+
375
+ private printSection(title: string, checks: CheckResult[]): void {
376
+ console.log('')
377
+ console.log(chalk.bold(title))
378
+
379
+ for (const check of checks) {
380
+ const icon = this.getStatusIcon(check.status, check.optional)
381
+ const name = check.name.padEnd(14)
382
+ const detail = check.version || check.message || ''
383
+ const optionalTag = check.optional && check.status === 'error' ? chalk.dim(' (optional)') : ''
384
+
385
+ console.log(` ${icon} ${name} ${chalk.dim(detail)}${optionalTag}`)
386
+ }
387
+ }
388
+
389
+ private printRecommendations(recommendations: string[]): void {
390
+ console.log('')
391
+ console.log(chalk.bold('Recommendations'))
392
+
393
+ for (const rec of recommendations) {
394
+ console.log(` ${chalk.yellow('•')} ${rec}`)
395
+ }
396
+ }
397
+
398
+ private printSummary(result: DoctorResult): void {
399
+ console.log('')
400
+ console.log(chalk.dim('─'.repeat(40)))
401
+
402
+ if (result.hasErrors) {
403
+ console.log(chalk.red('✗ Some required checks failed'))
404
+ } else if (result.hasWarnings) {
405
+ console.log(chalk.yellow('⚠ All required checks passed (some warnings)'))
406
+ } else {
407
+ console.log(chalk.green('✓ All checks passed'))
408
+ }
409
+
410
+ console.log('')
411
+ }
412
+
413
+ private getStatusIcon(status: 'ok' | 'warn' | 'error', optional?: boolean): string {
414
+ switch (status) {
415
+ case 'ok':
416
+ return chalk.green('✓')
417
+ case 'warn':
418
+ return chalk.yellow('⚠')
419
+ case 'error':
420
+ return optional ? chalk.dim('○') : chalk.red('✗')
421
+ }
422
+ }
423
+ }
424
+
425
+ export const doctorService = new DoctorService()
426
+ export { DoctorService, type DoctorResult, type CheckResult }