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
JavaScript
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;
}