| 71 | ]; |
| 72 | |
| 73 | async function walkDir( |
| 74 | root: string, |
| 75 | maxFiles: number, |
| 76 | ): Promise<{ files: FileStat[]; truncated: boolean }> { |
| 77 | const files: FileStat[] = []; |
| 78 | let truncated = false; |
| 79 | const stack: string[] = [root]; |
| 80 | while (stack.length > 0 && files.length < maxFiles) { |
| 81 | const dir = stack.pop()!; |
| 82 | let entries; |
| 83 | try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { continue; } |
| 84 | for (const entry of entries) { |
| 85 | if (files.length >= maxFiles) { truncated = true; break; } |
| 86 | if (entry.name.startsWith('.') && entry.name !== '.github' && entry.name !== '.gitlab-ci.yml' && entry.name !== '.env.example' && entry.name !== '.editorconfig' && entry.name !== '.eslintrc' && entry.name !== '.prettierrc' && entry.name !== '.gitignore') continue; |
| 87 | const full = path.join(dir, entry.name); |
| 88 | if (entry.isDirectory()) { |
| 89 | if (SKIP_DIRS.has(entry.name)) continue; |
| 90 | stack.push(full); |
| 91 | } else if (entry.isFile()) { |
| 92 | try { |
| 93 | const stat = await fs.stat(full); |
| 94 | if (stat.size > 5_000_000) continue; // skip huge files |
| 95 | const buf = await fs.readFile(full, 'utf-8').catch(() => null); |
| 96 | const lines = buf ? buf.split('\n').length : 0; |
| 97 | files.push({ |
| 98 | path: path.relative(root, full), |
| 99 | size: stat.size, |
| 100 | lines, |
| 101 | }); |
| 102 | } catch { /* unreadable, skip */ } |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | return { files, truncated }; |
| 107 | } |
| 108 | |
| 109 | function detectLanguages(files: FileStat[]): Record<string, { files: number; lines: number }> { |
| 110 | const stats: Record<string, { files: number; lines: number }> = {}; |