UNPKG

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
"use strict"; 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