From 4b1a57e0dbff36d6f3f35dfb61699e4d2f81bd5f Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 08:07:31 +0000 Subject: [PATCH] fix: Add command source files --- src/commands/merge.ts | 158 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/commands/merge.ts diff --git a/src/commands/merge.ts b/src/commands/merge.ts new file mode 100644 index 0000000..3ae3081 --- /dev/null +++ b/src/commands/merge.ts @@ -0,0 +1,158 @@ +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; +}