import { DiffProviderPort, LLMPort, OutputPort, Diff, CodeBlock } from '../ports'; import * as fs from 'fs'; import * as path from 'path'; import { glob } from 'glob'; export interface ExplainCodeOptions { readonly baseRef?: string; readonly headRef?: string; readonly includeStaged?: boolean; readonly includeUnstaged?: boolean; readonly filePatterns?: string[]; readonly excludePatterns?: string[]; readonly outputFormat?: 'console' | 'markdown' | 'json' | 'html' | 'file'; readonly outputPath?: string; readonly level?: 'beginner' | 'intermediate' | 'advanced'; readonly includeExamples?: boolean; readonly style?: 'technical' | 'simple' | 'detailed'; } export interface ExplainCodeUseCase { execute(options?: ExplainCodeOptions): Promise; } export class ExplainCodeUseCaseImpl implements ExplainCodeUseCase { constructor( private readonly diffProvider: DiffProviderPort, private readonly llmPort: LLMPort, private readonly outputPort: OutputPort ) {} async execute(options: ExplainCodeOptions = {}): Promise { try { // Get diffs const diffOptions: any = { includeStaged: options.includeStaged, includeUnstaged: options.includeUnstaged, }; if (options.baseRef) { diffOptions.baseRef = options.baseRef; } if (options.headRef) { diffOptions.headRef = options.headRef; } if (options.filePatterns) { diffOptions.filePatterns = options.filePatterns; } if (options.excludePatterns) { diffOptions.excludePatterns = options.excludePatterns; } const diffs = await this.diffProvider.getDiffs(diffOptions); let codeBlocks: CodeBlock[] = []; // Prioritize file patterns when provided for code explanation if (options.filePatterns && options.filePatterns.length > 0) { // Read files directly for explanation codeBlocks = await this.readFilesFromPatterns(options.filePatterns, options.excludePatterns); } else if (diffs.length > 0) { // Convert diffs to code blocks codeBlocks = this.convertDiffsToCodeBlocks(diffs); } else { throw new Error('No changes found to explain'); } // Explain each code block const explanations: string[] = []; for (const codeBlock of codeBlocks) { const explanation = await this.explainCodeBlock(codeBlock, options); explanations.push(explanation); } // Combine explanations const combinedExplanation = this.combineExplanations(explanations, codeBlocks); // Output the explanation const outputOptions: any = { format: options.outputFormat || 'console', }; if (options.outputPath) { outputOptions.outputPath = options.outputPath; } await this.outputPort.displayMessage(combinedExplanation, outputOptions); return combinedExplanation; } catch (error) { await this.outputPort.displayError(`Explanation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } private async explainDiff(diff: Diff, options: ExplainCodeOptions): Promise { // Skip binary files if (diff.isBinary) { return `\n## ${diff.filePath}\n\nThis is a binary file and cannot be explained in detail.\n`; } // Create prompt for LLM const prompt = this.createExplanationPrompt(diff, options); const messages = [ { role: 'system' as const, content: 'You are an expert software developer and teacher. Explain code changes in a clear, educational way that helps developers understand what was changed and why.', }, { role: 'user' as const, content: prompt, }, ]; try { const response = await this.llmPort.generateResponse(messages, { temperature: 0.4, maxTokens: 1500, }); return response.content; } catch (error) { return `\n## ${diff.filePath}\n\nUnable to explain this file due to service issues.\n`; } } private createExplanationPrompt(item: Diff | CodeBlock, options: ExplainCodeOptions): string { const level = options.level || 'intermediate'; const style = options.style || 'technical'; const includeExamples = options.includeExamples !== false; if ('hunks' in item) { // Handle Diff object const diff = item as Diff; const changes = diff.hunks .map(hunk => hunk.lines .filter(line => line.type === 'added' || line.type === 'removed') .map(line => `${line.type === 'added' ? '+' : '-'}${line.content}`) .join('\n') ) .join('\n'); return ` Explain the following code changes in ${diff.filePath} for a ${level} level developer: ${changes} Please provide: 1. A summary of what was changed 2. Why these changes were made 3. The impact of these changes 4. Any potential side effects or considerations ${includeExamples ? '5. Examples of how the changed code works' : ''} Use clear, educational language and explain technical concepts in an accessible way. `; } else { // Handle CodeBlock object const codeBlock = item as CodeBlock; return `Please explain the following ${codeBlock.language} code in a ${style} style for a ${level} developer: \`\`\`${codeBlock.language} ${codeBlock.content} \`\`\` Please provide: 1. What this code does 2. How it works 3. Key concepts and patterns used ${includeExamples ? '4. Examples of similar patterns or usage' : ''} Use clear, educational language and explain technical concepts in an accessible way.`; } } private combineExplanations(explanations: string[], codeBlocks: CodeBlock[]): string { const header = `# Code Explanation\n\nThis document explains the code in the specified files.\n\n`; const summary = `## Summary\n\n${codeBlocks.length} file(s) were analyzed:\n${codeBlocks.map(c => `- ${c.filePath}`).join('\n')}\n\n`; const body = explanations.join('\n\n---\n\n'); return header + summary + body; } private async readFilesFromPatterns(filePatterns: string[], excludePatterns: string[] = []): Promise { const codeBlocks: CodeBlock[] = []; for (const pattern of filePatterns) { const resolvedPattern = this.resolvePattern(pattern); const files = await this.expandPattern(resolvedPattern); for (const filePath of files) { if (this.matchesExcludePatterns(filePath, excludePatterns)) { continue; } if (this.isReadableFile(filePath)) { try { const content = fs.readFileSync(filePath, 'utf-8'); const language = this.getLanguageFromFile(filePath); codeBlocks.push({ filePath, language, content, startLine: 1, endLine: content.split('\n').length, metadata: { isNewFile: true, isDeletedFile: false, } }); } catch (error) { console.warn(`Warning: Could not read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } } return codeBlocks; } private async explainCodeBlock(codeBlock: CodeBlock, options: ExplainCodeOptions): Promise { const prompt = this.createExplanationPrompt(codeBlock, options); const response = await this.llmPort.generateResponse([ { role: 'user', content: prompt } ], { temperature: 0.7, maxTokens: 1000, }); return response.content; } private resolvePattern(pattern: string): string { if (path.isAbsolute(pattern)) { return pattern; } if (pattern.startsWith('/')) { const cleanPattern = pattern.substring(1); return path.resolve(process.cwd(), cleanPattern); } return path.resolve(process.cwd(), pattern); } private async expandPattern(pattern: string): Promise { try { const stats = fs.statSync(pattern); if (stats.isFile()) { return [pattern]; } else if (stats.isDirectory()) { const extensions = [ '*.js', '*.ts', '*.jsx', '*.tsx', '*.py', '*.java', '*.cs', '*.cpp', '*.c', '*.h', '*.go', '*.rs', '*.php', '*.rb', '*.swift', '*.kt', '*.scala', '*.dart', '*.yaml', '*.yml', '*.json', '*.xml', '*.html', '*.css', '*.scss', '*.less', '*.sql', '*.sh', '*.bash', ]; const files: string[] = []; for (const ext of extensions) { const patternPath = path.join(pattern, '**', ext); const matches = await glob(patternPath); files.push(...matches); } return files; } return []; } catch (error) { console.warn(`Warning: Could not expand pattern ${pattern}: ${error instanceof Error ? error.message : 'Unknown error'}`); return []; } } private matchesExcludePatterns(filePath: string, excludePatterns: string[]): boolean { return excludePatterns.some(pattern => { const resolvedPattern = this.resolvePattern(pattern); return filePath.includes(resolvedPattern) || filePath.match(new RegExp(pattern.replace(/\*/g, '.*'))); }); } private isReadableFile(filePath: string): boolean { try { const stats = fs.statSync(filePath); return stats.isFile() && stats.size < 10 * 1024 * 1024; // 10MB limit } catch { return false; } } private getLanguageFromFile(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); const languageMap: Record = { '.js': 'javascript', '.ts': 'typescript', '.jsx': 'javascript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.cs': 'csharp', '.cpp': 'cpp', '.c': 'c', '.h': 'c', '.go': 'go', '.rs': 'rust', '.php': 'php', '.rb': 'ruby', '.swift': 'swift', '.kt': 'kotlin', '.scala': 'scala', '.dart': 'dart', '.yaml': 'yaml', '.yml': 'yaml', '.json': 'json', '.xml': 'xml', '.html': 'html', '.css': 'css', '.scss': 'scss', '.less': 'less', '.sql': 'sql', '.sh': 'bash', '.bash': 'bash', }; return languageMap[ext] || 'text'; } private convertDiffsToCodeBlocks(diffs: Diff[]): CodeBlock[] { const codeBlocks: CodeBlock[] = []; for (const diff of diffs) { if (diff.newContent) { codeBlocks.push({ filePath: diff.filePath, language: diff.language || 'text', content: diff.newContent, startLine: 1, endLine: diff.newContent.split('\n').length, metadata: { isNewFile: diff.isNewFile || false, isDeletedFile: diff.isDeletedFile || false, } }); } } return codeBlocks; } }