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
JavaScript
/**
* 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