UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

884 lines (883 loc) 35.9 kB
/** * ModuleService — 多语言统一模块扫描服务 * * 通过 DiscovererRegistry 自动检测项目类型, * 统一 SPM / Node / Go / JVM / Python / Generic 等语言的模块扫描、依赖分析、AI 提取管线。 * 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。 */ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { basename as _pathBasename, extname as _pathExtname, isAbsolute as _pathIsAbsolute, join as _pathJoin, relative, } from 'node:path'; import { getDiscovererRegistry } from '../../core/discovery/index.js'; import { inferLang } from '../../external/mcp/handlers/LanguageExtensions.js'; import Logger from '../../infrastructure/logging/Logger.js'; /** 全局排除目录 */ const SCAN_EXCLUDE_DIRS = new Set([ 'node_modules', '.git', 'dist', 'build', '.next', 'Pods', 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'venv', 'target', '.gradle', '.idea', 'out', 'coverage', '.cache', '.tox', '.mypy_cache', '.pytest_cache', // DEFAULT_KNOWLEDGE_BASE_DIR — 知识库目录排除(与 ProjectMarkers.ts 同步) 'AutoSnippet', ]); /** 源码文件扩展名 */ const SOURCE_CODE_EXTS = new Set([ '.swift', '.m', '.mm', '.h', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.py', '.java', '.kt', '.kts', '.go', '.rs', '.rb', '.vue', '.svelte', '.c', '.cpp', '.cs', ]); export class ModuleService { #projectRoot; #registry; /** >} */ #activeDiscoverers = []; #loaded = false; #logger; // AI pipeline deps #agentFactory; #container; #qualityScorer; #recipeExtractor; #guardCheckEngine; #violationsStore; constructor(projectRoot, options = {}) { this.#projectRoot = projectRoot; this.#registry = getDiscovererRegistry(); this.#logger = Logger.getInstance(); this.#agentFactory = options.agentFactory || null; this.#container = options.container || null; this.#qualityScorer = options.qualityScorer || null; this.#recipeExtractor = options.recipeExtractor || null; this.#guardCheckEngine = options.guardCheckEngine || null; this.#violationsStore = options.violationsStore || null; } // ═══════════════════════════════════════════════════════ // Lifecycle // ═══════════════════════════════════════════════════════ /** 自动检测项目类型并加载所有匹配的 Discoverer */ async load() { if (this.#loaded) { return; } const matches = await this.#registry.detectAll(this.#projectRoot); this.#activeDiscoverers = []; for (const { discoverer, confidence } of matches) { try { await discoverer.load(this.#projectRoot); this.#activeDiscoverers.push({ discoverer, confidence }); this.#logger.info(`[ModuleService] Loaded discoverer: ${discoverer.displayName} (confidence=${confidence.toFixed(2)})`); } catch (err) { this.#logger.warn(`[ModuleService] Failed to load discoverer ${discoverer.id}: ${err.message}`); } } if (this.#activeDiscoverers.length === 0) { this.#logger.warn('[ModuleService] No discoverer matched, using empty state'); } this.#loaded = true; } /** 清除缓存,重新检测 */ async reload() { this.#loaded = false; this.#activeDiscoverers = []; await this.load(); } /** 确保已加载 */ async #ensureLoaded() { if (!this.#loaded) { await this.load(); } } // ═══════════════════════════════════════════════════════ // Query — 委托到 Discoverer // ═══════════════════════════════════════════════════════ /** 列出所有模块/Target(合并所有 Discoverer 的结果) */ async listTargets() { await this.#ensureLoaded(); const allTargets = []; const seenNames = new Set(); let hasRealDiscovererTargets = false; // 第一遍:加载非 generic 的 Discoverer(真实项目结构识别器) for (const { discoverer } of this.#activeDiscoverers) { if (discoverer.id === 'generic') { continue; } try { const targets = await discoverer.listTargets(); for (const t of targets) { const key = `${discoverer.id}::${t.name}`; if (seenNames.has(key)) { continue; } seenNames.add(key); allTargets.push(this.#normalizeTarget(t, discoverer)); hasRealDiscovererTargets = true; } } catch (err) { this.#logger.warn(`[ModuleService] listTargets failed for ${discoverer.id}: ${err.message}`); } } // 第二遍:仅当没有真实 Discoverer 产出 target 时,才加载 GenericDiscoverer 的结果(兜底) if (!hasRealDiscovererTargets) { for (const { discoverer } of this.#activeDiscoverers) { if (discoverer.id !== 'generic') { continue; } try { const targets = await discoverer.listTargets(); for (const t of targets) { const key = `${discoverer.id}::${t.name}`; if (seenNames.has(key)) { continue; } seenNames.add(key); allTargets.push(this.#normalizeTarget(t, discoverer)); } } catch (err) { this.#logger.warn(`[ModuleService] listTargets failed for ${discoverer.id}: ${err.message}`); } } } return allTargets; } /** * 统一 target 格式 — 兼容前端 ModuleTarget 接口 * 各 Discoverer 返回 { name, path, type, language, framework, metadata } * 前端还需要 { packageName, packagePath, targetDir, info } 等扩展字段 */ #normalizeTarget(t, discoverer) { return { ...t, // 兼容字段 — 如果 discoverer 已设置则保留,否则从通用字段推导 packageName: t.packageName || t.metadata?.modulePath || t.name, packagePath: t.packagePath || t.path || '', targetDir: t.targetDir || t.path || '', info: t.info || t.metadata || {}, // discoverer 来源 discovererId: discoverer.id, discovererName: discoverer.displayName, // 确保语言字段始终存在 language: t.language || discoverer.id || 'unknown', }; } /** 获取 Target 的文件列表 */ async getTargetFiles(target) { await this.#ensureLoaded(); const targetObj = typeof target === 'string' ? { name: target } : target; const discovererId = targetObj.discovererId; // 虚拟目录扫描 — 直接收集文件(无需 discoverer) if (discovererId === 'folder-scan' && targetObj.path && existsSync(targetObj.path)) { return this.#collectFolderFiles(targetObj.path); } // 如果指定了 discovererId,直接找对应的 discoverer if (discovererId) { const entry = this.#activeDiscoverers.find((e) => e.discoverer.id === discovererId); if (entry) { return entry.discoverer.getTargetFiles(targetObj); } } // 否则遍历所有 discoverer 找到第一个有该 target 的 for (const { discoverer } of this.#activeDiscoverers) { try { const targets = await discoverer.listTargets(); if (targets.some((t) => t.name === targetObj.name)) { return discoverer.getTargetFiles(targetObj); } } catch { } } // 兜底:如果 target 有 path 属性且目录存在,直接收集 if (targetObj.path && existsSync(targetObj.path)) { this.#logger.info(`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`); return this.#collectFolderFiles(targetObj.path); } return []; } /** * 获取依赖关系图 * @param [options] * @returns [] }>} */ async getDependencyGraph(options = {}) { await this.#ensureLoaded(); // 合并所有 Discoverer 的依赖图 const allNodes = []; const allEdges = []; // 如果有专业 Discoverer(非 generic),则跳过 GenericDiscoverer 的依赖图 // 避免 generic fallback 生成的冗余根节点(如项目名本身)干扰图结构 const hasSpecializedDiscoverer = this.#activeDiscoverers.some(({ discoverer }) => discoverer.id !== 'generic'); for (const { discoverer } of this.#activeDiscoverers) { if (hasSpecializedDiscoverer && discoverer.id === 'generic') { continue; } try { const graph = await discoverer.getDependencyGraph(); for (const _n of graph.nodes || []) { const n = _n; const id = typeof n === 'string' ? n : n.id || _n; allNodes.push({ id: `${discoverer.id}::${id}`, label: typeof n === 'string' ? n : (n.label || n.id), type: (typeof n === 'object' && n.type) || options.level || 'module', discovererId: discoverer.id, ...(typeof n === 'object' && n.fullPath ? { fullPath: n.fullPath } : {}), ...(typeof n === 'object' && n.indirect != null ? { indirect: n.indirect } : {}), }); } for (const e of graph.edges || []) { allEdges.push({ from: `${discoverer.id}::${e.from}`, to: `${discoverer.id}::${e.to}`, type: e.type || 'depends_on', source: discoverer.id, }); } } catch (err) { this.#logger.warn(`[ModuleService] getDependencyGraph failed for ${discoverer.id}: ${err.message}`); } } return { nodes: allNodes, edges: allEdges, projectRoot: this.#projectRoot, generatedAt: new Date().toISOString(), }; } /** 项目信息摘要 */ getProjectInfo() { const discoverers = this.#activeDiscoverers.map((e) => ({ id: e.discoverer.id, name: e.discoverer.displayName, confidence: e.confidence, })); const languages = [...new Set(discoverers.map((d) => d.id).filter((id) => id !== 'generic'))]; const primaryDiscoverer = discoverers[0] || null; return { projectRoot: this.#projectRoot, projectName: _pathBasename(this.#projectRoot) || '', primaryLanguage: primaryDiscoverer ? this.#discovererToLanguage(primaryDiscoverer.id) : 'unknown', discoverers, languages, hasSpm: this.#activeDiscoverers.some((d) => d.discoverer.id === 'spm'), }; } // ═══════════════════════════════════════════════════════ // Scanning — AI Pipeline // ═══════════════════════════════════════════════════════ /** * AI 扫描 Target 发现候选项 * 完整管线: 读文件 → AI 提取 → Header 解析 → 工具增强 */ async scanTarget(target, options = {}) { await this.#ensureLoaded(); const targetName = typeof target === 'string' ? target : String(target?.name ?? ''); const onProgress = options.onProgress; // 1. 获取源文件列表 onProgress?.({ type: 'scan:started', targetName }); const fileList = await this.getTargetFiles(target); if (!fileList || fileList.length === 0) { return { recipes: [], scannedFiles: [], message: `No source files found for module: ${targetName}`, }; } const scannedFilesMeta = fileList.map((f) => { const filePath = typeof f === 'string' ? f : f.path; return { name: _pathBasename(filePath), path: f.relativePath || _pathBasename(filePath) }; }); onProgress?.({ type: 'scan:files-loaded', files: scannedFilesMeta, count: fileList.length }); // 2. 读取文件内容 onProgress?.({ type: 'scan:reading', count: fileList.length }); const files = fileList .map((f) => { const filePath = typeof f === 'string' ? f : f.path; try { return { name: _pathBasename(filePath), path: filePath, relativePath: f.relativePath || _pathBasename(filePath), content: readFileSync(filePath, 'utf8'), }; } catch (err) { this.#logger.warn(`[ModuleService] Failed to read: ${filePath} — ${err.message}`); return null; } }) .filter((f) => f !== null); if (files.length === 0) { return { recipes: [], scannedFiles: [], message: 'All source files unreadable' }; } const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath })); this.#logger.info(`[ModuleService] scanTarget: ${targetName}, ${files.length} files`); // 3. AI 提取 — mock 模式或无 agentFactory 时直接跳过 const aiManager = this.#container?.singletons ?._aiProviderManager; if (!this.#agentFactory || aiManager?.isMock) { return { recipes: [], scannedFiles, noAi: true, message: 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。', }; } onProgress?.({ type: 'scan:ai-extracting', fileCount: files.length, targetName }); let recipes = await this.#aiExtractRecipes(targetName, files); if (!Array.isArray(recipes)) { recipes = []; } // 3.5 moduleName 注入 for (const recipe of recipes) { recipe.moduleName = targetName; } // 4. 工具增强 onProgress?.({ type: 'scan:enriching', recipeCount: recipes.length }); this.#enrichRecipes(recipes); const result = { recipes, scannedFiles }; if (recipes.length === 0) { result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`; } onProgress?.({ type: 'scan:completed', recipeCount: recipes.length, fileCount: scannedFiles.length, }); return result; } /** 全项目扫描 — 遍历所有 Target,AI 提取候选 + Guard 审计 */ async scanProject(options = {}) { await this.#ensureLoaded(); this.#logger.info('[ModuleService] scanProject: starting full-project scan'); // 1. 列出所有 target const allTargets = await this.listTargets(); // 2. 收集所有源文件(去重) const seenPaths = new Set(); const allFiles = []; const MAX_FILES = options.maxFiles || 200; if (allTargets && allTargets.length > 0) { for (const t of allTargets) { try { const fileList = await this.getTargetFiles(t); for (const f of fileList) { const fp = (typeof f === 'string' ? f : f.path); if (seenPaths.has(fp)) { continue; } seenPaths.add(fp); try { const content = readFileSync(fp, 'utf8'); allFiles.push({ name: _pathBasename(fp), path: fp, relativePath: f.relativePath || _pathBasename(fp), content, targetName: t.name, }); } catch { /* unreadable */ } if (allFiles.length >= MAX_FILES) { break; } } } catch (e) { this.#logger.warn(`[ModuleService] scanProject: skipping module ${t.name}: ${e.message}`); } if (allFiles.length >= MAX_FILES) { break; } } } // 如果没有 target 收集到文件,回退到目录扫描 if (allFiles.length === 0) { this.#logger.info('[ModuleService] scanProject: No module targets, falling back to directory scan'); this.#walkProjectForFiles(allFiles, seenPaths, MAX_FILES); } this.#logger.info(`[ModuleService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} modules`); if (allFiles.length === 0) { return { targets: (allTargets || []).map((t) => t.name), recipes: [], guardAudit: null, scannedFiles: [], message: 'No readable source files', }; } const scannedFiles = allFiles.map((f) => ({ name: f.name, path: f.relativePath, targetName: f.targetName, })); // 3. AI 提取 Recipes — mock 模式跳过 const allRecipes = []; const PER_BATCH_TIMEOUT = options.batchTimeout || 90000; const startTime = Date.now(); const TOTAL_TIMEOUT = options.totalTimeout || 540000; let timedOut = false; const scanAiMgr = this.#container?.singletons ?._aiProviderManager; if (this.#agentFactory && !scanAiMgr?.isMock) { const BATCH_SIZE = options.batchSize || 20; for (let i = 0; i < allFiles.length; i += BATCH_SIZE) { if (Date.now() - startTime > TOTAL_TIMEOUT) { this.#logger.warn(`[ModuleService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s`); timedOut = true; break; } const batch = allFiles.slice(i, i + BATCH_SIZE); const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`; try { const recipes = await Promise.race([ this.#aiExtractRecipes(batchLabel, batch), new Promise((_, reject) => setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)), ]); if (Array.isArray(recipes)) { allRecipes.push(...recipes); } } catch (err) { this.#logger.warn(`[ModuleService] scanProject batch ${batchLabel} failed: ${err.message}`); } } this.#enrichRecipes(allRecipes); } // 4. Guard 审计 let guardAudit = null; if (this.#guardCheckEngine) { try { const guardFiles = allFiles.map((f) => ({ path: f.path, content: f.content, })); const engine = this.#guardCheckEngine; guardAudit = engine.auditFiles(guardFiles, { scope: 'project' }); if (this.#violationsStore && guardAudit && guardAudit.files) { const auditFileResults = guardAudit.files; const store = this.#violationsStore; for (const fileResult of auditFileResults) { if (fileResult.violations.length > 0) { store.appendRun({ filePath: fileResult.filePath, violations: fileResult.violations, summary: `Project scan: ${fileResult.summary.errors} errors, ${fileResult.summary.warnings} warnings`, }); } } } } catch (e) { this.#logger.warn(`[ModuleService] Guard audit failed: ${e.message}`); } } this.#logger.info(`[ModuleService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`); return { targets: allTargets.map((t) => t.name), recipes: allRecipes, guardAudit, scannedFiles, partial: timedOut, }; } /** 刷新模块映射(替代 updateDependencyMap) */ async updateModuleMap(options = {}) { // 重新加载 discoverer await this.reload(); const targets = await this.listTargets(); const graph = await this.getDependencyGraph(); return { success: true, message: `Module map updated (${targets.length} modules)`, targets: targets.length, edges: (graph.edges || []).length, projectRoot: this.#projectRoot, }; } // ═══════════════════════════════════════════════════════ // Folder Scanning — 目录浏览与手动扫描 // ═══════════════════════════════════════════════════════ /** * 浏览项目目录结构 — 供前端目录选择器使用 * @param [basePath=''] 相对于项目根目录的起始路径 * @param [maxDepth=2] 最大递归深度 * @returns >>} */ async browseDirectories(basePath = '', maxDepth = 2) { const root = basePath ? _pathJoin(this.#projectRoot, basePath) : this.#projectRoot; if (!existsSync(root)) { return []; } const dirs = []; this.#walkDirsForBrowse(root, dirs, 0, maxDepth); return dirs; } /** * 扫描任意文件夹 — 创建虚拟 Target 并走标准 AI 管线 * 用于 Discoverer 未覆盖的目录(自定义目录名、新语言等) * @param folderPath 相对/绝对路径 * @param [options] scanTarget options (onProgress 等) * @returns >} */ async scanFolder(folderPath, options = {}) { await this.#ensureLoaded(); const absPath = _pathIsAbsolute(folderPath) ? folderPath : _pathJoin(this.#projectRoot, folderPath); if (!existsSync(absPath)) { throw new Error(`目录不存在: ${folderPath}`); } const lang = this.#detectFolderLanguage(absPath); const folderName = _pathBasename(absPath); // 构建虚拟 Target — 兼容 ModuleTarget 接口 const virtualTarget = { name: folderName, path: absPath, packageName: folderName, packagePath: absPath, targetDir: absPath, type: 'directory', language: lang, discovererId: 'folder-scan', discovererName: '目录扫描', info: { source: 'manual-folder-scan', originalPath: folderPath }, isVirtual: true, }; this.#logger.info(`[ModuleService] scanFolder: ${folderPath} (lang=${lang})`); return this.scanTarget(virtualTarget, options); } /** 静态语义标准化 */ static normalizeSemanticFields(recipe) { return recipe; } // ═══════════════════════════════════════════════════════ // Private Helpers // ═══════════════════════════════════════════════════════ /** Discoverer ID → 语言映射 */ #discovererToLanguage(id) { const map = { spm: 'swift', node: 'javascript', go: 'go', jvm: 'java', python: 'python', customConfig: 'swift', generic: 'unknown', }; return map[id] || 'unknown'; } /** * AI 提取 Recipes — 委托 AgentFactory.scanKnowledge * * AgentFactory.scanKnowledge 内部创建 insight Agent, * Agent(LLM) 直接分析代码 + 使用 AST 工具,输出 Recipe JSON。 */ async #aiExtractRecipes(targetName, files) { if (!this.#agentFactory) { return []; } try { const factory = this.#agentFactory; const result = await factory.scanKnowledge({ label: targetName, files, task: 'extract', }); const recipes = (result.recipes || []); if (recipes.length === 0) { this.#logger.info(`[ModuleService] Agent 未产出 recipe (${targetName}, ${files.length} files)`); } else { this.#logger.info(`[ModuleService] Agent 提取 ${recipes.length} recipes (${targetName})`); } return recipes; } catch (err) { if (err.code === 'API_KEY_MISSING' || /API_KEY_MISSING|API.Key.未配置|unregistered callers/i.test(err.message)) { this.#logger.info(`[ModuleService] AI 未启用(未配置 API Key),跳过 AI 提取。`); } else { this.#logger.warn(`[ModuleService] AI extraction failed: ${err.message}`); } return []; } } /** 质量评分 enrichment */ #enrichRecipes(recipes) { for (const recipe of recipes) { if (!recipe.quality && this.#qualityScorer) { try { const scorer = this.#qualityScorer; const scoreResult = scorer.score(recipe); recipe.quality = { completeness: 0, adaptation: 0, documentation: 0, overall: scoreResult.score ?? 0, grade: scoreResult.grade || '', }; } catch (e) { this.#logger.debug(`[ModuleService] QualityScorer failed: ${e.message}`); } } } } /** 目录遍历 — 浏览子目录结构 */ #walkDirsForBrowse(dir, dirs, depth, maxDepth) { if (depth >= maxDepth) { return; } try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) { continue; } if (entry.name.startsWith('.')) { continue; } if (SCAN_EXCLUDE_DIRS.has(entry.name)) { continue; } const fullPath = _pathJoin(dir, entry.name); const relativePath = relative(this.#projectRoot, fullPath); // 递归统计源码文件数(覆盖 Java/Go 等深层包目录结构) const sourceFileCount = this.#countSourceFilesDeep(fullPath, 8); // 快速检测主要语言 const lang = sourceFileCount > 0 ? this.#detectFolderLanguage(fullPath) : 'unknown'; dirs.push({ name: entry.name, path: relativePath, depth, language: lang, sourceFileCount, hasSourceFiles: sourceFileCount > 0, }); this.#walkDirsForBrowse(fullPath, dirs, depth + 1, maxDepth); } } catch { /* skip */ } } /** 递归统计目录下源码文件数(限深度 + 上限 999 防止超大目录卡顿) */ #countSourceFilesDeep(dir, maxDepth, depth = 0) { if (depth >= maxDepth) { return 0; } let count = 0; try { const entries = readdirSync(dir, { withFileTypes: true }); for (const e of entries) { if (e.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(e.name).toLowerCase())) { count++; } else if (e.isDirectory() && !e.name.startsWith('.') && !SCAN_EXCLUDE_DIRS.has(e.name)) { count += this.#countSourceFilesDeep(_pathJoin(dir, e.name), maxDepth, depth + 1); } if (count >= 999) { return count; } } } catch { /* skip */ } return count; } /** 从目录收集源码文件列表 */ #collectFolderFiles(dirPath, maxDepth = 15) { const files = []; this.#walkCollectSourceFiles(dirPath, dirPath, files, 0, maxDepth); return files; } /** 递归收集源码文件 */ #walkCollectSourceFiles(dir, rootDir, files, depth, maxDepth) { if (depth > maxDepth || files.length > 500) { return; } try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.')) { continue; } if (SCAN_EXCLUDE_DIRS.has(entry.name)) { continue; } const fullPath = _pathJoin(dir, entry.name); if (entry.isDirectory()) { this.#walkCollectSourceFiles(fullPath, rootDir, files, depth + 1, maxDepth); } else if (entry.isFile()) { const ext = _pathExtname(entry.name).toLowerCase(); if (SOURCE_CODE_EXTS.has(ext)) { files.push({ name: entry.name, path: fullPath, relativePath: relative(rootDir, fullPath), language: inferLang(entry.name) || 'unknown', }); } } } } catch { /* skip */ } } /** 检测目录主要编程语言 */ #detectFolderLanguage(dirPath) { const langCount = {}; try { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile()) { continue; } const ext = _pathExtname(entry.name).toLowerCase(); if (!SOURCE_CODE_EXTS.has(ext)) { continue; } const lang = inferLang(entry.name); if (lang) { langCount[lang] = (langCount[lang] || 0) + 1; } } } catch { /* skip */ } let maxLang = 'unknown'; let maxCount = 0; for (const [lang, count] of Object.entries(langCount)) { if (count > maxCount) { maxCount = count; maxLang = lang; } } return maxLang; } /** 目录遍历兜底(收集源码文件) */ #walkProjectForFiles(allFiles, seenPaths, maxFiles) { const srcDirs = [ 'Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages', 'cmd', 'internal', 'pkg', ]; const walkDir = (dir, targetName) => { if (allFiles.length >= maxFiles) { return; } let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const ent of entries) { if (allFiles.length >= maxFiles) { break; } if (ent.name.startsWith('.')) { continue; } const fp = _pathJoin(dir, ent.name); if (ent.isDirectory()) { if (SCAN_EXCLUDE_DIRS.has(ent.name)) { continue; } walkDir(fp, targetName); } else if (ent.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(ent.name).toLowerCase())) { if (seenPaths.has(fp)) { continue; } seenPaths.add(fp); try { const st = statSync(fp); if (st.size > 512 * 1024) { continue; } const content = readFileSync(fp, 'utf8'); if (content.split('\n').length < 5) { continue; } allFiles.push({ name: ent.name, path: fp, relativePath: relative(this.#projectRoot, fp), content, targetName, }); } catch { /* unreadable */ } } } }; for (const dir of srcDirs) { const dirPath = _pathJoin(this.#projectRoot, dir); if (existsSync(dirPath)) { walkDir(dirPath, dir); } } if (allFiles.length === 0) { walkDir(this.#projectRoot, 'root'); } } }