@tarunpahade/asl-parser 1.0.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 +201 -0
- package/README.md +226 -0
- package/bin/asl.js +127 -0
- package/lib/commands/rag-command.js +204 -0
- package/lib/parser.js +335 -0
- package/lib/promptAnalyzer.js +358 -0
- package/lib/rag/minimal-rag.js +170 -0
- package/package.json +42 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// lib/rag-command.js
|
|
2
|
+
// Simple RAG search command for asl-parser
|
|
3
|
+
// Usage: asl rag "your search query"
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { glob } = require('glob');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
// Same ignore patterns as parser.js
|
|
11
|
+
const IGNORE_DIRS = [
|
|
12
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build',
|
|
13
|
+
'.cache', 'coverage', '.vscode', '.idea', 'vendor',
|
|
14
|
+
'__pycache__', '.pytest_cache', 'target', 'bin', 'obj'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const CODE_EXTENSIONS = [
|
|
18
|
+
'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.cpp', '.c', '.h',
|
|
19
|
+
'.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala',
|
|
20
|
+
'.vue', '.svelte', '.html', '.css', '.scss', '.less',
|
|
21
|
+
'.json', '.yaml', '.yml', '.toml', '.xml'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Simple RAG Search - finds relevant code sections
|
|
26
|
+
*/
|
|
27
|
+
class RagSearch {
|
|
28
|
+
constructor(basePath) {
|
|
29
|
+
this.basePath = basePath;
|
|
30
|
+
this.chunks = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Index all code files by creating searchable chunks
|
|
35
|
+
*/
|
|
36
|
+
async index() {
|
|
37
|
+
console.log(chalk.blue('📚 Indexing codebase...'));
|
|
38
|
+
|
|
39
|
+
const patterns = CODE_EXTENSIONS.map(ext => `**/*${ext}`);
|
|
40
|
+
|
|
41
|
+
for (const pattern of patterns) {
|
|
42
|
+
try {
|
|
43
|
+
const files = await glob(pattern, {
|
|
44
|
+
cwd: this.basePath,
|
|
45
|
+
absolute: true,
|
|
46
|
+
ignore: IGNORE_DIRS.map(dir => `**/${dir}/**`)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const filePath of files) {
|
|
50
|
+
this.indexFile(filePath);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Silently skip glob errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.green(`✅ Indexed ${this.chunks.length} code sections\n`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Break a file into searchable chunks
|
|
62
|
+
*/
|
|
63
|
+
indexFile(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
66
|
+
const relativePath = path.relative(this.basePath, filePath);
|
|
67
|
+
const lines = content.split('\n');
|
|
68
|
+
const chunkSize = 20;
|
|
69
|
+
|
|
70
|
+
// Create overlapping chunks
|
|
71
|
+
for (let i = 0; i < lines.length; i += chunkSize - 5) {
|
|
72
|
+
const chunk = lines.slice(i, i + chunkSize).join('\n');
|
|
73
|
+
|
|
74
|
+
if (chunk.trim().length > 0) {
|
|
75
|
+
this.chunks.push({
|
|
76
|
+
file: relativePath,
|
|
77
|
+
lineStart: i + 1,
|
|
78
|
+
lineEnd: Math.min(i + chunkSize, lines.length),
|
|
79
|
+
content: chunk,
|
|
80
|
+
keywords: this.extractKeywords(chunk, relativePath)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Skip unreadable files
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract keywords from code chunk
|
|
91
|
+
*/
|
|
92
|
+
extractKeywords(content, filePath) {
|
|
93
|
+
const keywords = [];
|
|
94
|
+
|
|
95
|
+
// Extract function/class names
|
|
96
|
+
const funcMatches = content.match(/(?:function|const|async|def|class)\s+(\w+)/g);
|
|
97
|
+
if (funcMatches) {
|
|
98
|
+
keywords.push(...funcMatches.map(f => f.split(/\s+/)[1]));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract from file path
|
|
102
|
+
const pathParts = filePath.split('/').filter(p => p.length > 2);
|
|
103
|
+
keywords.push(...pathParts);
|
|
104
|
+
|
|
105
|
+
return keywords.map(k => k.toLowerCase()).filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Search for relevant chunks
|
|
110
|
+
*/
|
|
111
|
+
search(query, topK = 10) {
|
|
112
|
+
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
113
|
+
const results = [];
|
|
114
|
+
|
|
115
|
+
for (const chunk of this.chunks) {
|
|
116
|
+
let score = 0;
|
|
117
|
+
const contentLower = chunk.content.toLowerCase();
|
|
118
|
+
|
|
119
|
+
// Score based on token matches
|
|
120
|
+
for (const token of queryTokens) {
|
|
121
|
+
// Content matches (high weight)
|
|
122
|
+
const contentMatches = (contentLower.match(new RegExp(`\\b${token}\\w*`, 'g')) || []).length;
|
|
123
|
+
score += contentMatches * 2;
|
|
124
|
+
|
|
125
|
+
// Keyword matches (medium weight)
|
|
126
|
+
const keywordMatches = chunk.keywords.filter(k => k.includes(token)).length;
|
|
127
|
+
score += keywordMatches * 1.5;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (score > 0) {
|
|
131
|
+
results.push({ ...chunk, score });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return results
|
|
136
|
+
.sort((a, b) => b.score - a.score)
|
|
137
|
+
.slice(0, topK);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Format results for display
|
|
142
|
+
*/
|
|
143
|
+
displayResults(results, query) {
|
|
144
|
+
if (results.length === 0) {
|
|
145
|
+
console.log(chalk.yellow(`❌ No relevant code found for: "${query}"\n`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(chalk.green(`✅ Found ${results.length} relevant sections:\n`));
|
|
150
|
+
console.log(chalk.cyan(`📍 Query: "${query}"\n`));
|
|
151
|
+
|
|
152
|
+
results.forEach((result, idx) => {
|
|
153
|
+
const relevance = Math.min(100, Math.round((result.score / 5) * 10));
|
|
154
|
+
const fileIcon = result.file.includes('test') ? '🧪' : '📄';
|
|
155
|
+
|
|
156
|
+
console.log(chalk.yellow(`${idx + 1}. ${fileIcon} ${result.file}`));
|
|
157
|
+
console.log(chalk.gray(` Lines ${result.lineStart}-${result.lineEnd} | Relevance: ${relevance}%`));
|
|
158
|
+
|
|
159
|
+
// Show code snippet
|
|
160
|
+
const snippet = result.content
|
|
161
|
+
.split('\n')
|
|
162
|
+
.slice(0, 2)
|
|
163
|
+
.join('\n')
|
|
164
|
+
.substring(0, 70);
|
|
165
|
+
|
|
166
|
+
console.log(chalk.white(` ${snippet}...`));
|
|
167
|
+
console.log('');
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Main RAG command function
|
|
174
|
+
*/
|
|
175
|
+
async function ragCommand(query, targetPath = './') {
|
|
176
|
+
try {
|
|
177
|
+
const absolutePath = path.resolve(targetPath);
|
|
178
|
+
|
|
179
|
+
if (!fs.existsSync(absolutePath)) {
|
|
180
|
+
console.error(chalk.red(`Error: Path "${targetPath}" does not exist`));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
185
|
+
console.error(chalk.red(`Error: "${targetPath}" is not a directory`));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(chalk.blue(`\n🔍 RAG Search: "${query}"\n`));
|
|
190
|
+
|
|
191
|
+
// Initialize and search
|
|
192
|
+
const rag = new RagSearch(absolutePath);
|
|
193
|
+
await rag.index();
|
|
194
|
+
|
|
195
|
+
const results = rag.search(query, 10);
|
|
196
|
+
rag.displayResults(results, query);
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(chalk.red(`\nError: ${error.message}\n`));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = ragCommand;
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { glob } = require('glob');
|
|
4
|
+
|
|
5
|
+
// File extensions to analyze
|
|
6
|
+
const CODE_EXTENSIONS = [
|
|
7
|
+
'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.cpp', '.c', '.h',
|
|
8
|
+
'.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala',
|
|
9
|
+
'.vue', '.svelte', '.html', '.css', '.scss', '.less',
|
|
10
|
+
'.json', '.yaml', '.yml', '.toml', '.xml'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Directories to ignore
|
|
14
|
+
const IGNORE_DIRS = [
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'.next',
|
|
18
|
+
'.nuxt',
|
|
19
|
+
'dist',
|
|
20
|
+
'build',
|
|
21
|
+
'.cache',
|
|
22
|
+
'coverage',
|
|
23
|
+
'.vscode',
|
|
24
|
+
'.idea',
|
|
25
|
+
'vendor',
|
|
26
|
+
'__pycache__',
|
|
27
|
+
'.pytest_cache',
|
|
28
|
+
'target',
|
|
29
|
+
'bin',
|
|
30
|
+
'obj'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Files to ignore
|
|
34
|
+
const IGNORE_FILES = [
|
|
35
|
+
'package-lock.json',
|
|
36
|
+
'yarn.lock',
|
|
37
|
+
'pnpm-lock.yaml',
|
|
38
|
+
'.DS_Store',
|
|
39
|
+
'Thumbs.db'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a file should be ignored
|
|
44
|
+
*/
|
|
45
|
+
function shouldIgnore(filePath, basePath) {
|
|
46
|
+
const relativePath = path.relative(basePath, filePath);
|
|
47
|
+
const parts = relativePath.split(path.sep);
|
|
48
|
+
|
|
49
|
+
// Check if any part matches ignore directories
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
if (IGNORE_DIRS.includes(part) || part.startsWith('.')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if file matches ignore patterns
|
|
57
|
+
const fileName = path.basename(filePath);
|
|
58
|
+
if (IGNORE_FILES.includes(fileName)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get file extension
|
|
67
|
+
*/
|
|
68
|
+
function getFileExtension(filePath) {
|
|
69
|
+
return path.extname(filePath).toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Analyze a single file
|
|
74
|
+
*/
|
|
75
|
+
function analyzeFile(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
78
|
+
const ext = getFileExtension(filePath);
|
|
79
|
+
const fileName = path.basename(filePath);
|
|
80
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
81
|
+
|
|
82
|
+
// Basic analysis
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
const totalLines = lines.length;
|
|
85
|
+
const codeLines = lines.filter(line => line.trim() && !line.trim().startsWith('//') && !line.trim().startsWith('/*') && !line.trim().startsWith('*')).length;
|
|
86
|
+
const commentLines = totalLines - codeLines;
|
|
87
|
+
|
|
88
|
+
// Detect functions/classes (basic heuristics)
|
|
89
|
+
const functions = [];
|
|
90
|
+
const classes = [];
|
|
91
|
+
|
|
92
|
+
// JavaScript/TypeScript patterns
|
|
93
|
+
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
94
|
+
// Match: function name(
|
|
95
|
+
const funcDeclMatches = content.matchAll(/function\s+(\w+)\s*\(/g);
|
|
96
|
+
for (const match of funcDeclMatches) {
|
|
97
|
+
functions.push(match[1]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Match: const name = (async)? (
|
|
101
|
+
const arrowFuncMatches = content.matchAll(/const\s+(\w+)\s*=\s*(?:async\s+)?\(/g);
|
|
102
|
+
for (const match of arrowFuncMatches) {
|
|
103
|
+
functions.push(match[1]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Match: name: (async)? (
|
|
107
|
+
const methodMatches = content.matchAll(/(\w+)\s*:\s*(?:async\s+)?\(/g);
|
|
108
|
+
for (const match of methodMatches) {
|
|
109
|
+
functions.push(match[1]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
113
|
+
for (const match of classMatches) {
|
|
114
|
+
classes.push(match[1]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Python patterns
|
|
119
|
+
if (ext === '.py') {
|
|
120
|
+
const functionMatches = content.matchAll(/def\s+(\w+)/g);
|
|
121
|
+
for (const match of functionMatches) {
|
|
122
|
+
functions.push(match[1]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
126
|
+
for (const match of classMatches) {
|
|
127
|
+
classes.push(match[1]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Java patterns
|
|
132
|
+
if (ext === '.java') {
|
|
133
|
+
const methodMatches = content.matchAll(/(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(/g);
|
|
134
|
+
for (const match of methodMatches) {
|
|
135
|
+
functions.push(match[1]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
139
|
+
for (const match of classMatches) {
|
|
140
|
+
classes.push(match[1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
path: relativePath,
|
|
146
|
+
fileName,
|
|
147
|
+
extension: ext,
|
|
148
|
+
totalLines,
|
|
149
|
+
codeLines,
|
|
150
|
+
commentLines,
|
|
151
|
+
functions: [...new Set(functions)],
|
|
152
|
+
classes: [...new Set(classes)],
|
|
153
|
+
size: content.length
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
path: path.relative(process.cwd(), filePath),
|
|
158
|
+
fileName: path.basename(filePath),
|
|
159
|
+
error: error.message
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse entire codebase
|
|
167
|
+
*/
|
|
168
|
+
async function parseCodebase(basePath, outputPath) {
|
|
169
|
+
const files = [];
|
|
170
|
+
const stats = {
|
|
171
|
+
totalFiles: 0,
|
|
172
|
+
totalLines: 0,
|
|
173
|
+
totalSize: 0,
|
|
174
|
+
byExtension: {},
|
|
175
|
+
errors: []
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Find all code files
|
|
179
|
+
const patterns = CODE_EXTENSIONS.map(ext => `**/*${ext}`);
|
|
180
|
+
|
|
181
|
+
for (const pattern of patterns) {
|
|
182
|
+
try {
|
|
183
|
+
const foundFiles = await glob(pattern, {
|
|
184
|
+
cwd: basePath,
|
|
185
|
+
absolute: true,
|
|
186
|
+
ignore: IGNORE_DIRS.map(dir => `**/${dir}/**`)
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
for (const filePath of foundFiles) {
|
|
190
|
+
if (!shouldIgnore(filePath, basePath)) {
|
|
191
|
+
files.push(filePath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
stats.errors.push(`Error globbing ${pattern}: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Analyze each file
|
|
200
|
+
const analyzedFiles = files.map(filePath => {
|
|
201
|
+
const analysis = analyzeFile(filePath);
|
|
202
|
+
stats.totalFiles++;
|
|
203
|
+
|
|
204
|
+
if (!analysis.error) {
|
|
205
|
+
stats.totalLines += analysis.totalLines;
|
|
206
|
+
stats.totalSize += analysis.size;
|
|
207
|
+
|
|
208
|
+
const ext = analysis.extension;
|
|
209
|
+
if (!stats.byExtension[ext]) {
|
|
210
|
+
stats.byExtension[ext] = { count: 0, lines: 0 };
|
|
211
|
+
}
|
|
212
|
+
stats.byExtension[ext].count++;
|
|
213
|
+
stats.byExtension[ext].lines += analysis.totalLines;
|
|
214
|
+
} else {
|
|
215
|
+
stats.errors.push(`Error analyzing ${analysis.path}: ${analysis.error}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return analysis;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Generate markdown content
|
|
222
|
+
const markdown = generateMarkdown(analyzedFiles, stats, basePath);
|
|
223
|
+
|
|
224
|
+
// Write to file
|
|
225
|
+
fs.writeFileSync(outputPath, markdown, 'utf-8');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generate markdown documentation
|
|
230
|
+
*/
|
|
231
|
+
function generateMarkdown(files, stats, basePath) {
|
|
232
|
+
const timestamp = new Date().toISOString();
|
|
233
|
+
const projectName = path.basename(basePath);
|
|
234
|
+
|
|
235
|
+
let md = `# ASL Parser Report: ${projectName}\n\n`;
|
|
236
|
+
md += `**Generated:** ${timestamp}\n\n`;
|
|
237
|
+
md += `---\n\n`;
|
|
238
|
+
|
|
239
|
+
// Summary Statistics
|
|
240
|
+
md += `## 📊 Summary Statistics\n\n`;
|
|
241
|
+
md += `- **Total Files Analyzed:** ${stats.totalFiles}\n`;
|
|
242
|
+
md += `- **Total Lines of Code:** ${stats.totalLines.toLocaleString()}\n`;
|
|
243
|
+
md += `- **Total Size:** ${(stats.totalSize / 1024).toFixed(2)} KB\n\n`;
|
|
244
|
+
|
|
245
|
+
// Files by Extension
|
|
246
|
+
md += `### Files by Extension\n\n`;
|
|
247
|
+
md += `| Extension | Count | Lines |\n`;
|
|
248
|
+
md += `|-----------|-------|-------|\n`;
|
|
249
|
+
const sortedExtensions = Object.entries(stats.byExtension)
|
|
250
|
+
.sort((a, b) => b[1].count - a[1].count);
|
|
251
|
+
for (const [ext, data] of sortedExtensions) {
|
|
252
|
+
md += `| ${ext || '(no extension)'} | ${data.count} | ${data.lines.toLocaleString()} |\n`;
|
|
253
|
+
}
|
|
254
|
+
md += `\n`;
|
|
255
|
+
|
|
256
|
+
// File Details
|
|
257
|
+
md += `## 📁 File Analysis\n\n`;
|
|
258
|
+
|
|
259
|
+
// Group files by directory
|
|
260
|
+
const filesByDir = {};
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
if (file.error) continue;
|
|
263
|
+
|
|
264
|
+
const dir = path.dirname(file.path);
|
|
265
|
+
if (!filesByDir[dir]) {
|
|
266
|
+
filesByDir[dir] = [];
|
|
267
|
+
}
|
|
268
|
+
filesByDir[dir].push(file);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const sortedDirs = Object.keys(filesByDir).sort();
|
|
272
|
+
|
|
273
|
+
for (const dir of sortedDirs) {
|
|
274
|
+
const dirFiles = filesByDir[dir];
|
|
275
|
+
md += `### ${dir || '(root)'}\n\n`;
|
|
276
|
+
|
|
277
|
+
for (const file of dirFiles) {
|
|
278
|
+
md += `#### \`${file.fileName}\`\n\n`;
|
|
279
|
+
md += `- **Path:** \`${file.path}\`\n`;
|
|
280
|
+
md += `- **Extension:** ${file.extension || '(none)'}\n`;
|
|
281
|
+
md += `- **Lines:** ${file.totalLines} (${file.codeLines} code, ${file.commentLines} comments)\n`;
|
|
282
|
+
md += `- **Size:** ${(file.size / 1024).toFixed(2)} KB\n`;
|
|
283
|
+
|
|
284
|
+
if (file.classes.length > 0) {
|
|
285
|
+
md += `- **Classes:** ${file.classes.join(', ')}\n`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (file.functions.length > 0) {
|
|
289
|
+
md += `- **Functions:** ${file.functions.slice(0, 10).join(', ')}${file.functions.length > 10 ? ` (and ${file.functions.length - 10} more)` : ''}\n`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
md += `\n`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Errors
|
|
297
|
+
if (stats.errors.length > 0) {
|
|
298
|
+
md += `## ⚠️ Errors\n\n`;
|
|
299
|
+
for (const error of stats.errors) {
|
|
300
|
+
md += `- ${error}\n`;
|
|
301
|
+
}
|
|
302
|
+
md += `\n`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Agent Specifications
|
|
306
|
+
md += `---\n\n`;
|
|
307
|
+
md += `## 🤖 Agent Specifications\n\n`;
|
|
308
|
+
md += `This section contains structured information about the codebase that can be used by AI agents.\n\n`;
|
|
309
|
+
|
|
310
|
+
// Generate agent-friendly structure
|
|
311
|
+
md += `### Codebase Structure\n\n`;
|
|
312
|
+
md += `\`\`\`json\n`;
|
|
313
|
+
md += JSON.stringify({
|
|
314
|
+
projectName,
|
|
315
|
+
totalFiles: stats.totalFiles,
|
|
316
|
+
totalLines: stats.totalLines,
|
|
317
|
+
extensions: stats.byExtension,
|
|
318
|
+
structure: sortedDirs.map(dir => ({
|
|
319
|
+
directory: dir,
|
|
320
|
+
files: filesByDir[dir].map(f => ({
|
|
321
|
+
name: f.fileName,
|
|
322
|
+
path: f.path,
|
|
323
|
+
extension: f.extension,
|
|
324
|
+
lines: f.totalLines,
|
|
325
|
+
classes: f.classes,
|
|
326
|
+
functions: f.functions
|
|
327
|
+
}))
|
|
328
|
+
}))
|
|
329
|
+
}, null, 2);
|
|
330
|
+
md += `\n\`\`\`\n\n`;
|
|
331
|
+
|
|
332
|
+
return md;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { parseCodebase, analyzeFile };
|