Download packages/core/src/hooks/hookRegistry.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.54 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/hooks/hookRegistry.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/hooks/hookRegistry.ts
-
curl -L -o hookRegistry.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/hooks/hookRegistry.ts
9.54 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import type { Config } from '../config/config.js'; | |
| import { | |
| HookEventName, | |
| ConfigSource, | |
| HOOKS_CONFIG_FIELDS, | |
| type HookDefinition, | |
| type HookConfig, | |
| } from './types.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| import { TrustedHooksManager } from './trustedHooks.js'; | |
| import { coreEvents } from '../utils/events.js'; | |
| /** | |
| * Hook registry entry with source information | |
| */ | |
| export interface HookRegistryEntry { | |
| config: HookConfig; | |
| source: ConfigSource; | |
| eventName: HookEventName; | |
| matcher?: string; | |
| sequential?: boolean; | |
| enabled: boolean; | |
| } | |
| /** | |
| * Hook registry that loads and validates hook definitions from multiple sources | |
| */ | |
| export class HookRegistry { | |
| private readonly config: Config; | |
| private entries: HookRegistryEntry[] = []; | |
| constructor(config: Config) { | |
| this.config = config; | |
| } | |
| /** | |
| * Register a new hook programmatically | |
| */ | |
| registerHook( | |
| config: HookConfig, | |
| eventName: HookEventName, | |
| options?: { matcher?: string; sequential?: boolean; source?: ConfigSource }, | |
| ): void { | |
| const source = options?.source ?? ConfigSource.Runtime; | |
| if (!this.validateHookConfig(config, eventName, source)) { | |
| throw new Error( | |
| `Invalid hook configuration for ${eventName} from ${source}`, | |
| ); | |
| } | |
| this.entries.push({ | |
| config, | |
| source, | |
| eventName, | |
| matcher: options?.matcher, | |
| sequential: options?.sequential, | |
| enabled: true, | |
| }); | |
| } | |
| /** | |
| * Initialize the registry by processing hooks from config | |
| */ | |
| async initialize(): Promise<void> { | |
| const runtimeHooks = this.entries.filter( | |
| (entry) => entry.source === ConfigSource.Runtime, | |
| ); | |
| this.entries = [...runtimeHooks]; | |
| this.processHooksFromConfig(); | |
| debugLogger.debug( | |
| `Hook registry initialized with ${this.entries.length} hook entries`, | |
| ); | |
| } | |
| /** | |
| * Get all hook entries for a specific event | |
| */ | |
| getHooksForEvent(eventName: HookEventName): HookRegistryEntry[] { | |
| return this.entries | |
| .filter((entry) => entry.eventName === eventName && entry.enabled) | |
| .sort( | |
| (a, b) => | |
| this.getSourcePriority(a.source) - this.getSourcePriority(b.source), | |
| ); | |
| } | |
| /** | |
| * Get all registered hooks | |
| */ | |
| getAllHooks(): HookRegistryEntry[] { | |
| return [...this.entries]; | |
| } | |
| /** | |
| * Enable or disable a specific hook | |
| */ | |
| setHookEnabled(hookName: string, enabled: boolean): void { | |
| const updated = this.entries.filter((entry) => { | |
| const name = this.getHookName(entry); | |
| if (name === hookName) { | |
| entry.enabled = enabled; | |
| return true; | |
| } | |
| return false; | |
| }); | |
| if (updated.length > 0) { | |
| debugLogger.log( | |
| `${enabled ? 'Enabled' : 'Disabled'} ${updated.length} hook(s) matching "${hookName}"`, | |
| ); | |
| } else { | |
| debugLogger.warn(`No hooks found matching "${hookName}"`); | |
| } | |
| } | |
| /** | |
| * Get hook name for identification and display purposes | |
| */ | |
| private getHookName( | |
| entry: HookRegistryEntry | { config: HookConfig }, | |
| ): string { | |
| if (entry.config.type === 'command') { | |
| return entry.config.name || entry.config.command || 'unknown-command'; | |
| } | |
| return entry.config.name || 'unknown-hook'; | |
| } | |
| /** | |
| * Check for untrusted project hooks and warn the user | |
| */ | |
| private checkProjectHooksTrust(): void { | |
| const projectHooks = this.config.getProjectHooks(); | |
| if (!projectHooks) return; | |
| try { | |
| const trustedHooksManager = new TrustedHooksManager(); | |
| const untrusted = trustedHooksManager.getUntrustedHooks( | |
| this.config.getProjectRoot(), | |
| projectHooks, | |
| ); | |
| if (untrusted.length > 0) { | |
| const message = `WARNING: The following project-level hooks have been detected in this workspace: | |
| ${untrusted.map((h) => ` - ${h}`).join('\n')} | |
| These hooks will be executed. If you did not configure these hooks or do not trust this project, | |
| please review the project settings (.gemini/settings.json) and remove them.`; | |
| coreEvents.emitFeedback('warning', message); | |
| // Trust them so we don't warn again | |
| trustedHooksManager.trustHooks( | |
| this.config.getProjectRoot(), | |
| projectHooks, | |
| ); | |
| } | |
| } catch (error) { | |
| debugLogger.warn('Failed to check project hooks trust', error); | |
| } | |
| } | |
| /** | |
| * Process hooks from the config that was already loaded by the CLI | |
| */ | |
| private processHooksFromConfig(): void { | |
| if (this.config.isTrustedFolder()) { | |
| this.checkProjectHooksTrust(); | |
| } | |
| // Get hooks from the main config (this comes from the merged settings) | |
| const configHooks = this.config.getHooks(); | |
| if (configHooks) { | |
| if (this.config.isTrustedFolder()) { | |
| this.processHooksConfiguration(configHooks, ConfigSource.Project); | |
| } else { | |
| debugLogger.warn( | |
| 'Project hooks disabled because the folder is not trusted.', | |
| ); | |
| } | |
| } | |
| // Get hooks from extensions | |
| const extensions = this.config.getExtensions() || []; | |
| for (const extension of extensions) { | |
| if (extension.isActive && extension.hooks) { | |
| this.processHooksConfiguration( | |
| extension.hooks, | |
| ConfigSource.Extensions, | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * Process hooks configuration and add entries | |
| */ | |
| private processHooksConfiguration( | |
| hooksConfig: { [K in HookEventName]?: HookDefinition[] }, | |
| source: ConfigSource, | |
| ): void { | |
| for (const [eventName, definitions] of Object.entries(hooksConfig)) { | |
| if (HOOKS_CONFIG_FIELDS.includes(eventName)) { | |
| continue; | |
| } | |
| if (!this.isValidEventName(eventName)) { | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| `Invalid hook event name: "${eventName}" from ${source} config. Skipping.`, | |
| ); | |
| continue; | |
| } | |
| const typedEventName = eventName; | |
| if (!Array.isArray(definitions)) { | |
| debugLogger.warn( | |
| `Hook definitions for event "${eventName}" from source "${source}" is not an array. Skipping.`, | |
| ); | |
| continue; | |
| } | |
| for (const definition of definitions) { | |
| this.processHookDefinition(definition, typedEventName, source); | |
| } | |
| } | |
| } | |
| /** | |
| * Process a single hook definition | |
| */ | |
| private processHookDefinition( | |
| definition: HookDefinition, | |
| eventName: HookEventName, | |
| source: ConfigSource, | |
| ): void { | |
| if ( | |
| !definition || | |
| typeof definition !== 'object' || | |
| !Array.isArray(definition.hooks) | |
| ) { | |
| debugLogger.warn( | |
| `Discarding invalid hook definition for ${eventName} from ${source}:`, | |
| definition, | |
| ); | |
| return; | |
| } | |
| // Get disabled hooks list from settings | |
| const disabledHooks = this.config.getDisabledHooks() || []; | |
| for (const hookConfig of definition.hooks) { | |
| if ( | |
| hookConfig && | |
| typeof hookConfig === 'object' && | |
| this.validateHookConfig(hookConfig, eventName, source) | |
| ) { | |
| // Check if this hook is in the disabled list | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const hookName = this.getHookName({ | |
| config: hookConfig, | |
| } as HookRegistryEntry); | |
| const isDisabled = disabledHooks.includes(hookName); | |
| // Add source to hook config | |
| hookConfig.source = source; | |
| this.entries.push({ | |
| config: hookConfig, | |
| source, | |
| eventName, | |
| matcher: definition.matcher, | |
| sequential: definition.sequential, | |
| enabled: !isDisabled, | |
| }); | |
| } else { | |
| // Invalid hooks are logged and discarded here, they won't reach HookRunner | |
| debugLogger.warn( | |
| `Discarding invalid hook configuration for ${eventName} from ${source}:`, | |
| hookConfig, | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * Validate a hook configuration | |
| */ | |
| private validateHookConfig( | |
| config: HookConfig, | |
| eventName: HookEventName, | |
| source: ConfigSource, | |
| ): boolean { | |
| if ( | |
| !config.type || | |
| !['command', 'plugin', 'runtime'].includes(config.type) | |
| ) { | |
| debugLogger.warn( | |
| `Invalid hook ${eventName} from ${source} type: ${config.type}`, | |
| ); | |
| return false; | |
| } | |
| if (config.type === 'command' && !config.command) { | |
| debugLogger.warn( | |
| `Command hook ${eventName} from ${source} missing command field`, | |
| ); | |
| return false; | |
| } | |
| if (config.type === 'runtime' && !config.name) { | |
| debugLogger.warn( | |
| `Runtime hook ${eventName} from ${source} missing name field`, | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Check if an event name is valid | |
| */ | |
| private isValidEventName(eventName: string): eventName is HookEventName { | |
| const validEventNames = Object.values(HookEventName); | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| return validEventNames.includes(eventName as HookEventName); | |
| } | |
| /** | |
| * Get source priority (lower number = higher priority) | |
| */ | |
| private getSourcePriority(source: ConfigSource): number { | |
| switch (source) { | |
| case ConfigSource.Runtime: | |
| return 0; // Highest | |
| case ConfigSource.Project: | |
| return 1; | |
| case ConfigSource.User: | |
| return 2; | |
| case ConfigSource.System: | |
| return 3; | |
| case ConfigSource.Extensions: | |
| return 4; | |
| default: | |
| return 999; | |
| } | |
| } | |
| } | |