@codebakers/cli 3.9.31 → 3.9.33
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/dist/commands/coherence.d.ts +10 -0
- package/dist/commands/coherence.js +396 -0
- package/dist/commands/go.js +68 -21
- package/dist/index.js +17 -1
- package/dist/mcp/server.js +728 -26
- package/package.json +1 -1
- package/src/commands/coherence.ts +455 -0
- package/src/commands/go.ts +18 -10
- package/src/index.ts +18 -1
- package/src/mcp/server.ts +768 -12
- package/tmpclaude-1ef9-cwd +1 -0
- package/tmpclaude-333f-cwd +1 -0
- package/tmpclaude-77b7-cwd +1 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1353,6 +1353,28 @@ class CodeBakersServer {
|
|
|
1353
1353
|
properties: {},
|
|
1354
1354
|
},
|
|
1355
1355
|
},
|
|
1356
|
+
{
|
|
1357
|
+
name: 'coherence_audit',
|
|
1358
|
+
description: 'Full codebase coherence audit. Checks all imports/exports, type flows, schema dependencies, API contracts, env vars, circular dependencies, and dead code. Use this for the /coherence command or when user asks to check wiring/dependencies.',
|
|
1359
|
+
inputSchema: {
|
|
1360
|
+
type: 'object',
|
|
1361
|
+
properties: {
|
|
1362
|
+
focus: {
|
|
1363
|
+
type: 'string',
|
|
1364
|
+
enum: ['all', 'imports', 'types', 'schema', 'api', 'env', 'circular', 'dead-code'],
|
|
1365
|
+
description: 'Focus area for the audit (default: all)',
|
|
1366
|
+
},
|
|
1367
|
+
autoFix: {
|
|
1368
|
+
type: 'boolean',
|
|
1369
|
+
description: 'Automatically fix issues that can be auto-fixed (default: false)',
|
|
1370
|
+
},
|
|
1371
|
+
includeNodeModules: {
|
|
1372
|
+
type: 'boolean',
|
|
1373
|
+
description: 'Include node_modules in analysis (default: false, very slow)',
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1356
1378
|
// ============================================
|
|
1357
1379
|
// PROJECT TRACKING - Server-Side Dashboard
|
|
1358
1380
|
// ============================================
|
|
@@ -1632,6 +1654,8 @@ class CodeBakersServer {
|
|
|
1632
1654
|
return this.handleGuardianVerify(args);
|
|
1633
1655
|
case 'guardian_status':
|
|
1634
1656
|
return this.handleGuardianStatus();
|
|
1657
|
+
case 'coherence_audit':
|
|
1658
|
+
return this.handleCoherenceAudit(args);
|
|
1635
1659
|
// Project Tracking - Server-Side Dashboard
|
|
1636
1660
|
case 'project_sync':
|
|
1637
1661
|
return this.handleProjectSync(args);
|
|
@@ -2466,9 +2490,17 @@ Or if user declines, call without fullDeploy:
|
|
|
2466
2490
|
catch {
|
|
2467
2491
|
// Use default
|
|
2468
2492
|
}
|
|
2469
|
-
results.push(`# 🎨 Adding CodeBakers v6.
|
|
2470
|
-
// v6.
|
|
2471
|
-
const V6_CLAUDE_MD = `# CodeBakers v6.
|
|
2493
|
+
results.push(`# 🎨 Adding CodeBakers v6.17 to: ${projectName}\n`);
|
|
2494
|
+
// v6.17 bootstrap content - magic phrase + rules at START and END + coherence
|
|
2495
|
+
const V6_CLAUDE_MD = `# CodeBakers v6.17
|
|
2496
|
+
|
|
2497
|
+
## 🪄 MAGIC PHRASE: "codebakers go"
|
|
2498
|
+
When user says "codebakers go" in chat, start the onboarding conversation:
|
|
2499
|
+
1. Ask: "Is this an existing project or are you starting fresh?" (1=existing, 2=new)
|
|
2500
|
+
2. If existing: "Great! What would you like to work on?"
|
|
2501
|
+
3. If new: "What do you want to build? Describe your project."
|
|
2502
|
+
4. Then call \`init_project()\` to set up CodeBakers files
|
|
2503
|
+
5. Continue helping them build
|
|
2472
2504
|
|
|
2473
2505
|
## 🚨 ALWAYS DO THIS (Critical - Read First)
|
|
2474
2506
|
1. ALWAYS call \`project_status()\` FIRST → If works show "🍪 CodeBakers is working..." / If fails show "⚠️ Not connected"
|
|
@@ -2487,10 +2519,13 @@ Or if user declines, call without fullDeploy:
|
|
|
2487
2519
|
project_status() → Verify connection FIRST
|
|
2488
2520
|
discover_patterns({ task: "what you're building" }) → Get patterns BEFORE code
|
|
2489
2521
|
validate_complete({ feature: "name", files: [...] }) → Validate BEFORE done
|
|
2522
|
+
coherence_audit() → Check wiring & dependencies
|
|
2490
2523
|
\`\`\`
|
|
2491
2524
|
|
|
2525
|
+
Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
|
|
2526
|
+
|
|
2492
2527
|
Header (after project_status succeeds): 🍪 CodeBakers is working on this...
|
|
2493
|
-
Header (if project_status fails): ⚠️ CodeBakers not connected
|
|
2528
|
+
Header (if project_status fails): ⚠️ CodeBakers not connected
|
|
2494
2529
|
Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
2495
2530
|
|
|
2496
2531
|
## 🚨 ALWAYS DO THIS (Critical - Repeated at End)
|
|
@@ -2499,7 +2534,10 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
2499
2534
|
3. Call \`validate_complete()\` before done
|
|
2500
2535
|
4. Show footer after code responses
|
|
2501
2536
|
`;
|
|
2502
|
-
const V6_CURSORRULES = `# CodeBakers v6.
|
|
2537
|
+
const V6_CURSORRULES = `# CodeBakers v6.17
|
|
2538
|
+
|
|
2539
|
+
## 🪄 "codebakers go" = Start onboarding conversation
|
|
2540
|
+
Ask existing/new → Ask what to build → Call init_project() → Help them build
|
|
2503
2541
|
|
|
2504
2542
|
## 🚨 ALWAYS (Read First)
|
|
2505
2543
|
1. Call project_status() FIRST → 🍪 Working... or ⚠️ Not connected
|
|
@@ -2513,30 +2551,33 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
2513
2551
|
3. Show header without project_status succeeding
|
|
2514
2552
|
4. Skip writing tests for new features
|
|
2515
2553
|
|
|
2554
|
+
Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
|
|
2555
|
+
Use coherence_audit() to check wiring & dependencies
|
|
2556
|
+
|
|
2516
2557
|
## 🚨 ALWAYS (Repeated at End)
|
|
2517
2558
|
1. project_status() FIRST
|
|
2518
2559
|
2. discover_patterns() before code
|
|
2519
2560
|
3. validate_complete() before done
|
|
2520
2561
|
`;
|
|
2521
|
-
// Check if already v6.
|
|
2562
|
+
// Check if already v6.16
|
|
2522
2563
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
2523
2564
|
if (fs.existsSync(claudeMdPath)) {
|
|
2524
2565
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
2525
|
-
if (content.includes('v6.
|
|
2526
|
-
results.push('✓ CodeBakers v6.
|
|
2566
|
+
if ((content.includes('v6.16') || content.includes('v6.17')) && content.includes('discover_patterns')) {
|
|
2567
|
+
results.push('✓ CodeBakers v6.17 already installed\n');
|
|
2527
2568
|
results.push('Patterns are server-enforced. Just call `discover_patterns` before coding!');
|
|
2528
2569
|
return {
|
|
2529
2570
|
content: [{ type: 'text', text: results.join('\n') }],
|
|
2530
2571
|
};
|
|
2531
2572
|
}
|
|
2532
|
-
results.push('⚠️ Upgrading to v6.
|
|
2573
|
+
results.push('⚠️ Upgrading to v6.16 (server-enforced patterns)...\n');
|
|
2533
2574
|
}
|
|
2534
2575
|
try {
|
|
2535
|
-
// Write v6.
|
|
2576
|
+
// Write v6.16 bootstrap files
|
|
2536
2577
|
fs.writeFileSync(claudeMdPath, V6_CLAUDE_MD);
|
|
2537
|
-
results.push('✓ Created CLAUDE.md (v6.
|
|
2578
|
+
results.push('✓ Created CLAUDE.md (v6.16 bootstrap)');
|
|
2538
2579
|
fs.writeFileSync(path.join(cwd, '.cursorrules'), V6_CURSORRULES);
|
|
2539
|
-
results.push('✓ Created .cursorrules (v6.
|
|
2580
|
+
results.push('✓ Created .cursorrules (v6.16 bootstrap)');
|
|
2540
2581
|
// Remove old .claude folder if it exists (v5 → v6 migration)
|
|
2541
2582
|
const claudeDir = path.join(cwd, '.claude');
|
|
2542
2583
|
if (fs.existsSync(claudeDir)) {
|
|
@@ -2581,7 +2622,7 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
2581
2622
|
state.updatedAt = new Date().toISOString();
|
|
2582
2623
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
2583
2624
|
results.push('\n---\n');
|
|
2584
|
-
results.push('## ✅ CodeBakers v6.
|
|
2625
|
+
results.push('## ✅ CodeBakers v6.17 Installed!\n');
|
|
2585
2626
|
results.push('**How it works now:**');
|
|
2586
2627
|
results.push('1. Call `discover_patterns` before writing code');
|
|
2587
2628
|
results.push('2. Server returns all patterns and rules');
|
|
@@ -6675,7 +6716,7 @@ ${handlers.join('\n')}
|
|
|
6675
6716
|
`;
|
|
6676
6717
|
}
|
|
6677
6718
|
/**
|
|
6678
|
-
* Update to CodeBakers v6.
|
|
6719
|
+
* Update to CodeBakers v6.17 - server-enforced patterns with magic phrase + coherence
|
|
6679
6720
|
* This is the MCP equivalent of the `codebakers upgrade` CLI command
|
|
6680
6721
|
*/
|
|
6681
6722
|
async handleUpdatePatterns(args) {
|
|
@@ -6684,9 +6725,17 @@ ${handlers.join('\n')}
|
|
|
6684
6725
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
6685
6726
|
const claudeDir = path.join(cwd, '.claude');
|
|
6686
6727
|
const codebakersJson = path.join(cwd, '.codebakers.json');
|
|
6687
|
-
let response = `# 🔄 CodeBakers v6.
|
|
6688
|
-
// v6.
|
|
6689
|
-
const V6_CLAUDE_MD = `# CodeBakers v6.
|
|
6728
|
+
let response = `# 🔄 CodeBakers v6.17 Update\n\n`;
|
|
6729
|
+
// v6.17 bootstrap content - magic phrase + rules at START and END + coherence
|
|
6730
|
+
const V6_CLAUDE_MD = `# CodeBakers v6.17
|
|
6731
|
+
|
|
6732
|
+
## 🪄 MAGIC PHRASE: "codebakers go"
|
|
6733
|
+
When user says "codebakers go" in chat, start the onboarding conversation:
|
|
6734
|
+
1. Ask: "Is this an existing project or are you starting fresh?" (1=existing, 2=new)
|
|
6735
|
+
2. If existing: "Great! What would you like to work on?"
|
|
6736
|
+
3. If new: "What do you want to build? Describe your project."
|
|
6737
|
+
4. Then call \`init_project()\` to set up CodeBakers files
|
|
6738
|
+
5. Continue helping them build
|
|
6690
6739
|
|
|
6691
6740
|
## 🚨 ALWAYS DO THIS (Critical - Read First)
|
|
6692
6741
|
1. ALWAYS call \`project_status()\` FIRST → If works show "🍪 CodeBakers is working..." / If fails show "⚠️ Not connected"
|
|
@@ -6705,10 +6754,13 @@ ${handlers.join('\n')}
|
|
|
6705
6754
|
project_status() → Verify connection FIRST
|
|
6706
6755
|
discover_patterns({ task: "what you're building" }) → Get patterns BEFORE code
|
|
6707
6756
|
validate_complete({ feature: "name", files: [...] }) → Validate BEFORE done
|
|
6757
|
+
coherence_audit() → Check wiring & dependencies
|
|
6708
6758
|
\`\`\`
|
|
6709
6759
|
|
|
6760
|
+
Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
|
|
6761
|
+
|
|
6710
6762
|
Header (after project_status succeeds): 🍪 CodeBakers is working on this...
|
|
6711
|
-
Header (if project_status fails): ⚠️ CodeBakers not connected
|
|
6763
|
+
Header (if project_status fails): ⚠️ CodeBakers not connected
|
|
6712
6764
|
Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
6713
6765
|
|
|
6714
6766
|
## 🚨 ALWAYS DO THIS (Critical - Repeated at End)
|
|
@@ -6717,7 +6769,10 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
6717
6769
|
3. Call \`validate_complete()\` before done
|
|
6718
6770
|
4. Show footer after code responses
|
|
6719
6771
|
`;
|
|
6720
|
-
const V6_CURSORRULES = `# CodeBakers v6.
|
|
6772
|
+
const V6_CURSORRULES = `# CodeBakers v6.17
|
|
6773
|
+
|
|
6774
|
+
## 🪄 "codebakers go" = Start onboarding conversation
|
|
6775
|
+
Ask existing/new → Ask what to build → Call init_project() → Help them build
|
|
6721
6776
|
|
|
6722
6777
|
## 🚨 ALWAYS (Read First)
|
|
6723
6778
|
1. Call project_status() FIRST → 🍪 Working... or ⚠️ Not connected
|
|
@@ -6731,6 +6786,9 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
6731
6786
|
3. Show header without project_status succeeding
|
|
6732
6787
|
4. Skip writing tests for new features
|
|
6733
6788
|
|
|
6789
|
+
Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
|
|
6790
|
+
Use coherence_audit() to check wiring & dependencies
|
|
6791
|
+
|
|
6734
6792
|
## 🚨 ALWAYS (Repeated at End)
|
|
6735
6793
|
1. project_status() FIRST
|
|
6736
6794
|
2. discover_patterns() before code
|
|
@@ -6742,7 +6800,7 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
6742
6800
|
let isV6 = false;
|
|
6743
6801
|
if (fs.existsSync(claudeMdPath)) {
|
|
6744
6802
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
6745
|
-
isV6 = content.includes('v6.
|
|
6803
|
+
isV6 = content.includes('v6.16') && content.includes('discover_patterns');
|
|
6746
6804
|
}
|
|
6747
6805
|
if (fs.existsSync(codebakersJson)) {
|
|
6748
6806
|
try {
|
|
@@ -6755,10 +6813,10 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
6755
6813
|
}
|
|
6756
6814
|
response += `## Current Status\n`;
|
|
6757
6815
|
response += `- Version: ${currentVersion || 'Unknown'}\n`;
|
|
6758
|
-
response += `- v6.
|
|
6816
|
+
response += `- v6.16 (Server-Enforced): ${isV6 ? 'Yes ✓' : 'No'}\n\n`;
|
|
6759
6817
|
// Check if already on v6
|
|
6760
6818
|
if (isV6 && !force) {
|
|
6761
|
-
response += `✅ **Already on v6.
|
|
6819
|
+
response += `✅ **Already on v6.16!**\n\n`;
|
|
6762
6820
|
response += `Your patterns are server-enforced. Just use \`discover_patterns\` before coding.\n`;
|
|
6763
6821
|
response += `Use \`force: true\` to reinstall bootstrap files.\n`;
|
|
6764
6822
|
response += this.getUpdateNotice();
|
|
@@ -6769,12 +6827,12 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
|
|
|
6769
6827
|
}],
|
|
6770
6828
|
};
|
|
6771
6829
|
}
|
|
6772
|
-
response += `## Upgrading to v6.
|
|
6773
|
-
// Write v6.
|
|
6830
|
+
response += `## Upgrading to v6.16...\n\n`;
|
|
6831
|
+
// Write v6.16 bootstrap files
|
|
6774
6832
|
fs.writeFileSync(claudeMdPath, V6_CLAUDE_MD);
|
|
6775
|
-
response += `✓ Updated CLAUDE.md (v6.
|
|
6833
|
+
response += `✓ Updated CLAUDE.md (v6.16 bootstrap)\n`;
|
|
6776
6834
|
fs.writeFileSync(path.join(cwd, '.cursorrules'), V6_CURSORRULES);
|
|
6777
|
-
response += `✓ Updated .cursorrules (v6.
|
|
6835
|
+
response += `✓ Updated .cursorrules (v6.16 bootstrap)\n`;
|
|
6778
6836
|
// Remove old .claude folder (v5 → v6 migration)
|
|
6779
6837
|
if (fs.existsSync(claudeDir)) {
|
|
6780
6838
|
try {
|
|
@@ -7950,6 +8008,650 @@ ${events.includes('call-started') ? ` case 'call-started':
|
|
|
7950
8008
|
return { content: [{ type: 'text', text: response }] };
|
|
7951
8009
|
}
|
|
7952
8010
|
// ============================================================================
|
|
8011
|
+
// COHERENCE AUDIT - Full Codebase Wiring Check
|
|
8012
|
+
// ============================================================================
|
|
8013
|
+
/**
|
|
8014
|
+
* Full coherence audit - checks all wiring, dependencies, and connections
|
|
8015
|
+
*/
|
|
8016
|
+
handleCoherenceAudit(args) {
|
|
8017
|
+
const { focus = 'all', autoFix = false } = args;
|
|
8018
|
+
const cwd = process.cwd();
|
|
8019
|
+
const issues = [];
|
|
8020
|
+
const stats = {
|
|
8021
|
+
filesScanned: 0,
|
|
8022
|
+
importsChecked: 0,
|
|
8023
|
+
exportsFound: 0,
|
|
8024
|
+
typesAnalyzed: 0,
|
|
8025
|
+
envVarsFound: 0,
|
|
8026
|
+
};
|
|
8027
|
+
// Helper to extract imports from a file
|
|
8028
|
+
const extractImports = (content) => {
|
|
8029
|
+
const imports = [];
|
|
8030
|
+
const lines = content.split('\n');
|
|
8031
|
+
lines.forEach((line, i) => {
|
|
8032
|
+
// Match: import { X, Y } from 'path'
|
|
8033
|
+
const namedMatch = line.match(/import\s+(type\s+)?{([^}]+)}\s+from\s+['"]([^'"]+)['"]/);
|
|
8034
|
+
if (namedMatch) {
|
|
8035
|
+
const isTypeOnly = !!namedMatch[1];
|
|
8036
|
+
const names = namedMatch[2].split(',').map(n => n.trim().split(' as ')[0].trim()).filter(Boolean);
|
|
8037
|
+
imports.push({ path: namedMatch[3], names, line: i + 1, isTypeOnly });
|
|
8038
|
+
}
|
|
8039
|
+
// Match: import X from 'path'
|
|
8040
|
+
const defaultMatch = line.match(/import\s+(type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
8041
|
+
if (defaultMatch && !namedMatch) {
|
|
8042
|
+
imports.push({ path: defaultMatch[3], names: ['default'], line: i + 1, isTypeOnly: !!defaultMatch[1] });
|
|
8043
|
+
}
|
|
8044
|
+
// Match: import * as X from 'path'
|
|
8045
|
+
const namespaceMatch = line.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
8046
|
+
if (namespaceMatch) {
|
|
8047
|
+
imports.push({ path: namespaceMatch[2], names: ['*'], line: i + 1, isTypeOnly: false });
|
|
8048
|
+
}
|
|
8049
|
+
});
|
|
8050
|
+
return imports;
|
|
8051
|
+
};
|
|
8052
|
+
// Helper to extract exports from a file
|
|
8053
|
+
const extractExports = (content) => {
|
|
8054
|
+
const exports = [];
|
|
8055
|
+
// Named exports: export { X, Y }
|
|
8056
|
+
const namedExportMatches = content.matchAll(/export\s+{([^}]+)}/g);
|
|
8057
|
+
for (const match of namedExportMatches) {
|
|
8058
|
+
const names = match[1].split(',').map(n => n.trim().split(' as ').pop()?.trim() || '').filter(Boolean);
|
|
8059
|
+
exports.push(...names);
|
|
8060
|
+
}
|
|
8061
|
+
// Direct exports: export const/function/class/type/interface X
|
|
8062
|
+
const directExportMatches = content.matchAll(/export\s+(const|let|var|function|class|type|interface|enum)\s+(\w+)/g);
|
|
8063
|
+
for (const match of directExportMatches) {
|
|
8064
|
+
exports.push(match[2]);
|
|
8065
|
+
}
|
|
8066
|
+
// Default export
|
|
8067
|
+
if (content.includes('export default')) {
|
|
8068
|
+
exports.push('default');
|
|
8069
|
+
}
|
|
8070
|
+
// Re-exports: export * from, export { X } from
|
|
8071
|
+
const reExportMatches = content.matchAll(/export\s+(?:\*|{[^}]+})\s+from\s+['"]([^'"]+)['"]/g);
|
|
8072
|
+
for (const match of reExportMatches) {
|
|
8073
|
+
exports.push(`__reexport:${match[1]}`);
|
|
8074
|
+
}
|
|
8075
|
+
return exports;
|
|
8076
|
+
};
|
|
8077
|
+
// Helper to resolve import path to file
|
|
8078
|
+
const resolveImportPath = (importPath, fromFile) => {
|
|
8079
|
+
// Skip external packages
|
|
8080
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('@/') && !importPath.startsWith('~/')) {
|
|
8081
|
+
return null;
|
|
8082
|
+
}
|
|
8083
|
+
// Handle @ alias (common Next.js pattern)
|
|
8084
|
+
let resolvedPath = importPath;
|
|
8085
|
+
if (importPath.startsWith('@/')) {
|
|
8086
|
+
resolvedPath = path.join(cwd, 'src', importPath.slice(2));
|
|
8087
|
+
}
|
|
8088
|
+
else if (importPath.startsWith('~/')) {
|
|
8089
|
+
resolvedPath = path.join(cwd, importPath.slice(2));
|
|
8090
|
+
}
|
|
8091
|
+
else {
|
|
8092
|
+
resolvedPath = path.resolve(path.dirname(fromFile), importPath);
|
|
8093
|
+
}
|
|
8094
|
+
// Try different extensions
|
|
8095
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
|
|
8096
|
+
for (const ext of extensions) {
|
|
8097
|
+
const fullPath = resolvedPath + ext;
|
|
8098
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
8099
|
+
return fullPath;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
return null;
|
|
8103
|
+
};
|
|
8104
|
+
// Build export map for all files
|
|
8105
|
+
const exportMap = new Map();
|
|
8106
|
+
const importGraph = new Map(); // file -> files it imports
|
|
8107
|
+
const usedExports = new Set(); // track which exports are actually used
|
|
8108
|
+
const searchDirs = ['src', 'app', 'lib', 'components', 'services', 'types', 'utils', 'hooks', 'pages'];
|
|
8109
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
8110
|
+
// First pass: collect all exports
|
|
8111
|
+
const collectExports = (dir) => {
|
|
8112
|
+
if (!fs.existsSync(dir))
|
|
8113
|
+
return;
|
|
8114
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8115
|
+
for (const entry of entries) {
|
|
8116
|
+
const fullPath = path.join(dir, entry.name);
|
|
8117
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
8118
|
+
collectExports(fullPath);
|
|
8119
|
+
}
|
|
8120
|
+
else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
8121
|
+
try {
|
|
8122
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8123
|
+
const exports = extractExports(content);
|
|
8124
|
+
exportMap.set(fullPath, exports);
|
|
8125
|
+
stats.exportsFound += exports.length;
|
|
8126
|
+
stats.filesScanned++;
|
|
8127
|
+
}
|
|
8128
|
+
catch {
|
|
8129
|
+
// Skip unreadable files
|
|
8130
|
+
}
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
};
|
|
8134
|
+
// Collect exports from all directories
|
|
8135
|
+
for (const dir of searchDirs) {
|
|
8136
|
+
collectExports(path.join(cwd, dir));
|
|
8137
|
+
}
|
|
8138
|
+
// Also check root-level files
|
|
8139
|
+
try {
|
|
8140
|
+
const rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
8141
|
+
for (const entry of rootEntries) {
|
|
8142
|
+
if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
8143
|
+
const fullPath = path.join(cwd, entry.name);
|
|
8144
|
+
try {
|
|
8145
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8146
|
+
const exports = extractExports(content);
|
|
8147
|
+
exportMap.set(fullPath, exports);
|
|
8148
|
+
}
|
|
8149
|
+
catch {
|
|
8150
|
+
// Skip
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
}
|
|
8154
|
+
}
|
|
8155
|
+
catch {
|
|
8156
|
+
// Skip if can't read root
|
|
8157
|
+
}
|
|
8158
|
+
// Second pass: check imports and build graph
|
|
8159
|
+
const checkImports = (dir) => {
|
|
8160
|
+
if (!fs.existsSync(dir))
|
|
8161
|
+
return;
|
|
8162
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8163
|
+
for (const entry of entries) {
|
|
8164
|
+
const fullPath = path.join(dir, entry.name);
|
|
8165
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
8166
|
+
checkImports(fullPath);
|
|
8167
|
+
}
|
|
8168
|
+
else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
8169
|
+
try {
|
|
8170
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8171
|
+
const imports = extractImports(content);
|
|
8172
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
8173
|
+
const importedFiles = [];
|
|
8174
|
+
for (const imp of imports) {
|
|
8175
|
+
stats.importsChecked++;
|
|
8176
|
+
const resolvedPath = resolveImportPath(imp.path, fullPath);
|
|
8177
|
+
if (resolvedPath === null) {
|
|
8178
|
+
// External package or unresolvable - skip
|
|
8179
|
+
continue;
|
|
8180
|
+
}
|
|
8181
|
+
importedFiles.push(resolvedPath);
|
|
8182
|
+
// Check if file exists
|
|
8183
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
8184
|
+
if (focus === 'all' || focus === 'imports') {
|
|
8185
|
+
issues.push({
|
|
8186
|
+
category: 'import',
|
|
8187
|
+
severity: 'error',
|
|
8188
|
+
file: relativePath,
|
|
8189
|
+
line: imp.line,
|
|
8190
|
+
message: `Import target not found: '${imp.path}'`,
|
|
8191
|
+
fix: `Create the file or update the import path`,
|
|
8192
|
+
autoFixable: false,
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8195
|
+
continue;
|
|
8196
|
+
}
|
|
8197
|
+
// Check if named imports exist in the target
|
|
8198
|
+
const targetExports = exportMap.get(resolvedPath) || [];
|
|
8199
|
+
for (const name of imp.names) {
|
|
8200
|
+
if (name === '*' || name === 'default') {
|
|
8201
|
+
if (name === 'default' && !targetExports.includes('default')) {
|
|
8202
|
+
if (focus === 'all' || focus === 'imports') {
|
|
8203
|
+
issues.push({
|
|
8204
|
+
category: 'import',
|
|
8205
|
+
severity: 'error',
|
|
8206
|
+
file: relativePath,
|
|
8207
|
+
line: imp.line,
|
|
8208
|
+
message: `No default export in '${imp.path}'`,
|
|
8209
|
+
fix: `Add 'export default' or change to named import`,
|
|
8210
|
+
autoFixable: false,
|
|
8211
|
+
});
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
continue;
|
|
8215
|
+
}
|
|
8216
|
+
// Track that this export is used
|
|
8217
|
+
usedExports.add(`${resolvedPath}:${name}`);
|
|
8218
|
+
if (!targetExports.includes(name)) {
|
|
8219
|
+
if (focus === 'all' || focus === 'imports') {
|
|
8220
|
+
issues.push({
|
|
8221
|
+
category: 'export',
|
|
8222
|
+
severity: 'error',
|
|
8223
|
+
file: relativePath,
|
|
8224
|
+
line: imp.line,
|
|
8225
|
+
message: `'${name}' is not exported from '${imp.path}'`,
|
|
8226
|
+
fix: `Add 'export { ${name} }' to ${path.basename(resolvedPath)} or update import`,
|
|
8227
|
+
autoFixable: false,
|
|
8228
|
+
});
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
importGraph.set(fullPath, importedFiles);
|
|
8234
|
+
}
|
|
8235
|
+
catch {
|
|
8236
|
+
// Skip unreadable files
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
};
|
|
8241
|
+
// Run import checks
|
|
8242
|
+
for (const dir of searchDirs) {
|
|
8243
|
+
checkImports(path.join(cwd, dir));
|
|
8244
|
+
}
|
|
8245
|
+
// Check for circular dependencies
|
|
8246
|
+
if (focus === 'all' || focus === 'circular') {
|
|
8247
|
+
const visited = new Set();
|
|
8248
|
+
const recursionStack = new Set();
|
|
8249
|
+
const circularPaths = [];
|
|
8250
|
+
const detectCircular = (file, pathStack) => {
|
|
8251
|
+
if (recursionStack.has(file)) {
|
|
8252
|
+
// Found circular dependency
|
|
8253
|
+
const cycleStart = pathStack.indexOf(file);
|
|
8254
|
+
if (cycleStart !== -1) {
|
|
8255
|
+
circularPaths.push(pathStack.slice(cycleStart).concat(file));
|
|
8256
|
+
}
|
|
8257
|
+
return true;
|
|
8258
|
+
}
|
|
8259
|
+
if (visited.has(file))
|
|
8260
|
+
return false;
|
|
8261
|
+
visited.add(file);
|
|
8262
|
+
recursionStack.add(file);
|
|
8263
|
+
const imports = importGraph.get(file) || [];
|
|
8264
|
+
for (const imported of imports) {
|
|
8265
|
+
detectCircular(imported, [...pathStack, file]);
|
|
8266
|
+
}
|
|
8267
|
+
recursionStack.delete(file);
|
|
8268
|
+
return false;
|
|
8269
|
+
};
|
|
8270
|
+
for (const file of importGraph.keys()) {
|
|
8271
|
+
detectCircular(file, []);
|
|
8272
|
+
}
|
|
8273
|
+
// Add unique circular dependencies as issues
|
|
8274
|
+
const seenCycles = new Set();
|
|
8275
|
+
for (const cycle of circularPaths) {
|
|
8276
|
+
const cycleKey = cycle.map(f => path.relative(cwd, f)).sort().join(' -> ');
|
|
8277
|
+
if (!seenCycles.has(cycleKey)) {
|
|
8278
|
+
seenCycles.add(cycleKey);
|
|
8279
|
+
issues.push({
|
|
8280
|
+
category: 'circular',
|
|
8281
|
+
severity: 'warning',
|
|
8282
|
+
file: path.relative(cwd, cycle[0]),
|
|
8283
|
+
message: `Circular dependency: ${cycle.map(f => path.basename(f)).join(' → ')}`,
|
|
8284
|
+
fix: 'Break the cycle by extracting shared code to a separate module',
|
|
8285
|
+
autoFixable: false,
|
|
8286
|
+
});
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
// Check for dead code (unused exports)
|
|
8291
|
+
if (focus === 'all' || focus === 'dead-code') {
|
|
8292
|
+
for (const [file, exports] of exportMap.entries()) {
|
|
8293
|
+
const relativePath = path.relative(cwd, file);
|
|
8294
|
+
// Skip entry points and config files
|
|
8295
|
+
if (relativePath.includes('page.tsx') ||
|
|
8296
|
+
relativePath.includes('layout.tsx') ||
|
|
8297
|
+
relativePath.includes('route.ts') ||
|
|
8298
|
+
relativePath.includes('middleware.ts') ||
|
|
8299
|
+
relativePath.endsWith('.config.ts') ||
|
|
8300
|
+
relativePath.endsWith('.config.js')) {
|
|
8301
|
+
continue;
|
|
8302
|
+
}
|
|
8303
|
+
for (const exp of exports) {
|
|
8304
|
+
if (exp.startsWith('__reexport:') || exp === 'default')
|
|
8305
|
+
continue;
|
|
8306
|
+
const exportKey = `${file}:${exp}`;
|
|
8307
|
+
if (!usedExports.has(exportKey)) {
|
|
8308
|
+
issues.push({
|
|
8309
|
+
category: 'dead-code',
|
|
8310
|
+
severity: 'info',
|
|
8311
|
+
file: relativePath,
|
|
8312
|
+
message: `Export '${exp}' is never imported`,
|
|
8313
|
+
fix: `Remove if unused, or verify it's used externally`,
|
|
8314
|
+
autoFixable: true,
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
// Check environment variables
|
|
8321
|
+
if (focus === 'all' || focus === 'env') {
|
|
8322
|
+
const envVarsUsed = new Set();
|
|
8323
|
+
const envVarsDefined = new Set();
|
|
8324
|
+
// Scan for process.env usage
|
|
8325
|
+
const scanEnvUsage = (dir) => {
|
|
8326
|
+
if (!fs.existsSync(dir))
|
|
8327
|
+
return;
|
|
8328
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8329
|
+
for (const entry of entries) {
|
|
8330
|
+
const fullPath = path.join(dir, entry.name);
|
|
8331
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
8332
|
+
scanEnvUsage(fullPath);
|
|
8333
|
+
}
|
|
8334
|
+
else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
8335
|
+
try {
|
|
8336
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8337
|
+
const envMatches = content.matchAll(/process\.env\.(\w+)|process\.env\[['"](\w+)['"]\]/g);
|
|
8338
|
+
for (const match of envMatches) {
|
|
8339
|
+
const varName = match[1] || match[2];
|
|
8340
|
+
envVarsUsed.add(varName);
|
|
8341
|
+
stats.envVarsFound++;
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
catch {
|
|
8345
|
+
// Skip
|
|
8346
|
+
}
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
};
|
|
8350
|
+
for (const dir of searchDirs) {
|
|
8351
|
+
scanEnvUsage(path.join(cwd, dir));
|
|
8352
|
+
}
|
|
8353
|
+
// Check .env.example and .env.local
|
|
8354
|
+
const envFiles = ['.env', '.env.local', '.env.example', '.env.development'];
|
|
8355
|
+
for (const envFile of envFiles) {
|
|
8356
|
+
const envPath = path.join(cwd, envFile);
|
|
8357
|
+
if (fs.existsSync(envPath)) {
|
|
8358
|
+
try {
|
|
8359
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
8360
|
+
const lines = content.split('\n');
|
|
8361
|
+
for (const line of lines) {
|
|
8362
|
+
const match = line.match(/^([A-Z][A-Z0-9_]*)=/);
|
|
8363
|
+
if (match) {
|
|
8364
|
+
envVarsDefined.add(match[1]);
|
|
8365
|
+
}
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
catch {
|
|
8369
|
+
// Skip
|
|
8370
|
+
}
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
// Find env vars used but not defined
|
|
8374
|
+
for (const varName of envVarsUsed) {
|
|
8375
|
+
// Skip common Next.js vars
|
|
8376
|
+
if (varName.startsWith('NEXT_') || varName === 'NODE_ENV')
|
|
8377
|
+
continue;
|
|
8378
|
+
if (!envVarsDefined.has(varName)) {
|
|
8379
|
+
issues.push({
|
|
8380
|
+
category: 'env',
|
|
8381
|
+
severity: 'warning',
|
|
8382
|
+
file: '.env.example',
|
|
8383
|
+
message: `Environment variable '${varName}' is used but not defined in .env files`,
|
|
8384
|
+
fix: `Add ${varName}= to .env.example`,
|
|
8385
|
+
autoFixable: true,
|
|
8386
|
+
});
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
8389
|
+
// Find env vars defined but not used
|
|
8390
|
+
for (const varName of envVarsDefined) {
|
|
8391
|
+
if (!envVarsUsed.has(varName) && !varName.startsWith('NEXT_')) {
|
|
8392
|
+
issues.push({
|
|
8393
|
+
category: 'env',
|
|
8394
|
+
severity: 'info',
|
|
8395
|
+
file: '.env',
|
|
8396
|
+
message: `Environment variable '${varName}' is defined but never used`,
|
|
8397
|
+
fix: 'Remove if no longer needed',
|
|
8398
|
+
autoFixable: false,
|
|
8399
|
+
});
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
// Check for Drizzle schema issues
|
|
8404
|
+
if (focus === 'all' || focus === 'schema') {
|
|
8405
|
+
const schemaPath = path.join(cwd, 'src', 'db', 'schema.ts');
|
|
8406
|
+
const altSchemaPath = path.join(cwd, 'drizzle', 'schema.ts');
|
|
8407
|
+
const schemaFile = fs.existsSync(schemaPath) ? schemaPath : fs.existsSync(altSchemaPath) ? altSchemaPath : null;
|
|
8408
|
+
if (schemaFile) {
|
|
8409
|
+
try {
|
|
8410
|
+
const schemaContent = fs.readFileSync(schemaFile, 'utf-8');
|
|
8411
|
+
// Extract table names
|
|
8412
|
+
const tableMatches = schemaContent.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:pgTable|mysqlTable|sqliteTable)\s*\(/g);
|
|
8413
|
+
const tableNames = new Set();
|
|
8414
|
+
for (const match of tableMatches) {
|
|
8415
|
+
tableNames.add(match[1]);
|
|
8416
|
+
}
|
|
8417
|
+
// Scan for .insert(), .update(), .delete() calls on non-existent tables
|
|
8418
|
+
const scanSchemaUsage = (dir) => {
|
|
8419
|
+
if (!fs.existsSync(dir))
|
|
8420
|
+
return;
|
|
8421
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8422
|
+
for (const entry of entries) {
|
|
8423
|
+
const fullPath = path.join(dir, entry.name);
|
|
8424
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
8425
|
+
scanSchemaUsage(fullPath);
|
|
8426
|
+
}
|
|
8427
|
+
else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
8428
|
+
try {
|
|
8429
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8430
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
8431
|
+
// Check for db operations on tables
|
|
8432
|
+
const opMatches = content.matchAll(/db\.(insert|update|delete|select)\((\w+)\)/g);
|
|
8433
|
+
for (const match of opMatches) {
|
|
8434
|
+
const tableName = match[2];
|
|
8435
|
+
if (!tableNames.has(tableName) && !tableName.startsWith('sql')) {
|
|
8436
|
+
issues.push({
|
|
8437
|
+
category: 'schema',
|
|
8438
|
+
severity: 'error',
|
|
8439
|
+
file: relativePath,
|
|
8440
|
+
message: `Database operation on unknown table '${tableName}'`,
|
|
8441
|
+
fix: `Verify table exists in schema or fix the table name`,
|
|
8442
|
+
autoFixable: false,
|
|
8443
|
+
});
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8447
|
+
catch {
|
|
8448
|
+
// Skip
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
};
|
|
8453
|
+
for (const dir of searchDirs) {
|
|
8454
|
+
scanSchemaUsage(path.join(cwd, dir));
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
catch {
|
|
8458
|
+
// Skip if can't read schema
|
|
8459
|
+
}
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
// Check API contracts (Next.js API routes vs fetch calls)
|
|
8463
|
+
if (focus === 'all' || focus === 'api') {
|
|
8464
|
+
const apiRoutes = new Map();
|
|
8465
|
+
// Find all API routes
|
|
8466
|
+
const apiDir = path.join(cwd, 'src', 'app', 'api');
|
|
8467
|
+
const altApiDir = path.join(cwd, 'app', 'api');
|
|
8468
|
+
const pagesApiDir = path.join(cwd, 'pages', 'api');
|
|
8469
|
+
const scanApiRoutes = (dir, prefix = '/api') => {
|
|
8470
|
+
if (!fs.existsSync(dir))
|
|
8471
|
+
return;
|
|
8472
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8473
|
+
for (const entry of entries) {
|
|
8474
|
+
const fullPath = path.join(dir, entry.name);
|
|
8475
|
+
if (entry.isDirectory()) {
|
|
8476
|
+
scanApiRoutes(fullPath, `${prefix}/${entry.name}`);
|
|
8477
|
+
}
|
|
8478
|
+
else if (entry.name === 'route.ts' || entry.name === 'route.js') {
|
|
8479
|
+
try {
|
|
8480
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8481
|
+
const methods = [];
|
|
8482
|
+
if (content.includes('export async function GET') || content.includes('export function GET'))
|
|
8483
|
+
methods.push('GET');
|
|
8484
|
+
if (content.includes('export async function POST') || content.includes('export function POST'))
|
|
8485
|
+
methods.push('POST');
|
|
8486
|
+
if (content.includes('export async function PUT') || content.includes('export function PUT'))
|
|
8487
|
+
methods.push('PUT');
|
|
8488
|
+
if (content.includes('export async function DELETE') || content.includes('export function DELETE'))
|
|
8489
|
+
methods.push('DELETE');
|
|
8490
|
+
if (content.includes('export async function PATCH') || content.includes('export function PATCH'))
|
|
8491
|
+
methods.push('PATCH');
|
|
8492
|
+
apiRoutes.set(prefix, { methods, file: path.relative(cwd, fullPath) });
|
|
8493
|
+
}
|
|
8494
|
+
catch {
|
|
8495
|
+
// Skip
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8499
|
+
};
|
|
8500
|
+
scanApiRoutes(apiDir);
|
|
8501
|
+
scanApiRoutes(altApiDir);
|
|
8502
|
+
// Scan for fetch calls to API routes
|
|
8503
|
+
const scanFetchCalls = (dir) => {
|
|
8504
|
+
if (!fs.existsSync(dir))
|
|
8505
|
+
return;
|
|
8506
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8507
|
+
for (const entry of entries) {
|
|
8508
|
+
const fullPath = path.join(dir, entry.name);
|
|
8509
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'api') {
|
|
8510
|
+
scanFetchCalls(fullPath);
|
|
8511
|
+
}
|
|
8512
|
+
else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
8513
|
+
try {
|
|
8514
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
8515
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
8516
|
+
// Match fetch('/api/...') or fetch(`/api/...`)
|
|
8517
|
+
const fetchMatches = content.matchAll(/fetch\s*\(\s*[`'"]([^`'"]+)[`'"]/g);
|
|
8518
|
+
for (const match of fetchMatches) {
|
|
8519
|
+
const url = match[1];
|
|
8520
|
+
if (url.startsWith('/api/')) {
|
|
8521
|
+
// Extract base path (before query params or dynamic segments)
|
|
8522
|
+
const basePath = url.split('?')[0].replace(/\$\{[^}]+\}/g, '[dynamic]');
|
|
8523
|
+
// Check if this route exists
|
|
8524
|
+
let routeFound = false;
|
|
8525
|
+
for (const [route] of apiRoutes) {
|
|
8526
|
+
// Simple matching - could be improved
|
|
8527
|
+
if (basePath === route || basePath.startsWith(route + '/')) {
|
|
8528
|
+
routeFound = true;
|
|
8529
|
+
break;
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
if (!routeFound && !basePath.includes('[dynamic]')) {
|
|
8533
|
+
issues.push({
|
|
8534
|
+
category: 'api',
|
|
8535
|
+
severity: 'warning',
|
|
8536
|
+
file: relativePath,
|
|
8537
|
+
message: `Fetch to '${url}' but no matching API route found`,
|
|
8538
|
+
fix: `Create the API route or fix the URL`,
|
|
8539
|
+
autoFixable: false,
|
|
8540
|
+
});
|
|
8541
|
+
}
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8545
|
+
catch {
|
|
8546
|
+
// Skip
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
8549
|
+
}
|
|
8550
|
+
};
|
|
8551
|
+
for (const dir of ['src', 'app', 'components', 'lib']) {
|
|
8552
|
+
scanFetchCalls(path.join(cwd, dir));
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
// Generate report
|
|
8556
|
+
let response = `# 🔗 Coherence Audit Report\n\n`;
|
|
8557
|
+
// Summary
|
|
8558
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
8559
|
+
const warningCount = issues.filter(i => i.severity === 'warning').length;
|
|
8560
|
+
const infoCount = issues.filter(i => i.severity === 'info').length;
|
|
8561
|
+
const autoFixableCount = issues.filter(i => i.autoFixable).length;
|
|
8562
|
+
response += `## 📊 Summary\n\n`;
|
|
8563
|
+
response += `| Metric | Value |\n`;
|
|
8564
|
+
response += `|--------|-------|\n`;
|
|
8565
|
+
response += `| Files scanned | ${stats.filesScanned} |\n`;
|
|
8566
|
+
response += `| Imports checked | ${stats.importsChecked} |\n`;
|
|
8567
|
+
response += `| Exports found | ${stats.exportsFound} |\n`;
|
|
8568
|
+
response += `| 🔴 Errors | ${errorCount} |\n`;
|
|
8569
|
+
response += `| 🟡 Warnings | ${warningCount} |\n`;
|
|
8570
|
+
response += `| 🔵 Info | ${infoCount} |\n`;
|
|
8571
|
+
response += `| 🔧 Auto-fixable | ${autoFixableCount} |\n\n`;
|
|
8572
|
+
if (issues.length === 0) {
|
|
8573
|
+
response += `## ✅ All Clear!\n\n`;
|
|
8574
|
+
response += `No coherence issues found. Your codebase wiring is solid.\n`;
|
|
8575
|
+
}
|
|
8576
|
+
else {
|
|
8577
|
+
// Group issues by category
|
|
8578
|
+
const categories = {
|
|
8579
|
+
import: [],
|
|
8580
|
+
export: [],
|
|
8581
|
+
type: [],
|
|
8582
|
+
schema: [],
|
|
8583
|
+
api: [],
|
|
8584
|
+
env: [],
|
|
8585
|
+
circular: [],
|
|
8586
|
+
'dead-code': [],
|
|
8587
|
+
};
|
|
8588
|
+
for (const issue of issues) {
|
|
8589
|
+
categories[issue.category].push(issue);
|
|
8590
|
+
}
|
|
8591
|
+
const categoryNames = {
|
|
8592
|
+
import: '🔴 Broken Imports',
|
|
8593
|
+
export: '🔴 Missing Exports',
|
|
8594
|
+
type: '🟠 Type Mismatches',
|
|
8595
|
+
schema: '🟠 Schema Issues',
|
|
8596
|
+
api: '🟡 API Contract Issues',
|
|
8597
|
+
env: '🟡 Environment Variables',
|
|
8598
|
+
circular: '⚪ Circular Dependencies',
|
|
8599
|
+
'dead-code': '🔵 Dead Code',
|
|
8600
|
+
};
|
|
8601
|
+
for (const [category, catIssues] of Object.entries(categories)) {
|
|
8602
|
+
if (catIssues.length === 0)
|
|
8603
|
+
continue;
|
|
8604
|
+
response += `## ${categoryNames[category]} (${catIssues.length})\n\n`;
|
|
8605
|
+
// Show first 10 issues per category
|
|
8606
|
+
const displayIssues = catIssues.slice(0, 10);
|
|
8607
|
+
for (const issue of displayIssues) {
|
|
8608
|
+
response += `### \`${issue.file}${issue.line ? `:${issue.line}` : ''}\`\n`;
|
|
8609
|
+
response += `**Issue:** ${issue.message}\n`;
|
|
8610
|
+
if (issue.fix) {
|
|
8611
|
+
response += `**Fix:** ${issue.fix}${issue.autoFixable ? ' 🔧' : ''}\n`;
|
|
8612
|
+
}
|
|
8613
|
+
response += `\n`;
|
|
8614
|
+
}
|
|
8615
|
+
if (catIssues.length > 10) {
|
|
8616
|
+
response += `*... and ${catIssues.length - 10} more ${category} issues*\n\n`;
|
|
8617
|
+
}
|
|
8618
|
+
}
|
|
8619
|
+
}
|
|
8620
|
+
// Recommendations
|
|
8621
|
+
response += `## 💡 Recommendations\n\n`;
|
|
8622
|
+
if (errorCount > 0) {
|
|
8623
|
+
response += `1. **Fix ${errorCount} errors first** - These will cause runtime failures\n`;
|
|
8624
|
+
}
|
|
8625
|
+
if (warningCount > 0) {
|
|
8626
|
+
response += `2. **Review ${warningCount} warnings** - These may cause issues\n`;
|
|
8627
|
+
}
|
|
8628
|
+
response += `3. Run \`npx tsc --noEmit\` to verify TypeScript compiles\n`;
|
|
8629
|
+
response += `4. Run your test suite to verify functionality\n`;
|
|
8630
|
+
if (autoFixableCount > 0) {
|
|
8631
|
+
response += `\n---\n\n`;
|
|
8632
|
+
response += `**${autoFixableCount} issues can be auto-fixed.** Run \`guardian_heal\` to fix them.\n`;
|
|
8633
|
+
}
|
|
8634
|
+
// Save state for guardian_heal
|
|
8635
|
+
const statePath = path.join(cwd, '.codebakers', 'coherence-state.json');
|
|
8636
|
+
try {
|
|
8637
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
8638
|
+
fs.writeFileSync(statePath, JSON.stringify({
|
|
8639
|
+
lastAudit: new Date().toISOString(),
|
|
8640
|
+
focus,
|
|
8641
|
+
stats,
|
|
8642
|
+
issues: issues.map(i => ({
|
|
8643
|
+
...i,
|
|
8644
|
+
// Include only auto-fixable for healing
|
|
8645
|
+
})),
|
|
8646
|
+
summary: { errors: errorCount, warnings: warningCount, info: infoCount, autoFixable: autoFixableCount },
|
|
8647
|
+
}, null, 2));
|
|
8648
|
+
}
|
|
8649
|
+
catch {
|
|
8650
|
+
// Ignore write errors
|
|
8651
|
+
}
|
|
8652
|
+
return { content: [{ type: 'text', text: response }] };
|
|
8653
|
+
}
|
|
8654
|
+
// ============================================================================
|
|
7953
8655
|
// PROJECT TRACKING - Server-Side Dashboard
|
|
7954
8656
|
// ============================================================================
|
|
7955
8657
|
/**
|