feat: add create command
Some checks failed
/ test (push) Has been cancelled

This commit is contained in:
2026-02-03 09:03:57 +00:00
parent 5a8386abb1
commit 1c965cd1f9

View File

@@ -1,145 +1,67 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { Command } from 'commander';
import ora from 'ora';
import chalk from 'chalk';
import {
createWorktree,
createGit,
getMainBranch
} from '../utils/git-utils';
import {
loadGlobalConfig,
saveWorkspaceConfig,
getWorkspacePath,
isValidAgentName,
detectPackageJson,
installDependencies,
getGitUserInfo
} from '../utils/file-utils';
import { setupEnvironment, runCustomSetupScript } from '../utils/env-utils';
import { sanitizeAgentName } from '../utils/file-utils';
import { createWorktree, createGit, getMainBranch } from '../utils/git-utils';
import { loadGlobalConfig, saveWorkspaceConfig, getWorkspacePath, isValidAgentName, detectPackageJson, installDependencies, getGitUserInfo, sanitizeAgentName } from '../utils/file-utils';
import { setupEnvironment } from '../utils/env-utils';
import { GitAgentSyncError, InvalidAgentNameError } from '../utils/errors';
import { CreateOptions } from '../types';
export function createCreateCommand(): Command {
const cmd = new Command('create')
.description('Create an isolated git worktree for an AI agent')
.argument('<agent-name>', 'Name identifier for the agent (will become agent-{name})')
.option('-p, --path <path>', 'Custom path for the workspace')
.option('-t, --template <template>', 'Path to environment template directory')
.option('--no-install', 'Skip automatic dependency installation')
.option('--env <key=value>', 'Set environment variable (can be used multiple times)', collectEnvVars)
.action(async (agentName, options) => {
try {
const sanitizedName = sanitizeAgentName(agentName);
if (!isValidAgentName(sanitizedName)) {
throw new InvalidAgentNameError(agentName);
const cmd = new Command('create').description('Create an isolated git worktree for an AI agent').argument('<agent-name>', 'Name identifier for the agent (will become agent-{name})').option('-p, --path <path>', 'Custom path for the workspace').option('-t, --template <template>', 'Path to environment template directory').option('--no-install', 'Skip automatic dependency installation').option('--env <key=value>', 'Set environment variable', collectEnvVars).action(async (agentName, options) => {
try {
const sanitizedName = sanitizeAgentName(agentName);
if (!isValidAgentName(sanitizedName)) throw new InvalidAgentNameError(agentName);
const currentPath = process.cwd();
const spinner = ora('Initializing...').start();
const globalConfig = await loadGlobalConfig();
const branchName = `agent-${sanitizedName}`;
const workspacePath = options.path ? path.resolve(currentPath, options.path) : getWorkspacePath(currentPath, sanitizedName);
spinner.text = 'Checking git repository...';
const git = createGit(currentPath);
try { await git.raw(['rev-parse', '--is-inside-work-tree']); } catch { throw new GitAgentSyncError('Not a git repository.'); }
spinner.text = 'Checking workspace existence...';
if (await fs.pathExists(workspacePath)) throw new GitAgentSyncError(`Workspace already exists at ${workspacePath}.`);
spinner.text = 'Determining main branch...';
const mainBranch = await getMainBranch(git, globalConfig.defaultBranch);
spinner.text = 'Creating worktree...';
await createWorktree(currentPath, workspacePath, branchName, mainBranch);
const gitUserInfo = await getGitUserInfo();
const createOptions: CreateOptions = { path: options.path, template: options.template, installDeps: options.install !== false, environment: parseEnvVars(options.env || []) };
spinner.text = 'Setting up environment...';
await setupEnvironment(workspacePath, sanitizedName, createOptions);
if (createOptions.installDeps) {
spinner.text = 'Checking for dependencies...';
if (await detectPackageJson(workspacePath)) {
spinner.text = 'Installing dependencies...';
await installDependencies(workspacePath);
}
const currentPath = process.cwd();
const spinner = ora('Initializing...').start();
const globalConfig = await loadGlobalConfig();
const branchName = `agent-${sanitizedName}`;
const workspacePath = options.path
? path.resolve(currentPath, options.path)
: getWorkspacePath(currentPath, sanitizedName);
spinner.text = 'Checking git repository...';
const git = createGit(currentPath);
try {
await git.raw(['rev-parse', '--is-inside-work-tree']);
} catch {
throw new GitAgentSyncError('Not a git repository. Please run this command from within a git repository.');
}
spinner.text = 'Checking workspace existence...';
if (await fs.pathExists(workspacePath)) {
throw new GitAgentSyncError(`Workspace already exists at ${workspacePath}. Use a different agent name or destroy the existing workspace first.`);
}
spinner.text = 'Determining main branch...';
const mainBranch = await getMainBranch(git, globalConfig.defaultBranch);
spinner.text = 'Creating worktree...';
await createWorktree(currentPath, workspacePath, branchName, mainBranch);
const gitUserInfo = await getGitUserInfo();
const createOptions: CreateOptions = {
path: options.path,
template: options.template,
installDeps: options.install !== false,
environment: parseEnvVars(options.env || [])
};
spinner.text = 'Setting up environment...';
await setupEnvironment(workspacePath, sanitizedName, createOptions);
if (createOptions.installDeps) {
spinner.text = 'Checking for dependencies...';
if (await detectPackageJson(workspacePath)) {
spinner.text = 'Installing dependencies...';
await installDependencies(workspacePath);
}
}
const workspaceConfig = {
agentName: sanitizedName,
branch: branchName,
createdAt: new Date().toISOString(),
mainBranch,
environment: {
AGENT_ID: sanitizedName,
WORKSPACE_PATH: workspacePath,
GIT_USER_EMAIL: gitUserInfo.email,
GIT_USER_NAME: gitUserInfo.name
}
};
await saveWorkspaceConfig(workspacePath, workspaceConfig);
spinner.succeed(chalk.green('Workspace created successfully!'));
console.log(chalk.cyan('\n📁 Workspace Details:'));
console.log(` Agent Name: ${chalk.white(sanitizedName)}`);
console.log(` Branch: ${chalk.white(branchName)}`);
console.log(` Path: ${chalk.white(workspacePath)}`);
console.log(` Main Branch: ${chalk.white(mainBranch)}`);
console.log(` Created: ${chalk.white(new Date().toISOString())}`);
console.log(chalk.cyan('\n📝 Next Steps:'));
console.log(` 1. ${chalk.white(\`cd ${workspacePath}\`)}`);
console.log(` 2. ${chalk.white('Start your coding tasks')}`);
console.log(` 3. ${chalk.white('git-agent-sync status')} - Track changes`);
console.log(` 4. ${chalk.white('git-agent-sync diff')} - Generate diffs`);
console.log(` 5. ${chalk.white('git-agent-sync merge')} - Merge back to main`);
} catch (error) {
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
console.error(chalk.red(`\n Error: ${gitError.message}`));
process.exit(1);
}
});
const workspaceConfig = { agentName: sanitizedName, branch: branchName, createdAt: new Date().toISOString(), mainBranch, environment: { AGENT_ID: sanitizedName, WORKSPACE_PATH: workspacePath, GIT_USER_EMAIL: gitUserInfo.email, GIT_USER_NAME: gitUserInfo.name } };
await saveWorkspaceConfig(workspacePath, workspaceConfig);
spinner.succeed(chalk.green('Workspace created successfully!'));
console.log(chalk.cyan('\n📁 Workspace Details:'));
console.log(` Agent Name: ${chalk.white(sanitizedName)}`);
console.log(` Branch: ${chalk.white(branchName)}`);
console.log(` Path: ${chalk.white(workspacePath)}`);
console.log(` Main Branch: ${chalk.white(mainBranch)}`);
} catch (error) {
const gitError = error instanceof GitAgentSyncError ? error : new GitAgentSyncError(String(error));
console.error(chalk.red(`\n❌ Error: ${gitError.message}`));
process.exit(1);
}
});
return cmd;
}
function collectEnvVars(value: string, previous: string[]): string[] {
return [...(previous || []), value];
}
function collectEnvVars(value: string, previous: string[]): string[] { return [...(previous || []), value]; }
function parseEnvVars(envVars: string[]): Record<string, string> {
const result: Record<string, string> = {};
for (const envVar of envVars) {
const [key, ...valueParts] = envVar.split('=');
if (key && valueParts.length > 0) {
result[key] = valueParts.join('=');
}
if (key && valueParts.length > 0) result[key] = valueParts.join('=');
}
return result;
}
async function fs.pathExists(path: string): Promise<boolean> { return require('fs-extra').pathExists(path); }