import { readdir, stat } from "node:fs/promises"; import { extname, join } from "node:path"; export interface FileModification { path: string; mtime: Date; size: number; extension: string; } // Directories to skip during file scanning const SKIP_DIRS = new Set([ "node_modules", ".git", "dist", "build", ".next", "target", "vendor", "__pycache__", ".venv", "venv", ".cache", ".turbo", ".idea", ".vscode", ]); // File extensions to include (source code and documents) const INCLUDE_EXTENSIONS = new Set([ // Source code ".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".swift", ".kt", ".scala", ".vue", ".svelte", // Config/data ".json", ".yaml", ".yml", ".toml", ".xml", ".sql", // Documents ".md", ".txt", ".rst", // Styles ".css", ".scss", ".sass", ".less", // Templates ".html", ".htm", ".ejs", ".hbs", ]); /** * Find files modified within a date range (returns all at once - legacy) */ export async function findModifiedFiles( rootPath: string, options: { since?: Date; until?: Date; maxDepth?: number; maxFiles?: number; } = {}, ): Promise { const files: FileModification[] = []; for await (const file of findModifiedFilesStreaming(rootPath, options)) { files.push(file); } return files; } /** * Find files modified within a date range (streaming via async generator) */ export async function* findModifiedFilesStreaming( rootPath: string, options: { since?: Date; until?: Date; maxDepth?: number; maxFiles?: number; } = {}, ): AsyncGenerator { const { since, until, maxDepth = 6, maxFiles = 10000 } = options; let count = 0; async function* scanDir( dir: string, depth: number, ): AsyncGenerator { if (depth > maxDepth || count >= maxFiles) return; try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (count >= maxFiles) break; const fullPath = join(dir, entry.name); if (entry.isDirectory()) { if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) { continue; } yield* scanDir(fullPath, depth + 1); } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase(); if (!INCLUDE_EXTENSIONS.has(ext)) continue; try { const stats = await stat(fullPath); const mtime = stats.mtime; if (since && mtime < since) continue; if (until && mtime > until) continue; count++; yield { path: fullPath, mtime, size: stats.size, extension: ext, }; } catch { // File may have been deleted or permission denied } } } } catch { // Permission denied or other error } } yield* scanDir(rootPath, 0); } /** * Group file modifications by date */ export function groupFilesByDate( files: FileModification[], ): Map { const byDate = new Map(); for (const file of files) { const dateKey = file.mtime.toISOString().split("T")[0]; // YYYY-MM-DD const existing = byDate.get(dateKey) || []; existing.push(file); byDate.set(dateKey, existing); } return byDate; }