diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 0c142cf1b2..009fe3ed73 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -514,10 +514,13 @@ export class ExtensionManager extends ExtensionLoader { ) .filter((contextFilePath) => fs.existsSync(contextFilePath)); - const hooks = await this.loadExtensionHooks(effectiveExtensionPath, { - extensionPath: effectiveExtensionPath, - workspacePath: this.workspaceDir, - }); + let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined; + if (this.settings.tools?.enableHooks) { + hooks = await this.loadExtensionHooks(effectiveExtensionPath, { + extensionPath: effectiveExtensionPath, + workspacePath: this.workspaceDir, + }); + } const extension = { name: config.name, diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 32ce455d31..ffef6f794f 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -749,6 +749,17 @@ describe('extension tests', () => { JSON.stringify(hooksConfig), ); + const settings = loadSettings(tempWorkspaceDir).merged; + if (!settings.tools) settings.tools = {}; + settings.tools.enableHooks = true; + + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings, + }); + const extensions = await extensionManager.loadExtensions(); expect(extensions).toHaveLength(1); const extension = extensions[0]; @@ -760,6 +771,36 @@ describe('extension tests', () => { ); }); + it('should not load hooks if enableHooks is false', async () => { + const extDir = createExtension({ + extensionsDir: userExtensionsDir, + name: 'hook-extension-disabled', + version: '1.0.0', + }); + + const hooksDir = path.join(extDir, 'hooks'); + fs.mkdirSync(hooksDir); + fs.writeFileSync( + path.join(hooksDir, 'hooks.json'), + JSON.stringify({ hooks: { BeforeTool: [] } }), + ); + + const settings = loadSettings(tempWorkspaceDir).merged; + if (!settings.tools) settings.tools = {}; + settings.tools.enableHooks = false; + + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings, + }); + + const extensions = await extensionManager.loadExtensions(); + expect(extensions).toHaveLength(1); + expect(extensions[0].hooks).toBeUndefined(); + }); + it('should warn about hooks during installation', async () => { const requestConsentSpy = vi.fn().mockResolvedValue(true); extensionManager.setRequestConsent(requestConsentSpy); diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index 4709b8150c..dccf13e298 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -43,6 +43,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { debugLogger: { error: vi.fn(), log: vi.fn(), + warn: vi.fn(), }, }; }); @@ -263,6 +264,25 @@ describe('github.ts', () => { ExtensionUpdateState.UP_TO_DATE, ); }); + + it('should return NOT_UPDATABLE if local extension config cannot be loaded', async () => { + vi.mocked(mockExtensionManager.loadExtensionConfig).mockImplementation( + () => { + throw new Error('Config not found'); + }, + ); + + const ext = { + name: 'local-ext', + version: '1.0.0', + path: '/path/to/installed/ext', + installMetadata: { type: 'local', source: '/path/to/source/ext' }, + } as unknown as GeminiCLIExtension; + + expect(await checkForExtensionUpdate(ext, mockExtensionManager)).toBe( + ExtensionUpdateState.NOT_UPDATABLE, + ); + }); }); describe('downloadFromGitHubRelease', () => { diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 37a974bf8a..335ce542aa 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -19,6 +19,7 @@ import * as path from 'node:path'; import * as tar from 'tar'; import extract from 'extract-zip'; import { fetchJson, getGitHubToken } from './github_fetch.js'; +import type { ExtensionConfig } from '../extension.js'; import type { ExtensionManager } from '../extension-manager.js'; import { EXTENSIONS_CONFIG_FILENAME } from './variables.js'; @@ -172,14 +173,23 @@ export async function checkForExtensionUpdate( ): Promise { const installMetadata = extension.installMetadata; if (installMetadata?.type === 'local') { - const latestConfig = extensionManager.loadExtensionConfig( - installMetadata.source, - ); + let latestConfig: ExtensionConfig | undefined; + try { + latestConfig = extensionManager.loadExtensionConfig( + installMetadata.source, + ); + } catch (e) { + debugLogger.warn( + `Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}. Error: ${getErrorMessage(e)}`, + ); + return ExtensionUpdateState.NOT_UPDATABLE; + } + if (!latestConfig) { - debugLogger.error( + debugLogger.warn( `Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`, ); - return ExtensionUpdateState.ERROR; + return ExtensionUpdateState.NOT_UPDATABLE; } if (latestConfig.version !== extension.version) { return ExtensionUpdateState.UPDATE_AVAILABLE;