UNPKG

erosolar-cli

Version:

Unified AI agent framework for the command line - Multi-provider support with schema-driven tools, code intelligence, and transparent reasoning

564 lines 22.8 kB
/** * Automated Documentation Generator * * Analyzes code structure and generates comprehensive documentation * including API docs, README sections, and architecture diagrams. * * Principal Investigator: Bo Shang * Framework: erosolar-cli */ import * as fs from 'fs'; import * as path from 'path'; // ============================================================================ // Parser // ============================================================================ class CodeParser { parseFile(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const items = []; // Parse exported functions const functionRegex = /(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(export\s+)?(?:async\s+)?function\s+(\w+)\s*(<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g; let match; while ((match = functionRegex.exec(content)) !== null) { const [, docComment, exportKeyword, name, generics, params, returnType] = match; if (name && params !== undefined) { const position = this.getPosition(content, match.index); items.push({ type: 'function', name, file: filePath, line: position.line, signature: `${name}${generics || ''}(${params})${returnType ? `: ${returnType.trim()}` : ''}`, description: this.parseDescription(docComment), params: this.parseParams(params, docComment), returns: returnType ? { type: returnType.trim(), description: this.parseReturnDoc(docComment) } : undefined, exported: !!exportKeyword, tags: this.parseTags(docComment), }); } } // Parse exported arrow functions const arrowRegex = /(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(export\s+)?const\s+(\w+)\s*(?::\s*([^=]+))?\s*=\s*(?:async\s*)?\(([^)]*)\)(?:\s*:\s*([^=]+))?\s*=>/g; while ((match = arrowRegex.exec(content)) !== null) { const [, docComment, exportKeyword, name, _typeAnnotation, params, returnType] = match; if (name && params !== undefined) { const position = this.getPosition(content, match.index); items.push({ type: 'function', name, file: filePath, line: position.line, signature: `${name}(${params})${returnType ? `: ${returnType.trim()}` : ''}`, description: this.parseDescription(docComment), params: this.parseParams(params, docComment), returns: returnType ? { type: returnType.trim() } : undefined, exported: !!exportKeyword, tags: this.parseTags(docComment), }); } } // Parse classes const classRegex = /(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?\s*\{/g; while ((match = classRegex.exec(content)) !== null) { const [, docComment, exportKeyword, name, extendsClass, implementsInterfaces] = match; if (name) { const position = this.getPosition(content, match.index); let signature = `class ${name}`; if (extendsClass) signature += ` extends ${extendsClass}`; if (implementsInterfaces) signature += ` implements ${implementsInterfaces.trim()}`; items.push({ type: 'class', name, file: filePath, line: position.line, signature, description: this.parseDescription(docComment), exported: !!exportKeyword, tags: this.parseTags(docComment), }); } } // Parse interfaces const interfaceRegex = /(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(export\s+)?interface\s+(\w+)(?:\s+extends\s+([^{]+))?\s*\{/g; while ((match = interfaceRegex.exec(content)) !== null) { const [, docComment, exportKeyword, name, extendsInterfaces] = match; if (name) { const position = this.getPosition(content, match.index); items.push({ type: 'interface', name, file: filePath, line: position.line, signature: `interface ${name}${extendsInterfaces ? ` extends ${extendsInterfaces.trim()}` : ''}`, description: this.parseDescription(docComment), exported: !!exportKeyword, tags: this.parseTags(docComment), }); } } // Parse type aliases const typeRegex = /(?:\/\*\*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*)?(export\s+)?type\s+(\w+)(?:<[^>]+>)?\s*=/g; while ((match = typeRegex.exec(content)) !== null) { const [, docComment, exportKeyword, name] = match; if (name) { const position = this.getPosition(content, match.index); items.push({ type: 'type', name, file: filePath, line: position.line, description: this.parseDescription(docComment), exported: !!exportKeyword, tags: this.parseTags(docComment), }); } } return items; } getPosition(content, index) { const before = content.substring(0, index); const lines = before.split('\n'); const lastLine = lines[lines.length - 1] || ''; return { line: lines.length, column: lastLine.length + 1, }; } parseDescription(docComment) { if (!docComment) return undefined; // Remove * from each line and get first paragraph const lines = docComment .split('\n') .map(line => line.replace(/^\s*\*\s?/, '').trim()) .filter(line => !line.startsWith('@')); const description = lines.join(' ').trim(); return description || undefined; } parseParams(paramsString, docComment) { if (!paramsString.trim()) return []; const params = []; const paramParts = this.splitParams(paramsString); const paramDocs = new Map(); if (docComment) { const paramRegex = /@param\s+(?:\{[^}]+\}\s+)?(\w+)\s+(.+)/g; let match; while ((match = paramRegex.exec(docComment)) !== null) { const pName = match[1]; const pDesc = match[2]; if (pName && pDesc) { paramDocs.set(pName, pDesc.trim()); } } } for (const param of paramParts) { const paramMatch = param.match(/^(\w+)(\?)?\s*(?::\s*(.+?))?(?:\s*=\s*(.+))?$/); if (paramMatch) { const [, paramName, optional, type, defaultValue] = paramMatch; if (paramName) { params.push({ name: paramName, type: type?.trim() || 'any', optional: !!optional || !!defaultValue, defaultValue: defaultValue?.trim(), description: paramDocs.get(paramName), }); } } } return params; } splitParams(paramsString) { const params = []; let current = ''; let depth = 0; for (const char of paramsString) { if (char === '(' || char === '<' || char === '{' || char === '[') { depth++; current += char; } else if (char === ')' || char === '>' || char === '}' || char === ']') { depth--; current += char; } else if (char === ',' && depth === 0) { params.push(current.trim()); current = ''; } else { current += char; } } if (current.trim()) { params.push(current.trim()); } return params; } parseReturnDoc(docComment) { if (!docComment) return undefined; const match = docComment.match(/@returns?\s+(?:\{[^}]+\}\s+)?(.+)/); return match && match[1] ? match[1].trim() : undefined; } parseTags(docComment) { if (!docComment) return []; const tags = []; const tagRegex = /@(\w+)/g; let match; while ((match = tagRegex.exec(docComment)) !== null) { const tag = match[1]; if (tag && !['param', 'returns', 'return', 'example'].includes(tag)) { tags.push(tag); } } return tags; } } // ============================================================================ // Documentation Generator // ============================================================================ export class DocGenerator { workingDir; parser; fileExtensions = ['.ts', '.tsx', '.js', '.jsx']; excludeDirs = ['node_modules', '.git', 'dist', 'build', '__tests__', 'test']; constructor(workingDir) { this.workingDir = workingDir; this.parser = new CodeParser(); } generateProjectDoc() { const modules = this.analyzeModules(); const architecture = this.analyzeArchitecture(modules); const apiSummary = this.generateAPISummary(modules); // Try to read package.json for project info let name = path.basename(this.workingDir); let version = '1.0.0'; let description = ''; const pkgPath = path.join(this.workingDir, 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); name = pkg.name || name; version = pkg.version || version; description = pkg.description || description; } return { name, version, description, modules, architecture, apiSummary, }; } analyzeModules() { const files = this.collectFiles(this.workingDir); const modules = []; for (const file of files) { const items = this.parser.parseFile(file); const content = fs.readFileSync(file, 'utf-8'); const dependencies = this.extractDependencies(content); const relativePath = path.relative(this.workingDir, file); const moduleName = relativePath.replace(/\.[^/.]+$/, '').replace(/\//g, '.'); modules.push({ path: relativePath, name: moduleName, exports: items.filter(i => i.exported), dependencies, }); } return modules; } collectFiles(dir) { const files = []; if (!fs.existsSync(dir)) return files; if (fs.statSync(dir).isFile()) { return this.fileExtensions.some(ext => dir.endsWith(ext)) ? [dir] : []; } const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!this.excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) { files.push(...this.collectFiles(fullPath)); } } else if (entry.isFile()) { if (this.fileExtensions.some(ext => entry.name.endsWith(ext))) { files.push(fullPath); } } } return files; } extractDependencies(content) { const deps = []; const importRegex = /import\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { const dep = match[1]; if (dep) { deps.push(dep); } } return [...new Set(deps)]; } analyzeArchitecture(modules) { // Detect layers based on common patterns const layers = []; const layerPatterns = [ { name: 'Core', patterns: [/^core\//, /^lib\//], description: 'Core business logic and utilities' }, { name: 'UI', patterns: [/^ui\//, /^components\//], description: 'User interface components' }, { name: 'API', patterns: [/^api\//, /^routes\//], description: 'API endpoints and handlers' }, { name: 'Services', patterns: [/^services\//], description: 'Business services' }, { name: 'Models', patterns: [/^models\//, /^types\//], description: 'Data models and types' }, { name: 'Utils', patterns: [/^utils\//, /^helpers\//], description: 'Utility functions' }, ]; for (const layerDef of layerPatterns) { const matchingModules = modules.filter(m => layerDef.patterns.some(p => p.test(m.path))); if (matchingModules.length > 0) { layers.push({ name: layerDef.name, modules: matchingModules.map(m => m.name), description: layerDef.description, }); } } // Build dependency links const dependencies = []; for (const module of modules) { for (const dep of module.dependencies) { if (dep.startsWith('.')) { // Resolve relative import const fromDir = path.dirname(module.path); const resolved = path.normalize(path.join(fromDir, dep)); dependencies.push({ from: module.name, to: resolved.replace(/\.[^/.]+$/, '').replace(/\//g, '.'), type: 'imports', }); } } } return { layers, dependencies }; } generateAPISummary(modules) { let totalExports = 0; let publicFunctions = 0; let publicClasses = 0; let publicInterfaces = 0; let documented = 0; for (const module of modules) { for (const item of module.exports) { totalExports++; if (item.description) documented++; switch (item.type) { case 'function': publicFunctions++; break; case 'class': publicClasses++; break; case 'interface': publicInterfaces++; break; } } } return { totalExports, publicFunctions, publicClasses, publicInterfaces, documentedPercent: totalExports > 0 ? Math.round((documented / totalExports) * 100) : 0, }; } generateMarkdown(project) { const lines = []; // Header lines.push(`# ${project.name}`); lines.push(''); if (project.description) { lines.push(project.description); lines.push(''); } lines.push(`**Version:** ${project.version}`); lines.push(''); // Table of Contents lines.push('## Table of Contents'); lines.push(''); lines.push('- [API Summary](#api-summary)'); lines.push('- [Architecture](#architecture)'); lines.push('- [Modules](#modules)'); lines.push(''); // API Summary lines.push('## API Summary'); lines.push(''); lines.push('| Metric | Count |'); lines.push('|--------|-------|'); lines.push(`| Total Exports | ${project.apiSummary.totalExports} |`); lines.push(`| Public Functions | ${project.apiSummary.publicFunctions} |`); lines.push(`| Public Classes | ${project.apiSummary.publicClasses} |`); lines.push(`| Public Interfaces | ${project.apiSummary.publicInterfaces} |`); lines.push(`| Documentation Coverage | ${project.apiSummary.documentedPercent}% |`); lines.push(''); // Architecture if (project.architecture.layers.length > 0) { lines.push('## Architecture'); lines.push(''); lines.push('### Layers'); lines.push(''); for (const layer of project.architecture.layers) { lines.push(`#### ${layer.name}`); lines.push(''); lines.push(layer.description); lines.push(''); lines.push('Modules:'); for (const mod of layer.modules.slice(0, 10)) { lines.push(`- \`${mod}\``); } if (layer.modules.length > 10) { lines.push(`- ... and ${layer.modules.length - 10} more`); } lines.push(''); } } // Modules lines.push('## Modules'); lines.push(''); for (const module of project.modules.filter(m => m.exports.length > 0)) { lines.push(`### ${module.name}`); lines.push(''); lines.push(`**File:** \`${module.path}\``); lines.push(''); if (module.exports.length > 0) { lines.push('#### Exports'); lines.push(''); for (const item of module.exports) { const icon = { function: '⚡', class: '📦', interface: '📋', type: '🏷️', variable: '📌', module: '📁', }[item.type]; lines.push(`##### ${icon} ${item.name}`); lines.push(''); if (item.signature) { lines.push('```typescript'); lines.push(item.signature); lines.push('```'); lines.push(''); } if (item.description) { lines.push(item.description); lines.push(''); } if (item.params && item.params.length > 0) { lines.push('**Parameters:**'); lines.push(''); for (const param of item.params) { const optional = param.optional ? '?' : ''; const defaultVal = param.defaultValue ? ` = ${param.defaultValue}` : ''; lines.push(`- \`${param.name}${optional}: ${param.type}${defaultVal}\`${param.description ? ` - ${param.description}` : ''}`); } lines.push(''); } if (item.returns) { lines.push(`**Returns:** \`${item.returns.type}\`${item.returns.description ? ` - ${item.returns.description}` : ''}`); lines.push(''); } } } } return lines.join('\n'); } generateAPIReference(project) { const lines = []; lines.push('# API Reference'); lines.push(''); // Group by type const functions = []; const classes = []; const interfaces = []; const types = []; for (const module of project.modules) { for (const item of module.exports) { switch (item.type) { case 'function': functions.push(item); break; case 'class': classes.push(item); break; case 'interface': interfaces.push(item); break; case 'type': types.push(item); break; } } } // Functions if (functions.length > 0) { lines.push('## Functions'); lines.push(''); lines.push('| Function | Description |'); lines.push('|----------|-------------|'); for (const fn of functions) { const desc = fn.description?.substring(0, 60) || '-'; lines.push(`| \`${fn.name}\` | ${desc} |`); } lines.push(''); } // Classes if (classes.length > 0) { lines.push('## Classes'); lines.push(''); lines.push('| Class | Description |'); lines.push('|-------|-------------|'); for (const cls of classes) { const desc = cls.description?.substring(0, 60) || '-'; lines.push(`| \`${cls.name}\` | ${desc} |`); } lines.push(''); } // Interfaces if (interfaces.length > 0) { lines.push('## Interfaces'); lines.push(''); lines.push('| Interface | Description |'); lines.push('|-----------|-------------|'); for (const iface of interfaces) { const desc = iface.description?.substring(0, 60) || '-'; lines.push(`| \`${iface.name}\` | ${desc} |`); } lines.push(''); } // Types if (types.length > 0) { lines.push('## Types'); lines.push(''); lines.push('| Type | Description |'); lines.push('|------|-------------|'); for (const type of types) { const desc = type.description?.substring(0, 60) || '-'; lines.push(`| \`${type.name}\` | ${desc} |`); } lines.push(''); } return lines.join('\n'); } } // ============================================================================ // Exports // ============================================================================ export default DocGenerator; //# sourceMappingURL=docGenerator.js.map