| 31 | const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.php', '.rb', '.go']); |
| 32 | |
| 33 | async function walkSource(root: string, maxFiles: number): Promise<{ abs: string; rel: string; content: string; lines: number }[]> { |
| 34 | const out: { abs: string; rel: string; content: string; lines: number }[] = []; |
| 35 | const stack = [root]; |
| 36 | while (stack.length > 0 && out.length < maxFiles) { |
| 37 | const dir = stack.pop()!; |
| 38 | let entries; |
| 39 | try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { continue; } |
| 40 | for (const e of entries) { |
| 41 | if (out.length >= maxFiles) break; |
| 42 | if (e.isDirectory()) { |
| 43 | if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue; |
| 44 | stack.push(path.join(dir, e.name)); |
| 45 | } else if (e.isFile()) { |
| 46 | const ext = path.extname(e.name).toLowerCase(); |
| 47 | if (!SOURCE_EXTS.has(ext)) continue; |
| 48 | const abs = path.join(dir, e.name); |
| 49 | try { |
| 50 | const stat = await fs.stat(abs); |
| 51 | if (stat.size > 2_000_000) continue; |
| 52 | const content = await fs.readFile(abs, 'utf-8'); |
| 53 | out.push({ abs, rel: path.relative(root, abs), content, lines: content.split('\n').length }); |
| 54 | } catch { /* skip */ } |
| 55 | } |
| 56 | } |
| 57 | } |
| 58 | return out; |
| 59 | } |
| 60 | |
| 61 | function categorizeFile(rel: string): string { |
| 62 | const l = rel.toLowerCase(); |