aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/git/worktree.ts179
-rw-r--r--src/main/ipc/handlers.ts45
-rw-r--r--src/main/preload.ts18
3 files changed, 5 insertions, 237 deletions
diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts
deleted file mode 100644
index 3264e5e..0000000
--- a/src/main/git/worktree.ts
+++ /dev/null
@@ -1,179 +0,0 @@
1import { execSync } from "node:child_process";
2import fs from "node:fs";
3import path from "node:path";
4
5export interface GitWorktreeInfo {
6 path: string;
7 branch: string;
8 commit?: string;
9}
10
11/**
12 * Check if a directory is a git repository
13 */
14export function isGitRepo(dir: string): boolean {
15 try {
16 execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
17 return true;
18 } catch {
19 return false;
20 }
21}
22
23/**
24 * Get the default branch name (main or master)
25 */
26export function getDefaultBranch(dir: string): string {
27 try {
28 const branches = execSync("git branch -rl '*/HEAD'", { cwd: dir, stdio: "pipe", encoding: "utf-8" });
29 const match = branches.match(/origin\/(main|master)/);
30 return match?.[1] || "main";
31 } catch {
32 return "main";
33 }
34}
35
36/**
37 * Create a new worktree for a session
38 * Returns the worktree path
39 */
40export function createWorktree(projectPath: string, sessionId: string, baseBranch?: string): string {
41 if (!isGitRepo(projectPath)) {
42 throw new Error("Not a git repository");
43 }
44
45 const branchName = `claude-flow/${sessionId}`;
46 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
47
48 // Ensure parent directory exists
49 fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
50
51 // Create branch from base (or current HEAD)
52 const base = baseBranch || getDefaultBranch(projectPath);
53
54 try {
55 // Create worktree with new branch
56 execSync(
57 `git worktree add -b "${branchName}" "${worktreePath}" ${base}`,
58 { cwd: projectPath, stdio: "pipe" }
59 );
60 } catch (error) {
61 // If branch already exists, just add worktree pointing to it
62 try {
63 execSync(
64 `git worktree add "${worktreePath}" "${branchName}"`,
65 { cwd: projectPath, stdio: "pipe" }
66 );
67 } catch {
68 throw new Error(`Failed to create worktree: ${error}`);
69 }
70 }
71
72 return worktreePath;
73}
74
75/**
76 * Remove a worktree
77 */
78export function removeWorktree(projectPath: string, sessionId: string): void {
79 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
80 const branchName = `claude-flow/${sessionId}`;
81
82 try {
83 // Remove worktree
84 execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath, stdio: "pipe" });
85 } catch {
86 // Worktree might not exist, that's ok
87 }
88
89 try {
90 // Delete branch
91 execSync(`git branch -D "${branchName}"`, { cwd: projectPath, stdio: "pipe" });
92 } catch {
93 // Branch might not exist, that's ok
94 }
95}
96
97/**
98 * Get worktree info for a session
99 */
100export function getWorktreeInfo(projectPath: string, sessionId: string): GitWorktreeInfo | null {
101 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
102
103 if (!fs.existsSync(worktreePath)) {
104 return null;
105 }
106
107 try {
108 const branch = execSync("git rev-parse --abbrev-ref HEAD", {
109 cwd: worktreePath,
110 encoding: "utf-8"
111 }).trim();
112
113 const commit = execSync("git rev-parse --short HEAD", {
114 cwd: worktreePath,
115 encoding: "utf-8"
116 }).trim();
117
118 return { path: worktreePath, branch, commit };
119 } catch {
120 return null;
121 }
122}
123
124/**
125 * Commit changes in a worktree
126 */
127export function commitChanges(
128 worktreePath: string,
129 message: string,
130 files: string[] = ["."]
131): void {
132 try {
133 // Stage files
134 execSync(`git add ${files.join(" ")}`, { cwd: worktreePath, stdio: "pipe" });
135
136 // Commit
137 execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: worktreePath, stdio: "pipe" });
138 } catch (error) {
139 throw new Error(`Failed to commit: ${error}`);
140 }
141}
142
143/**
144 * Check if there are uncommitted changes
145 */
146export function hasUncommittedChanges(worktreePath: string): boolean {
147 try {
148 const status = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf-8" });
149 return status.trim().length > 0;
150 } catch {
151 return false;
152 }
153}
154
155/**
156 * Get diff summary of uncommitted changes
157 */
158export function getDiffSummary(worktreePath: string): string {
159 try {
160 return execSync("git diff --stat", { cwd: worktreePath, encoding: "utf-8" });
161 } catch {
162 return "";
163 }
164}
165
166/**
167 * Get the main project path from a worktree path
168 */
169export function getMainRepoPath(worktreePath: string): string {
170 try {
171 const gitDir = execSync("git rev-parse --git-common-dir", {
172 cwd: worktreePath,
173 encoding: "utf-8"
174 }).trim();
175 return path.dirname(gitDir);
176 } catch {
177 return worktreePath;
178 }
179}
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts
index bc0d024..2d5e3d3 100644
--- a/src/main/ipc/handlers.ts
+++ b/src/main/ipc/handlers.ts
@@ -3,7 +3,6 @@ import * as projects from "../db/projects";
3import * as sessions from "../db/sessions"; 3import * as sessions from "../db/sessions";
4import * as claude from "../claude"; 4import * as claude from "../claude";
5import type { UserPermissionMode } from "../claude/phases"; 5import type { UserPermissionMode } from "../claude/phases";
6import * as git from "../git/worktree";
7 6
8export function registerIpcHandlers(mainWindow: BrowserWindow): void { 7export function registerIpcHandlers(mainWindow: BrowserWindow): void {
9 // Projects 8 // Projects
@@ -19,35 +18,16 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void {
19 ipcMain.handle("sessions:list", (_, projectId: string) => 18 ipcMain.handle("sessions:list", (_, projectId: string) =>
20 sessions.listSessions(projectId) 19 sessions.listSessions(projectId)
21 ); 20 );
22 21
23 ipcMain.handle("sessions:create", async (_, projectId: string, name: string) => { 22 ipcMain.handle("sessions:create", (_, projectId: string, name: string) =>
24 const session = sessions.createSession(projectId, name); 23 sessions.createSession(projectId, name)
25 const project = projects.getProject(projectId); 24 );
26
27 if (project && git.isGitRepo(project.path)) {
28 try {
29 // Create git worktree for this session
30 git.createWorktree(project.path, session.id);
31 } catch (error) {
32 console.error("Failed to create worktree:", error);
33 // Continue without worktree - not fatal
34 }
35 }
36
37 return session;
38 });
39 25
40 ipcMain.handle("sessions:delete", (_, id: string) => { 26 ipcMain.handle("sessions:delete", (_, id: string) => {
41 const session = sessions.getSession(id); 27 const session = sessions.getSession(id);
42 if (session) { 28 if (session) {
43 const project = projects.getProject(session.project_id); 29 const project = projects.getProject(session.project_id);
44 if (project) { 30 if (project) {
45 // Clean up worktree if exists
46 try {
47 git.removeWorktree(project.path, id);
48 } catch {
49 // Worktree might not exist, that's ok
50 }
51 // Clean up session artifacts 31 // Clean up session artifacts
52 claude.clearSessionArtifacts(project.path, id); 32 claude.clearSessionArtifacts(project.path, id);
53 } 33 }
@@ -162,23 +142,6 @@ export function registerIpcHandlers(mainWindow: BrowserWindow): void {
162 } 142 }
163 ); 143 );
164 144
165 // Git
166 ipcMain.handle("git:isRepo", (_, projectPath: string) => {
167 return git.isGitRepo(projectPath);
168 });
169
170 ipcMain.handle("git:worktreeInfo", (_, projectPath: string, sessionId: string) => {
171 return git.getWorktreeInfo(projectPath, sessionId);
172 });
173
174 ipcMain.handle("git:commit", (_, worktreePath: string, message: string, files?: string[]) => {
175 git.commitChanges(worktreePath, message, files);
176 });
177
178 ipcMain.handle("git:hasChanges", (_, worktreePath: string) => {
179 return git.hasUncommittedChanges(worktreePath);
180 });
181
182 // Dialogs 145 // Dialogs
183 ipcMain.handle("dialog:selectDirectory", async () => { 146 ipcMain.handle("dialog:selectDirectory", async () => {
184 const result = await dialog.showOpenDialog(mainWindow, { 147 const result = await dialog.showOpenDialog(mainWindow, {
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 1747763..7c1d634 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -3,7 +3,6 @@ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
3import type { Project } from "./db/projects"; 3import type { Project } from "./db/projects";
4import type { Session, Message } from "./db/sessions"; 4import type { Session, Message } from "./db/sessions";
5import type { Phase, UserPermissionMode } from "./claude/phases"; 5import type { Phase, UserPermissionMode } from "./claude/phases";
6import type { GitWorktreeInfo } from "./git/worktree";
7 6
8export interface ClaudeFlowAPI { 7export interface ClaudeFlowAPI {
9 // Projects 8 // Projects
@@ -32,7 +31,7 @@ export interface ClaudeFlowAPI {
32 mode: UserPermissionMode 31 mode: UserPermissionMode
33 ) => Promise<void>; 32 ) => Promise<void>;
34 33
35 // Session Artifacts (new session-specific) 34 // Session Artifacts (session-specific)
36 readSessionArtifact: ( 35 readSessionArtifact: (
37 projectPath: string, 36 projectPath: string,
38 sessionId: string, 37 sessionId: string,
@@ -60,12 +59,6 @@ export interface ClaudeFlowAPI {
60 content: string 59 content: string
61 ) => Promise<void>; 60 ) => Promise<void>;
62 61
63 // Git
64 isGitRepo: (projectPath: string) => Promise<boolean>;
65 getWorktreeInfo: (projectPath: string, sessionId: string) => Promise<GitWorktreeInfo | null>;
66 commitChanges: (worktreePath: string, message: string, files?: string[]) => Promise<void>;
67 hasUncommittedChanges: (worktreePath: string) => Promise<boolean>;
68
69 // Events 62 // Events
70 onClaudeMessage: ( 63 onClaudeMessage: (
71 callback: (sessionId: string, message: SDKMessage) => void 64 callback: (sessionId: string, message: SDKMessage) => void
@@ -121,15 +114,6 @@ const api: ClaudeFlowAPI = {
121 writeArtifact: (projectPath, filename, content) => 114 writeArtifact: (projectPath, filename, content) =>
122 ipcRenderer.invoke("artifact:write", projectPath, filename, content), 115 ipcRenderer.invoke("artifact:write", projectPath, filename, content),
123 116
124 // Git
125 isGitRepo: (projectPath) => ipcRenderer.invoke("git:isRepo", projectPath),
126 getWorktreeInfo: (projectPath, sessionId) =>
127 ipcRenderer.invoke("git:worktreeInfo", projectPath, sessionId),
128 commitChanges: (worktreePath, message, files) =>
129 ipcRenderer.invoke("git:commit", worktreePath, message, files),
130 hasUncommittedChanges: (worktreePath) =>
131 ipcRenderer.invoke("git:hasChanges", worktreePath),
132
133 // Events 117 // Events
134 onClaudeMessage: (callback) => { 118 onClaudeMessage: (callback) => {
135 const handler = ( 119 const handler = (