UNPKG

coderrr-cli

Version:

AI-powered coding agent that understands natural language requests and autonomously creates, modifies, and manages code across your projects

319 lines (271 loc) 8.47 kB
const { exec } = require('child_process'); const { promisify } = require('util'); const ui = require('./ui'); const fs = require('fs'); const path = require('path'); const execAsync = promisify(exec); /** * Git operations manager for safety checkpoints and rollback */ class GitOperations { constructor(workingDir = process.cwd()) { this.workingDir = workingDir; this.gitAvailable = null; this.isRepo = null; } /** * Check if git command is available on the system */ async isGitAvailable() { if (this.gitAvailable !== null) { return this.gitAvailable; } try { await execAsync('git --version'); this.gitAvailable = true; return true; } catch (error) { this.gitAvailable = false; return false; } } /** * Check if current directory is a git repository */ async isGitRepository() { if (this.isRepo !== null) { return this.isRepo; } try { await execAsync('git rev-parse --git-dir', { cwd: this.workingDir }); this.isRepo = true; return true; } catch (error) { this.isRepo = false; return false; } } /** * Check if there are uncommitted changes in the working directory */ async hasUncommittedChanges() { try { const { stdout } = await execAsync('git status --porcelain', { cwd: this.workingDir }); return stdout.trim().length > 0; } catch (error) { ui.warning(`Could not check git status: ${error.message}`); return false; } } /** * Get current branch name */ async getCurrentBranch() { try { const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: this.workingDir }); return stdout.trim(); } catch (error) { return 'unknown'; } } /** * Create a checkpoint commit before making changes */ async createCheckpoint(description) { try { // Stage all changes await execAsync('git add .', { cwd: this.workingDir }); // Create checkpoint commit const message = `[Coderrr Checkpoint] Before: ${description}`; await execAsync(`git commit -m "${message}"`, { cwd: this.workingDir }); ui.success(`Created checkpoint: ${description}`); return true; } catch (error) { // If there's nothing to commit, that's okay if (error.message.includes('nothing to commit')) { ui.info('No changes to checkpoint'); return true; } ui.warning(`Could not create checkpoint: ${error.message}`); return false; } } /** * Commit changes after successful operation */ async commitChanges(description) { try { // Stage all changes await execAsync('git add .', { cwd: this.workingDir }); // Check if there's anything to commit const { stdout } = await execAsync('git status --porcelain', { cwd: this.workingDir }); if (stdout.trim().length === 0) { ui.info('No changes to commit'); return true; } // Create commit const message = `[Coderrr] ${description}`; const { stdout: commitOutput } = await execAsync(`git commit -m "${message}"`, { cwd: this.workingDir }); // Extract commit hash const hashMatch = commitOutput.match(/\[.+ ([a-f0-9]+)\]/); const hash = hashMatch ? hashMatch[1] : ''; ui.success(`Auto-committed changes${hash ? ` (${hash})` : ''}`); return true; } catch (error) { ui.warning(`Could not auto-commit: ${error.message}`); return false; } } /** * Get list of recent Coderrr commits for rollback menu */ async getCoderrCommits(limit = 10) { try { const { stdout } = await execAsync( `git log --grep="\\[Coderrr\\]" --grep="\\[Coderrr Checkpoint\\]" --oneline -n ${limit}`, { cwd: this.workingDir } ); if (!stdout.trim()) { return []; } const commits = stdout.trim().split('\n').map(line => { const match = line.match(/^([a-f0-9]+)\s+(.+)$/); if (match) { return { hash: match[1], message: match[2], shortHash: match[1].substring(0, 7) }; } return null; }).filter(Boolean); return commits; } catch (error) { ui.warning(`Could not get commit history: ${error.message}`); return []; } } /** * Get detailed commit information */ async getCommitDetails(commitHash) { try { const { stdout } = await execAsync( `git show --stat --format="%an|%ar|%s" ${commitHash}`, { cwd: this.workingDir } ); const lines = stdout.split('\n'); const [author, timeAgo, subject] = lines[0].split('|'); return { author, timeAgo, subject, hash: commitHash }; } catch (error) { return null; } } /** * Revert a specific commit */ async revertCommit(commitHash) { try { ui.info(`Reverting commit ${commitHash}...`); const { stdout } = await execAsync(`git revert ${commitHash} --no-edit`, { cwd: this.workingDir }); ui.success('Successfully rolled back changes'); return true; } catch (error) { // Handle merge conflicts if (error.message.includes('conflict')) { ui.error('Rollback caused merge conflicts'); ui.warning('Please resolve conflicts manually or run: git revert --abort'); return false; } ui.error(`Rollback failed: ${error.message}`); return false; } } /** * Interactive rollback - show menu and let user select commit to revert */ async interactiveRollback() { // Check git availability const gitAvailable = await this.isGitAvailable(); if (!gitAvailable) { ui.error('Git is not installed or not available'); return false; } const isRepo = await this.isGitRepository(); if (!isRepo) { ui.error('Not a git repository'); return false; } // Get commits ui.info('Fetching Coderrr commits...'); const commits = await this.getCoderrCommits(10); if (commits.length === 0) { ui.warning('No Coderrr commits found to rollback'); return false; } // Show interactive menu ui.section('Available Rollback Points'); const choices = commits.map((commit, idx) => ({ name: `${commit.shortHash} - ${commit.message}`, value: commit.hash })); choices.push({ name: 'Cancel', value: null }); const selected = await ui.select('Select commit to revert:', choices); if (!selected) { ui.info('Rollback cancelled'); return false; } // Get commit details const details = await this.getCommitDetails(selected); if (details) { ui.info(`Commit by: ${details.author} (${details.timeAgo})`); } // Confirm const confirmed = await ui.confirm('This will revert the selected commit. Continue?', false); if (!confirmed) { ui.info('Rollback cancelled'); return false; } // Perform revert return await this.revertCommit(selected); } /** * Validate git setup and show warnings if needed */ async validateGitSetup() { const gitAvailable = await this.isGitAvailable(); if (!gitAvailable) { ui.warning('Git not detected. Auto-commit features disabled.'); ui.info('Install git to enable automatic checkpoints and rollback.'); return false; } const isRepo = await this.isGitRepository(); if (!isRepo) { ui.info('Not a git repository. Auto-commit features disabled.'); return false; } ui.success('Git repository detected'); return true; } /** * Check and warn about uncommitted changes */ async checkUncommittedChanges() { const hasChanges = await this.hasUncommittedChanges(); if (hasChanges) { ui.warning('You have uncommitted changes in your working directory'); const proceed = await ui.confirm('Proceed with Coderrr operation?', true); return proceed; } return true; } } module.exports = GitOperations;