forgedev 1.0.0 → 1.0.2
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 +3 -3
- package/README.md +246 -246
- package/bin/devforge.js +4 -4
- package/package.json +33 -33
- package/src/claude-configurator.js +260 -260
- package/src/cli.js +119 -119
- package/src/composer.js +214 -214
- package/src/doctor-checks.js +743 -743
- package/src/doctor-prompts.js +295 -295
- package/src/doctor.js +281 -281
- package/src/guided.js +315 -315
- package/src/index.js +148 -148
- package/src/init-mode.js +138 -134
- package/src/prompts.js +155 -155
- package/src/scanner.js +368 -368
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -41
- package/templates/claude-code/agents/production-readiness.md +55 -55
- package/templates/claude-code/agents/security-reviewer.md +41 -41
- package/templates/claude-code/agents/spec-validator.md +34 -34
- package/templates/claude-code/agents/uat-validator.md +37 -37
- package/templates/claude-code/claude-md/base.md +33 -33
- package/templates/claude-code/commands/done.md +19 -19
- package/templates/claude-code/commands/generate-prd.md +45 -45
- package/templates/claude-code/commands/generate-uat.md +35 -35
- package/templates/claude-code/commands/next.md +20 -20
- package/templates/claude-code/commands/optimize-claude-md.md +31 -31
- package/templates/claude-code/commands/status.md +24 -24
- package/templates/claude-code/commands/workflows.md +26 -0
- package/templates/claude-code/hooks/polyglot.json +36 -36
- package/templates/claude-code/hooks/python.json +36 -36
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -16
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -14
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -14
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -21
- package/templates/claude-code/hooks/typescript.json +36 -36
- package/templates/claude-code/commands/help.md +0 -26
package/src/doctor.js
CHANGED
|
@@ -1,281 +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
|
-
}
|
|
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
|
+
}
|