UNPKG

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
"use strict"; 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