/** * TodoScanner - Auto-scan TODO comments from project files (Vibe Upgrade) */ import fs from 'fs'; import path from 'path'; const TODO_PATTERNS = [ /\/\/\s*TODO:?\s*(.+)/gi, // JS/TS: // TODO /#\s*TODO:?\s*(.+)/gi, // Python/Shell: # TODO //gi, // HTML/MD: /\/\*+\s*TODO:?\s*(.+?)\*+\//gi // C-style: /* TODO */ ]; const SCAN_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.md', '.html', '.css', '.json']; /** * Recursively scan directory for TODO comments * @param {string} rootPath - Root directory to scan * @param {number} maxFiles - Max files to scan (to prevent performance issues) * @returns {Array} - Array of { file, line, text } */ export async function scanTodos(rootPath, maxFiles = 100) { const todos = []; let filesScanned = 0; const scan = async (dir) => { if (filesScanned >= maxFiles) return; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (filesScanned >= maxFiles) break; const fullPath = path.join(dir, entry.name); // Skip common ignored directories if (entry.isDirectory()) { if (['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'vendor'].includes(entry.name)) { continue; } await scan(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (SCAN_EXTENSIONS.includes(ext)) { filesScanned++; try { const content = fs.readFileSync(fullPath, 'utf8'); const lines = content.split('\n'); lines.forEach((line, index) => { for (const pattern of TODO_PATTERNS) { // Reset lastIndex for global patterns pattern.lastIndex = 0; const match = pattern.exec(line); if (match && match[1]) { todos.push({ file: path.relative(rootPath, fullPath), line: index + 1, text: match[1].trim() }); } } }); } catch (e) { // Skip unreadable files } } } } } catch (e) { // Skip unreadable directories } }; await scan(rootPath); return todos; } /** * Get formatted TODO display for Sidebar * @param {Array} todos - Array of { file, line, text } * @param {number} limit - Max items to display * @returns {string} - Formatted display string */ export function formatTodoDisplay(todos, limit = 5) { if (!todos || todos.length === 0) { return '📝 No TODOs found'; } const display = todos.slice(0, limit).map(t => { const shortFile = t.file.length > 20 ? '...' + t.file.slice(-17) : t.file; const shortText = t.text.length > 30 ? t.text.slice(0, 27) + '...' : t.text; return `• ${shortFile}:${t.line}\n ${shortText}`; }).join('\n'); const remaining = todos.length > limit ? `\n... and ${todos.length - limit} more` : ''; return display + remaining; } export default { scanTodos, formatTodoDisplay };