UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

445 lines • 16.3 kB
/** * Interactive approval workflow for smart git push * * Handles user interaction for approving/rejecting files with issues */ import { createInterface } from 'readline'; import { writeFileSync } from 'fs'; /** * Main interactive approval function */ export async function handleInteractiveApproval(items, options) { const result = { approved: [], rejected: [], moved: [], ignored: [], actions: [], proceed: false, }; if (items.length === 0) { result.proceed = true; return result; } // Handle non-interactive mode if (!options.interactiveMode) { return handleNonInteractiveApproval(items, options); } // Display summary displayApprovalSummary(items); // Group items by severity for better UX const errorItems = items.filter(item => item.severity === 'error'); const warningItems = items.filter(item => item.severity === 'warning'); const infoItems = items.filter(item => item.severity === 'info'); // Handle errors first (these are most critical) if (errorItems.length > 0) { console.log('\n🚨 CRITICAL ISSUES (Must be resolved before proceeding)'); for (const item of errorItems) { const action = await handleItemApproval(item, options, true); result.actions.push(action); applyAction(action, result); } } // Handle warnings if (warningItems.length > 0) { console.log('\nāš ļø WARNING ISSUES (Review recommended)'); for (const item of warningItems) { const action = await handleItemApproval(item, options, false); result.actions.push(action); applyAction(action, result); } } // Handle info items (auto-approve if option set) if (infoItems.length > 0) { if (options.autoApproveInfo) { console.log(`\nāœ… Auto-approving ${infoItems.length} info-level items`); for (const item of infoItems) { const action = { type: 'approve', filePath: item.filePath }; result.actions.push(action); applyAction(action, result); } } else { console.log('\nšŸ’” INFO ITEMS (Low priority)'); for (const item of infoItems) { const action = await handleItemApproval(item, options, false); result.actions.push(action); applyAction(action, result); } } } // Final confirmation result.proceed = await getFinalConfirmation(result, options); return result; } /** * Handle non-interactive approval */ function handleNonInteractiveApproval(items, options) { const result = { approved: [], rejected: [], moved: [], ignored: [], actions: [], proceed: false, }; for (const item of items) { let action; if (item.severity === 'error' && options.autoRejectErrors) { action = { type: 'reject', filePath: item.filePath, reason: 'Auto-rejected due to errors' }; } else if (item.severity === 'info' && options.autoApproveInfo) { action = { type: 'approve', filePath: item.filePath }; } else { // Default behavior based on severity switch (item.severity) { case 'error': action = { type: 'reject', filePath: item.filePath, reason: 'Automatic rejection due to errors', }; break; case 'warning': action = item.allowedInLocation ? { type: 'approve', filePath: item.filePath } : { type: 'reject', filePath: item.filePath, reason: 'Location violation' }; break; case 'info': action = { type: 'approve', filePath: item.filePath }; break; } } result.actions.push(action); applyAction(action, result); } // In non-interactive mode, proceed if no errors were rejected result.proceed = result.rejected.length === 0; return result; } /** * Handle approval for a single item */ async function handleItemApproval(item, options, isError) { console.log(`\nšŸ“„ ${item.filePath}`); console.log(` Severity: ${item.severity.toUpperCase()}`); console.log(` Confidence: ${(item.confidence * 100).toFixed(1)}%`); console.log(` Location Valid: ${item.allowedInLocation ? 'āœ…' : 'āŒ'}`); // Display issues console.log('\n Issues:'); for (const issue of item.issues) { console.log(` - ${issue.severity.toUpperCase()}: ${issue.message}`); if (issue.line) { console.log(` Line ${issue.line}`); } if (issue.context) { const contextLines = issue.context.split('\n').slice(0, 3); console.log(` Context: ${contextLines.join(' | ')}`); } } // Display suggestions if (item.suggestions.length > 0) { console.log('\n Suggestions:'); for (let i = 0; i < item.suggestions.length; i++) { console.log(` ${i + 1}. ${item.suggestions[i]}`); } } // Get user choice const choices = buildChoices(item, isError); const choice = await getUserChoice(choices, options); return processChoice(choice, item, options); } /** * Build available choices for user */ function buildChoices(item, isError) { const choices = [ { key: 'a', description: 'Approve (include in commit)', available: !isError }, { key: 'r', description: 'Reject (exclude from commit)', available: true }, { key: 'i', description: 'Ignore (add to .gitignore)', available: true }, { key: 'm', description: 'Move to different location', available: !item.allowedInLocation }, { key: 'v', description: 'View file content', available: true }, { key: 'e', description: 'Edit file', available: true }, { key: 's', description: 'Skip for now', available: false }, // Not implemented yet { key: 'h', description: 'Show help', available: true }, ]; return choices; } /** * Get user choice */ async function getUserChoice(choices, options) { const availableChoices = choices.filter(c => c.available); console.log('\n Available actions:'); for (const choice of availableChoices) { console.log(` [${choice.key}] ${choice.description}`); } if (options.batchMode) { console.log(' [all] Apply to all similar files'); } const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { const askQuestion = () => { rl.question('\n Your choice: ', answer => { const choice = answer.trim().toLowerCase(); if (choice === 'h') { displayHelp(); askQuestion(); } else if (availableChoices.some(c => c.key === choice) || choice === 'all') { rl.close(); resolve(choice); } else { console.log(' Invalid choice. Please try again.'); askQuestion(); } }); }; askQuestion(); }); } /** * Process user choice into action */ function processChoice(choice, item, options) { switch (choice) { case 'a': return { type: 'approve', filePath: item.filePath }; case 'r': return { type: 'reject', filePath: item.filePath, reason: 'User rejected' }; case 'i': return { type: 'ignore', filePath: item.filePath }; case 'm': const targetDir = getMoveTarget(item); return { type: 'move', filePath: item.filePath, target: targetDir }; case 'v': viewFileContent(item.filePath); return processChoice(choice, item, options); // Re-prompt after viewing case 'e': editFile(item.filePath); return processChoice(choice, item, options); // Re-prompt after editing default: return { type: 'reject', filePath: item.filePath, reason: 'Invalid choice' }; } } /** * Get move target from suggestions */ function getMoveTarget(item) { const moveSuggestions = item.suggestions.filter(s => s.includes('Move') || s.includes('move')); if (moveSuggestions.length > 0) { console.log('\n Suggested locations:'); for (let i = 0; i < moveSuggestions.length; i++) { console.log(` ${i + 1}. ${moveSuggestions[i]}`); } // For now, extract first suggested directory const firstSuggestion = moveSuggestions[0]; if (firstSuggestion) { const dirMatch = firstSuggestion.match(/(?:to|in)\s+(\w+\/)/); return dirMatch ? dirMatch[1] : 'scripts/'; } } return 'scripts/'; // Default fallback } /** * View file content */ function viewFileContent(filePath) { try { const fs = require('fs'); const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n'); console.log(`\n Content of ${filePath}:`); console.log(' ' + '='.repeat(50)); // Show first 20 lines for (let i = 0; i < Math.min(20, lines.length); i++) { console.log(` ${(i + 1).toString().padStart(3)}: ${lines[i]}`); } if (lines.length > 20) { console.log(` ... (${lines.length - 20} more lines)`); } console.log(' ' + '='.repeat(50)); } catch (error) { console.log(` Error reading file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Edit file (placeholder - would open in editor) */ function editFile(filePath) { console.log(`\n Opening ${filePath} in editor...`); console.log(' (This would open your default editor in a real implementation)'); console.log(' Press Enter to continue after editing...'); // In a real implementation, this would: // 1. Open the file in the user's default editor // 2. Wait for the editor to close // 3. Re-analyze the file for issues } /** * Apply action to result */ function applyAction(action, result) { switch (action.type) { case 'approve': result.approved.push(action.filePath); break; case 'reject': result.rejected.push(action.filePath); break; case 'move': if (action.target) { result.moved.push({ from: action.filePath, to: action.target }); } break; case 'ignore': result.ignored.push(action.filePath); break; } } /** * Display approval summary */ function displayApprovalSummary(items) { console.log('\n' + '='.repeat(60)); console.log('šŸ” SMART GIT PUSH - VALIDATION RESULTS'); console.log('='.repeat(60)); const errorCount = items.filter(item => item.severity === 'error').length; const warningCount = items.filter(item => item.severity === 'warning').length; const infoCount = items.filter(item => item.severity === 'info').length; console.log(`šŸ“Š Summary: ${items.length} files with issues`); console.log(` 🚨 Errors: ${errorCount}`); console.log(` āš ļø Warnings: ${warningCount}`); console.log(` šŸ’” Info: ${infoCount}`); if (errorCount > 0) { console.log('\nāŒ Files with errors must be resolved before proceeding'); } console.log('\nšŸ“‹ Review each file and choose an action:'); } /** * Get final confirmation */ async function getFinalConfirmation(result, options) { console.log('\n' + '='.repeat(60)); console.log('šŸ“‹ FINAL SUMMARY'); console.log('='.repeat(60)); console.log(`āœ… Approved: ${result.approved.length} files`); console.log(`āŒ Rejected: ${result.rejected.length} files`); console.log(`šŸ“ To Move: ${result.moved.length} files`); console.log(`šŸ™ˆ To Ignore: ${result.ignored.length} files`); if (result.approved.length > 0) { console.log('\nāœ… Files to be committed:'); for (const file of result.approved) { console.log(` - ${file}`); } } if (result.rejected.length > 0) { console.log('\nāŒ Files excluded from commit:'); for (const file of result.rejected) { console.log(` - ${file}`); } } if (result.moved.length > 0) { console.log('\nšŸ“ Files to be moved:'); for (const move of result.moved) { console.log(` - ${move.from} → ${move.to}`); } } if (options.dryRun) { console.log('\nšŸ” DRY RUN - No actual changes will be made'); return true; } const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { rl.question('\nšŸš€ Proceed with git push? (y/N): ', answer => { rl.close(); const proceed = answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes'; resolve(proceed); }); }); } /** * Display help */ function displayHelp() { console.log('\n' + '='.repeat(40)); console.log('šŸ“– HELP'); console.log('='.repeat(40)); console.log('Actions:'); console.log(' [a] Approve - Include file in commit'); console.log(' [r] Reject - Exclude file from commit'); console.log(' [i] Ignore - Add file to .gitignore'); console.log(' [m] Move - Move file to appropriate location'); console.log(' [v] View - Show file content'); console.log(' [e] Edit - Open file in editor'); console.log(' [h] Help - Show this help'); console.log(''); console.log('Tips:'); console.log(' - Files with errors must be resolved before proceeding'); console.log(' - Use [m] to move files to proper directories'); console.log(' - Use [i] to permanently ignore temporary files'); console.log(' - Use [v] to examine file content before deciding'); console.log('='.repeat(40)); } /** * Batch approval for similar files */ export function batchApproval(items, action, criteria) { const actions = []; const referenceItem = items.find(item => item.filePath === action.filePath); if (!referenceItem) { return [action]; } for (const item of items) { let matches = true; if (criteria.sameSeverity && item.severity !== referenceItem.severity) { matches = false; } if (criteria.sameIssueType) { const itemTypes = item.issues.map(i => i.type); const refTypes = referenceItem.issues.map(i => i.type); if (!itemTypes.some(type => refTypes.includes(type))) { matches = false; } } if (matches) { actions.push({ type: action.type, filePath: item.filePath, target: action.target, reason: `Batch ${action.type} - similar to ${action.filePath}`, }); } } return actions; } /** * Save approval preferences for future use */ export async function saveApprovalPreferences(actions, configPath = '.smartgit-approvals.json') { try { const preferences = { timestamp: new Date().toISOString(), actions: actions.map(action => ({ pattern: action.filePath.replace(/[^/]+$/, '*'), // Replace filename with wildcard type: action.type, reason: action.reason, })), }; writeFileSync(configPath, JSON.stringify(preferences, null, 2)); console.log(`\nšŸ’¾ Approval preferences saved to ${configPath}`); } catch (error) { console.log(`\nāš ļø Could not save preferences: ${error instanceof Error ? error.message : String(error)}`); } } //# sourceMappingURL=interactive-approval.js.map