#!/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 = { 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 { const areas: Record = { 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, 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();