From 0edd7235cd861ef77d4ceb37a594ae65df52624b Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 28 Feb 2026 15:35:00 -0800 Subject: Add session-specific artifacts, CLAUDE.md, and git worktree support - Store artifacts in .claude-flow/sessions/{sessionId}/ - Each session now has isolated research.md and plan.md - Concurrent sessions no longer conflict - Add CLAUDE.md support for shared codebase documentation - Add git worktree creation on session start - Add git commit/status IPC handlers - Update all artifact APIs to be session-specific - Remove artifact clearing on new session (no longer needed) --- src/main/git/worktree.ts | 179 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/main/git/worktree.ts (limited to 'src/main/git') diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts new file mode 100644 index 0000000..3264e5e --- /dev/null +++ b/src/main/git/worktree.ts @@ -0,0 +1,179 @@ +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; + } +} -- cgit v1.2.3