import { DiffProviderPort, LLMPort, OutputPort, Diff } from '../ports'; export interface CommitMessageOptions { readonly baseRef?: string; readonly headRef?: string; readonly includeStaged?: boolean; readonly includeUnstaged?: boolean; readonly filePatterns?: string[]; readonly excludePatterns?: string[]; readonly outputFormat?: 'console' | 'markdown' | 'json' | 'file'; readonly outputPath?: string; readonly style?: 'conventional' | 'simple' | 'detailed'; readonly maxLength?: number; readonly includeBody?: boolean; } export interface CommitMessageUseCase { execute(options?: CommitMessageOptions): Promise; } export class GenerateCommitMessageUseCaseImpl implements CommitMessageUseCase { constructor( private readonly diffProvider: DiffProviderPort, private readonly llmPort: LLMPort, private readonly outputPort: OutputPort ) {} async execute(options: CommitMessageOptions = {}): Promise { try { // Get diffs const diffs = await this.diffProvider.getDiffs({ baseRef: options.baseRef, headRef: options.headRef, includeStaged: options.includeStaged, includeUnstaged: options.includeUnstaged, filePatterns: options.filePatterns, excludePatterns: options.excludePatterns, }); if (diffs.length === 0) { throw new Error('No changes found to generate commit message for'); } // Generate commit message const commitMessage = await this.generateCommitMessage(diffs, options); // Output the commit message await this.outputPort.displayMessage(commitMessage, { format: options.outputFormat || 'console', outputPath: options.outputPath, }); return commitMessage; } catch (error) { await this.outputPort.displayError(`Commit message generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } private async generateCommitMessage(diffs: Diff[], options: CommitMessageOptions): Promise { const style = options.style || 'conventional'; const maxLength = options.maxLength || 50; const includeBody = options.includeBody !== false; // Create prompt for LLM const prompt = this.createCommitMessagePrompt(diffs, style, maxLength, includeBody); const messages = [ { role: 'system' as const, content: 'You are an expert at writing clear, concise commit messages. Generate commit messages that follow best practices and clearly describe the changes made.', }, { role: 'user' as const, content: prompt, }, ]; try { const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 500, }); return this.formatCommitMessage(response.content, style, maxLength, includeBody); } catch (error) { // Fallback to simple commit message return this.generateFallbackCommitMessage(diffs); } } private createCommitMessagePrompt(diffs: Diff[], style: string, maxLength: number, includeBody: boolean): string { const fileList = diffs.map(d => d.filePath).join(', '); const changes = diffs .map(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 `\n## ${diff.filePath}\n${changes}`; }) .join('\n'); let styleInstructions = ''; switch (style) { case 'conventional': styleInstructions = ` Use the Conventional Commits format: - feat: for new features - fix: for bug fixes - docs: for documentation changes - style: for formatting changes - refactor: for code refactoring - test: for test changes - chore: for maintenance tasks Format: type(scope): description Example: feat(auth): add user authentication `; break; case 'simple': styleInstructions = 'Use a simple, clear format without prefixes.'; break; case 'detailed': styleInstructions = 'Provide a detailed description of all changes made.'; break; } return ` Generate a commit message for the following changes: Files changed: ${fileList} Changes: ${changes} Requirements: - Style: ${styleInstructions} - Maximum length: ${maxLength} characters for the subject line - ${includeBody ? 'Include a body with more details' : 'No body needed'} - Be clear and concise - Focus on what was changed and why ${includeBody ? 'Provide both a subject line and a body.' : 'Provide only the subject line.'} `; } private formatCommitMessage(content: string, style: string, maxLength: number, includeBody: boolean): string { // Clean up the response let formatted = content.trim(); // Remove markdown formatting if present formatted = formatted.replace(/^#+\s*/gm, ''); formatted = formatted.replace(/\*\*(.*?)\*\*/g, '$1'); formatted = formatted.replace(/\*(.*?)\*/g, '$1'); // Split into subject and body if needed const lines = formatted.split('\n'); const subject = lines[0] || ''; const body = lines.slice(1).filter(line => line.trim()).join('\n'); // Ensure subject line is within max length const truncatedSubject = subject.length > maxLength ? subject.substring(0, maxLength - 3) + '...' : subject; if (includeBody && body) { return `${truncatedSubject}\n\n${body}`; } return truncatedSubject; } private generateFallbackCommitMessage(diffs: Diff[]): string { const fileCount = diffs.length; const hasNewFiles = diffs.some(d => d.isNewFile); const hasDeletedFiles = diffs.some(d => d.isDeletedFile); if (hasNewFiles && hasDeletedFiles) { return `Update ${fileCount} files (add new files, remove old files)`; } else if (hasNewFiles) { return `Add new files (${fileCount} files)`; } else if (hasDeletedFiles) { return `Remove files (${fileCount} files)`; } else { return `Update ${fileCount} files`; } } }