pan-wizard 2.8.1
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/LICENSE +21 -0
- package/README.md +772 -0
- package/agents/pan-debugger.md +1246 -0
- package/agents/pan-document_code.md +965 -0
- package/agents/pan-executor.md +469 -0
- package/agents/pan-integration-checker.md +443 -0
- package/agents/pan-phase-researcher.md +572 -0
- package/agents/pan-plan-checker.md +763 -0
- package/agents/pan-planner.md +1297 -0
- package/agents/pan-project-researcher.md +647 -0
- package/agents/pan-research-synthesizer.md +239 -0
- package/agents/pan-reviewer.md +112 -0
- package/agents/pan-roadmapper.md +642 -0
- package/agents/pan-verifier.md +672 -0
- package/assets/pan-logo-2000-transparent.svg +30 -0
- package/assets/pan-logo-2000.svg +43 -0
- package/assets/terminal.svg +119 -0
- package/bin/install-lib.cjs +616 -0
- package/bin/install.js +1936 -0
- package/commands/pan/add-phase.md +44 -0
- package/commands/pan/assumptions.md +47 -0
- package/commands/pan/audit-deployment.md +378 -0
- package/commands/pan/debug.md +168 -0
- package/commands/pan/discord.md +19 -0
- package/commands/pan/discuss-phase.md +84 -0
- package/commands/pan/exec-phase.md +45 -0
- package/commands/pan/focus-auto.md +323 -0
- package/commands/pan/focus-design.md +816 -0
- package/commands/pan/focus-exec.md +316 -0
- package/commands/pan/focus-plan.md +101 -0
- package/commands/pan/focus-scan.md +272 -0
- package/commands/pan/focus-sync.md +104 -0
- package/commands/pan/health.md +23 -0
- package/commands/pan/help.md +23 -0
- package/commands/pan/insert-phase.md +33 -0
- package/commands/pan/map-codebase.md +72 -0
- package/commands/pan/milestone-audit.md +37 -0
- package/commands/pan/milestone-cleanup.md +19 -0
- package/commands/pan/milestone-done.md +137 -0
- package/commands/pan/milestone-gaps.md +35 -0
- package/commands/pan/milestone-new.md +45 -0
- package/commands/pan/new-project.md +43 -0
- package/commands/pan/patches.md +110 -0
- package/commands/pan/pause.md +39 -0
- package/commands/pan/phase-budget.md +23 -0
- package/commands/pan/phase-tests.md +42 -0
- package/commands/pan/plan-phase.md +46 -0
- package/commands/pan/profile.md +36 -0
- package/commands/pan/progress.md +25 -0
- package/commands/pan/quick.md +42 -0
- package/commands/pan/remove-phase.md +32 -0
- package/commands/pan/research-phase.md +190 -0
- package/commands/pan/resume.md +41 -0
- package/commands/pan/retro.md +33 -0
- package/commands/pan/settings.md +37 -0
- package/commands/pan/todo-add.md +48 -0
- package/commands/pan/todo-check.md +46 -0
- package/commands/pan/update.md +38 -0
- package/commands/pan/verify-phase.md +39 -0
- package/hooks/dist/pan-check-update.js +62 -0
- package/hooks/dist/pan-context-monitor.js +122 -0
- package/hooks/dist/pan-statusline.js +108 -0
- package/package.json +66 -0
- package/pan-wizard-core/bin/lib/codebase.cjs +746 -0
- package/pan-wizard-core/bin/lib/commands.cjs +1435 -0
- package/pan-wizard-core/bin/lib/config.cjs +611 -0
- package/pan-wizard-core/bin/lib/constants.cjs +696 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +150 -0
- package/pan-wizard-core/bin/lib/core.cjs +650 -0
- package/pan-wizard-core/bin/lib/focus.cjs +900 -0
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -0
- package/pan-wizard-core/bin/lib/init.cjs +881 -0
- package/pan-wizard-core/bin/lib/milestone.cjs +276 -0
- package/pan-wizard-core/bin/lib/phase.cjs +1212 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +470 -0
- package/pan-wizard-core/bin/lib/state.cjs +1029 -0
- package/pan-wizard-core/bin/lib/template.cjs +314 -0
- package/pan-wizard-core/bin/lib/utils.cjs +171 -0
- package/pan-wizard-core/bin/lib/verify.cjs +1808 -0
- package/pan-wizard-core/bin/pan-tools.cjs +773 -0
- package/pan-wizard-core/references/checkpoints.md +776 -0
- package/pan-wizard-core/references/continuation-format.md +249 -0
- package/pan-wizard-core/references/decimal-phase-calculation.md +65 -0
- package/pan-wizard-core/references/git-integration.md +248 -0
- package/pan-wizard-core/references/git-planning-commit.md +38 -0
- package/pan-wizard-core/references/model-profile-resolution.md +34 -0
- package/pan-wizard-core/references/model-profiles.md +111 -0
- package/pan-wizard-core/references/phase-argument-parsing.md +61 -0
- package/pan-wizard-core/references/planning-config.md +196 -0
- package/pan-wizard-core/references/questioning.md +145 -0
- package/pan-wizard-core/references/tdd.md +263 -0
- package/pan-wizard-core/references/ui-brand.md +160 -0
- package/pan-wizard-core/references/verification-patterns.md +612 -0
- package/pan-wizard-core/templates/codebase/architecture.md +283 -0
- package/pan-wizard-core/templates/codebase/best-practices.md +133 -0
- package/pan-wizard-core/templates/codebase/concerns.md +325 -0
- package/pan-wizard-core/templates/codebase/conventions.md +307 -0
- package/pan-wizard-core/templates/codebase/integrations.md +305 -0
- package/pan-wizard-core/templates/codebase/relationships.md +124 -0
- package/pan-wizard-core/templates/codebase/stack.md +199 -0
- package/pan-wizard-core/templates/codebase/structure.md +298 -0
- package/pan-wizard-core/templates/codebase/testing.md +480 -0
- package/pan-wizard-core/templates/config.json +37 -0
- package/pan-wizard-core/templates/context.md +283 -0
- package/pan-wizard-core/templates/continue-here.md +78 -0
- package/pan-wizard-core/templates/debug-subagent-prompt.md +91 -0
- package/pan-wizard-core/templates/debug.md +164 -0
- package/pan-wizard-core/templates/discovery.md +146 -0
- package/pan-wizard-core/templates/milestone-archive.md +123 -0
- package/pan-wizard-core/templates/milestone.md +115 -0
- package/pan-wizard-core/templates/phase-prompt.md +593 -0
- package/pan-wizard-core/templates/planner-subagent-prompt.md +117 -0
- package/pan-wizard-core/templates/project.md +184 -0
- package/pan-wizard-core/templates/requirements.md +231 -0
- package/pan-wizard-core/templates/research-project/architecture.md +204 -0
- package/pan-wizard-core/templates/research-project/features.md +147 -0
- package/pan-wizard-core/templates/research-project/pitfalls.md +200 -0
- package/pan-wizard-core/templates/research-project/stack.md +120 -0
- package/pan-wizard-core/templates/research-project/summary.md +170 -0
- package/pan-wizard-core/templates/research.md +552 -0
- package/pan-wizard-core/templates/retrospective.md +54 -0
- package/pan-wizard-core/templates/roadmap.md +202 -0
- package/pan-wizard-core/templates/standards.md +24 -0
- package/pan-wizard-core/templates/state.md +176 -0
- package/pan-wizard-core/templates/summary-complex.md +59 -0
- package/pan-wizard-core/templates/summary-minimal.md +41 -0
- package/pan-wizard-core/templates/summary-standard.md +49 -0
- package/pan-wizard-core/templates/summary.md +249 -0
- package/pan-wizard-core/templates/uat.md +247 -0
- package/pan-wizard-core/templates/user-setup.md +311 -0
- package/pan-wizard-core/templates/validation.md +76 -0
- package/pan-wizard-core/templates/verification-report.md +322 -0
- package/pan-wizard-core/workflows/add-phase.md +111 -0
- package/pan-wizard-core/workflows/assumptions.md +178 -0
- package/pan-wizard-core/workflows/diagnose-issues.md +219 -0
- package/pan-wizard-core/workflows/discuss-phase.md +542 -0
- package/pan-wizard-core/workflows/exec-phase.md +572 -0
- package/pan-wizard-core/workflows/execute-plan.md +448 -0
- package/pan-wizard-core/workflows/health.md +156 -0
- package/pan-wizard-core/workflows/help.md +431 -0
- package/pan-wizard-core/workflows/insert-phase.md +129 -0
- package/pan-wizard-core/workflows/map-codebase.md +401 -0
- package/pan-wizard-core/workflows/milestone-audit.md +297 -0
- package/pan-wizard-core/workflows/milestone-cleanup.md +152 -0
- package/pan-wizard-core/workflows/milestone-gaps.md +274 -0
- package/pan-wizard-core/workflows/milestone-new.md +382 -0
- package/pan-wizard-core/workflows/new-project.md +1178 -0
- package/pan-wizard-core/workflows/pause.md +122 -0
- package/pan-wizard-core/workflows/phase-tests.md +388 -0
- package/pan-wizard-core/workflows/plan-phase.md +569 -0
- package/pan-wizard-core/workflows/profile.md +115 -0
- package/pan-wizard-core/workflows/progress.md +381 -0
- package/pan-wizard-core/workflows/quick.md +453 -0
- package/pan-wizard-core/workflows/remove-phase.md +154 -0
- package/pan-wizard-core/workflows/research-phase.md +73 -0
- package/pan-wizard-core/workflows/resume-project.md +306 -0
- package/pan-wizard-core/workflows/retro.md +121 -0
- package/pan-wizard-core/workflows/settings.md +213 -0
- package/pan-wizard-core/workflows/todo-add.md +157 -0
- package/pan-wizard-core/workflows/todo-check.md +176 -0
- package/pan-wizard-core/workflows/transition.md +544 -0
- package/pan-wizard-core/workflows/update.md +219 -0
- package/pan-wizard-core/workflows/verify-phase.md +301 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase — Language-aware import analysis, dependency graphs, best-practices detection
|
|
3
|
+
*
|
|
4
|
+
* Zero runtime dependencies: uses regex-based parsing only (no AST, no tree-sitter).
|
|
5
|
+
* Supports JS/TS (v0), with extensible language registry for Python/Go/Rust/Java/C# (v1).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { CODEBASE_DIR, DRIFT_MAX_FILE_SIZE } = require('./constants.cjs');
|
|
11
|
+
const { output, error, safeReadFile, toPosix } = require('./core.cjs');
|
|
12
|
+
const { planningPath } = require('./utils.cjs');
|
|
13
|
+
|
|
14
|
+
// ─── Language Detection ─────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Extension → language mapping */
|
|
17
|
+
const EXTENSION_MAP = {
|
|
18
|
+
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
|
|
19
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
|
|
20
|
+
'.py': 'python', '.pyw': 'python',
|
|
21
|
+
'.go': 'go',
|
|
22
|
+
'.rs': 'rust',
|
|
23
|
+
'.java': 'java',
|
|
24
|
+
'.cs': 'csharp',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Package manifest → language mapping */
|
|
28
|
+
const MANIFEST_MAP = {
|
|
29
|
+
'package.json': 'javascript',
|
|
30
|
+
'tsconfig.json': 'typescript',
|
|
31
|
+
'requirements.txt': 'python',
|
|
32
|
+
'pyproject.toml': 'python',
|
|
33
|
+
'setup.py': 'python',
|
|
34
|
+
'go.mod': 'go',
|
|
35
|
+
'Cargo.toml': 'rust',
|
|
36
|
+
'pom.xml': 'java',
|
|
37
|
+
'build.gradle': 'java',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Directories to skip during scanning */
|
|
41
|
+
const SKIP_DIRS = new Set([
|
|
42
|
+
'node_modules', '.git', 'dist', 'build', '.planning',
|
|
43
|
+
'coverage', '.next', '.nuxt', '__pycache__', '.venv',
|
|
44
|
+
'venv', 'target', 'vendor', 'bin', '.cache',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Walk a directory recursively, collecting source files grouped by language.
|
|
49
|
+
* @param {string} dir - Directory to walk
|
|
50
|
+
* @param {string} baseCwd - Project root for relative paths
|
|
51
|
+
* @returns {{ files_by_language: Object<string, string[]>, total: number }}
|
|
52
|
+
*/
|
|
53
|
+
function walkSourceFiles(dir, baseCwd) {
|
|
54
|
+
const filesByLang = {};
|
|
55
|
+
let total = 0;
|
|
56
|
+
|
|
57
|
+
function walk(current) {
|
|
58
|
+
let entries;
|
|
59
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
63
|
+
walk(path.join(current, entry.name));
|
|
64
|
+
}
|
|
65
|
+
} else if (entry.isFile()) {
|
|
66
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
67
|
+
const lang = EXTENSION_MAP[ext];
|
|
68
|
+
if (lang) {
|
|
69
|
+
const rel = toPosix(path.relative(baseCwd, path.join(current, entry.name)));
|
|
70
|
+
if (!filesByLang[lang]) filesByLang[lang] = [];
|
|
71
|
+
filesByLang[lang].push(rel);
|
|
72
|
+
total++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
walk(dir);
|
|
79
|
+
return { files_by_language: filesByLang, total };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect languages used in a codebase.
|
|
84
|
+
* @param {string} cwd - Project root
|
|
85
|
+
* @returns {{ primary: string|null, secondary: string[], files_by_language: Object, file_count: number }}
|
|
86
|
+
*/
|
|
87
|
+
function detectLanguages(cwd) {
|
|
88
|
+
const { files_by_language, total } = walkSourceFiles(cwd, cwd);
|
|
89
|
+
|
|
90
|
+
// Check manifests for additional signals
|
|
91
|
+
for (const [manifest, lang] of Object.entries(MANIFEST_MAP)) {
|
|
92
|
+
try {
|
|
93
|
+
fs.accessSync(path.join(cwd, manifest));
|
|
94
|
+
// Promote language if manifest exists but no source files found yet
|
|
95
|
+
if (!files_by_language[lang]) files_by_language[lang] = [];
|
|
96
|
+
} catch { /* manifest not found */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// TypeScript subsumes javascript if tsconfig.json exists
|
|
100
|
+
if (files_by_language.typescript && files_by_language.javascript) {
|
|
101
|
+
// Merge JS files under TypeScript project
|
|
102
|
+
files_by_language.typescript = files_by_language.typescript.concat(files_by_language.javascript);
|
|
103
|
+
delete files_by_language.javascript;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Determine primary (most files) and secondary (>5% of total)
|
|
107
|
+
const sorted = Object.entries(files_by_language)
|
|
108
|
+
.filter(([, files]) => files.length > 0)
|
|
109
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
110
|
+
|
|
111
|
+
const primary = sorted.length > 0 ? sorted[0][0] : null;
|
|
112
|
+
const threshold = Math.max(1, total * 0.05);
|
|
113
|
+
const secondary = sorted.slice(1)
|
|
114
|
+
.filter(([, files]) => files.length >= threshold)
|
|
115
|
+
.map(([lang]) => lang);
|
|
116
|
+
|
|
117
|
+
return { primary, secondary, files_by_language, file_count: total };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Import/Export Parsing ──────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/** Import regex pattern factories per language (fresh instances avoid g-flag state leaks) */
|
|
123
|
+
function getImportPatterns(lang) {
|
|
124
|
+
if (lang === 'javascript' || lang === 'typescript') {
|
|
125
|
+
return [
|
|
126
|
+
{ re: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, type: 'require' },
|
|
127
|
+
{ re: /import\s+.*?from\s+['"]([^'"]+)['"]/g, type: 'esm' },
|
|
128
|
+
{ re: /(?:^|[\s;=])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, type: 'dynamic' },
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Export regex pattern factories per language */
|
|
135
|
+
function getExportPatterns(lang) {
|
|
136
|
+
if (lang === 'javascript' || lang === 'typescript') {
|
|
137
|
+
return [
|
|
138
|
+
{ re: /module\.exports\s*=/g, type: 'cjs-default' },
|
|
139
|
+
{ re: /exports\.(\w+)\s*=/g, type: 'cjs-named' },
|
|
140
|
+
{ re: /export\s+default\s+(?:function|class|const|let|var)?\s*(\w*)/g, type: 'esm-default' },
|
|
141
|
+
{ re: /export\s+(?:function|class|const|let|var)\s+(\w+)/g, type: 'esm-named' },
|
|
142
|
+
{ re: /export\s*\{([^}]+)\}/g, type: 'esm-destructured' },
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Strip single-line and multi-line comments from source code.
|
|
150
|
+
* @param {string} content - Source code
|
|
151
|
+
* @returns {string} Content with comments replaced by whitespace (preserving line count)
|
|
152
|
+
*/
|
|
153
|
+
function stripComments(content) {
|
|
154
|
+
// Replace multi-line comments with equivalent newlines
|
|
155
|
+
let result = content.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '));
|
|
156
|
+
// Replace single-line comments
|
|
157
|
+
result = result.replace(/\/\/.*$/gm, '');
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse import statements from a file.
|
|
163
|
+
* @param {string} filePath - Absolute path to source file
|
|
164
|
+
* @param {string} lang - Language identifier
|
|
165
|
+
* @returns {Array<{ source: string, line: number, type: string }>}
|
|
166
|
+
*/
|
|
167
|
+
function parseImports(filePath, lang) {
|
|
168
|
+
const patterns = getImportPatterns(lang);
|
|
169
|
+
if (patterns.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
const content = safeReadFile(filePath);
|
|
172
|
+
if (!content) return [];
|
|
173
|
+
|
|
174
|
+
const stripped = stripComments(content);
|
|
175
|
+
const lines = stripped.split('\n');
|
|
176
|
+
const results = [];
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
179
|
+
const line = lines[i];
|
|
180
|
+
for (const { re, type } of patterns) {
|
|
181
|
+
re.lastIndex = 0;
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = re.exec(line)) !== null) {
|
|
184
|
+
results.push({ source: match[1], line: i + 1, type });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse export statements from a file.
|
|
193
|
+
* @param {string} filePath - Absolute path to source file
|
|
194
|
+
* @param {string} lang - Language identifier
|
|
195
|
+
* @returns {Array<{ name: string, type: string, line: number, default: boolean }>}
|
|
196
|
+
*/
|
|
197
|
+
function parseExports(filePath, lang) {
|
|
198
|
+
const patterns = getExportPatterns(lang);
|
|
199
|
+
if (patterns.length === 0) return [];
|
|
200
|
+
|
|
201
|
+
const content = safeReadFile(filePath);
|
|
202
|
+
if (!content) return [];
|
|
203
|
+
|
|
204
|
+
const stripped = stripComments(content);
|
|
205
|
+
const lines = stripped.split('\n');
|
|
206
|
+
const results = [];
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const line = lines[i];
|
|
210
|
+
for (const { re, type } of patterns) {
|
|
211
|
+
re.lastIndex = 0;
|
|
212
|
+
let match;
|
|
213
|
+
while ((match = re.exec(line)) !== null) {
|
|
214
|
+
if (type === 'cjs-default') {
|
|
215
|
+
results.push({ name: 'default', type, line: i + 1, default: true });
|
|
216
|
+
} else if (type === 'esm-destructured') {
|
|
217
|
+
const names = match[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
218
|
+
for (const name of names) {
|
|
219
|
+
results.push({ name, type, line: i + 1, default: false });
|
|
220
|
+
}
|
|
221
|
+
} else if (type === 'esm-default') {
|
|
222
|
+
results.push({ name: match[1] || 'default', type, line: i + 1, default: true });
|
|
223
|
+
} else {
|
|
224
|
+
results.push({ name: match[1], type, line: i + 1, default: false });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Dependency Graph ───────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/** Common file extensions to try when resolving imports */
|
|
235
|
+
const RESOLVE_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'];
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolve an import source to an actual file path.
|
|
239
|
+
* @param {string} source - Import source (e.g., './module', '../utils')
|
|
240
|
+
* @param {string} importerDir - Directory of the importing file
|
|
241
|
+
* @param {string} cwd - Project root
|
|
242
|
+
* @returns {string|null} Resolved relative path or null
|
|
243
|
+
*/
|
|
244
|
+
function resolveImport(source, importerDir, cwd) {
|
|
245
|
+
// Skip non-relative imports (node_modules, builtins)
|
|
246
|
+
if (!source.startsWith('.')) return null;
|
|
247
|
+
|
|
248
|
+
const resolved = path.resolve(importerDir, source);
|
|
249
|
+
|
|
250
|
+
// Try exact match
|
|
251
|
+
try { if (fs.statSync(resolved).isFile()) return toPosix(path.relative(cwd, resolved)); } catch { /* */ }
|
|
252
|
+
|
|
253
|
+
// Try with extensions
|
|
254
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
255
|
+
try { if (fs.statSync(resolved + ext).isFile()) return toPosix(path.relative(cwd, resolved + ext)); } catch { /* */ }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try index files
|
|
259
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
260
|
+
const indexPath = path.join(resolved, 'index' + ext);
|
|
261
|
+
try { if (fs.statSync(indexPath).isFile()) return toPosix(path.relative(cwd, indexPath)); } catch { /* */ }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build a dependency graph from import analysis.
|
|
269
|
+
* @param {string} cwd - Project root
|
|
270
|
+
* @returns {{ nodes: string[], edges: Array<{from: string, to: string}>, adjacency: Object<string, string[]> }}
|
|
271
|
+
*/
|
|
272
|
+
function buildDependencyGraph(cwd) {
|
|
273
|
+
const { files_by_language } = detectLanguages(cwd);
|
|
274
|
+
const nodeSet = new Set();
|
|
275
|
+
const edges = [];
|
|
276
|
+
const adjacency = {};
|
|
277
|
+
|
|
278
|
+
for (const [lang, files] of Object.entries(files_by_language)) {
|
|
279
|
+
for (const relPath of files) {
|
|
280
|
+
const absPath = path.join(cwd, relPath);
|
|
281
|
+
|
|
282
|
+
// Skip large files
|
|
283
|
+
try {
|
|
284
|
+
const stat = fs.statSync(absPath);
|
|
285
|
+
if (stat.size > DRIFT_MAX_FILE_SIZE) continue;
|
|
286
|
+
} catch { continue; }
|
|
287
|
+
|
|
288
|
+
nodeSet.add(relPath);
|
|
289
|
+
if (!adjacency[relPath]) adjacency[relPath] = [];
|
|
290
|
+
|
|
291
|
+
const imports = parseImports(absPath, lang);
|
|
292
|
+
const importerDir = path.dirname(absPath);
|
|
293
|
+
|
|
294
|
+
for (const imp of imports) {
|
|
295
|
+
const resolved = resolveImport(imp.source, importerDir, cwd);
|
|
296
|
+
if (resolved && resolved !== relPath) {
|
|
297
|
+
nodeSet.add(resolved);
|
|
298
|
+
if (!adjacency[relPath].includes(resolved)) {
|
|
299
|
+
adjacency[relPath].push(resolved);
|
|
300
|
+
edges.push({ from: relPath, to: resolved });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { nodes: Array.from(nodeSet), edges, adjacency };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Circular Dependency Detection ──────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Find circular dependencies using DFS cycle detection.
|
|
314
|
+
* @param {{ adjacency: Object<string, string[]> }} graph - Dependency graph
|
|
315
|
+
* @returns {Array<string[]>} Array of cycles (each cycle is an array of file paths)
|
|
316
|
+
*/
|
|
317
|
+
function findCircularDeps(graph) {
|
|
318
|
+
const { adjacency } = graph;
|
|
319
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
320
|
+
const color = {};
|
|
321
|
+
const cycles = [];
|
|
322
|
+
|
|
323
|
+
for (const node of Object.keys(adjacency)) {
|
|
324
|
+
color[node] = WHITE;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function dfs(node, pathStack) {
|
|
328
|
+
color[node] = GRAY;
|
|
329
|
+
pathStack.push(node);
|
|
330
|
+
|
|
331
|
+
const neighbors = adjacency[node] || [];
|
|
332
|
+
for (const neighbor of neighbors) {
|
|
333
|
+
if (color[neighbor] === GRAY) {
|
|
334
|
+
// Found cycle — extract from pathStack
|
|
335
|
+
const cycleStart = pathStack.indexOf(neighbor);
|
|
336
|
+
const cycle = pathStack.slice(cycleStart).concat(neighbor);
|
|
337
|
+
cycles.push(cycle);
|
|
338
|
+
} else if (color[neighbor] === WHITE || color[neighbor] === undefined) {
|
|
339
|
+
dfs(neighbor, pathStack);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
pathStack.pop();
|
|
344
|
+
color[node] = BLACK;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const node of Object.keys(adjacency)) {
|
|
348
|
+
if (color[node] === WHITE) {
|
|
349
|
+
dfs(node, []);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return cycles;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Entry Points & Orphans ─────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Find entry points (files with no incoming edges).
|
|
360
|
+
* @param {{ nodes: string[], adjacency: Object }} graph
|
|
361
|
+
* @returns {string[]}
|
|
362
|
+
*/
|
|
363
|
+
function findEntryPoints(graph) {
|
|
364
|
+
const { nodes, adjacency } = graph;
|
|
365
|
+
const hasIncoming = new Set();
|
|
366
|
+
|
|
367
|
+
for (const deps of Object.values(adjacency)) {
|
|
368
|
+
for (const dep of deps) {
|
|
369
|
+
hasIncoming.add(dep);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return nodes.filter(n => !hasIncoming.has(n));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Find orphan modules (files that export but are never imported).
|
|
378
|
+
* @param {string} cwd - Project root
|
|
379
|
+
* @param {{ nodes: string[], adjacency: Object }} graph
|
|
380
|
+
* @returns {string[]}
|
|
381
|
+
*/
|
|
382
|
+
function findOrphanExports(cwd, graph) {
|
|
383
|
+
const hasIncoming = new Set();
|
|
384
|
+
|
|
385
|
+
for (const deps of Object.values(graph.adjacency)) {
|
|
386
|
+
for (const dep of deps) {
|
|
387
|
+
hasIncoming.add(dep);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const orphans = [];
|
|
392
|
+
for (const node of graph.nodes) {
|
|
393
|
+
if (hasIncoming.has(node)) continue;
|
|
394
|
+
|
|
395
|
+
// Check if file has exports but no outgoing edges (completely disconnected exporter)
|
|
396
|
+
const outgoing = graph.adjacency[node] || [];
|
|
397
|
+
if (outgoing.length > 0) continue; // Has imports — likely an entry point, not orphan
|
|
398
|
+
|
|
399
|
+
const absPath = path.join(cwd, node);
|
|
400
|
+
const lang = EXTENSION_MAP[path.extname(node).toLowerCase()] || 'javascript';
|
|
401
|
+
const exports = parseExports(absPath, lang);
|
|
402
|
+
if (exports.length > 0) {
|
|
403
|
+
orphans.push(node);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return orphans;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Mermaid Graph Generation ───────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Generate a Mermaid dependency graph.
|
|
414
|
+
* @param {{ nodes: string[], edges: Array<{from: string, to: string}> }} graph
|
|
415
|
+
* @param {number} [maxNodes=15] - Maximum nodes to show
|
|
416
|
+
* @returns {string} Mermaid graph source
|
|
417
|
+
*/
|
|
418
|
+
function generateMermaidGraph(graph, maxNodes = 15) {
|
|
419
|
+
const { nodes, edges } = graph;
|
|
420
|
+
if (nodes.length === 0) return 'graph LR\n empty[No modules found]';
|
|
421
|
+
|
|
422
|
+
// Score nodes by total edge count (incoming + outgoing)
|
|
423
|
+
const edgeCount = {};
|
|
424
|
+
for (const node of nodes) edgeCount[node] = 0;
|
|
425
|
+
for (const { from, to } of edges) {
|
|
426
|
+
edgeCount[from] = (edgeCount[from] || 0) + 1;
|
|
427
|
+
edgeCount[to] = (edgeCount[to] || 0) + 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Pick top N nodes
|
|
431
|
+
const topNodes = Object.entries(edgeCount)
|
|
432
|
+
.sort((a, b) => b[1] - a[1])
|
|
433
|
+
.slice(0, maxNodes)
|
|
434
|
+
.map(([node]) => node);
|
|
435
|
+
|
|
436
|
+
const topSet = new Set(topNodes);
|
|
437
|
+
|
|
438
|
+
// Build Mermaid
|
|
439
|
+
const lines = ['graph LR'];
|
|
440
|
+
const nodeIds = {};
|
|
441
|
+
let idCounter = 0;
|
|
442
|
+
|
|
443
|
+
function nodeId(name) {
|
|
444
|
+
if (!nodeIds[name]) {
|
|
445
|
+
nodeIds[name] = `N${idCounter++}`;
|
|
446
|
+
}
|
|
447
|
+
return nodeIds[name];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function shortName(filepath) {
|
|
451
|
+
const parts = filepath.split('/');
|
|
452
|
+
return parts[parts.length - 1].replace(/\.\w+$/, '');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const { from, to } of edges) {
|
|
456
|
+
if (topSet.has(from) && topSet.has(to)) {
|
|
457
|
+
lines.push(` ${nodeId(from)}[${shortName(from)}] --> ${nodeId(to)}[${shortName(to)}]`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (nodes.length > maxNodes) {
|
|
462
|
+
lines.push(` note[... and ${nodes.length - maxNodes} more modules]`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return lines.join('\n');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── Best Practices Detection ───────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Detect best practices in a codebase across 5 categories.
|
|
472
|
+
* @param {string} cwd - Project root
|
|
473
|
+
* @returns {{ categories: Array, score: number, recommendations: string[] }}
|
|
474
|
+
*/
|
|
475
|
+
function detectBestPractices(cwd) {
|
|
476
|
+
const { files_by_language, file_count } = detectLanguages(cwd);
|
|
477
|
+
const categories = [];
|
|
478
|
+
const recommendations = [];
|
|
479
|
+
|
|
480
|
+
// Flatten all source files
|
|
481
|
+
const allFiles = [];
|
|
482
|
+
for (const files of Object.values(files_by_language)) {
|
|
483
|
+
allFiles.push(...files);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Sample up to 30 files for detailed analysis
|
|
487
|
+
const sampleFiles = allFiles.slice(0, 30);
|
|
488
|
+
const sampleContents = [];
|
|
489
|
+
for (const rel of sampleFiles) {
|
|
490
|
+
const content = safeReadFile(path.join(cwd, rel));
|
|
491
|
+
if (content) sampleContents.push({ path: rel, content });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Category 1: Error Handling
|
|
495
|
+
const errorCat = detectErrorHandling(sampleContents);
|
|
496
|
+
categories.push(errorCat);
|
|
497
|
+
if (errorCat.score < 7) recommendations.push(...errorCat.recommendations);
|
|
498
|
+
|
|
499
|
+
// Category 2: Testing
|
|
500
|
+
const testCat = detectTestingPractices(cwd, file_count);
|
|
501
|
+
categories.push(testCat);
|
|
502
|
+
if (testCat.score < 7) recommendations.push(...testCat.recommendations);
|
|
503
|
+
|
|
504
|
+
// Category 3: Naming Conventions
|
|
505
|
+
const namingCat = detectNamingConventions(sampleContents);
|
|
506
|
+
categories.push(namingCat);
|
|
507
|
+
if (namingCat.score < 7) recommendations.push(...namingCat.recommendations);
|
|
508
|
+
|
|
509
|
+
// Category 4: Security
|
|
510
|
+
const secCat = detectSecurityPractices(cwd, sampleContents);
|
|
511
|
+
categories.push(secCat);
|
|
512
|
+
if (secCat.score < 7) recommendations.push(...secCat.recommendations);
|
|
513
|
+
|
|
514
|
+
// Category 5: Performance
|
|
515
|
+
const perfCat = detectPerformancePractices(sampleContents);
|
|
516
|
+
categories.push(perfCat);
|
|
517
|
+
if (perfCat.score < 7) recommendations.push(...perfCat.recommendations);
|
|
518
|
+
|
|
519
|
+
const score = categories.length > 0
|
|
520
|
+
? Math.round(categories.reduce((sum, c) => sum + c.score, 0) / categories.length * 10) / 10
|
|
521
|
+
: 0;
|
|
522
|
+
|
|
523
|
+
return { categories, score, recommendations };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function detectErrorHandling(samples) {
|
|
527
|
+
let tryCatchCount = 0;
|
|
528
|
+
let functionCount = 0;
|
|
529
|
+
const findings = [];
|
|
530
|
+
|
|
531
|
+
for (const { path: fp, content } of samples) {
|
|
532
|
+
const tryCatches = (content.match(/\btry\s*\{/g) || []).length;
|
|
533
|
+
const functions = (content.match(/\bfunction\s+\w+|=>\s*\{|\bconst\s+\w+\s*=\s*(?:async\s+)?\(/g) || []).length;
|
|
534
|
+
tryCatchCount += tryCatches;
|
|
535
|
+
functionCount += functions;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const ratio = functionCount > 0 ? tryCatchCount / functionCount : 0;
|
|
539
|
+
const score = Math.min(10, Math.round(ratio * 20));
|
|
540
|
+
const recs = [];
|
|
541
|
+
if (ratio < 0.3) recs.push('Add try-catch to more functions, especially async operations');
|
|
542
|
+
|
|
543
|
+
return { name: 'Error Handling', score, detected_patterns: [`try-catch ratio: ${Math.round(ratio * 100)}%`], recommendations: recs };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function detectTestingPractices(cwd, sourceFileCount) {
|
|
547
|
+
const patterns = ['*.test.*', '*.spec.*', '__tests__/**'];
|
|
548
|
+
let testFileCount = 0;
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (entry.name === 'tests' || entry.name === '__tests__' || entry.name === 'test') {
|
|
554
|
+
try {
|
|
555
|
+
const testDir = path.join(cwd, entry.name);
|
|
556
|
+
const testFiles = fs.readdirSync(testDir).filter(f => f.endsWith('.test.cjs') || f.endsWith('.test.js') || f.endsWith('.test.ts') || f.endsWith('.spec.js') || f.endsWith('.spec.ts'));
|
|
557
|
+
testFileCount += testFiles.length;
|
|
558
|
+
} catch { /* */ }
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} catch { /* */ }
|
|
562
|
+
|
|
563
|
+
// Check for test framework config
|
|
564
|
+
let hasConfig = false;
|
|
565
|
+
for (const cfg of ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', '.mocharc.yml', '.mocharc.json']) {
|
|
566
|
+
try { fs.accessSync(path.join(cwd, cfg)); hasConfig = true; break; } catch { /* */ }
|
|
567
|
+
}
|
|
568
|
+
// node:test doesn't need config — check package.json for test script
|
|
569
|
+
try {
|
|
570
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
|
|
571
|
+
if (pkg.scripts && pkg.scripts.test) hasConfig = true;
|
|
572
|
+
} catch { /* */ }
|
|
573
|
+
|
|
574
|
+
const ratio = sourceFileCount > 0 ? testFileCount / sourceFileCount : 0;
|
|
575
|
+
const score = Math.min(10, Math.round((ratio * 10) + (hasConfig ? 3 : 0) + (testFileCount > 0 ? 2 : 0)));
|
|
576
|
+
const recs = [];
|
|
577
|
+
if (!hasConfig) recs.push('Add test framework configuration');
|
|
578
|
+
if (testFileCount === 0) recs.push('Create test files for source modules');
|
|
579
|
+
|
|
580
|
+
return { name: 'Testing', score, detected_patterns: [`${testFileCount} test files found`, hasConfig ? 'Test config present' : 'No test config'], recommendations: recs };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function detectNamingConventions(samples) {
|
|
584
|
+
let consistent = 0;
|
|
585
|
+
let total = 0;
|
|
586
|
+
const filePatterns = new Set();
|
|
587
|
+
|
|
588
|
+
for (const { path: fp } of samples) {
|
|
589
|
+
const filename = fp.split('/').pop().replace(/\.\w+$/, '');
|
|
590
|
+
total++;
|
|
591
|
+
if (/^[a-z][a-z0-9-]*$/.test(filename)) { consistent++; filePatterns.add('kebab-case'); }
|
|
592
|
+
else if (/^[a-z][a-zA-Z0-9]*$/.test(filename)) { consistent++; filePatterns.add('camelCase'); }
|
|
593
|
+
else if (/^[A-Z][a-zA-Z0-9]*$/.test(filename)) { consistent++; filePatterns.add('PascalCase'); }
|
|
594
|
+
else if (/^[a-z][a-z0-9_]*$/.test(filename)) { consistent++; filePatterns.add('snake_case'); }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const ratio = total > 0 ? consistent / total : 0;
|
|
598
|
+
const singlePattern = filePatterns.size <= 2;
|
|
599
|
+
const score = Math.min(10, Math.round(ratio * 7 + (singlePattern ? 3 : 0)));
|
|
600
|
+
const recs = [];
|
|
601
|
+
if (filePatterns.size > 2) recs.push('Standardize file naming convention — multiple patterns detected');
|
|
602
|
+
|
|
603
|
+
return { name: 'Naming Conventions', score, detected_patterns: [`File patterns: ${Array.from(filePatterns).join(', ')}`], recommendations: recs };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function detectSecurityPractices(cwd, samples) {
|
|
607
|
+
let score = 5; // Start at baseline
|
|
608
|
+
const patterns = [];
|
|
609
|
+
const recs = [];
|
|
610
|
+
|
|
611
|
+
// Check for .gitignore with .env
|
|
612
|
+
const gitignore = safeReadFile(path.join(cwd, '.gitignore'));
|
|
613
|
+
if (gitignore && /\.env/.test(gitignore)) { score += 2; patterns.push('.env in .gitignore'); }
|
|
614
|
+
else { recs.push('Add .env to .gitignore'); }
|
|
615
|
+
|
|
616
|
+
// Check for .env.example
|
|
617
|
+
try { fs.accessSync(path.join(cwd, '.env.example')); score += 1; patterns.push('.env.example exists'); } catch { /* */ }
|
|
618
|
+
|
|
619
|
+
// Check for hardcoded secrets patterns in code
|
|
620
|
+
let secretsFound = 0;
|
|
621
|
+
for (const { content } of samples) {
|
|
622
|
+
if (/(?:password|secret|api_key|token)\s*=\s*['"][^'"]{8,}/i.test(content)) {
|
|
623
|
+
secretsFound++;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (secretsFound === 0) { score += 2; patterns.push('No hardcoded secrets detected'); }
|
|
627
|
+
else { score -= 2; recs.push(`Found ${secretsFound} files with potential hardcoded secrets`); }
|
|
628
|
+
|
|
629
|
+
return { name: 'Security', score: Math.max(0, Math.min(10, score)), detected_patterns: patterns, recommendations: recs };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function detectPerformancePractices(samples) {
|
|
633
|
+
let score = 5;
|
|
634
|
+
const patterns = [];
|
|
635
|
+
const recs = [];
|
|
636
|
+
|
|
637
|
+
let hasMemoization = false;
|
|
638
|
+
let hasLazyLoading = false;
|
|
639
|
+
|
|
640
|
+
for (const { content } of samples) {
|
|
641
|
+
if (/\buseMemo\b|\buseCallback\b|\bmemoize\b/.test(content)) hasMemoization = true;
|
|
642
|
+
if (/\blazy\s*\(|\bimport\s*\(/.test(content)) hasLazyLoading = true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (hasMemoization) { score += 2; patterns.push('Memoization patterns detected'); }
|
|
646
|
+
if (hasLazyLoading) { score += 2; patterns.push('Lazy loading detected'); }
|
|
647
|
+
if (!hasMemoization && !hasLazyLoading) { recs.push('Consider memoization or lazy loading for performance'); }
|
|
648
|
+
|
|
649
|
+
return { name: 'Performance', score: Math.min(10, score), detected_patterns: patterns.length > 0 ? patterns : ['No specific performance patterns detected'], recommendations: recs };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ─── Lowercase Codebase Doc Helper ──────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Find a codebase document, checking lowercase first then UPPERCASE.
|
|
656
|
+
* @param {string} cwd - Project root
|
|
657
|
+
* @param {string} docName - Document name without path (e.g., 'conventions.md')
|
|
658
|
+
* @returns {string|null} Full path to found document, or null
|
|
659
|
+
*/
|
|
660
|
+
function findCodebaseDoc(cwd, docName) {
|
|
661
|
+
const codebaseDir = path.join(planningPath(cwd), CODEBASE_DIR);
|
|
662
|
+
// Try lowercase first
|
|
663
|
+
const lowercase = path.join(codebaseDir, docName.toLowerCase());
|
|
664
|
+
const content = safeReadFile(lowercase);
|
|
665
|
+
if (content !== null) return lowercase;
|
|
666
|
+
|
|
667
|
+
// Try UPPERCASE fallback
|
|
668
|
+
const uppercase = path.join(codebaseDir, docName.toUpperCase());
|
|
669
|
+
const upperContent = safeReadFile(uppercase);
|
|
670
|
+
if (upperContent !== null) return uppercase;
|
|
671
|
+
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ─── CLI Commands ───────────────────────────────────────────────────────────
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* CLI: Detect languages in codebase.
|
|
679
|
+
* @param {string} cwd - Project root
|
|
680
|
+
* @param {boolean} raw - Raw output flag
|
|
681
|
+
*/
|
|
682
|
+
function cmdDetectLanguages(cwd, raw) {
|
|
683
|
+
const result = detectLanguages(cwd);
|
|
684
|
+
output(result, raw);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* CLI: Analyze imports and build dependency graph.
|
|
689
|
+
* @param {string} cwd - Project root
|
|
690
|
+
* @param {boolean} raw - Raw output flag
|
|
691
|
+
* @param {string[]} args - Additional arguments (--files f1,f2)
|
|
692
|
+
*/
|
|
693
|
+
function cmdAnalyzeImports(cwd, raw, args) {
|
|
694
|
+
const graph = buildDependencyGraph(cwd);
|
|
695
|
+
const circularDeps = findCircularDeps(graph);
|
|
696
|
+
const entryPoints = findEntryPoints(graph);
|
|
697
|
+
const orphans = findOrphanExports(cwd, graph);
|
|
698
|
+
const { primary } = detectLanguages(cwd);
|
|
699
|
+
const mermaid = generateMermaidGraph(graph);
|
|
700
|
+
|
|
701
|
+
const result = {
|
|
702
|
+
language: primary,
|
|
703
|
+
modules: graph.nodes.length,
|
|
704
|
+
imports: graph.edges.length,
|
|
705
|
+
circular_deps: circularDeps,
|
|
706
|
+
entry_points: entryPoints,
|
|
707
|
+
orphan_modules: orphans,
|
|
708
|
+
dependency_graph: mermaid,
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
output(result, raw);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* CLI: Detect best practices.
|
|
716
|
+
* @param {string} cwd - Project root
|
|
717
|
+
* @param {boolean} raw - Raw output flag
|
|
718
|
+
*/
|
|
719
|
+
function cmdBestPractices(cwd, raw) {
|
|
720
|
+
const result = detectBestPractices(cwd);
|
|
721
|
+
output(result, raw);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
module.exports = {
|
|
727
|
+
// Core analysis
|
|
728
|
+
detectLanguages,
|
|
729
|
+
parseImports,
|
|
730
|
+
parseExports,
|
|
731
|
+
buildDependencyGraph,
|
|
732
|
+
findCircularDeps,
|
|
733
|
+
findEntryPoints,
|
|
734
|
+
findOrphanExports,
|
|
735
|
+
generateMermaidGraph,
|
|
736
|
+
detectBestPractices,
|
|
737
|
+
// Helper
|
|
738
|
+
findCodebaseDoc,
|
|
739
|
+
stripComments,
|
|
740
|
+
walkSourceFiles,
|
|
741
|
+
resolveImport,
|
|
742
|
+
// CLI commands
|
|
743
|
+
cmdDetectLanguages,
|
|
744
|
+
cmdAnalyzeImports,
|
|
745
|
+
cmdBestPractices,
|
|
746
|
+
};
|