Files
git-agent-sync/.src/commands/merge.ts

159 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('<agent-name>', '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 <msg>', '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;
}