diff --git a/src/main/engine/GitEngine.ts b/src/main/engine/GitEngine.ts index 0fce343..2e8706e 100644 --- a/src/main/engine/GitEngine.ts +++ b/src/main/engine/GitEngine.ts @@ -66,6 +66,19 @@ export interface GitIgnoreEnsureResult { addedEntries: string[]; } +export interface GitLfsPruneOptions { + dryRun?: boolean; + verifyRemote?: boolean; +} + +export interface GitLfsPruneResult { + success: boolean; + dryRun: boolean; + verifyRemote: boolean; + output?: string; + error?: string; +} + let gitEngineInstance: GitEngine | null = null; export function getGitEngine(): GitEngine { @@ -250,6 +263,38 @@ export class GitEngine { }; } + async pruneLfsCache(projectPath: string, options: GitLfsPruneOptions = {}): Promise { + const git = simpleGit(projectPath); + const verifyRemote = options.verifyRemote ?? true; + const dryRun = options.dryRun ?? false; + + const args = ['lfs', 'prune']; + if (verifyRemote) { + args.push('--verify-remote'); + } + if (dryRun) { + args.push('--dry-run'); + } + + try { + const output = await git.raw(args); + return { + success: true, + dryRun, + verifyRemote, + output, + }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to prune Git LFS cache.'; + return { + success: false, + dryRun, + verifyRemote, + error: message, + }; + } + } + async initializeRepo( projectPath: string, remoteUrl?: string, diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index ac7b089..8d8027b 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -60,6 +60,11 @@ export function registerIpcHandlers(): void { return engine.ensureGitignore(projectPath); }); + safeHandle('git:pruneLfs', async (_, projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => { + const engine = getGitEngine(); + return engine.pruneLfsCache(projectPath, options); + }); + // ============ Project Handlers ============ safeHandle('projects:create', async (_, data: { name: string; description?: string; slug?: string; dataPath?: string }) => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 4549f5f..8afc077 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -11,6 +11,7 @@ export const electronAPI: ElectronAPI = { getRepoState: (projectPath: string) => ipcRenderer.invoke('git:getRepoState', projectPath), getStatus: (projectPath: string) => ipcRenderer.invoke('git:status', projectPath), ensureGitignore: (projectPath: string) => ipcRenderer.invoke('git:ensureGitignore', projectPath), + pruneLfs: (projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => ipcRenderer.invoke('git:pruneLfs', projectPath, options), init: (projectPath: string, remoteUrl?: string) => { if (remoteUrl) { return ipcRenderer.invoke('git:init', projectPath, remoteUrl); diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index f71c69e..951e07f 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -266,6 +266,14 @@ export interface GitIgnoreEnsureResult { addedEntries: string[]; } +export interface GitLfsPruneResult { + success: boolean; + dryRun: boolean; + verifyRemote: boolean; + output?: string; + error?: string; +} + // Post-Media Link types export interface MediaLinkData { id: string; @@ -341,6 +349,7 @@ export interface ElectronAPI { getRepoState: (projectPath: string) => Promise; getStatus: (projectPath: string) => Promise; ensureGitignore: (projectPath: string) => Promise; + pruneLfs: (projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => Promise; init: (projectPath: string, remoteUrl?: string) => Promise; onInitProgress: (callback: (progress: GitInitProgress) => void) => () => void; }; diff --git a/tests/engine/GitEngine.test.ts b/tests/engine/GitEngine.test.ts index 2db32cb..747a274 100644 --- a/tests/engine/GitEngine.test.ts +++ b/tests/engine/GitEngine.test.ts @@ -350,4 +350,36 @@ describe('GitEngine', () => { expect(result.error).toContain('install Git LFS'); }); }); + + describe('pruneLfsCache', () => { + it('should run git lfs prune with verify-remote by default', async () => { + mockRaw.mockResolvedValue('prune complete'); + + const result = await gitEngine.pruneLfsCache('/tmp/project'); + + expect(mockRaw).toHaveBeenCalledWith(['lfs', 'prune', '--verify-remote']); + expect(result.success).toBe(true); + expect(result.dryRun).toBe(false); + expect(result.verifyRemote).toBe(true); + }); + + it('should run git lfs prune in dry-run mode when requested', async () => { + mockRaw.mockResolvedValue('would prune'); + + const result = await gitEngine.pruneLfsCache('/tmp/project', { dryRun: true }); + + expect(mockRaw).toHaveBeenCalledWith(['lfs', 'prune', '--verify-remote', '--dry-run']); + expect(result.success).toBe(true); + expect(result.dryRun).toBe(true); + }); + + it('should return error result when git lfs prune fails', async () => { + mockRaw.mockRejectedValue(new Error('prune failed')); + + const result = await gitEngine.pruneLfsCache('/tmp/project'); + + expect(result.success).toBe(false); + expect(result.error).toContain('prune failed'); + }); + }); }); diff --git a/tests/ipc/handlers.test.ts b/tests/ipc/handlers.test.ts index 67f36f2..c66abf9 100644 --- a/tests/ipc/handlers.test.ts +++ b/tests/ipc/handlers.test.ts @@ -146,6 +146,7 @@ const mockGitEngine = { getStatus: vi.fn(), initializeRepo: vi.fn(), ensureGitignore: vi.fn(), + pruneLfsCache: vi.fn(), }; const mockTaskManager = { @@ -379,6 +380,22 @@ describe('IPC Handlers', () => { }); }); }); + + describe('git:pruneLfs', () => { + it('should pass project path and options to GitEngine.pruneLfsCache', async () => { + mockGitEngine.pruneLfsCache.mockResolvedValue({ + success: true, + dryRun: true, + verifyRemote: true, + output: 'would prune', + }); + + const result = await invokeHandler('git:pruneLfs', '/repo', { dryRun: true, verifyRemote: true }); + + expect(mockGitEngine.pruneLfsCache).toHaveBeenCalledWith('/repo', { dryRun: true, verifyRemote: true }); + expect(result.success).toBe(true); + }); + }); }); // ============ Project Handlers ============