mcp-repl
Version:
MCP REPL with code execution, semantic code search, and comprehensive ast-grep integration
432 lines (381 loc) • 12.6 kB
JavaScript
import { spawn } from 'child_process';
import * as path from 'node:path';
import { writeFileSync, readFileSync, existsSync } from 'fs';
/**
* Project configuration management for ast-grep
* Handles project initialization and scanning operations
*/
const executeAstGrepCommand = async (args, workingDirectory, timeout = 30000) => {
const startTime = Date.now();
return new Promise((resolve) => {
const childProcess = spawn('ast-grep', args, {
cwd: workingDirectory,
timeout,
env: process.env,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
childProcess.stdout.on('data', (data) => {
stdout += data;
});
childProcess.stderr.on('data', (data) => {
stderr += data;
});
childProcess.on('close', (code) => {
const executionTimeMs = Date.now() - startTime;
resolve({
success: code === 0,
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: code,
executionTimeMs,
command: `ast-grep ${args.join(' ')}`
});
});
childProcess.on('error', (err) => {
resolve({
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime,
command: `ast-grep ${args.join(' ')}`
});
});
});
};
export const astgrepProjectInit = async (options = {}) => {
const {
workingDirectory,
projectType = 'javascript', // javascript, typescript, python, rust, go
includeTests = true,
createRules = true,
ruleCategories = ['security', 'performance', 'style']
} = options;
const configPath = path.join(workingDirectory, 'sgconfig.yml');
const rulesDir = path.join(workingDirectory, '.ast-grep', 'rules');
try {
// Generate project-specific configuration
const config = generateProjectConfig(projectType, includeTests);
// Create configuration file
writeFileSync(configPath, config, 'utf8');
let createdRules = [];
if (createRules) {
// Create rules directory structure
const { mkdirSync } = await import('fs');
mkdirSync(path.dirname(path.join(rulesDir, 'temp')), { recursive: true });
// Generate category-specific rule files
for (const category of ruleCategories) {
const rules = generateCategoryRules(projectType, category);
const rulePath = path.join(rulesDir, `${category}.yml`);
writeFileSync(rulePath, rules, 'utf8');
createdRules.push(rulePath);
}
}
return {
success: true,
configPath,
rulesCreated: createdRules,
projectType,
message: `Initialized ast-grep project for ${projectType}`,
files: {
config: configPath,
rules: createdRules
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
};
export const astgrepProjectScan = async (options = {}) => {
const {
workingDirectory,
scanType = 'comprehensive', // 'quick', 'comprehensive', 'security'
outputFormat = 'summary',
includeMetrics = true
} = options;
const startTime = Date.now();
try {
// Check if project has ast-grep configuration
const configPath = path.join(workingDirectory, 'sgconfig.yml');
const hasConfig = existsSync(configPath);
// Perform different scan types
let scanResults = {};
switch (scanType) {
case 'quick':
scanResults = await performQuickScan(workingDirectory);
break;
case 'comprehensive':
scanResults = await performComprehensiveScan(workingDirectory);
break;
case 'security':
scanResults = await performSecurityScan(workingDirectory);
break;
}
const executionTimeMs = Date.now() - startTime;
return {
success: true,
scanType,
hasConfiguration: hasConfig,
results: scanResults,
executionTimeMs,
...(includeMetrics && {
metrics: {
filesScanned: scanResults.totalFiles || 0,
issuesFound: scanResults.totalIssues || 0,
performance: {
filesPerSecond: Math.round((scanResults.totalFiles || 0) / (executionTimeMs / 1000)),
executionTimeMs
}
}
})
};
} catch (error) {
return {
success: false,
error: error.message,
executionTimeMs: Date.now() - startTime
};
}
};
const generateProjectConfig = (projectType, includeTests) => {
const baseConfig = {
javascript: {
ruleDirs: [".ast-grep/rules"],
testDirs: includeTests ? ["test", "tests", "__tests__", "spec"] : [],
languageGlobs: {
javascript: ["**/*.js", "**/*.jsx", "**/*.mjs"],
typescript: ["**/*.ts", "**/*.tsx"]
}
},
typescript: {
ruleDirs: [".ast-grep/rules"],
testDirs: includeTests ? ["test", "tests", "__tests__", "spec"] : [],
languageGlobs: {
typescript: ["**/*.ts", "**/*.tsx"],
javascript: ["**/*.js", "**/*.jsx"]
}
},
python: {
ruleDirs: [".ast-grep/rules"],
testDirs: includeTests ? ["test", "tests", "test_*"] : [],
languageGlobs: {
python: ["**/*.py"]
}
}
};
const config = baseConfig[projectType] || baseConfig.javascript;
return `# ast-grep project configuration
# Generated for ${projectType} project
ruleDirs:
${config.ruleDirs.map(dir => ` - "${dir}"`).join('\n')}
${config.testDirs.length > 0 ? `testDirs:
${config.testDirs.map(dir => ` - "${dir}"`).join('\n')}` : ''}
languageGlobs:
${Object.entries(config.languageGlobs).map(([lang, globs]) =>
` ${lang}:\n${globs.map(glob => ` - "${glob}"`).join('\n')}`
).join('\n')}
# Ignore patterns
ignore:
- "node_modules/**"
- "dist/**"
- "build/**"
- ".git/**"
- "*.min.js"
`;
};
const generateCategoryRules = (projectType, category) => {
const ruleTemplates = {
security: {
javascript: `# Security rules for JavaScript/TypeScript
rules:
- id: no-eval
message: "Avoid using eval() - security risk"
rule:
pattern: eval($EXPR)
severity: error
- id: no-inner-html
message: "innerHTML assignment may lead to XSS"
rule:
pattern: $OBJ.innerHTML = $UNSAFE
severity: warning
- id: no-exec-without-validation
message: "exec() usage without input validation"
rule:
pattern: $$.exec($INPUT)
severity: error`,
python: `# Security rules for Python
rules:
- id: no-exec
message: "Avoid using exec() - security risk"
rule:
pattern: exec($EXPR)
severity: error
- id: no-eval
message: "Avoid using eval() - security risk"
rule:
pattern: eval($EXPR)
severity: error`
},
performance: {
javascript: `# Performance rules for JavaScript/TypeScript
rules:
- id: avoid-sync-fs
message: "Avoid synchronous filesystem operations"
rule:
any:
- pattern: fs.readFileSync($$$)
- pattern: fs.writeFileSync($$$)
severity: warning
- id: missing-await
message: "Async function without await may indicate missing await"
rule:
pattern: async function $NAME($$$) { return $SYNC_VALUE }
severity: info`,
python: `# Performance rules for Python
rules:
- id: avoid-string-concat-loop
message: "String concatenation in loop is inefficient"
rule:
pattern: |
for $VAR in $ITERABLE:
$STR += $EXPR
severity: warning`
},
style: {
javascript: `# Style rules for JavaScript/TypeScript
rules:
- id: prefer-const
message: "Use const for variables that are never reassigned"
rule:
pattern: let $VAR = $VALUE
severity: hint
- id: no-console-log
message: "Remove console.log statements"
rule:
pattern: console.log($$$)
severity: info`,
python: `# Style rules for Python
rules:
- id: snake-case-functions
message: "Use snake_case for function names"
rule:
pattern: def $CAMELCASE($$$):
where:
$CAMELCASE:
regex: "^[a-z]+[A-Z].*"
severity: hint`
}
};
const templates = ruleTemplates[category];
return templates?.[projectType] || templates?.javascript || `# ${category} rules\nrules: []`;
};
const performQuickScan = async (workingDirectory) => {
// Quick scan focuses on common issues
const commonIssues = [
{ pattern: 'console.log($$$)', message: 'Debug statements found' },
{ pattern: 'eval($$$)', message: 'Security risk: eval usage' },
{ pattern: '$$.innerHTML = $UNSAFE', message: 'Potential XSS risk' }
];
let totalFiles = 0;
let totalIssues = 0;
const issuesByType = {};
for (const issue of commonIssues) {
const args = ['run', '--json=compact', '--pattern', issue.pattern, '.'];
const result = await executeAstGrepCommand(args, workingDirectory);
if (result.success && result.stdout) {
try {
const matches = JSON.parse(result.stdout);
if (Array.isArray(matches)) {
totalIssues += matches.length;
issuesByType[issue.message] = matches.length;
totalFiles += new Set(matches.map(m => m.file)).size;
}
} catch (e) {
// Ignore parse errors in quick scan
}
}
}
return {
totalFiles,
totalIssues,
issuesByType,
scanType: 'quick'
};
};
const performComprehensiveScan = async (workingDirectory) => {
// Check if custom rules exist
const rulesDir = path.join(workingDirectory, '.ast-grep', 'rules');
const hasCustomRules = existsSync(rulesDir);
let results = {
totalFiles: 0,
totalIssues: 0,
ruleCategories: {},
hasCustomRules,
scanType: 'comprehensive'
};
if (hasCustomRules) {
// Scan using custom rules
const args = ['scan', '--json', '.'];
const result = await executeAstGrepCommand(args, workingDirectory);
if (result.success && result.stdout) {
try {
const scanResults = JSON.parse(result.stdout);
results.totalIssues = scanResults.length || 0;
results.totalFiles = new Set((scanResults || []).map(r => r.file)).size;
} catch (e) {
// Fallback to pattern-based scan
results = await performQuickScan(workingDirectory);
}
}
} else {
// Fallback to quick scan
results = await performQuickScan(workingDirectory);
}
return results;
};
const performSecurityScan = async (workingDirectory) => {
const securityPatterns = [
{ pattern: 'eval($$$)', severity: 'high', message: 'Code injection risk: eval usage' },
{ pattern: 'new Function($$$)', severity: 'high', message: 'Code injection risk: Function constructor' },
{ pattern: '$$.innerHTML = $UNSAFE', severity: 'medium', message: 'XSS risk: innerHTML assignment' },
{ pattern: '$$.exec($INPUT)', severity: 'medium', message: 'Command injection risk: exec usage' },
{ pattern: 'document.write($$$)', severity: 'medium', message: 'XSS risk: document.write' }
];
let totalFiles = 0;
let totalIssues = 0;
const issuesBySeverity = { high: 0, medium: 0, low: 0 };
const detailedFindings = [];
for (const pattern of securityPatterns) {
const args = ['run', '--json=compact', '--pattern', pattern.pattern, '.'];
const result = await executeAstGrepCommand(args, workingDirectory);
if (result.success && result.stdout) {
try {
const matches = JSON.parse(result.stdout);
if (Array.isArray(matches)) {
totalIssues += matches.length;
issuesBySeverity[pattern.severity] += matches.length;
totalFiles += new Set(matches.map(m => m.file)).size;
detailedFindings.push({
pattern: pattern.pattern,
severity: pattern.severity,
message: pattern.message,
occurrences: matches.length,
files: [...new Set(matches.map(m => m.file))]
});
}
} catch (e) {
// Continue with other patterns
}
}
}
return {
totalFiles,
totalIssues,
issuesBySeverity,
detailedFindings,
scanType: 'security'
};
};