159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
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;
|
||
}
|