repository-analyzer
Version:
Transform code repositories into strategic intelligence using extensible AI agents. Analyze technical debt, business value, and deployment readiness automatically.
623 lines ⢠27.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentOrchestrator = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const execa_1 = require("execa");
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const AgentDiscovery_1 = require("./AgentDiscovery");
class AgentOrchestrator {
constructor() {
this.discovery = new AgentDiscovery_1.AgentDiscovery();
this.tempDir = path.join(process.cwd(), '.temp-analysis');
}
async executeAnalysis(options) {
const { outputDir, repositoryPath, selectedAgents, parallelJobs = 1, verbose = false, claudeCommand = 'claude' } = options;
// Discover available agents
const allAgents = await this.discovery.discoverAgents();
// Determine which agents to run
const agentsToRun = selectedAgents
? this.filterSelectedAgents(allAgents, selectedAgents)
: this.getDefaultAgents(allAgents);
if (agentsToRun.length === 0) {
throw new Error('No agents selected for execution');
}
// Validate dependencies
const validation = await this.discovery.validateAgentDependencies(agentsToRun.map(a => a.id));
if (!validation.valid) {
throw new Error(`Missing dependencies: ${validation.missing.join(', ')}`);
}
// Create execution plan
const executionPlan = await this.discovery.createAgentExecutionPlan(agentsToRun.map(a => a.id));
// Prepare environment
await this.prepareEnvironment(outputDir, repositoryPath);
// Execute agents in batches
const results = [];
const spinner = (0, ora_1.default)('Preparing analysis...').start();
try {
for (let batchIndex = 0; batchIndex < executionPlan.length; batchIndex++) {
const batch = executionPlan[batchIndex];
spinner.text = `Executing batch ${batchIndex + 1}/${executionPlan.length}`;
const batchResults = await this.executeBatch(batch, allAgents, {
outputDir,
repositoryPath,
claudeCommand,
verbose,
parallelJobs,
previousResults: results
});
results.push(...batchResults);
// Check if any critical agents failed
const failedCritical = batchResults.find(r => !r.success && this.isCriticalAgent(r.agentId));
if (failedCritical) {
spinner.fail(`Critical agent ${failedCritical.agentId} failed`);
throw new Error(`Analysis stopped due to critical failure in ${failedCritical.agentId}`);
}
}
spinner.succeed(`Analysis complete! Executed ${results.length} agents`);
return results;
}
catch (error) {
spinner.fail('Analysis failed');
throw error;
}
finally {
await this.cleanup();
}
}
filterSelectedAgents(allAgents, selectedIds) {
const agentMap = new Map(allAgents.map(agent => [agent.id, agent]));
const filtered = [];
for (const id of selectedIds) {
const agent = agentMap.get(id);
if (agent) {
filtered.push(agent);
}
else {
console.warn(chalk_1.default.yellow(`Warning: Agent '${id}' not found`));
}
}
return filtered.sort((a, b) => a.order - b.order);
}
getDefaultAgents(allAgents) {
// Use all core agents by default
return allAgents
.filter(agent => agent.category === 'core')
.sort((a, b) => a.order - b.order);
}
async prepareEnvironment(outputDir, repositoryPath) {
// Create output directory
await fs.ensureDir(outputDir);
// Create temp directory for intermediate files
await fs.ensureDir(this.tempDir);
// Prepare repository information
const repoInfo = await this.gatherRepositoryInfo(repositoryPath);
const repoInfoFile = path.join(this.tempDir, 'repository-info.json');
await fs.writeJson(repoInfoFile, repoInfo);
}
async gatherRepositoryInfo(repositoryPath) {
const info = {
path: repositoryPath,
name: path.basename(repositoryPath),
timestamp: new Date().toISOString()
};
try {
// Check if it's a git repository
const gitDir = path.join(repositoryPath, '.git');
if (await fs.pathExists(gitDir)) {
info.isGitRepo = true;
// Get git info
try {
const gitLog = await (0, execa_1.execa)('git', ['log', '-1', '--format=%H|%s|%ai'], { cwd: repositoryPath });
const [hash, subject, date] = gitLog.stdout.split('|');
info.git = { latestCommit: hash, latestMessage: subject, latestDate: date };
}
catch {
// Ignore git errors
}
}
// Read key files
const keyFiles = ['package.json', 'requirements.txt', 'Cargo.toml', 'go.mod', 'pom.xml', 'README.md'];
info.keyFiles = {};
for (const file of keyFiles) {
const filePath = path.join(repositoryPath, file);
if (await fs.pathExists(filePath)) {
try {
const content = await fs.readFile(filePath, 'utf8');
info.keyFiles[file] = file.endsWith('.json') ? JSON.parse(content) : content;
}
catch {
// Ignore file read errors
}
}
}
// Get directory structure (first 2 levels)
info.structure = await this.getDirectoryStructure(repositoryPath, 2);
}
catch (error) {
console.warn(chalk_1.default.yellow(`Warning: Could not gather repository info: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
return info;
}
async getDirectoryStructure(dirPath, maxDepth, currentDepth = 0) {
if (currentDepth >= maxDepth)
return null;
try {
const items = await fs.readdir(dirPath);
const structure = {};
for (const item of items.slice(0, 20)) { // Limit to first 20 items
if (item.startsWith('.') && item !== '.git')
continue;
const itemPath = path.join(dirPath, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
structure[item + '/'] = await this.getDirectoryStructure(itemPath, maxDepth, currentDepth + 1);
}
else {
structure[item] = stat.size;
}
}
return structure;
}
catch {
return null;
}
}
async executeBatch(agentIds, allAgents, options) {
const agentMap = new Map(allAgents.map(agent => [agent.id, agent]));
const batchAgents = agentIds.map(id => agentMap.get(id));
// Execute agents in parallel (up to parallelJobs limit)
const promises = batchAgents.map(agent => this.executeAgent(agent, options));
const results = await Promise.allSettled(promises);
return results.map((result, index) => {
const agentId = batchAgents[index].id;
if (result.status === 'fulfilled') {
return result.value;
}
else {
return {
agentId,
success: false,
executionTime: 0,
error: result.reason.message || 'Unknown error'
};
}
});
}
async executeAgent(agent, options) {
const startTime = Date.now();
try {
if (options.verbose) {
console.log(chalk_1.default.blue(`\nš¤ Executing agent: ${agent.name}`));
}
// Prepare agent variables
const variables = await this.prepareAgentVariables(agent, options);
// Read agent prompt file
const agentPrompt = await fs.readFile(agent.filePath, 'utf8');
// Create enhanced prompt with variables and context
const enhancedPrompt = await this.createEnhancedPrompt(agentPrompt, variables, options.repositoryPath);
// Prepare Claude command with --print flag for non-interactive execution
const claudeArgs = [
'--print',
'--output-format', 'json',
'--add-dir', options.repositoryPath
];
if (options.verbose) {
console.log(chalk_1.default.gray(`Command: ${options.claudeCommand} ${claudeArgs.join(' ')}`));
console.log(chalk_1.default.gray(`Prompt length: ${enhancedPrompt.length} characters`));
}
// Execute Claude with prompt as stdin
const result = await (0, execa_1.execa)(options.claudeCommand, claudeArgs, {
input: enhancedPrompt,
timeout: 5 * 60 * 1000, // 5 minute timeout
stdio: 'pipe' // Always capture stdout, stderr can be inherited if needed
});
const executionTime = Date.now() - startTime;
// Parse and save output
const outputFile = path.join(options.outputDir, `${agent.id}_output.json`);
try {
// Parse the JSON response from Claude
if (options.verbose) {
console.log(chalk_1.default.gray(`Raw stdout length: ${result.stdout?.length || 0}`));
console.log(chalk_1.default.gray(`First 200 chars: ${result.stdout?.substring(0, 200) || 'undefined'}`));
}
const claudeResponse = JSON.parse(result.stdout);
// Save the raw Claude response (with metadata)
const rawOutputFile = path.join(options.outputDir, `${agent.id}_raw_output.json`);
await fs.writeJson(rawOutputFile, claudeResponse, { spaces: 2 });
// Extract the actual analysis content from Claude's response
let analysisContent;
let cleanJsonContent;
if (claudeResponse.result && typeof claudeResponse.result === 'string') {
// Extract JSON from the result text
cleanJsonContent = this.extractJsonFromText(claudeResponse.result);
analysisContent = cleanJsonContent || claudeResponse.result;
}
else if (claudeResponse.content) {
analysisContent = claudeResponse.content;
cleanJsonContent = claudeResponse.content;
}
else {
analysisContent = claudeResponse;
cleanJsonContent = claudeResponse;
}
// Save the clean analysis JSON
if (cleanJsonContent && typeof cleanJsonContent === 'object') {
await fs.writeJson(outputFile, cleanJsonContent, { spaces: 2 });
// Generate and save markdown report
const markdownFile = path.join(options.outputDir, `${agent.id}_report.md`);
const markdownContent = this.generateMarkdownReport(agent, cleanJsonContent);
await fs.writeFile(markdownFile, markdownContent);
}
else {
// Fallback: save as text if we can't parse JSON
const textFile = path.join(options.outputDir, `${agent.id}_output.txt`);
await fs.writeFile(textFile, typeof analysisContent === 'string' ? analysisContent : JSON.stringify(analysisContent, null, 2));
}
if (options.verbose) {
console.log(chalk_1.default.green(`ā
${agent.name} completed (${executionTime}ms)`));
}
return {
agentId: agent.id,
success: true,
outputFile,
executionTime
};
}
catch (parseError) {
// Fallback: if JSON parsing fails, save raw output and try to extract JSON
const fallbackContent = this.extractJsonFromText(result.stdout);
if (fallbackContent) {
await fs.writeJson(outputFile, fallbackContent, { spaces: 2 });
if (options.verbose) {
console.log(chalk_1.default.yellow(`ā ļø ${agent.name} completed with fallback parsing (${executionTime}ms)`));
}
return {
agentId: agent.id,
success: true,
outputFile,
executionTime
};
}
else {
throw new Error(`Could not parse JSON output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
}
}
}
catch (error) {
const executionTime = Date.now() - startTime;
if (options.verbose) {
console.log(chalk_1.default.red(`ā ${agent.name} failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
}
return {
agentId: agent.id,
success: false,
executionTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async prepareAgentVariables(agent, options) {
const variables = {};
// Standard variables
variables.REPO_PATH = options.repositoryPath;
variables.OUTPUT_DIR = options.outputDir;
variables.AGENT_ID = agent.id;
variables.TIMESTAMP = new Date().toISOString();
// Repository info
const repoInfoFile = path.join(this.tempDir, 'repository-info.json');
if (await fs.pathExists(repoInfoFile)) {
const repoInfo = await fs.readJson(repoInfoFile);
variables.REPO_INFO = JSON.stringify(repoInfo);
// Add specific common variables
if (repoInfo.keyFiles?.['package.json']) {
variables.PACKAGE_JSON = JSON.stringify(repoInfo.keyFiles['package.json']);
}
if (repoInfo.keyFiles?.['README.md']) {
variables.README_CONTENT = repoInfo.keyFiles['README.md'];
}
}
// Previous agent outputs
for (const dependency of agent.dependencies) {
const depResult = options.previousResults.find(r => r.agentId === dependency && r.success);
if (depResult && depResult.outputFile) {
try {
const depOutput = await fs.readJson(depResult.outputFile);
variables[`${dependency.toUpperCase()}_OUTPUT`] = JSON.stringify(depOutput);
}
catch {
console.warn(chalk_1.default.yellow(`Warning: Could not read output from ${dependency}`));
}
}
}
return variables;
}
isCriticalAgent(agentId) {
// Core agents that should not fail
const criticalAgents = ['scanner', 'scan', 'discovery'];
return criticalAgents.includes(agentId);
}
async createEnhancedPrompt(agentPrompt, variables, repositoryPath) {
// Replace variables in the prompt
let enhancedPrompt = agentPrompt;
for (const [key, value] of Object.entries(variables)) {
const placeholder = `{{${key}}}`;
enhancedPrompt = enhancedPrompt.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
}
// Add context about the repository
const contextSection = `\n\n## Repository Context\n\n`;
const repoInfo = JSON.parse(variables.REPO_INFO || '{}');
enhancedPrompt += contextSection;
enhancedPrompt += `**Repository Path:** ${repositoryPath}\n`;
enhancedPrompt += `**Repository Name:** ${repoInfo.name || path.basename(repositoryPath)}\n`;
if (repoInfo.git) {
enhancedPrompt += `**Latest Commit:** ${repoInfo.git.latestCommit}\n`;
enhancedPrompt += `**Commit Message:** ${repoInfo.git.latestMessage}\n`;
}
enhancedPrompt += `\n**Output Format:** Return ONLY valid JSON matching the schema specified in the prompt. Do not include any markdown formatting, explanations, or additional text.\n`;
return enhancedPrompt;
}
extractJsonFromText(text) {
// Add null/undefined check
if (!text || typeof text !== 'string') {
return null;
}
// Try to extract JSON from text that might contain other content
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0]);
}
catch {
// Try to find JSON block markers
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
if (codeBlockMatch) {
try {
return JSON.parse(codeBlockMatch[1].trim());
}
catch {
return null;
}
}
}
}
return null;
}
generateMarkdownReport(agent, data) {
const timestamp = new Date().toISOString();
let markdown = `# ${agent.name} Report\n\n`;
markdown += `**Generated:** ${timestamp}\n`;
markdown += `**Agent ID:** ${agent.id}\n\n`;
if (agent.description) {
markdown += `## Overview\n${agent.description}\n\n`;
}
// Handle different agent types with specific formatting
if (agent.id.includes('scanner')) {
return this.generateScannerMarkdown(data);
}
else if (agent.id.includes('technical')) {
return this.generateTechnicalMarkdown(data);
}
else if (agent.id.includes('business')) {
return this.generateBusinessMarkdown(data);
}
else if (agent.id.includes('report')) {
return this.generateReportMarkdown(data);
}
// Generic fallback
return this.generateGenericMarkdown(agent, data);
}
generateScannerMarkdown(data) {
let md = `# Repository Scanner Analysis\n\n`;
if (data.repository_name) {
md += `## ${data.repository_name}\n\n`;
}
if (data.project_type) {
md += `**Project Type:** ${data.project_type}\n`;
}
if (data.primary_language) {
md += `**Primary Language:** ${data.primary_language}\n`;
}
if (data.architecture_pattern) {
md += `**Architecture:** ${data.architecture_pattern}\n\n`;
}
if (data.language_breakdown) {
md += `## Language Breakdown\n\n`;
Object.entries(data.language_breakdown).forEach(([lang, percent]) => {
md += `- **${lang}:** ${percent}%\n`;
});
md += `\n`;
}
if (data.technology_stack) {
md += `## Technology Stack\n\n`;
const stack = data.technology_stack;
if (stack.frameworks?.length)
md += `**Frameworks:** ${stack.frameworks.join(', ')}\n`;
if (stack.build_tools?.length)
md += `**Build Tools:** ${stack.build_tools.join(', ')}\n`;
if (stack.databases?.length)
md += `**Databases:** ${stack.databases.join(', ')}\n`;
if (stack.infrastructure?.length)
md += `**Infrastructure:** ${stack.infrastructure.join(', ')}\n`;
md += `\n`;
}
if (data.metrics) {
md += `## Metrics\n\n`;
const metrics = data.metrics;
if (metrics.estimated_loc)
md += `- **Lines of Code:** ~${metrics.estimated_loc.toLocaleString()}\n`;
if (metrics.file_count)
md += `- **Files:** ${metrics.file_count}\n`;
if (metrics.directory_count)
md += `- **Directories:** ${metrics.directory_count}\n`;
md += `\n`;
}
if (data.notable_features?.length) {
md += `## Notable Features\n\n`;
data.notable_features.forEach((feature) => {
md += `- ${feature.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}\n`;
});
md += `\n`;
}
if (data.potential_issues?.length) {
md += `## Potential Issues\n\n`;
data.potential_issues.forEach((issue) => {
md += `- ${issue.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}\n`;
});
md += `\n`;
}
if (data.confidence_score) {
md += `## Confidence Score\n\n`;
md += `${(data.confidence_score * 100).toFixed(0)}% confidence in this analysis\n\n`;
}
return md;
}
generateTechnicalMarkdown(data) {
let md = `# Technical Analysis Report\n\n`;
// Add technical-specific formatting
if (data.code_quality) {
md += `## Code Quality\n\n`;
// Format code quality metrics
}
if (data.security_analysis) {
md += `## Security Analysis\n\n`;
// Format security findings
}
return md + this.formatJsonAsMarkdown(data);
}
generateBusinessMarkdown(data) {
let md = `# Business Analysis Report\n\n`;
if (data.business_value) {
md += `## Business Value\n\n`;
// Format business value assessment
}
return md + this.formatJsonAsMarkdown(data);
}
generateReportMarkdown(data) {
let md = `# Comprehensive Repository Report\n\n`;
return md + this.formatJsonAsMarkdown(data);
}
generateGenericMarkdown(agent, data) {
let md = `# ${agent.name} Report\n\n`;
md += `**Agent:** ${agent.id}\n\n`;
return md + this.formatJsonAsMarkdown(data);
}
formatJsonAsMarkdown(data) {
let md = `## Analysis Results\n\n`;
const formatValue = (value, depth = 0) => {
const indent = ' '.repeat(depth);
if (Array.isArray(value)) {
return value.map(item => `${indent}- ${typeof item === 'object' ? JSON.stringify(item) : item}`).join('\n');
}
else if (typeof value === 'object' && value !== null) {
return Object.entries(value).map(([key, val]) => {
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
return `${indent}**${formattedKey}:** ${typeof val === 'object' ? '\n' + formatValue(val, depth + 1) : val}`;
}).join('\n');
}
else {
return String(value);
}
};
md += formatValue(data);
md += `\n`;
return md;
}
async cleanup() {
try {
await fs.remove(this.tempDir);
}
catch {
// Ignore cleanup errors
}
}
async listAgents() {
const agents = await this.discovery.discoverAgents();
this.discovery.printAgentList(agents);
}
async validateSetup(claudeCommand = 'claude') {
console.log(chalk_1.default.blue('š Validating setup...\n'));
let allValid = true;
// Check Claude CLI
try {
await (0, execa_1.execa)(claudeCommand, ['--version']);
console.log(chalk_1.default.green('ā
Claude CLI found'));
}
catch {
console.log(chalk_1.default.red('ā Claude CLI not found'));
console.log(chalk_1.default.gray(' Install from: https://claude.ai/code'));
allValid = false;
}
// Check for agents
const agents = await this.discovery.discoverAgents();
const coreAgents = agents.filter(a => a.category === 'core');
if (coreAgents.length > 0) {
console.log(chalk_1.default.green(`ā
Found ${coreAgents.length} core agents`));
if (agents.length > coreAgents.length) {
console.log(chalk_1.default.blue(`ā¹ļø Found ${agents.length - coreAgents.length} additional custom agents`));
}
}
else {
console.log(chalk_1.default.red('ā No core agents found'));
allValid = false;
}
// Check write permissions
try {
const testDir = path.join(process.cwd(), '.test-permissions');
await fs.ensureDir(testDir);
await fs.remove(testDir);
console.log(chalk_1.default.green('ā
Write permissions OK'));
}
catch {
console.log(chalk_1.default.red('ā No write permissions in current directory'));
allValid = false;
}
console.log();
if (allValid) {
console.log(chalk_1.default.green('š Setup validation passed!'));
}
else {
console.log(chalk_1.default.red('š„ Setup validation failed. Please fix the issues above.'));
}
return allValid;
}
}
exports.AgentOrchestrator = AgentOrchestrator;
//# sourceMappingURL=AgentOrchestrator.js.map