cortexweaver
Version:
CortexWeaver is a command-line interface (CLI) tool that orchestrates a swarm of specialized AI agents, powered by Claude Code and Gemini CLI, to assist in software development. It transforms a high-level project plan (plan.md) into a series of coordinate
407 lines • 17.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkspaceOperations = void 0;
const fs = __importStar(require("fs"));
const fsp = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const file_analyzer_1 = require("./file-analyzer");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
/**
* WorkspaceOperations handles all workspace operations including worktree management,
* file operations, and code analysis
*/
class WorkspaceOperations {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.worktreesDir = path.join(projectRoot, 'worktrees');
this.fileAnalyzer = new file_analyzer_1.FileAnalyzer(projectRoot);
// Ensure worktrees directory exists
if (!fs.existsSync(this.worktreesDir)) {
fs.mkdirSync(this.worktreesDir, { recursive: true });
}
}
getWorktreePath(taskId) {
return path.join(this.worktreesDir, taskId);
}
async createWorktree(taskId, branchName, baseBranch = 'main') {
const worktreePath = this.getWorktreePath(taskId);
// Check if worktree already exists
if (fs.existsSync(worktreePath)) {
throw new Error(`Worktree for task ${taskId} already exists at ${worktreePath}`);
}
// Validate branch names
if (!this.isValidBranchName(branchName)) {
throw new Error(`Invalid branch name: ${branchName}`);
}
let originalBranch = 'main'; // Default fallback
try {
// Get current branch
const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: this.projectRoot });
originalBranch = currentBranch.trim() || 'main';
// Ensure we're working with a clean state
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: this.projectRoot });
if (statusOutput.trim() !== '') {
throw new Error('Working directory has uncommitted changes. Please commit or stash them first.');
}
// Fetch latest from remote to ensure base branch is up to date
try {
await execAsync(`git fetch origin ${baseBranch}`, { cwd: this.projectRoot });
}
catch (fetchError) {
console.warn(`Warning: Could not fetch latest ${baseBranch} from remote`);
}
// Check if base branch exists locally
try {
await execAsync(`git show-ref --verify --quiet refs/heads/${baseBranch}`, { cwd: this.projectRoot });
}
catch (branchError) {
// Try to create local branch from remote
try {
await execAsync(`git checkout -b ${baseBranch} origin/${baseBranch}`, { cwd: this.projectRoot });
await execAsync(`git checkout ${originalBranch}`, { cwd: this.projectRoot });
}
catch (remoteError) {
throw new Error(`Base branch ${baseBranch} not found locally or on remote`);
}
}
// Check if branch already exists
try {
await execAsync(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd: this.projectRoot });
throw new Error(`Branch ${branchName} already exists`);
}
catch (branchCheckError) {
// Branch doesn't exist, which is what we want
}
// Create new branch from base branch
await execAsync(`git checkout -b ${branchName} ${baseBranch}`, { cwd: this.projectRoot });
// Create worktree
await execAsync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: this.projectRoot });
// Return to original branch
await execAsync(`git checkout ${originalBranch}`, { cwd: this.projectRoot });
// Verify worktree was created successfully
if (!fs.existsSync(worktreePath)) {
throw new Error('Worktree directory was not created');
}
return {
id: taskId,
path: worktreePath,
branch: branchName,
baseBranch: baseBranch
};
}
catch (error) {
// Comprehensive cleanup on failure
await this.cleanupFailedWorktree(worktreePath, branchName, originalBranch);
throw new Error(`Failed to create worktree: ${error.message}`);
}
}
async cleanupFailedWorktree(worktreePath, branchName, originalBranch) {
const cleanupTasks = [];
// Remove worktree directory if it exists
if (fs.existsSync(worktreePath)) {
cleanupTasks.push(execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: this.projectRoot })
.catch(() => {
// If git worktree remove fails, try manual cleanup
try {
fs.rmSync(worktreePath, { recursive: true, force: true });
}
catch (rmError) {
console.warn(`Failed to manually remove worktree directory: ${rmError}`);
}
}));
}
// Remove branch if it was created
cleanupTasks.push(execAsync(`git branch -D ${branchName}`, { cwd: this.projectRoot })
.catch(() => { }) // Ignore errors if branch doesn't exist
);
// Return to original branch if specified
if (originalBranch) {
cleanupTasks.push(execAsync(`git checkout ${originalBranch}`, { cwd: this.projectRoot })
.catch(() => { }) // Ignore errors if checkout fails
);
}
await Promise.allSettled(cleanupTasks);
}
isValidBranchName(branchName) {
// Git branch naming rules
if (!branchName || branchName.length === 0)
return false;
if (branchName.startsWith('.') || branchName.endsWith('.'))
return false;
if (branchName.startsWith('-') || branchName.endsWith('-'))
return false;
if (branchName.includes('..') || branchName.includes('//'))
return false;
if (branchName.includes(' ') || branchName.includes('\t'))
return false;
if (/[~^:?*\[\]\\]/.test(branchName))
return false;
if (branchName.includes('@{'))
return false;
return true;
}
async removeWorktree(taskId) {
const worktreePath = this.getWorktreePath(taskId);
if (!fs.existsSync(worktreePath)) {
return false;
}
try {
// Remove worktree
await execAsync(`git worktree remove ${worktreePath}`, { cwd: this.projectRoot });
return true;
}
catch (error) {
console.error(`Failed to remove worktree: ${error.message}`);
return false;
}
}
async listWorktrees() {
try {
const { stdout } = await execAsync('git worktree list --porcelain', { cwd: this.projectRoot });
const worktrees = [];
// Parse git worktree list output
const lines = stdout.trim().split('\n');
let currentWorktree = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
const worktreePath = line.substring(9);
const taskId = path.basename(worktreePath);
currentWorktree = { id: taskId, path: worktreePath };
}
else if (line.startsWith('branch ')) {
const branch = line.substring(7);
if (currentWorktree.path) {
currentWorktree.branch = branch;
// Extract task ID from path
const taskId = path.basename(currentWorktree.path);
worktrees.push({
id: taskId,
path: currentWorktree.path,
branch: branch,
baseBranch: 'main' // Default, could be enhanced to track actual base
});
}
currentWorktree = {};
}
}
return worktrees.filter(w => w.path && w.path.includes('worktrees'));
}
catch (error) {
console.error(`Failed to list worktrees: ${error.message}`);
return [];
}
}
async executeCommand(taskId, command) {
const worktreePath = this.getWorktreePath(taskId);
if (!fs.existsSync(worktreePath)) {
throw new Error(`Worktree for task ${taskId} does not exist`);
}
try {
const { stdout, stderr } = await execAsync(command, { cwd: worktreePath });
return {
stdout,
stderr,
exitCode: 0
};
}
catch (error) {
return {
stdout: error.stdout || '',
stderr: error.stderr || error.message,
exitCode: error.code || 1
};
}
}
async commitChanges(taskId, message, files) {
const worktreePath = this.getWorktreePath(taskId);
if (!fs.existsSync(worktreePath)) {
throw new Error(`Worktree for task ${taskId} does not exist`);
}
try {
// Add files to staging
if (files && files.length > 0) {
await execAsync(`git add ${files.join(' ')}`, { cwd: worktreePath });
}
else {
await execAsync('git add .', { cwd: worktreePath });
}
// Commit changes
const { stdout } = await execAsync(`git commit -m "${message}"`, { cwd: worktreePath });
// Extract commit hash
const { stdout: hashOutput } = await execAsync('git rev-parse HEAD', { cwd: worktreePath });
return hashOutput.trim();
}
catch (error) {
throw new Error(`Failed to commit changes: ${error.message}`);
}
}
async mergeToBranch(taskId, targetBranch = 'main') {
const worktrees = await this.listWorktrees();
const worktree = worktrees.find(w => w.id === taskId);
if (!worktree) {
throw new Error(`Worktree for task ${taskId} not found`);
}
try {
// Switch to target branch
await execAsync(`git checkout ${targetBranch}`, { cwd: this.projectRoot });
// Merge the feature branch
await execAsync(`git merge ${worktree.branch}`, { cwd: this.projectRoot });
// Clean up the feature branch
await execAsync(`git branch -d ${worktree.branch}`, { cwd: this.projectRoot });
console.log(`Successfully merged ${worktree.branch} into ${targetBranch}`);
}
catch (error) {
throw new Error(`Failed to merge branch: ${error.message}`);
}
}
async getWorktreeStatus(taskId) {
try {
const result = await this.executeCommand(taskId, 'git status --porcelain');
const files = result.stdout.trim().split('\n').filter(line => line.trim() !== '');
return {
clean: files.length === 0,
files: files
};
}
catch (error) {
throw new Error(`Failed to get worktree status: ${error.message}`);
}
}
/**
* Scan workspace for code files with optional filtering
*/
async scanCodeFiles(options = {}) {
const defaults = {
includeTests: true,
includeDocs: true,
includeConfig: true,
maxFileSize: 1024 * 1024, // 1MB
extensions: ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.cpp', '.c', '.cs', '.go', '.rs', '.rb', '.php'],
excludePaths: ['node_modules', '.git', 'dist', 'build', 'coverage', 'worktrees']
};
const opts = { ...defaults, ...options };
const files = [];
try {
await this.scanDirectory(this.projectRoot, this.projectRoot, files, opts);
return files;
}
catch (error) {
console.error('Failed to scan code files:', error);
return [];
}
}
/**
* Get specific code files by paths
*/
async getCodeFiles(filePaths, includeContent = false) {
const files = [];
for (const filePath of filePaths) {
try {
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectRoot, filePath);
const fileInfo = await this.fileAnalyzer.analyzeCodeFile(fullPath, this.projectRoot, includeContent);
if (fileInfo) {
files.push(fileInfo);
}
}
catch (error) {
console.warn(`Failed to analyze file ${filePath}:`, error);
}
}
return files;
}
/**
* Search for files matching a pattern or containing specific text
*/
async searchFiles(pattern, options = {}) {
const allFiles = await this.scanCodeFiles(options);
const searchRegex = new RegExp(pattern, 'i');
return allFiles.filter(file => searchRegex.test(file.relativePath) ||
searchRegex.test(path.basename(file.path)));
}
/**
* Get recently modified files
*/
async getRecentlyModifiedFiles(sinceHours = 24, options = {}) {
const allFiles = await this.scanCodeFiles(options);
const cutoffTime = new Date(Date.now() - (sinceHours * 60 * 60 * 1000));
return allFiles
.filter(file => file.lastModified > cutoffTime)
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
}
/**
* Get files by language
*/
async getFilesByLanguage(language, options = {}) {
const allFiles = await this.scanCodeFiles(options);
return allFiles.filter(file => file.language.toLowerCase() === language.toLowerCase());
}
/**
* Read file content safely
*/
async readFileContent(filePath) {
return this.fileAnalyzer.readFileContent(filePath);
}
async scanDirectory(dirPath, rootPath, files, options) {
try {
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativePath = path.relative(rootPath, fullPath);
// Skip excluded paths
if (options.excludePaths?.some(excluded => relativePath.includes(excluded))) {
continue;
}
if (entry.isDirectory()) {
// Recursively scan subdirectories
await this.scanDirectory(fullPath, rootPath, files, options);
}
else if (entry.isFile()) {
const fileInfo = await this.fileAnalyzer.analyzeCodeFile(fullPath, rootPath, false);
if (fileInfo && this.fileAnalyzer.shouldIncludeFile(fileInfo, options)) {
files.push(fileInfo);
}
}
}
}
catch (error) {
console.warn(`Failed to scan directory ${dirPath}:`, error);
}
}
}
exports.WorkspaceOperations = WorkspaceOperations;
//# sourceMappingURL=workspace-operations.js.map