diff --git a/src/main/antigravity/hook-service.test.ts b/src/main/antigravity/hook-service.test.ts index b9f69b03cc1..6adcdc2448f 100644 --- a/src/main/antigravity/hook-service.test.ts +++ b/src/main/antigravity/hook-service.test.ts @@ -57,6 +57,49 @@ describe('AntigravityHookService', () => { rmSync(homeDir, { recursive: true, force: true }) }) + it.each(['darwin', 'win32'] as const)( + 'reports and repairs a missing core script on %s', + (platform) => { + withPlatform(platform, () => { + const service = new AntigravityHookService() + expect(service.install().state).toBe('installed') + const filename = platform === 'win32' ? 'antigravity-hook.cmd' : 'antigravity-hook.sh' + rmSync(join(homeDir, '.orca', 'agent-hooks', filename)) + expect(service.getStatus()).toMatchObject({ state: 'partial', managedHooksPresent: true }) + expect(service.getStatus().detail).toContain(filename) + expect(service.install().state).toBe('installed') + }) + } + ) + + it('reports and repairs a missing Windows event wrapper', () => { + withPlatform('win32', () => { + const service = new AntigravityHookService() + service.install() + rmSync(join(homeDir, '.orca', 'agent-hooks', 'antigravity-pre-tool-use.cmd')) + expect(service.getStatus()).toMatchObject({ state: 'partial', managedHooksPresent: true }) + expect(service.getStatus().detail).toContain('antigravity-pre-tool-use.cmd') + expect(service.install().state).toBe('installed') + }) + }) + + it('reinstalls a wiped bundle and missing scripts while preserving user hooks', () => { + withPlatform('win32', () => { + const service = new AntigravityHookService() + service.install() + const configPath = join(homeDir, '.gemini', 'config', 'hooks.json') + const userHooks = { 'user-hook': { Stop: [{ type: 'command', command: 'user-hook' }] } } + writeFileSync(configPath, JSON.stringify(userHooks)) + rmSync(join(homeDir, '.orca', 'agent-hooks', 'antigravity-pre-tool-use.cmd')) + expect(service.getStatus().state).toBe('not_installed') + expect(service.install().state).toBe('installed') + expect(JSON.parse(readFileSync(configPath, 'utf8'))).toMatchObject(userHooks) + expect( + readFileSync(join(homeDir, '.orca', 'agent-hooks', 'antigravity-pre-tool-use.cmd'), 'utf8') + ).toContain('ORCA_ANTIGRAVITY_EVENT=PreToolUse') + }) + }) + it('installs Antigravity global hooks.json bundle and managed script', () => { const status = new AntigravityHookService().install() diff --git a/src/main/antigravity/hook-service.ts b/src/main/antigravity/hook-service.ts index 0689be932ef..e49a522257a 100644 --- a/src/main/antigravity/hook-service.ts +++ b/src/main/antigravity/hook-service.ts @@ -1,3 +1,4 @@ +import { statSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' import type { SFTPWrapper } from 'ssh2' @@ -32,6 +33,14 @@ import { removeInstalledConfig } from './hooks-json-bundle' +function isManagedScriptFile(scriptPath: string): boolean { + try { + return statSync(scriptPath).isFile() + } catch { + return false + } +} + function getConfigPath(): string { // Why: Antigravity's hook docs define global hooks in ~/.gemini/config/hooks.json, // not in the CLI settings file used by Gemini CLI. @@ -132,6 +141,23 @@ export class AntigravityHookService { ? `Managed hook missing for events: ${missing.join(', ')}` : 'Stale managed hook entries need cleanup' } + if (managedHooksPresent) { + const scriptNames = [ + getManagedScriptFileName(), + ...(process.platform === 'win32' + ? ANTIGRAVITY_EVENTS.map((event) => event.windowsWrapperFileName) + : []) + ] + const unavailable = scriptNames.filter( + (name) => !isManagedScriptFile(getSharedManagedScriptPath(name)) + ) + if (unavailable.length > 0) { + state = 'partial' + detail = [detail, `Managed scripts unavailable: ${unavailable.join(', ')}`] + .filter(Boolean) + .join('; ') + } + } return { agent: 'antigravity', state, configPath, managedHooksPresent, detail } }