Files
everything-claude-code/scripts/codemaps/generate.ts

331 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* scripts/codemaps/generate.ts
*
* Codemap Generator for everything-claude-code (ECC)
*
* Scans the current working directory and generates architectural
* codemap documentation under docs/CODEMAPS/ as specified by the
* doc-updater agent.
*
* Usage:
* npx tsx scripts/codemaps/generate.ts [srcDir]
*
* Output:
* docs/CODEMAPS/INDEX.md
* docs/CODEMAPS/frontend.md
* docs/CODEMAPS/backend.md
* docs/CODEMAPS/database.md
* docs/CODEMAPS/integrations.md
* docs/CODEMAPS/workers.md
*/
import fs from 'fs';
import path from 'path';
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const ROOT = process.cwd();
const SRC_DIR = process.argv[2] ? path.resolve(process.argv[2]) : ROOT;
const OUTPUT_DIR = path.join(ROOT, 'docs', 'CODEMAPS');
const TODAY = new Date().toISOString().split('T')[0];
// Patterns used to classify files into codemap areas
const AREA_PATTERNS: Record<string, RegExp[]> = {
frontend: [
/\/(app|pages|components|hooks|contexts|ui|views|layouts|styles)\//i,
/\.(tsx|jsx|css|scss|sass|less|vue|svelte)$/i,
],
backend: [
/\/(api|routes|controllers|middleware|server|services|handlers)\//i,
/\.(route|controller|handler|middleware|service)\.(ts|js)$/i,
],
database: [
/\/(models|schemas|migrations|prisma|drizzle|db|database|repositories)\//i,
/\.(model|schema|migration|seed)\.(ts|js)$/i,
/prisma\/schema\.prisma$/,
/schema\.sql$/,
],
integrations: [
/\/(integrations?|third-party|external|plugins?|adapters?|connectors?)\//i,
/\.(integration|adapter|connector)\.(ts|js)$/i,
],
workers: [
/\/(workers?|jobs?|queues?|tasks?|cron|background)\//i,
/\.(worker|job|queue|task|cron)\.(ts|js)$/i,
],
};
// ---------------------------------------------------------------------------
// File System Helpers
// ---------------------------------------------------------------------------
/** Recursively collect all files under a directory, skipping common noise dirs. */
function walkDir(dir: string, results: string[] = []): string[] {
const SKIP = new Set([
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
'.turbo', 'coverage', '.cache', '__pycache__', '.venv', 'venv',
]);
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return results;
}
for (const entry of entries) {
if (SKIP.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath, results);
} else if (entry.isFile()) {
results.push(fullPath);
}
}
return results;
}
/** Return path relative to ROOT, always using forward slashes. */
function rel(p: string): string {
return path.relative(ROOT, p).replace(/\\/g, '/');
}
// ---------------------------------------------------------------------------
// Analysis
// ---------------------------------------------------------------------------
interface AreaInfo {
name: string;
files: string[];
entryPoints: string[];
directories: string[];
}
function classifyFiles(allFiles: string[]): Record<string, AreaInfo> {
const areas: Record<string, AreaInfo> = {
frontend: { name: 'Frontend', files: [], entryPoints: [], directories: [] },
backend: { name: 'Backend/API', files: [], entryPoints: [], directories: [] },
database: { name: 'Database', files: [], entryPoints: [], directories: [] },
integrations: { name: 'Integrations', files: [], entryPoints: [], directories: [] },
workers: { name: 'Workers', files: [], entryPoints: [], directories: [] },
};
for (const file of allFiles) {
const relPath = rel(file);
for (const [area, patterns] of Object.entries(AREA_PATTERNS)) {
if (patterns.some((p) => p.test(relPath))) {
areas[area].files.push(relPath);
break;
}
}
}
// Derive unique directories and entry points per area
for (const area of Object.values(areas)) {
const dirs = new Set(area.files.map((f) => path.dirname(f)));
area.directories = [...dirs].sort();
area.entryPoints = area.files
.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f) || /main\.(ts|tsx|js|jsx)$/.test(f))
.slice(0, 10);
}
return areas;
}
/** Count lines in a file (returns 0 on error). */
function lineCount(p: string): number {
try {
const content = fs.readFileSync(p, 'utf8');
return content.split('\n').length;
} catch {
return 0;
}
}
/** Build a simple directory tree ASCII diagram (max 3 levels deep). */
function buildTree(dir: string, prefix = '', depth = 0): string {
if (depth > 2) return '';
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']);
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return '';
}
const dirs = entries.filter((e) => e.isDirectory() && !SKIP.has(e.name));
const files = entries.filter((e) => e.isFile());
let result = '';
const items = [...dirs, ...files];
items.forEach((entry, i) => {
const isLast = i === items.length - 1;
const connector = isLast ? '└── ' : '├── ';
result += `${prefix}${connector}${entry.name}\n`;
if (entry.isDirectory()) {
const newPrefix = prefix + (isLast ? ' ' : '│ ');
result += buildTree(path.join(dir, entry.name), newPrefix, depth + 1);
}
});
return result;
}
// ---------------------------------------------------------------------------
// Markdown Generators
// ---------------------------------------------------------------------------
function generateAreaDoc(areaKey: string, area: AreaInfo, allFiles: string[]): string {
const fileCount = area.files.length;
const totalLines = area.files.reduce((sum, f) => sum + lineCount(path.join(ROOT, f)), 0);
const entrySection = area.entryPoints.length > 0
? area.entryPoints.map((e) => `- \`${e}\``).join('\n')
: '- *(no index/main entry points detected)*';
const dirSection = area.directories.slice(0, 20)
.map((d) => `- \`${d}/\``)
.join('\n') || '- *(no dedicated directories detected)*';
const fileSection = area.files.slice(0, 30)
.map((f) => `| \`${f}\` | ${lineCount(path.join(ROOT, f))} |`)
.join('\n');
const moreFiles = area.files.length > 30
? `\n*...and ${area.files.length - 30} more files*`
: '';
return `# ${area.name} Codemap
**Last Updated:** ${TODAY}
**Total Files:** ${fileCount}
**Total Lines:** ${totalLines}
## Entry Points
${entrySection}
## Architecture
\`\`\`
${area.name} Directory Structure
${dirSection.replace(/- `/g, '').replace(/`\/$/gm, '/')}
\`\`\`
## Key Modules
| File | Lines |
|------|-------|
${fileSection}${moreFiles}
## Data Flow
> Detected from file patterns. Review individual files for detailed data flow.
## External Dependencies
> Run \`npx jsdoc2md src/**/*.ts\` to extract JSDoc and identify external dependencies.
## Related Areas
- [INDEX](./INDEX.md) — Full overview
- [Frontend](./frontend.md)
- [Backend/API](./backend.md)
- [Database](./database.md)
- [Integrations](./integrations.md)
- [Workers](./workers.md)
`;
}
function generateIndex(areas: Record<string, AreaInfo>, allFiles: string[]): string {
const totalFiles = allFiles.length;
const areaRows = Object.entries(areas)
.map(([key, area]) => `| [${area.name}](./${key}.md) | ${area.files.length} files | ${area.directories.slice(0, 3).map((d) => `\`${d}\``).join(', ') || '—'} |`)
.join('\n');
const topLevelTree = buildTree(SRC_DIR);
return `# Codebase Overview — CODEMAPS Index
**Last Updated:** ${TODAY}
**Root:** \`${rel(SRC_DIR) || '.'}\`
**Total Files Scanned:** ${totalFiles}
## Areas
| Area | Size | Key Directories |
|------|------|-----------------|
${areaRows}
## Repository Structure
\`\`\`
${rel(SRC_DIR) || path.basename(SRC_DIR)}/
${topLevelTree}\`\`\`
## How to Regenerate
\`\`\`bash
npx tsx scripts/codemaps/generate.ts # Regenerate codemaps
npx madge --image graph.svg src/ # Dependency graph (requires graphviz)
npx jsdoc2md src/**/*.ts # Extract JSDoc
\`\`\`
## Related Documentation
- [Frontend](./frontend.md) — UI components, pages, hooks
- [Backend/API](./backend.md) — API routes, controllers, middleware
- [Database](./database.md) — Models, schemas, migrations
- [Integrations](./integrations.md) — External services & adapters
- [Workers](./workers.md) — Background jobs, queues, cron tasks
`;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main(): void {
console.log(`[generate.ts] Scanning: ${SRC_DIR}`);
console.log(`[generate.ts] Output: ${OUTPUT_DIR}`);
// Ensure output directory exists
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
// Walk the directory tree
const allFiles = walkDir(SRC_DIR);
console.log(`[generate.ts] Found ${allFiles.length} files`);
// Classify files into areas
const areas = classifyFiles(allFiles);
// Generate INDEX.md
const indexContent = generateIndex(areas, allFiles);
const indexPath = path.join(OUTPUT_DIR, 'INDEX.md');
fs.writeFileSync(indexPath, indexContent, 'utf8');
console.log(`[generate.ts] Written: ${rel(indexPath)}`);
// Generate per-area codemaps
for (const [key, area] of Object.entries(areas)) {
const content = generateAreaDoc(key, area, allFiles);
const outPath = path.join(OUTPUT_DIR, `${key}.md`);
fs.writeFileSync(outPath, content, 'utf8');
console.log(`[generate.ts] Written: ${rel(outPath)} (${area.files.length} files)`);
}
console.log('\n[generate.ts] Done! Codemaps written to docs/CODEMAPS/');
console.log('[generate.ts] Files generated:');
console.log(' docs/CODEMAPS/INDEX.md');
console.log(' docs/CODEMAPS/frontend.md');
console.log(' docs/CODEMAPS/backend.md');
console.log(' docs/CODEMAPS/database.md');
console.log(' docs/CODEMAPS/integrations.md');
console.log(' docs/CODEMAPS/workers.md');
}
main();