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.
- package/CLAUDE.md +38 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/devforge.js +4 -0
- package/package.json +33 -0
- package/src/claude-configurator.js +260 -0
- package/src/cli.js +119 -0
- package/src/composer.js +214 -0
- package/src/doctor-checks.js +743 -0
- package/src/doctor-prompts.js +295 -0
- package/src/doctor.js +281 -0
- package/src/guided.js +315 -0
- package/src/index.js +148 -0
- package/src/init-mode.js +134 -0
- package/src/prompts.js +155 -0
- package/src/recommender.js +186 -0
- package/src/scanner.js +368 -0
- package/src/uat-generator.js +189 -0
- package/src/utils.js +57 -0
- package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
- package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
- package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
- package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
- package/templates/auth/nextauth/src/middleware.ts.template +14 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
- package/templates/backend/fastapi/backend/app/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
- package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
- package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
- package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
- package/templates/backend/fastapi/backend/app/main.py.template +58 -0
- package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
- package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
- package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
- package/templates/base/.gitignore.template +29 -0
- package/templates/base/README.md.template +25 -0
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
- package/templates/claude-code/agents/production-readiness.md +55 -0
- package/templates/claude-code/agents/security-reviewer.md +41 -0
- package/templates/claude-code/agents/spec-validator.md +34 -0
- package/templates/claude-code/agents/uat-validator.md +37 -0
- package/templates/claude-code/claude-md/base.md +33 -0
- package/templates/claude-code/claude-md/fastapi.md +12 -0
- package/templates/claude-code/claude-md/fullstack.md +12 -0
- package/templates/claude-code/claude-md/nextjs.md +11 -0
- package/templates/claude-code/commands/audit-security.md +11 -0
- package/templates/claude-code/commands/audit-spec.md +9 -0
- package/templates/claude-code/commands/audit-wiring.md +17 -0
- package/templates/claude-code/commands/done.md +19 -0
- package/templates/claude-code/commands/generate-prd.md +45 -0
- package/templates/claude-code/commands/generate-uat.md +35 -0
- package/templates/claude-code/commands/help.md +26 -0
- package/templates/claude-code/commands/next.md +20 -0
- package/templates/claude-code/commands/optimize-claude-md.md +31 -0
- package/templates/claude-code/commands/pre-pr.md +19 -0
- package/templates/claude-code/commands/run-uat.md +21 -0
- package/templates/claude-code/commands/status.md +24 -0
- package/templates/claude-code/commands/verify-all.md +11 -0
- package/templates/claude-code/hooks/polyglot.json +36 -0
- package/templates/claude-code/hooks/python.json +36 -0
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
- package/templates/claude-code/hooks/typescript.json +36 -0
- package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
- package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
- package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
- package/templates/claude-code/skills/playwright/SKILL.md +37 -0
- package/templates/claude-code/skills/security-api/SKILL.md +47 -0
- package/templates/claude-code/skills/security-web/SKILL.md +41 -0
- package/templates/database/prisma-postgres/.env.example +1 -0
- package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
- package/templates/database/sqlalchemy-postgres/.env.example +1 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
- package/templates/frontend/nextjs/next.config.ts.template +7 -0
- package/templates/frontend/nextjs/package.json.template +41 -0
- package/templates/frontend/nextjs/postcss.config.mjs +7 -0
- package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
- package/templates/frontend/nextjs/src/app/globals.css +1 -0
- package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
- package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
- package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
- package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
- package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
- package/templates/frontend/nextjs/tsconfig.json +23 -0
- package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
- package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
- package/templates/testing/playwright/playwright.config.ts.template +22 -0
- 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
|
+
}
|