import * as path from 'path'; import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import { getWorkspacePath, loadWorkspaceConfig, sanitizeAgentName, hasUncommittedChanges, readPackageJson } from '../utils/file-utils'; import { mergeToMain, createGit, hasUncommittedChanges as gitHasUncommittedChanges } from '../utils/git-utils'; import { GitAgentSyncError, WorkspaceNotFoundError, MergeConflictError } from '../utils/errors'; import { ConflictInfo } from '../types/index'; import { MergeOptions } from '../types'; export function createMergeCommand(): Command { const cmd = new Command('merge') .description('Safely merge agent changes back to main branch') .argument('', 'Name of the agent workspace to merge') .option('--force', 'Force merge even with conflicts (use with caution)') .option('--dry-run', 'Preview merge without making changes') .option('--message ', 'Custom merge commit message') .action(async (agentName, options: MergeOptions) => { try { const currentPath = process.cwd(); const sanitizedName = sanitizeAgentName(agentName); const workspacePath = getWorkspacePath(currentPath, sanitizedName); const config = await loadWorkspaceConfig(workspacePath); if (!config) { throw new WorkspaceNotFoundError(sanitizedName); } const spinner = ora('Preparing for merge...').start(); const uncommitted = await gitHasUncommittedChanges(workspacePath); if (uncommitted) { spinner.stop(); console.log(chalk.yellow('\nāš ļø Workspace has uncommitted changes.')); const { shouldCommit } = await inquirer.prompt([ { type: 'confirm', name: 'shouldCommit', message: 'Commit changes before merging?', default: true } ]); if (shouldCommit) { const workspaceGit = createGit(workspacePath); await workspaceGit.add('.'); await workspaceGit.commit('Uncommitted changes before merge'); } } spinner.text = 'Running pre-merge validation...'; const mainBranch = config.mainBranch; const workspaceGit = createGit(workspacePath); const pkgJson = await readPackageJson(workspacePath); let testsPassed = true; if (pkgJson?.scripts?.test && !options.force) { spinner.text = 'Running tests...'; const { execa } = await import('execa'); try { await execa('npm', ['test'], { cwd: workspacePath, reject: false }); } catch { testsPassed = false; } } if (!testsPassed && !options.force) { spinner.fail(chalk.red('Tests failed. Use --force to bypass.')); process.exit(1); } spinner.text = 'Merging changes...'; const mergeResult = await mergeToMain( currentPath, workspacePath, sanitizedName, mainBranch, options.message, options.dryRun ); if (options.dryRun) { spinner.stop(); console.log(chalk.cyan('\nšŸ” Dry Run Result:')); console.log(chalk.gray('─'.repeat(50))); console.log(` ${mergeResult.message}`); console.log(chalk.gray('─'.repeat(50))); return; } if (!mergeResult.success && mergeResult.conflicts) { spinner.fail(chalk.red('Merge conflicts detected!')); console.log(chalk.yellow('\nšŸ“ Conflicting files:')); for (const conflict of mergeResult.conflicts) { console.log(` ${chalk.red(conflict.file)}`); } const { shouldRollback } = await inquirer.prompt([ { type: 'confirm', name: 'shouldRollback', message: 'Rollback merge?', default: true } ]); if (shouldRollback) { const { execa } = await import('execa'); await execa('git', ['merge', '--abort'], { cwd: currentPath }); console.log(chalk.green('\nāœ… Rollback complete. Resolve conflicts and try again.')); } throw new MergeConflictError(mergeResult.conflicts.map((c: ConflictInfo) => c.file)); } spinner.succeed(chalk.green('Merge completed successfully!')); console.log(chalk.cyan('\nšŸ“¦ Merge Summary')); console.log(chalk.gray('─'.repeat(50))); console.log(` Agent: ${chalk.white(sanitizedName)}`); console.log(` From Branch: ${chalk.white(config.branch)}`); console.log(` To Branch: ${chalk.white(mainBranch)}`); console.log(` Status: ${chalk.green('āœ“ Merged')}`); if (mergeResult.commitSha) { console.log(` Commit: ${chalk.white(mergeResult.commitSha.substring(0, 7))}`); } console.log(chalk.gray('─'.repeat(50))); console.log(chalk.cyan('\n✨ You can now delete the workspace with:')); console.log(` ${chalk.white(`git-agent-sync destroy ${sanitizedName}`)}`); } catch (error) { if (error instanceof MergeConflictError) { console.error(chalk.red(`\nāŒ ${error.message}`)); } else { const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error)); console.error(chalk.red(`\nāŒ Error: ${gitError.message}`)); } process.exit(1); } }); return cmd; }