UNPKG

@sbeeredd04/auto-git

Version:

AI-powered Git automation with intelligent commit decisions using Gemini function calling, smart diff optimization, push control, and enhanced interactive terminal session with persistent command history

560 lines (477 loc) 22.3 kB
#!/usr/bin/env node import { Command } from 'commander'; import { execa } from 'execa'; import { startWatcher, performSingleCommit, cleanup } from '../lib/watcher.js'; import { getConfig, validateConfig, getInteractiveConfig, getCommitConfig } from '../lib/config.js'; import { isGitRepository, hasRemote, getCurrentBranch } from '../lib/git.js'; import { forceExit } from '../lib/utils.js'; import { startInteractiveSession } from '../lib/repl.js'; import logger from '../utils/logger.js'; const program = new Command(); // Global Ctrl+C handler for force exit process.on('SIGINT', () => { logger.space(); logger.info('Force exiting Auto-Git...', 'SHUTDOWN'); cleanup(); forceExit(0); }); // Custom help formatter with styled output function displayStyledHelp() { logger.section('Auto-Git v3.10.4', 'AI-powered Git automation with intelligent commit decisions and enhanced interactive terminal session'); logger.space(); logger.info('USAGE:', 'COMMAND'); logger.info(' auto-git [command] [options]', ''); logger.space(); const commands = { 'watch': 'Watch files and auto-commit with AI messages (simple Ctrl+C to exit)', 'watch --mode intelligent': 'Watch with AI-driven commit decisions and 30s buffer', 'commit (c)': 'Generate AI commit for current changes', 'interactive': 'Start enhanced interactive terminal session with AI assistance', 'reset <count>': 'Undo commits with safety checks', 'config': 'Show configuration', 'setup': 'Interactive setup guide for first-time users', 'debug': 'Run system diagnostics and health check', 'help': 'Display this help information' }; logger.config('AVAILABLE COMMANDS', commands); logger.space(); logger.info('COMMIT MODES:', 'MODES'); logger.info(' Periodic Mode Time-based commits (default behavior)', ''); logger.info(' Intelligent Mode AI decides when to commit based on change significance', ''); logger.info(' • Analyzes code changes for completeness', ''); logger.info(' • 30-second buffer to cancel commits', ''); logger.info(' • Rate limited to 15 API calls per minute', ''); logger.info(' • Smart diff optimization reduces API usage', ''); logger.space(); logger.info('INTERACTIVE FEATURES:', 'FEATURES'); logger.info(' Enhanced Session Persistent command history and markdown AI responses', ''); logger.info(' Arrow Key Navigation Browse command history with ↑↓ keys', ''); logger.info(' Git Syntax Highlight Enhanced display for Git commands', ''); logger.info(' Session Persistence Command history saved across restarts', ''); logger.info(' Ctrl+C Exit from anywhere', ''); logger.space(); logger.info('EXAMPLES:', 'EXAMPLES'); logger.info(' auto-git setup # First-time setup guide', ''); logger.info(' auto-git watch # Start simple file watching (periodic)', ''); logger.info(' auto-git watch --mode intelligent # Start intelligent commit mode', ''); logger.info(' auto-git watch --no-push # Watch and commit without pushing', ''); logger.info(' auto-git interactive # Start enhanced interactive session', ''); logger.info(' auto-git commit --verbose # One-time commit with details', ''); logger.info(' auto-git reset 2 --soft # Undo last 2 commits (soft)', ''); logger.info(' auto-git config # Show current configuration', ''); logger.space(); logger.info('QUICK START:', 'SETUP'); logger.info(' 1. Get API key: https://aistudio.google.com/app/apikey', ''); logger.info(' 2. Set API key: export GEMINI_API_KEY="your-key"', ''); logger.info(' 3. Run setup: auto-git setup', ''); logger.info(' 4. Start using: auto-git watch or auto-git interactive', ''); logger.space(); logger.info('For detailed help on any command, use: auto-git [command] --help', 'HELP'); } // Enhanced error handler for missing API key function handleMissingApiKey(commandName) { logger.space(); logger.error('Gemini API Key Required', 'API key not found or configured'); logger.space(); logger.warning('QUICK SETUP REQUIRED', 'Auto-Git needs a Gemini API key to function'); logger.space(); logger.info('OPTION 1 - Use Setup Guide (Recommended):', 'SETUP'); logger.info(' auto-git setup', ''); logger.space(); logger.info('OPTION 2 - Manual Setup:', 'MANUAL'); logger.info(' 1. Get API key: https://aistudio.google.com/app/apikey', ''); logger.info(' 2. Set environment variable:', ''); logger.info(' export GEMINI_API_KEY="your-api-key-here"', ''); logger.info(' 3. Or create config file:', ''); logger.info(' echo \'{"apiKey": "your-key"}\' > ~/.auto-gitrc.json', ''); logger.space(); logger.info('OPTION 3 - Test Configuration:', 'TEST'); logger.info(' auto-git config # Check current setup', ''); logger.info(' auto-git debug # Run diagnostics', ''); logger.space(); logger.info(`After setup, retry: auto-git ${commandName}`, 'RETRY'); } program .name('auto-git') .description('AI-powered Git automation with intelligent commit decisions using Gemini function calling, smart diff optimization, push control, and enhanced interactive terminal session') .version('3.10.4') .configureHelp({ formatHelp: () => { displayStyledHelp(); return ''; // Return empty string since we handle formatting ourselves } }); program .command('watch') .description('Watch for file changes recursively and auto-commit with AI-generated messages') .option('-p, --paths <paths...>', 'Custom paths to watch (default: all files recursively)') .option('--no-push', 'Commit but do not push to remote') .option('-v, --verbose', 'Enable verbose output') .option('-m, --mode <mode>', 'Commit mode: "periodic" (time-based) or "intelligent" (AI-driven)', 'periodic') .action(async (options) => { try { // Set verbose mode if requested if (options.verbose) { logger.setVerbose(true); } // Validate commit mode if (options.mode && !['periodic', 'intelligent'].includes(options.mode)) { logger.error('Invalid commit mode', 'Mode must be either "periodic" or "intelligent"'); process.exit(1); } // Temporarily override config for this session if (options.mode) { process.env.AUTO_GIT_COMMIT_MODE = options.mode; } // Handle --no-push option if (options.push === false) { // Commander.js sets this when --no-push is used process.env.AUTO_GIT_NO_PUSH = 'true'; } // Validate configuration with enhanced error handling try { validateConfig(); } catch (error) { if (error.message.includes('GEMINI_API_KEY')) { handleMissingApiKey('watch'); process.exit(1); } throw error; } logger.section('Auto-Git Watcher v3.10.4', 'Simple file monitoring with auto-commit (Ctrl+C to exit)'); const isRepo = await isGitRepository(); if (!isRepo) { logger.error( 'Not a Git repository', 'Please run this command inside a Git repository or initialize one with: git init' ); logger.space(); logger.info('QUICK FIX:', 'SETUP'); logger.info(' git init # Initialize new repository', ''); logger.info(' git remote add origin <url> # Add remote (optional)', ''); logger.info(' auto-git watch # Start watching', ''); process.exit(1); } const branch = await getCurrentBranch(); const remote = await hasRemote(); const config = getConfig(); const interactiveConfig = getInteractiveConfig(); logger.repoStatus(branch, remote, !!config.apiKey); // Show simplified features status logger.space(); logger.info('Watch Mode Features:', 'FEATURES'); logger.info(` Auto-commit: ✓ Enabled`); logger.info(` AI Messages: ${config.apiKey ? '✓ Enabled' : '✗ Disabled (no API key)'}`); logger.info(` Error Recovery: ${interactiveConfig.interactiveOnError ? '✓ Enabled' : '✗ Disabled'}`); logger.info(` Simple Exit: ✓ Ctrl+C only`); // Pass custom paths if provided, otherwise use default recursive watching const watchPaths = options.paths && options.paths.length > 0 && !options.paths.includes('.') ? options.paths : null; // null will use config defaults const watcher = await startWatcher(watchPaths); // The global SIGINT handler will take care of cleanup // No need for a duplicate handler here } catch (error) { logger.error('Failed to start watcher', error.message); logger.space(); logger.info('TROUBLESHOOTING:', 'HELP'); logger.info(' auto-git debug # Run diagnostics', ''); logger.info(' auto-git config # Check configuration', ''); logger.info(' auto-git setup # Re-run setup', ''); process.exit(1); } }); program .command('commit') .alias('c') .description('Generate AI commit message for current changes and commit/push') .option('--dry-run', 'Show what would be committed without actually committing') .option('--no-push', 'Commit but do not push to remote') .option('-v, --verbose', 'Enable verbose output') .action(async (options) => { try { // Set verbose mode if requested if (options.verbose) { logger.setVerbose(true); } // Validate configuration with enhanced error handling try { validateConfig(); } catch (error) { if (error.message.includes('GEMINI_API_KEY')) { handleMissingApiKey('commit'); process.exit(1); } throw error; } if (options.dryRun) { logger.warning('Dry run mode enabled', 'No actual commits will be made'); logger.info('Dry run functionality coming soon!'); return; } await performSingleCommit(); } catch (error) { logger.error('Commit operation failed', error.message); logger.space(); logger.info('TROUBLESHOOTING:', 'HELP'); logger.info(' auto-git debug # Run diagnostics', ''); logger.info(' auto-git config # Check configuration', ''); logger.info(' git status # Check repository state', ''); process.exit(1); } }); program .command('reset') .description('Undo last commits with git reset') .argument('<count>', 'Number of commits to reset') .option('--hard', 'Hard reset (WARNING: destroys changes)') .option('--soft', 'Soft reset (keeps changes staged)') .option('--mixed', 'Mixed reset (default - keeps changes unstaged)') .action(async (count, options) => { try { const isRepo = await isGitRepository(); if (!isRepo) { logger.error('Not a Git repository', 'Please run this command inside a Git repository'); logger.space(); logger.info('QUICK FIX:', 'SETUP'); logger.info(' git init # Initialize repository', ''); logger.info(' cd /path/to/git/repo # Navigate to Git repository', ''); process.exit(1); } const resetCount = parseInt(count, 10); if (isNaN(resetCount) || resetCount < 1) { logger.error('Invalid count', 'Please provide a positive number'); logger.space(); logger.info('EXAMPLES:', 'USAGE'); logger.info(' auto-git reset 1 # Reset last commit (mixed)', ''); logger.info(' auto-git reset 2 --soft # Reset 2 commits (soft)', ''); logger.info(' auto-git reset 1 --hard # Reset 1 commit (hard)', ''); process.exit(1); } let resetType = ''; if (options.hard) resetType = '--hard'; else if (options.soft) resetType = '--soft'; else resetType = '--mixed'; // Warning for hard reset if (options.hard) { logger.warning( 'DESTRUCTIVE OPERATION', 'Hard reset will permanently destroy uncommitted changes!' ); const { default: inquirer } = await import('inquirer'); const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Are you sure you want to proceed with hard reset?', default: false }]); if (!confirm) { logger.info('Reset cancelled'); logger.space(); logger.info('SAFER ALTERNATIVES:', 'OPTIONS'); logger.info(' auto-git reset 1 --soft # Keep changes staged', ''); logger.info(' auto-git reset 1 --mixed # Keep changes unstaged', ''); logger.info(' git stash # Temporarily save changes', ''); return; } } const spinner = logger.startSpinner(`Resetting ${resetCount} commit(s) with ${resetType} mode...`); try { const args = ['reset', resetType, `HEAD~${resetCount}`].filter(Boolean); const result = await execa('git', args); logger.succeedSpinner(`Reset completed: ${resetType} HEAD~${resetCount}`); if (result.stdout) { logger.info('Git output:', result.stdout); } logger.info(`Successfully reset ${resetCount} commit(s)`, 'COMPLETE'); logger.space(); logger.info('NEXT STEPS:', 'GUIDE'); logger.info(' git status # Check current state', ''); logger.info(' auto-git commit # Make new commit', ''); logger.info(' auto-git watch # Resume watching', ''); } catch (error) { logger.failSpinner('Reset failed'); logger.error('Git reset error', error.message); logger.space(); logger.info('TROUBLESHOOTING:', 'HELP'); logger.info(' git log --oneline -10 # Check commit history', ''); logger.info(' git status # Check repository state', ''); logger.info(' auto-git debug # Run diagnostics', ''); process.exit(1); } } catch (error) { logger.error('Reset command failed', error.message); process.exit(1); } }); program .command('config') .description('Show current configuration') .action(() => { try { const config = getConfig(); const interactiveConfig = getInteractiveConfig(); const commitConfig = getCommitConfig(); const configItems = { 'API Key': config.apiKey ? '✓ Set' : '✗ Not set', 'Commit Mode': commitConfig.commitMode, 'Watch Paths': config.watchPaths.join(', '), 'Recursive Watching': config.watchOptions.depth === undefined, 'Debounce Time': `${config.debounceMs}ms`, 'Follow Symlinks': config.watchOptions.followSymlinks }; logger.config('AUTO-GIT CONFIGURATION', configItems); // Show commit mode specific settings logger.space(); const commitItems = { 'Commit Mode': commitConfig.commitMode.toUpperCase(), 'Push to Remote': commitConfig.noPush ? '✗ Disabled' : '✓ Enabled', 'Rate Limit': `${commitConfig.rateLimiting.maxCallsPerMinute} calls/minute`, 'Buffer Time': `${commitConfig.rateLimiting.bufferTimeSeconds} seconds`, 'Function Calling': commitConfig.commitMode === 'intelligent' ? '✓ Enabled' : '✗ Disabled' }; logger.config('COMMIT SETTINGS', commitItems); // Show interactive features logger.space(); const interactiveItems = { 'Interactive Session': '✓ Available (auto-git interactive)', 'AI Error Suggestions': interactiveConfig.enableSuggestions ? '✓ Enabled' : '✗ Disabled', 'Input Sanitization': '✓ Enabled', 'Terminal Pass-through': '✓ Enabled' }; logger.config('INTERACTIVE FEATURES', interactiveItems); logger.space(); logger.info('Configuration sources (in order of priority):'); logger.info(' 1. Environment variables (GEMINI_API_KEY, AUTO_GIT_*, etc.)'); logger.info(' 2. User config (~/.auto-gitrc.json)'); logger.info(' 3. .env file'); if (!config.apiKey) { logger.space(); logger.warning( 'API key not configured', 'Auto-Git requires a Gemini API key to function' ); logger.space(); logger.info('SETUP OPTIONS:', 'SETUP'); logger.info(' auto-git setup # Interactive setup guide', ''); logger.info(' export GEMINI_API_KEY="your-key" # Set environment variable', ''); logger.info(' # Or create ~/.auto-gitrc.json with your API key', ''); } logger.space(); logger.info('Ignored Patterns:'); config.watchOptions.ignored.forEach(pattern => { logger.info(` ${pattern.toString()}`, ''); }); logger.space(); logger.info('NEXT STEPS:', 'GUIDE'); if (!config.apiKey) { logger.info(' auto-git setup # Complete setup first', ''); } else { logger.info(' auto-git watch # Start watching files (periodic)', ''); logger.info(' auto-git watch --mode intelligent # Start intelligent commit mode', ''); logger.info(' auto-git interactive # Start interactive session', ''); logger.info(' auto-git commit # Make one-time commit', ''); } } catch (error) { logger.error('Failed to load configuration', error.message); logger.space(); logger.info('TROUBLESHOOTING:', 'HELP'); logger.info(' auto-git debug # Run system diagnostics', ''); logger.info(' auto-git setup # Re-run setup guide', ''); process.exit(1); } }); program .command('setup') .description('Interactive setup guide') .action(() => { const steps = [ 'Get a Gemini API key from: https://aistudio.google.com/app/apikey', 'Set your API key: export GEMINI_API_KEY="your-key"', 'Or create config file: echo \'{"apiKey": "your-key"}\' > ~/.auto-gitrc.json', 'Test the setup: auto-git config', 'Start using: auto-git watch (continuous) or auto-git interactive (manual)' ]; logger.setup(steps); logger.space(); logger.info('Interactive Session Features:', 'FEATURES'); logger.info(' • Full terminal pass-through - run any command'); logger.info(' • AI error analysis for failed commands'); logger.info(' • Automatic input sanitization'); logger.info(' • Simple Ctrl+C to exit'); logger.info(' • Git command suggestions'); logger.space(); logger.info('EXAMPLE CONFIG FILE (~/.auto-gitrc.json):', 'CONFIG'); logger.info(' {'); logger.info(' "apiKey": "your-gemini-api-key",'); logger.info(' "enableSuggestions": true'); logger.info(' }'); logger.space(); logger.info('VERIFICATION COMMANDS:', 'TEST'); logger.info(' auto-git config # Check configuration', ''); logger.info(' auto-git debug # Run diagnostics', ''); logger.info(' auto-git interactive # Test interactive session', ''); }); // Add debug command for troubleshooting program .command('debug') .description('Run system diagnostics') .action(async () => { logger.section('Auto-Git Diagnostics v3.10.4', 'System health check'); try { const config = getConfig(); const interactiveConfig = getInteractiveConfig(); const isRepo = await isGitRepository(); const branch = isRepo ? await getCurrentBranch() : null; const remote = isRepo ? await hasRemote() : false; const diagnostics = { 'Node.js Version': process.version, 'Platform': process.platform, 'Architecture': process.arch, 'Auto-Git Version': '3.10.4', 'Working Directory': process.cwd(), 'Git Repository': isRepo, 'Current Branch': branch || 'N/A', 'Remote Configured': remote, 'API Key Set': !!config.apiKey, 'Interactive Features': interactiveConfig.interactiveOnError, 'AI Suggestions': interactiveConfig.enableSuggestions, 'Session Persistence': '✓ Enabled', 'Markdown Formatting': '✓ Enabled' }; logger.config('SYSTEM DIAGNOSTICS', diagnostics); // Provide recommendations based on diagnostics logger.space(); logger.info('RECOMMENDATIONS:', 'GUIDE'); if (!config.apiKey) { logger.info(' ⚠️ Set up Gemini API key: auto-git setup', ''); } if (!isRepo) { logger.info(' ⚠️ Initialize Git repository: git init', ''); } if (isRepo && !remote) { logger.info(' 💡 Add remote for pushing: git remote add origin <url>', ''); } if (config.apiKey && isRepo) { logger.info(' ✅ Ready to use: auto-git watch or auto-git interactive', ''); } } catch (error) { logger.error('Diagnostics failed', error.message); logger.space(); logger.info('BASIC TROUBLESHOOTING:', 'HELP'); logger.info(' node --version # Check Node.js version', ''); logger.info(' git --version # Check Git installation', ''); logger.info(' pwd # Check current directory', ''); } }); program .command('interactive') .description('Start interactive terminal session with AI error assistance') .action(() => { startInteractiveSession(); }); // Show styled help if no command provided if (process.argv.length <= 2) { displayStyledHelp(); process.exit(0); } program.parse(process.argv);