UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

120 lines (119 loc) 6.55 kB
import fs from 'fs/promises'; import path from 'path'; import logger from '../../logger.js'; import { validatePathSecurity, createSecurePath, isPathWithin } from './pathUtils.js'; import { readDirSecure, statSecure } from './fsUtils.js'; import { splitIntoBatches } from './batchProcessor.js'; import { createIncrementalProcessor } from './incrementalProcessor.js'; const MAX_SCAN_DEPTH = 25; export async function collectSourceFiles(rootDir, supportedExtensions, ignoredPatterns, config, returnBatches = false) { const collectedFiles = []; const validationResult = validatePathSecurity(rootDir, config.allowedMappingDirectory); if (!validationResult.isValid) { logger.error(`Security violation: ${validationResult.error}`); return []; } const securePath = createSecurePath(rootDir, config.allowedMappingDirectory); try { await statSecure(securePath, config.allowedMappingDirectory); logger.debug(`Directory exists and is readable: ${securePath}`); } catch (error) { logger.error(`Cannot access directory: ${securePath}. Error: ${error instanceof Error ? error.message : String(error)}`); return []; } logger.debug(`Normalized root directory: ${securePath}`); logger.debug(`Looking for files with extensions: ${supportedExtensions.join(', ')}`); let incrementalProcessor = null; if (config.processing?.incremental) { incrementalProcessor = await createIncrementalProcessor(config); logger.info(`Incremental processing ${incrementalProcessor ? 'enabled' : 'disabled'}`); } const visitedSymlinks = new Set(); async function scanDir(currentPath, currentDepth) { if (currentDepth > MAX_SCAN_DEPTH) { logger.warn(`Reached maximum scan depth of ${MAX_SCAN_DEPTH} at ${currentPath}. Skipping further recursion in this branch.`); return; } if (!isPathWithin(currentPath, config.allowedMappingDirectory)) { logger.warn(`Security boundary violation: ${currentPath} is outside of allowed directory ${config.allowedMappingDirectory}. Skipping.`); return; } try { const entries = await readDirSecure(currentPath, config.allowedMappingDirectory); for (const entry of entries) { const entryPath = path.join(currentPath, entry.name); if (!isPathWithin(entryPath, config.allowedMappingDirectory)) { logger.warn(`Security boundary violation: ${entryPath} is outside of allowed directory ${config.allowedMappingDirectory}. Skipping.`); continue; } const relativeEntryPath = path.relative(securePath, entryPath); if (ignoredPatterns.some(pattern => pattern.test(relativeEntryPath)) || ignoredPatterns.some(pattern => pattern.test(entry.name))) { logger.debug(`Ignoring path: ${entryPath} due to ignore patterns.`); continue; } if (entry.isSymbolicLink()) { try { const realPath = await fs.realpath(entryPath); if (!isPathWithin(realPath, config.allowedMappingDirectory)) { logger.warn(`Security boundary violation: Symlink ${entryPath} resolves to ${realPath} which is outside of allowed directory ${config.allowedMappingDirectory}. Skipping.`); continue; } if (visitedSymlinks.has(realPath)) { logger.warn(`Detected symlink loop or already visited symlink: ${entryPath} -> ${realPath}. Skipping.`); continue; } visitedSymlinks.add(realPath); const targetStats = await statSecure(entryPath, config.allowedMappingDirectory); if (targetStats.isDirectory()) { await scanDir(entryPath, currentDepth + 1); } else if (targetStats.isFile()) { const fileExtension = path.extname(entryPath).toLowerCase(); if (supportedExtensions.includes(fileExtension)) { collectedFiles.push(entryPath); } } } catch (error) { logger.warn(`Error processing symlink ${entryPath}: ${error instanceof Error ? error.message : String(error)}. Skipping.`); } continue; } if (entry.isDirectory()) { await scanDir(entryPath, currentDepth + 1); } else if (entry.isFile()) { const fileExtension = path.extname(entryPath).toLowerCase(); if (supportedExtensions.includes(fileExtension)) { collectedFiles.push(entryPath); } } } } catch (error) { const errDetails = error instanceof Error ? { message: error.message, stack: error.stack } : { errorInfo: String(error) }; logger.warn({ err: errDetails, path: currentPath }, `Could not read directory, skipping.`); } } await scanDir(securePath, 0); const symlinksCount = visitedSymlinks.size; visitedSymlinks.clear(); logger.info(`Collected ${collectedFiles.length} files from ${securePath}. Cleared ${symlinksCount} symlink entries.`); let filesToProcess = collectedFiles; if (incrementalProcessor && collectedFiles.length > 0) { logger.info('Filtering files using incremental processor...'); filesToProcess = await incrementalProcessor.filterChangedFiles(collectedFiles); for (const filePath of filesToProcess) { await incrementalProcessor.updateFileMetadata(filePath); } logger.info(`Incremental processing: ${filesToProcess.length} of ${collectedFiles.length} files need processing`); } if (returnBatches && filesToProcess.length > 0) { const batchSize = config.processing?.batchSize || 100; const batches = splitIntoBatches(filesToProcess, batchSize); logger.info(`Split ${filesToProcess.length} files into ${batches.length} batches (batch size: ${batchSize})`); return batches; } return filesToProcess; }