import { execSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; export interface GitWorktreeInfo { path: string; branch: string; commit?: string; } /** * Check if a directory is a git repository */ export function isGitRepo(dir: string): boolean { try { execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" }); return true; } catch { return false; } } /** * Get the default branch name (main or master) */ export function getDefaultBranch(dir: string): string { try { const branches = execSync("git branch -rl '*/HEAD'", { cwd: dir, stdio: "pipe", encoding: "utf-8" }); const match = branches.match(/origin\/(main|master)/); return match?.[1] || "main"; } catch { return "main"; } } /** * Create a new worktree for a session * Returns the worktree path */ export function createWorktree(projectPath: string, sessionId: string, baseBranch?: string): string { if (!isGitRepo(projectPath)) { throw new Error("Not a git repository"); } const branchName = `claude-flow/${sessionId}`; const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId); // Ensure parent directory exists fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); // Create branch from base (or current HEAD) const base = baseBranch || getDefaultBranch(projectPath); try { // Create worktree with new branch execSync( `git worktree add -b "${branchName}" "${worktreePath}" ${base}`, { cwd: projectPath, stdio: "pipe" } ); } catch (error) { // If branch already exists, just add worktree pointing to it try { execSync( `git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectPath, stdio: "pipe" } ); } catch { throw new Error(`Failed to create worktree: ${error}`); } } return worktreePath; } /** * Remove a worktree */ export function removeWorktree(projectPath: string, sessionId: string): void { const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId); const branchName = `claude-flow/${sessionId}`; try { // Remove worktree execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath, stdio: "pipe" }); } catch { // Worktree might not exist, that's ok } try { // Delete branch execSync(`git branch -D "${branchName}"`, { cwd: projectPath, stdio: "pipe" }); } catch { // Branch might not exist, that's ok } } /** * Get worktree info for a session */ export function getWorktreeInfo(projectPath: string, sessionId: string): GitWorktreeInfo | null { const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId); if (!fs.existsSync(worktreePath)) { return null; } try { const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: worktreePath, encoding: "utf-8" }).trim(); const commit = execSync("git rev-parse --short HEAD", { cwd: worktreePath, encoding: "utf-8" }).trim(); return { path: worktreePath, branch, commit }; } catch { return null; } } /** * Commit changes in a worktree */ export function commitChanges( worktreePath: string, message: string, files: string[] = ["."] ): void { try { // Stage files execSync(`git add ${files.join(" ")}`, { cwd: worktreePath, stdio: "pipe" }); // Commit execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: worktreePath, stdio: "pipe" }); } catch (error) { throw new Error(`Failed to commit: ${error}`); } } /** * Check if there are uncommitted changes */ export function hasUncommittedChanges(worktreePath: string): boolean { try { const status = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf-8" }); return status.trim().length > 0; } catch { return false; } } /** * Get diff summary of uncommitted changes */ export function getDiffSummary(worktreePath: string): string { try { return execSync("git diff --stat", { cwd: worktreePath, encoding: "utf-8" }); } catch { return ""; } } /** * Get the main project path from a worktree path */ export function getMainRepoPath(worktreePath: string): string { try { const gitDir = execSync("git rev-parse --git-common-dir", { cwd: worktreePath, encoding: "utf-8" }).trim(); return path.dirname(gitDir); } catch { return worktreePath; } }