forgedev 1.0.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.
Files changed (99) hide show
  1. package/CLAUDE.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/bin/devforge.js +4 -0
  5. package/package.json +33 -0
  6. package/src/claude-configurator.js +260 -0
  7. package/src/cli.js +119 -0
  8. package/src/composer.js +214 -0
  9. package/src/doctor-checks.js +743 -0
  10. package/src/doctor-prompts.js +295 -0
  11. package/src/doctor.js +281 -0
  12. package/src/guided.js +315 -0
  13. package/src/index.js +148 -0
  14. package/src/init-mode.js +134 -0
  15. package/src/prompts.js +155 -0
  16. package/src/recommender.js +186 -0
  17. package/src/scanner.js +368 -0
  18. package/src/uat-generator.js +189 -0
  19. package/src/utils.js +57 -0
  20. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
  21. package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
  22. package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
  23. package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
  24. package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
  25. package/templates/auth/nextauth/src/middleware.ts.template +14 -0
  26. package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
  27. package/templates/backend/fastapi/backend/app/__init__.py +0 -0
  28. package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
  29. package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
  30. package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
  31. package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
  32. package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
  33. package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
  34. package/templates/backend/fastapi/backend/app/main.py.template +58 -0
  35. package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
  36. package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
  37. package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
  38. package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
  39. package/templates/base/.gitignore.template +29 -0
  40. package/templates/base/README.md.template +25 -0
  41. package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
  42. package/templates/claude-code/agents/production-readiness.md +55 -0
  43. package/templates/claude-code/agents/security-reviewer.md +41 -0
  44. package/templates/claude-code/agents/spec-validator.md +34 -0
  45. package/templates/claude-code/agents/uat-validator.md +37 -0
  46. package/templates/claude-code/claude-md/base.md +33 -0
  47. package/templates/claude-code/claude-md/fastapi.md +12 -0
  48. package/templates/claude-code/claude-md/fullstack.md +12 -0
  49. package/templates/claude-code/claude-md/nextjs.md +11 -0
  50. package/templates/claude-code/commands/audit-security.md +11 -0
  51. package/templates/claude-code/commands/audit-spec.md +9 -0
  52. package/templates/claude-code/commands/audit-wiring.md +17 -0
  53. package/templates/claude-code/commands/done.md +19 -0
  54. package/templates/claude-code/commands/generate-prd.md +45 -0
  55. package/templates/claude-code/commands/generate-uat.md +35 -0
  56. package/templates/claude-code/commands/help.md +26 -0
  57. package/templates/claude-code/commands/next.md +20 -0
  58. package/templates/claude-code/commands/optimize-claude-md.md +31 -0
  59. package/templates/claude-code/commands/pre-pr.md +19 -0
  60. package/templates/claude-code/commands/run-uat.md +21 -0
  61. package/templates/claude-code/commands/status.md +24 -0
  62. package/templates/claude-code/commands/verify-all.md +11 -0
  63. package/templates/claude-code/hooks/polyglot.json +36 -0
  64. package/templates/claude-code/hooks/python.json +36 -0
  65. package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
  66. package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
  67. package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
  68. package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
  69. package/templates/claude-code/hooks/typescript.json +36 -0
  70. package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
  71. package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
  72. package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
  73. package/templates/claude-code/skills/playwright/SKILL.md +37 -0
  74. package/templates/claude-code/skills/security-api/SKILL.md +47 -0
  75. package/templates/claude-code/skills/security-web/SKILL.md +41 -0
  76. package/templates/database/prisma-postgres/.env.example +1 -0
  77. package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
  78. package/templates/database/sqlalchemy-postgres/.env.example +1 -0
  79. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
  80. package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
  81. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
  82. package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
  83. package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
  84. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
  85. package/templates/frontend/nextjs/next.config.ts.template +7 -0
  86. package/templates/frontend/nextjs/package.json.template +41 -0
  87. package/templates/frontend/nextjs/postcss.config.mjs +7 -0
  88. package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
  89. package/templates/frontend/nextjs/src/app/globals.css +1 -0
  90. package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
  91. package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
  92. package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
  93. package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
  94. package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
  95. package/templates/frontend/nextjs/tsconfig.json +23 -0
  96. package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
  97. package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
  98. package/templates/testing/playwright/playwright.config.ts.template +22 -0
  99. package/templates/testing/vitest/src/__tests__/example.test.ts.template +12 -0
@@ -0,0 +1,295 @@
1
+ const PROMPT_TEMPLATES = {
2
+ CLAUDE_MD_TOO_LONG: (issue) => {
3
+ const lines = issue.title.match(/(\d+) lines/)?.[1] || '?';
4
+ return `Read CLAUDE.md. It's ${lines} lines — too long for Claude Code to follow reliably (target: <150).
5
+
6
+ Propose a split:
7
+ - What stays in CLAUDE.md (universal rules, commands, pitfalls)
8
+ - What moves to .claude/skills/ (schemas, specs, detailed lists)
9
+ - What moves to backend/CLAUDE.md or src/CLAUDE.md (directory-scoped rules)
10
+
11
+ Show me the proposal as a table. Do NOT modify until I approve.`;
12
+ },
13
+
14
+ UNAUTH_ENDPOINT: (issue) => {
15
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
16
+ return `These endpoints are missing authentication:
17
+ ${fileList}
18
+
19
+ For each: add the appropriate auth check.
20
+ - FastAPI: add Depends(get_current_user) and appropriate RBAC
21
+ - Next.js: add getServerSession or auth() check
22
+ Follow the pattern used in endpoints that already have auth.
23
+ Write a test for each endpoint confirming 401 without auth.`;
24
+ },
25
+
26
+ FLAKY_TESTS: (issue) => {
27
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
28
+ return `These tests use waitForTimeout() or hardcoded delays — they will fail randomly:
29
+ ${fileList}
30
+
31
+ For each:
32
+ 1. Replace waitForTimeout with waitForSelector, waitForResponse, or other event-based waits
33
+ 2. Replace cy.wait(N) with cy.intercept + cy.wait('@alias')
34
+ 3. Remove arbitrary setTimeout delays in tests
35
+ 4. Run each test 3x to verify it's stable`;
36
+ },
37
+
38
+ CROSS_DOMAIN_IMPORT: (issue) => {
39
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
40
+ return `These files import across the frontend/backend boundary:
41
+ ${fileList}
42
+
43
+ Fix by:
44
+ 1. Move shared types to a shared/ directory
45
+ 2. Use API calls instead of direct imports
46
+ 3. Duplicate small utilities rather than cross-importing
47
+ Never import server code into client bundles.`;
48
+ },
49
+
50
+ BARE_EXCEPT: (issue) => {
51
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
52
+ return `These locations use bare except: blocks (catch-all that swallows real errors):
53
+ ${fileList}
54
+
55
+ For each: replace with a specific exception type.
56
+ - If catching expected errors: except ValueError, except KeyError, etc.
57
+ - If catching any exception for logging: except Exception as e: with logging
58
+ - Never use bare except: — it hides bugs`;
59
+ },
60
+
61
+ MISSING_HEALTH: (issue) => {
62
+ return `Add a health check endpoint to the application.
63
+
64
+ For FastAPI: Add a /health endpoint that returns {"status": "ok"} and optionally checks database connectivity.
65
+ For Next.js: Add src/app/api/health/route.ts that returns {"status": "ok"}.
66
+ For both: Include database connectivity check if a database is configured.
67
+
68
+ This endpoint is used by load balancers and monitoring to verify the app is running.`;
69
+ },
70
+
71
+ MISSING_SHUTDOWN: (issue) => {
72
+ return `Add graceful shutdown handling to the application.
73
+
74
+ For FastAPI: Use the lifespan context manager to handle startup/shutdown events.
75
+ For Next.js: The framework handles this, but ensure any custom servers handle SIGTERM.
76
+ For Docker: Ensure the process responds to SIGTERM within the stop_grace_period.
77
+
78
+ On shutdown: close database connections, finish in-flight requests, flush logs.`;
79
+ },
80
+
81
+ MISSING_UAT: () => {
82
+ return `Generate UAT (User Acceptance Test) scenarios for this project.
83
+
84
+ Read the codebase to understand all user-facing features, then create:
85
+ 1. docs/uat/UAT_TEMPLATE.md with test scenarios for each feature
86
+ 2. docs/uat/UAT_CHECKLIST.csv with a checklist format
87
+
88
+ Each scenario should have:
89
+ - Description (what's being tested)
90
+ - Preconditions
91
+ - Steps (numbered)
92
+ - Expected result
93
+ - Priority (P0 = critical path, P1 = important, P2 = nice to have)`;
94
+ },
95
+
96
+ SCATTERED_API_CALLS: (issue) => {
97
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
98
+ return `These components call fetch() or axios directly instead of through a centralized API client:
99
+ ${fileList}
100
+
101
+ Create a centralized API client (e.g., src/lib/api.ts or src/services/api.ts) with typed methods for each endpoint. Then refactor each component to use the centralized client.
102
+
103
+ Benefits: single place to add auth headers, error handling, base URL config, and response typing.`;
104
+ },
105
+
106
+ LARGE_FILES: (issue) => {
107
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
108
+ return `These files are very large and should be split:
109
+ ${fileList}
110
+
111
+ For each:
112
+ 1. Identify logical sections that could be separate files
113
+ 2. Extract into focused modules (one responsibility per file)
114
+ 3. Update imports
115
+ 4. Run tests after each extraction
116
+
117
+ Don't refactor all at once — one file per session.`;
118
+ },
119
+
120
+ MISSING_SCOPED_CLAUDE_MD: (issue) => {
121
+ const dir = issue.title.includes('frontend') ? 'frontend' : 'backend';
122
+ return `Create ${dir}/CLAUDE.md with ${dir}-specific rules.
123
+
124
+ Keep it under 30 lines. Include:
125
+ - Framework-specific patterns and conventions
126
+ - Naming conventions for this layer
127
+ - Common pitfalls specific to this part of the codebase
128
+
129
+ This scopes rules so Claude Code only loads them when working in this directory.`;
130
+ },
131
+
132
+ UNUSED_DEPENDENCIES: (issue) => {
133
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
134
+ return `These dependencies may be unused:
135
+ ${fileList}
136
+
137
+ For each: search the codebase to confirm it's truly unused (some deps are used implicitly by frameworks or plugins). If confirmed unused, remove with npm uninstall.`;
138
+ },
139
+
140
+ HARDCODED_VALUES: (issue) => {
141
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
142
+ return `These locations have hardcoded values that should be environment variables:
143
+ ${fileList}
144
+
145
+ For each: move to .env and reference via process.env.VARIABLE_NAME (or os.environ in Python). Update .env.example with the variable name and a placeholder value.`;
146
+ },
147
+
148
+ UNUSED_EXPORTS: (issue) => {
149
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
150
+ return `These exported symbols may not be imported anywhere:
151
+ ${fileList}
152
+
153
+ For each: verify it's truly unused, then either remove the export keyword or delete the function/constant entirely. Run tests after each change.`;
154
+ },
155
+
156
+ DUPLICATE_CODE: (issue) => {
157
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
158
+ return `These files contain duplicated code blocks:
159
+ ${fileList}
160
+
161
+ For each group of duplicates: extract the shared logic into a utility function in the appropriate lib/ or utils/ directory. Update all call sites. Run tests after each extraction.`;
162
+ },
163
+
164
+ DEAD_FEATURES: (issue) => {
165
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
166
+ return `These files appear to be dead or stub features:
167
+ ${fileList}
168
+
169
+ For each: determine if the feature is planned (keep stub but add TODO with ticket reference) or abandoned (delete the file). Check git blame for context.`;
170
+ },
171
+
172
+ INLINE_AI_PROMPTS: (issue) => {
173
+ const fileList = (issue.files || []).map(f => `- ${f}`).join('\n');
174
+ return `These files have inline AI/LLM prompts mixed with application logic:
175
+ ${fileList}
176
+
177
+ Extract all prompts into a dedicated prompts file (e.g., src/lib/prompts.ts or prompts/). This makes prompts easier to version, A/B test, and iterate on without changing application code.`;
178
+ },
179
+ };
180
+
181
+ export function generatePrompt(issue) {
182
+ const template = PROMPT_TEMPLATES[issue.promptId];
183
+ if (!template) {
184
+ return `Fix: ${issue.title}\n\nImpact: ${issue.impact}\nFiles: ${(issue.files || []).join(', ')}`;
185
+ }
186
+ return template(issue);
187
+ }
188
+
189
+ export function generateAllPrompts(issues) {
190
+ const sections = [];
191
+ let sessionNum = 1;
192
+
193
+ // Group by severity
194
+ const critical = issues.filter(i => i.severity === 'critical');
195
+ const warnings = issues.filter(i => i.severity === 'warning');
196
+ const info = issues.filter(i => i.severity === 'info');
197
+
198
+ for (const issue of [...critical, ...warnings, ...info]) {
199
+ const prompt = generatePrompt(issue);
200
+ const effortLabel = issue.effort === 'quick' ? '~10 min' :
201
+ issue.effort === 'medium' ? '~30 min' : '~1 hour';
202
+
203
+ sections.push(`## Session ${sessionNum}: ${issue.title}
204
+ **Severity:** ${issue.severity} | **Effort:** ${effortLabel}
205
+ **Impact:** ${issue.impact}
206
+ ${issue.files && issue.files.length > 0 ? `**Files:** ${issue.files.join(', ')}` : ''}
207
+
208
+ \`\`\`
209
+ ${prompt}
210
+ \`\`\`
211
+ `);
212
+ sessionNum++;
213
+ }
214
+
215
+ return `# Fix Prompts — Run These in Claude Code (In Order)
216
+
217
+ ${sections.join('\n')}
218
+ Run each as a separate Claude Code session. /clear between sessions.
219
+ `;
220
+ }
221
+
222
+ export function generateReport(issues, projectName) {
223
+ const critical = issues.filter(i => i.severity === 'critical');
224
+ const warnings = issues.filter(i => i.severity === 'warning');
225
+ const info = issues.filter(i => i.severity === 'info');
226
+ const healthy = []; // Could be populated by passing in good checks
227
+
228
+ let report = `# DevForge Doctor Report — ${projectName}
229
+ Generated: ${new Date().toISOString().split('T')[0]}
230
+
231
+ ## Summary
232
+ - Critical: ${critical.length} issues
233
+ - Warning: ${warnings.length} issues
234
+ - Info: ${info.length} suggestions
235
+
236
+ `;
237
+
238
+ if (critical.length > 0) {
239
+ report += `## Critical Issues\n\n`;
240
+ for (let i = 0; i < critical.length; i++) {
241
+ const issue = critical[i];
242
+ const prompt = generatePrompt(issue);
243
+ report += `### ${i + 1}. ${issue.title}
244
+ **Impact:** ${issue.impact}
245
+ **Effort:** ${issue.effort}
246
+ ${issue.files && issue.files.length > 0 ? `**Files:** ${issue.files.join(', ')}` : ''}
247
+
248
+ **Fix prompt:**
249
+ \`\`\`
250
+ ${prompt}
251
+ \`\`\`
252
+
253
+ `;
254
+ }
255
+ }
256
+
257
+ if (warnings.length > 0) {
258
+ report += `## Warnings\n\n`;
259
+ for (let i = 0; i < warnings.length; i++) {
260
+ const issue = warnings[i];
261
+ const prompt = generatePrompt(issue);
262
+ report += `### ${i + 1}. ${issue.title}
263
+ **Impact:** ${issue.impact}
264
+ **Effort:** ${issue.effort}
265
+ ${issue.files && issue.files.length > 0 ? `**Files:** ${issue.files.join(', ')}` : ''}
266
+
267
+ **Fix prompt:**
268
+ \`\`\`
269
+ ${prompt}
270
+ \`\`\`
271
+
272
+ `;
273
+ }
274
+ }
275
+
276
+ if (info.length > 0) {
277
+ report += `## Suggestions\n\n`;
278
+ for (let i = 0; i < info.length; i++) {
279
+ const issue = info[i];
280
+ const prompt = generatePrompt(issue);
281
+ report += `### ${i + 1}. ${issue.title}
282
+ **Impact:** ${issue.impact}
283
+ **Effort:** ${issue.effort}
284
+
285
+ **Fix prompt:**
286
+ \`\`\`
287
+ ${prompt}
288
+ \`\`\`
289
+
290
+ `;
291
+ }
292
+ }
293
+
294
+ return report;
295
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,281 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { log, ensureDir, writeFile } from './utils.js';
5
+ import { scanProject } from './scanner.js';
6
+ import { runAllChecks } from './doctor-checks.js';
7
+ import { generatePrompt, generateAllPrompts, generateReport } from './doctor-prompts.js';
8
+ import { askDoctorAction } from './prompts.js';
9
+
10
+ export async function runDoctor(projectDir) {
11
+ console.log('');
12
+ console.log(chalk.bold.cyan(' 🔨 DevForge Doctor') + chalk.dim(' — Project Health Check'));
13
+ console.log('');
14
+ console.log(' Scanning...');
15
+ console.log('');
16
+
17
+ const scan = scanProject(projectDir);
18
+
19
+ if (scan.stackId === 'unknown') {
20
+ log.warn('Could not detect a supported stack in this directory.');
21
+ log.dim(' Supported: Next.js, FastAPI, or polyglot (Next.js + FastAPI)');
22
+ log.dim(' Make sure you\'re in the project root directory.');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Print vitals
27
+ printVitals(scan);
28
+
29
+ // Run all checks
30
+ const issues = runAllChecks(projectDir, scan);
31
+
32
+ if (issues.length === 0) {
33
+ console.log(chalk.green(' ✓ Your project looks healthy! No issues found.'));
34
+ console.log('');
35
+ return;
36
+ }
37
+
38
+ // Print issues summary
39
+ printIssues(issues);
40
+
41
+ // Ask what to do
42
+ const action = await askDoctorAction();
43
+
44
+ switch (action) {
45
+ case 'guided':
46
+ await guidedFix(issues);
47
+ break;
48
+ case 'report':
49
+ await saveReport(issues, scan, projectDir);
50
+ break;
51
+ case 'autofix':
52
+ await autoFixSafe(issues, projectDir);
53
+ break;
54
+ case 'prompts':
55
+ await exportPrompts(issues, projectDir);
56
+ break;
57
+ }
58
+ }
59
+
60
+ function printVitals(scan) {
61
+ console.log(chalk.bold(' 📊 Project Vitals:'));
62
+ console.log('');
63
+
64
+ const parts = [];
65
+ if (scan.frontend.detected) {
66
+ parts.push(` ${chalk.bold('Frontend:')} ${scan.frontend.framework} (${scan.frontend.language})`);
67
+ }
68
+ if (scan.backend.detected) {
69
+ parts.push(` ${chalk.bold('Backend:')} ${scan.backend.framework} (${scan.backend.language})`);
70
+ }
71
+ if (scan.database.detected) {
72
+ parts.push(` ${chalk.bold('Database:')} ${scan.database.type} (${scan.database.orm})`);
73
+ }
74
+
75
+ const testParts = [scan.testing.unit, scan.testing.e2e, scan.testing.backend].filter(Boolean);
76
+ if (testParts.length) {
77
+ parts.push(` ${chalk.bold('Testing:')} ${testParts.join(' + ')}`);
78
+ }
79
+
80
+ if (scan.ai) {
81
+ parts.push(` ${chalk.bold('AI:')} integration detected`);
82
+ }
83
+
84
+ console.log(parts.join('\n'));
85
+ console.log('');
86
+ }
87
+
88
+ function printIssues(issues) {
89
+ const critical = issues.filter(i => i.severity === 'critical');
90
+ const warnings = issues.filter(i => i.severity === 'warning');
91
+ const info = issues.filter(i => i.severity === 'info');
92
+
93
+ console.log(chalk.bold(' Health Issues Found:'));
94
+ console.log('');
95
+
96
+ if (critical.length > 0) {
97
+ console.log(chalk.red.bold(' 🔴 CRITICAL'));
98
+ for (let i = 0; i < critical.length; i++) {
99
+ const issue = critical[i];
100
+ console.log(chalk.red(` ${i + 1}. ${issue.title}`));
101
+ console.log(chalk.dim(` → ${issue.impact}`));
102
+ if (issue.files && issue.files.length > 0 && issue.files.length <= 3) {
103
+ console.log(chalk.dim(` Files: ${issue.files.join(', ')}`));
104
+ }
105
+ }
106
+ console.log('');
107
+ }
108
+
109
+ if (warnings.length > 0) {
110
+ console.log(chalk.yellow.bold(' 🟡 WARNING'));
111
+ for (let i = 0; i < warnings.length; i++) {
112
+ const issue = warnings[i];
113
+ console.log(chalk.yellow(` ${critical.length + i + 1}. ${issue.title}`));
114
+ console.log(chalk.dim(` → ${issue.impact}`));
115
+ }
116
+ console.log('');
117
+ }
118
+
119
+ if (info.length > 0) {
120
+ console.log(chalk.blue.bold(' ℹ️ SUGGESTIONS'));
121
+ for (let i = 0; i < info.length; i++) {
122
+ const issue = info[i];
123
+ console.log(chalk.blue(` ${critical.length + warnings.length + i + 1}. ${issue.title}`));
124
+ console.log(chalk.dim(` → ${issue.impact}`));
125
+ }
126
+ console.log('');
127
+ }
128
+
129
+ // Print good checks
130
+ const goodChecks = [];
131
+ if (!critical.some(i => i.promptId === 'UNAUTH_ENDPOINT') && !warnings.some(i => i.promptId === 'UNAUTH_ENDPOINT')) {
132
+ goodChecks.push('All endpoints have authentication');
133
+ }
134
+ if (!critical.some(i => i.promptId === 'FLAKY_TESTS')) {
135
+ goodChecks.push('No flaky test patterns detected');
136
+ }
137
+ if (!warnings.some(i => i.promptId === 'BARE_EXCEPT')) {
138
+ goodChecks.push('No bare except blocks');
139
+ }
140
+
141
+ if (goodChecks.length > 0) {
142
+ console.log(chalk.green.bold(' 🟢 GOOD'));
143
+ for (const check of goodChecks) {
144
+ console.log(chalk.green(` ✓ ${check}`));
145
+ }
146
+ console.log('');
147
+ }
148
+ }
149
+
150
+ async function guidedFix(issues) {
151
+ const { input } = await import('@inquirer/prompts');
152
+ const critical = issues.filter(i => i.severity === 'critical');
153
+ const warnings = issues.filter(i => i.severity === 'warning');
154
+ const allIssues = [...critical, ...warnings];
155
+
156
+ if (allIssues.length === 0) {
157
+ log.info('No critical or warning issues to fix.');
158
+ return;
159
+ }
160
+
161
+ for (let i = 0; i < allIssues.length; i++) {
162
+ const issue = allIssues[i];
163
+ const prompt = generatePrompt(issue);
164
+
165
+ console.log('');
166
+ console.log(chalk.bold(` Issue ${i + 1} of ${allIssues.length}: ${issue.title}`));
167
+ console.log('');
168
+ console.log(chalk.dim(` ${issue.impact}`));
169
+ console.log('');
170
+ console.log(' To fix this, open Claude Code and paste:');
171
+ console.log(chalk.cyan(' ┌' + '─'.repeat(68) + '┐'));
172
+ const promptLines = prompt.split('\n');
173
+ for (const line of promptLines) {
174
+ const padded = line.padEnd(68);
175
+ console.log(chalk.cyan(' │ ') + padded + chalk.cyan(' │'));
176
+ }
177
+ console.log(chalk.cyan(' └' + '─'.repeat(68) + '┘'));
178
+
179
+ if (i < allIssues.length - 1) {
180
+ await input({ message: 'Press enter for next issue, or type q to quit:' }).then(answer => {
181
+ if (answer.toLowerCase() === 'q') {
182
+ process.exit(0);
183
+ }
184
+ });
185
+ }
186
+ }
187
+
188
+ console.log('');
189
+ log.success('All issues shown. Fix them in Claude Code one at a time.');
190
+ console.log('');
191
+ }
192
+
193
+ async function saveReport(issues, scan, projectDir) {
194
+ const report = generateReport(issues, scan.projectName);
195
+ const reportPath = path.join(projectDir, 'docs', 'doctor-report.md');
196
+ writeFile(reportPath, report);
197
+ console.log('');
198
+ log.success(`Report saved to ${path.relative(projectDir, reportPath)}`);
199
+ console.log('');
200
+ }
201
+
202
+ async function autoFixSafe(issues, projectDir) {
203
+ console.log('');
204
+ console.log(' Auto-fixing safe issues...');
205
+ console.log('');
206
+
207
+ let fixed = 0;
208
+
209
+ // Create missing directories
210
+ const dirsToCreate = ['docs/uat', 'docs/plans'];
211
+ for (const dir of dirsToCreate) {
212
+ const fullPath = path.join(projectDir, dir);
213
+ if (!fs.existsSync(fullPath)) {
214
+ ensureDir(fullPath);
215
+ console.log(chalk.green(` ✓ Created ${dir}/ directory`));
216
+ fixed++;
217
+ }
218
+ }
219
+
220
+ // Fix hook script permissions (Unix only)
221
+ if (process.platform !== 'win32') {
222
+ const hooksDir = path.join(projectDir, '.claude', 'hooks');
223
+ if (fs.existsSync(hooksDir)) {
224
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
225
+ for (const hookFile of hookFiles) {
226
+ const hookPath = path.join(hooksDir, hookFile);
227
+ try {
228
+ fs.chmodSync(hookPath, '755');
229
+ console.log(chalk.green(` ✓ Fixed ${path.join('.claude/hooks', hookFile)} permissions (chmod +x)`));
230
+ fixed++;
231
+ } catch {
232
+ // Skip
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // Add missing .gitignore entries
239
+ const gitignorePath = path.join(projectDir, '.gitignore');
240
+ if (fs.existsSync(gitignorePath)) {
241
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
242
+ const entriesToAdd = [];
243
+
244
+ if (!content.includes('.claude/todos')) entriesToAdd.push('.claude/todos');
245
+ if (!content.includes('.claude/plans')) entriesToAdd.push('.claude/plans');
246
+
247
+ if (entriesToAdd.length > 0) {
248
+ const addition = '\n# Claude Code temp files\n' + entriesToAdd.join('\n') + '\n';
249
+ fs.appendFileSync(gitignorePath, addition);
250
+ console.log(chalk.green(` ✓ Added ${entriesToAdd.length} entries to .gitignore`));
251
+ fixed++;
252
+ }
253
+ }
254
+
255
+ const skipped = issues.filter(i => !i.autoFixable).length;
256
+
257
+ if (fixed === 0) {
258
+ console.log(chalk.dim(' Nothing to auto-fix.'));
259
+ }
260
+
261
+ if (skipped > 0) {
262
+ console.log(chalk.dim(` ⊘ Skipped ${skipped} issues that need Claude Code to fix`));
263
+ }
264
+
265
+ console.log('');
266
+ if (skipped > 0) {
267
+ console.log(` For the remaining issues, run: ${chalk.cyan('npx devforge doctor')} → option 1 or 2`);
268
+ console.log('');
269
+ }
270
+ }
271
+
272
+ async function exportPrompts(issues, projectDir) {
273
+ const content = generateAllPrompts(issues);
274
+ const promptsPath = path.join(projectDir, 'docs', 'doctor-prompts.md');
275
+ writeFile(promptsPath, content);
276
+ console.log('');
277
+ log.success(`Fix prompts saved to ${path.relative(projectDir, promptsPath)}`);
278
+ console.log(chalk.dim(' Open Claude Code and work through them one at a time.'));
279
+ console.log(chalk.dim(' /clear between sessions.'));
280
+ console.log('');
281
+ }