vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
142 lines (141 loc) • 5.85 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import logger from '../../../logger.js';
import { UnifiedSecurityConfigManager } from '../../vibe-task-manager/security/unified-security-config.js';
const execAsync = promisify(exec);
export class DependencyCruiserAdapter {
allowedDir;
outputDir;
securityValidator;
cache = new Map();
tempFiles = [];
constructor(allowedDir, outputDir) {
this.allowedDir = allowedDir;
this.outputDir = outputDir;
this.securityValidator = UnifiedSecurityConfigManager.getInstance();
}
async analyzeImports(filePath, options) {
try {
const cruiserOptions = {
baseDir: options.baseDir || path.dirname(filePath),
includeOnly: options.includeOnly,
exclude: options.exclude,
maxDepth: options.maxDepth,
outputFormat: options.outputFormat || 'json',
tsConfig: options.tsConfig
};
const cacheKey = `${filePath}:${JSON.stringify(cruiserOptions)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
if (!this.securityValidator.validatePathSecurity(filePath, { operation: 'read' })) {
logger.warn({ filePath }, 'File path is outside allowed directory');
return [];
}
const tempOutputFile = path.join(this.outputDir, `dependency-cruiser-${Date.now()}.json`);
this.tempFiles.push(tempOutputFile);
const command = this.buildDependencyCruiserCommand(filePath, tempOutputFile, cruiserOptions);
await execAsync(command);
const resultJson = await fs.promises.readFile(tempOutputFile, 'utf8');
const result = JSON.parse(resultJson);
const imports = this.convertToImportInfo(result, filePath);
await fs.promises.unlink(tempOutputFile);
const fileIndex = this.tempFiles.indexOf(tempOutputFile);
if (fileIndex !== -1) {
this.tempFiles.splice(fileIndex, 1);
}
this.cache.set(cacheKey, imports);
return imports;
}
catch (error) {
logger.error({ err: error, filePath }, 'Error analyzing imports with Dependency-Cruiser');
return [];
}
}
buildDependencyCruiserCommand(filePath, outputFile, options) {
const baseCommand = 'npx depcruise';
const outputFormat = options.outputFormat || 'json';
let command = `${baseCommand} --output-type ${outputFormat} --output-to ${outputFile}`;
if (options.includeOnly && options.includeOnly.length > 0) {
command += ` --include-only "${options.includeOnly.join(',')}"`;
}
if (options.exclude && options.exclude.length > 0) {
command += ` --exclude "${options.exclude.join(',')}"`;
}
if (options.maxDepth) {
command += ` --max-depth ${options.maxDepth}`;
}
if (options.tsConfig) {
command += ` --ts-config ${options.tsConfig}`;
}
command += ` "${filePath}"`;
return command;
}
convertToImportInfo(result, filePath) {
const imports = [];
const normalizedFilePath = path.normalize(filePath);
const module = result.modules.find(m => path.normalize(m.source) === normalizedFilePath);
if (!module) {
return imports;
}
for (const dependency of module.dependencies) {
const importedItems = [];
importedItems.push({
name: this.extractNameFromPath(dependency.module),
path: dependency.resolved,
isDefault: false,
isNamespace: false,
nodeText: dependency.module
});
const importInfo = {
path: dependency.resolved,
importedItems,
isDynamic: dependency.dynamic,
isRelative: this.isRelativePath(dependency.module),
isCore: dependency.coreModule,
moduleSystem: dependency.moduleSystem,
metadata: {
dependencyTypes: dependency.dependencyTypes,
exoticallyRequired: dependency.exoticallyRequired
}
};
imports.push(importInfo);
}
return imports;
}
extractNameFromPath(importPath) {
if (this.isRelativePath(importPath)) {
const basename = path.basename(importPath);
const extname = path.extname(basename);
return extname ? basename.slice(0, -extname.length) : basename;
}
const parts = importPath.split('/');
if (parts[0].startsWith('@') && parts.length > 1) {
return `${parts[0]}/${parts[1]}`;
}
return parts[0];
}
isRelativePath(importPath) {
return importPath.startsWith('./') || importPath.startsWith('../');
}
dispose() {
this.cache.clear();
if (this.tempFiles && this.tempFiles.length > 0) {
this.tempFiles.forEach(file => {
try {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
logger.debug({ file }, 'Deleted temporary file during DependencyCruiserAdapter disposal');
}
}
catch (error) {
logger.warn({ file, error }, 'Failed to delete temporary file during DependencyCruiserAdapter disposal');
}
});
this.tempFiles = [];
}
logger.debug('DependencyCruiserAdapter disposed');
}
}