Skip to content

Commit 2f4fc39

Browse files
committed
clean up orphaned worktrees on launch
1 parent 207b861 commit 2f4fc39

File tree

5 files changed

+141
-0
lines changed

5 files changed

+141
-0
lines changed

apps/array/src/main/preload.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
436436
ipcRenderer.invoke("remove-task-association", taskId),
437437
clearTaskWorktree: (taskId: string): Promise<void> =>
438438
ipcRenderer.invoke("clear-task-worktree", taskId),
439+
cleanupOrphanedWorktrees: (
440+
mainRepoPath: string,
441+
): Promise<{
442+
deleted: string[];
443+
errors: Array<{ path: string; error: string }>;
444+
}> => ipcRenderer.invoke("cleanup-orphaned-worktrees", mainRepoPath),
439445
},
440446
// Worktree API
441447
worktree: {

apps/array/src/main/services/folders.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,27 @@ async function clearTaskWorktree(taskId: string): Promise<void> {
141141
}
142142
}
143143

144+
async function cleanupOrphanedWorktreesForFolder(
145+
mainRepoPath: string,
146+
): Promise<{
147+
deleted: string[];
148+
errors: Array<{ path: string; error: string }>;
149+
}> {
150+
const WorktreeManager = (await import("@posthog/agent")).WorktreeManager;
151+
const manager = new WorktreeManager({ mainRepoPath });
152+
153+
const associations = foldersStore.get("taskAssociations", []);
154+
const associatedWorktreePaths: string[] = [];
155+
156+
for (const assoc of associations) {
157+
if (assoc.worktree?.worktreePath) {
158+
associatedWorktreePaths.push(assoc.worktree.worktreePath);
159+
}
160+
}
161+
162+
return await manager.cleanupOrphanedWorktrees(associatedWorktreePaths);
163+
}
164+
144165
export function registerFoldersIpc(): void {
145166
ipcMain.handle(
146167
"get-folders",
@@ -292,4 +313,28 @@ export function registerFoldersIpc(): void {
292313
}
293314
},
294315
);
316+
317+
ipcMain.handle(
318+
"cleanup-orphaned-worktrees",
319+
async (
320+
_event: IpcMainInvokeEvent,
321+
mainRepoPath: string,
322+
): Promise<{
323+
deleted: string[];
324+
errors: Array<{ path: string; error: string }>;
325+
}> => {
326+
try {
327+
return await cleanupOrphanedWorktreesForFolder(mainRepoPath);
328+
} catch (error) {
329+
console.error(
330+
`Failed to cleanup orphaned worktrees for ${mainRepoPath}:`,
331+
error,
332+
);
333+
return {
334+
deleted: [],
335+
errors: [{ path: mainRepoPath, error: String(error) }],
336+
};
337+
}
338+
},
339+
);
295340
}

apps/array/src/renderer/stores/registeredFoldersStore.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ interface RegisteredFoldersState {
99
removeFolder: (folderId: string) => Promise<void>;
1010
updateLastAccessed: (folderId: string) => Promise<void>;
1111
getFolderByPath: (path: string) => RegisteredFolder | undefined;
12+
cleanupOrphanedWorktrees: (
13+
mainRepoPath: string,
14+
) => Promise<{
15+
deleted: string[];
16+
errors: Array<{ path: string; error: string }>;
17+
}>;
1218
}
1319

1420
let updateDebounceTimer: ReturnType<typeof setTimeout> | null = null;
@@ -34,6 +40,17 @@ export const useRegisteredFoldersStore = create<RegisteredFoldersState>()(
3440
try {
3541
const folders = await loadFolders();
3642
set({ folders, isLoaded: true });
43+
44+
for (const folder of folders) {
45+
get()
46+
.cleanupOrphanedWorktrees(folder.path)
47+
.catch((error) => {
48+
console.error(
49+
`Failed to cleanup orphaned worktrees for ${folder.path}:`,
50+
error,
51+
);
52+
});
53+
}
3754
} catch (error) {
3855
console.error("Failed to load folders:", error);
3956
set({ folders: [], isLoaded: true });
@@ -106,6 +123,20 @@ export const useRegisteredFoldersStore = create<RegisteredFoldersState>()(
106123
getFolderByPath: (path: string) => {
107124
return get().folders.find((f) => f.path === path);
108125
},
126+
127+
cleanupOrphanedWorktrees: async (mainRepoPath: string) => {
128+
try {
129+
return await window.electronAPI.folders.cleanupOrphanedWorktrees(
130+
mainRepoPath,
131+
);
132+
} catch (error) {
133+
console.error("Failed to cleanup orphaned worktrees:", error);
134+
return {
135+
deleted: [],
136+
errors: [{ path: mainRepoPath, error: String(error) }],
137+
};
138+
}
139+
},
109140
};
110141
},
111142
);

apps/array/src/renderer/types/electron.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ declare global {
221221
) => Promise<TaskFolderAssociation | null>;
222222
removeTaskAssociation: (taskId: string) => Promise<void>;
223223
clearTaskWorktree: (taskId: string) => Promise<void>;
224+
cleanupOrphanedWorktrees: (
225+
mainRepoPath: string,
226+
) => Promise<{
227+
deleted: string[];
228+
errors: Array<{ path: string; error: string }>;
229+
}>;
224230
};
225231
worktree: {
226232
create: (mainRepoPath: string) => Promise<{

packages/agent/src/worktree-manager.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,4 +791,57 @@ export class WorktreeManager {
791791
return null;
792792
}
793793
}
794+
795+
async cleanupOrphanedWorktrees(
796+
associatedWorktreePaths: string[],
797+
): Promise<{
798+
deleted: string[];
799+
errors: Array<{ path: string; error: string }>;
800+
}> {
801+
this.logger.info("Starting cleanup of orphaned worktrees");
802+
803+
const allWorktrees = await this.listWorktrees();
804+
const deleted: string[] = [];
805+
const errors: Array<{ path: string; error: string }> = [];
806+
807+
const associatedPathsSet = new Set(
808+
associatedWorktreePaths.map((p) => path.resolve(p)),
809+
);
810+
811+
for (const worktree of allWorktrees) {
812+
const resolvedPath = path.resolve(worktree.worktreePath);
813+
814+
if (!associatedPathsSet.has(resolvedPath)) {
815+
this.logger.info("Found orphaned worktree", {
816+
path: worktree.worktreePath,
817+
});
818+
819+
try {
820+
await this.deleteWorktree(worktree.worktreePath);
821+
deleted.push(worktree.worktreePath);
822+
this.logger.info("Deleted orphaned worktree", {
823+
path: worktree.worktreePath,
824+
});
825+
} catch (error) {
826+
const errorMessage =
827+
error instanceof Error ? error.message : String(error);
828+
errors.push({
829+
path: worktree.worktreePath,
830+
error: errorMessage,
831+
});
832+
this.logger.error("Failed to delete orphaned worktree", {
833+
path: worktree.worktreePath,
834+
error: errorMessage,
835+
});
836+
}
837+
}
838+
}
839+
840+
this.logger.info("Cleanup completed", {
841+
deleted: deleted.length,
842+
errors: errors.length,
843+
});
844+
845+
return { deleted, errors };
846+
}
794847
}

0 commit comments

Comments
 (0)