mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-20 06:50:22 -07:00
feat(hooks): adds support for RuntimeHook functions. (#19598)
This commit is contained in:
@@ -8,7 +8,7 @@ import type { Config } from '../config/config.js';
|
||||
import type { HookPlanner, HookEventContext } from './hookPlanner.js';
|
||||
import type { HookRunner } from './hookRunner.js';
|
||||
import type { HookAggregator, AggregatedHookResult } from './hookAggregator.js';
|
||||
import { HookEventName } from './types.js';
|
||||
import { HookEventName, HookType } from './types.js';
|
||||
import type {
|
||||
HookConfig,
|
||||
HookInput,
|
||||
@@ -500,7 +500,10 @@ export class HookEventHandler {
|
||||
* Get hook name from config for display or telemetry
|
||||
*/
|
||||
private getHookName(config: HookConfig): string {
|
||||
return config.name || config.command || 'unknown-command';
|
||||
if (config.type === HookType.Command) {
|
||||
return config.name || config.command || 'unknown-command';
|
||||
}
|
||||
return config.name || 'unknown-hook';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +516,7 @@ export class HookEventHandler {
|
||||
/**
|
||||
* Get hook type from execution result for telemetry
|
||||
*/
|
||||
private getHookTypeFromResult(result: HookExecutionResult): 'command' {
|
||||
private getHookTypeFromResult(result: HookExecutionResult): HookType {
|
||||
return result.hookConfig.type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
HookEventName,
|
||||
HookType,
|
||||
HOOKS_CONFIG_FIELDS,
|
||||
type CommandHookConfig,
|
||||
} from './types.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { HookDefinition } from './types.js';
|
||||
@@ -153,7 +154,9 @@ describe('HookRegistry', () => {
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].eventName).toBe(HookEventName.BeforeTool);
|
||||
expect(hooks[0].config.type).toBe(HookType.Command);
|
||||
expect(hooks[0].config.command).toBe('./hooks/check_style.sh');
|
||||
expect((hooks[0].config as CommandHookConfig).command).toBe(
|
||||
'./hooks/check_style.sh',
|
||||
);
|
||||
expect(hooks[0].matcher).toBe('EditTool');
|
||||
expect(hooks[0].source).toBe(ConfigSource.Project);
|
||||
});
|
||||
@@ -186,7 +189,9 @@ describe('HookRegistry', () => {
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].eventName).toBe(HookEventName.AfterTool);
|
||||
expect(hooks[0].config.type).toBe(HookType.Command);
|
||||
expect(hooks[0].config.command).toBe('./hooks/after-tool.sh');
|
||||
expect((hooks[0].config as CommandHookConfig).command).toBe(
|
||||
'./hooks/after-tool.sh',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid configuration gracefully', async () => {
|
||||
@@ -632,7 +637,9 @@ describe('HookRegistry', () => {
|
||||
// Should only load the valid hook
|
||||
const hooks = hookRegistry.getAllHooks();
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].config.command).toBe('./valid-hook.sh');
|
||||
expect((hooks[0].config as CommandHookConfig).command).toBe(
|
||||
'./valid-hook.sh',
|
||||
);
|
||||
|
||||
// Verify the warnings for invalid configurations
|
||||
// 1st warning: non-object hookConfig ('invalid-string')
|
||||
|
||||
@@ -34,11 +34,40 @@ export class HookRegistry {
|
||||
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> {
|
||||
this.entries = [];
|
||||
const runtimeHooks = this.entries.filter(
|
||||
(entry) => entry.source === ConfigSource.Runtime,
|
||||
);
|
||||
this.entries = [...runtimeHooks];
|
||||
this.processHooksFromConfig();
|
||||
|
||||
debugLogger.debug(
|
||||
@@ -93,7 +122,10 @@ export class HookRegistry {
|
||||
private getHookName(
|
||||
entry: HookRegistryEntry | { config: HookConfig },
|
||||
): string {
|
||||
return entry.config.name || entry.config.command || 'unknown-command';
|
||||
if (entry.config.type === 'command') {
|
||||
return entry.config.name || entry.config.command || 'unknown-command';
|
||||
}
|
||||
return entry.config.name || 'unknown-hook';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +293,10 @@ please review the project settings (.gemini/settings.json) and remove them.`;
|
||||
eventName: HookEventName,
|
||||
source: ConfigSource,
|
||||
): boolean {
|
||||
if (!config.type || !['command', 'plugin'].includes(config.type)) {
|
||||
if (
|
||||
!config.type ||
|
||||
!['command', 'plugin', 'runtime'].includes(config.type)
|
||||
) {
|
||||
debugLogger.warn(
|
||||
`Invalid hook ${eventName} from ${source} type: ${config.type}`,
|
||||
);
|
||||
@@ -275,6 +310,13 @@ please review the project settings (.gemini/settings.json) and remove them.`;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.type === 'runtime' && !config.name) {
|
||||
debugLogger.warn(
|
||||
`Runtime hook ${eventName} from ${source} missing name field`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -292,6 +334,8 @@ please review the project settings (.gemini/settings.json) and remove them.`;
|
||||
*/
|
||||
private getSourcePriority(source: ConfigSource): number {
|
||||
switch (source) {
|
||||
case ConfigSource.Runtime:
|
||||
return 0; // Highest
|
||||
case ConfigSource.Project:
|
||||
return 1;
|
||||
case ConfigSource.User:
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import type {
|
||||
HookConfig,
|
||||
CommandHookConfig,
|
||||
RuntimeHookConfig,
|
||||
HookInput,
|
||||
HookOutput,
|
||||
HookExecutionResult,
|
||||
@@ -15,7 +17,7 @@ import type {
|
||||
BeforeModelOutput,
|
||||
BeforeToolInput,
|
||||
} from './types.js';
|
||||
import { HookEventName, ConfigSource } from './types.js';
|
||||
import { HookEventName, ConfigSource, HookType } from './types.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { LLMRequest } from './hookTranslator.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
@@ -75,6 +77,15 @@ export class HookRunner {
|
||||
}
|
||||
|
||||
try {
|
||||
if (hookConfig.type === HookType.Runtime) {
|
||||
return await this.executeRuntimeHook(
|
||||
hookConfig,
|
||||
eventName,
|
||||
input,
|
||||
startTime,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.executeCommandHook(
|
||||
hookConfig,
|
||||
eventName,
|
||||
@@ -83,7 +94,10 @@ export class HookRunner {
|
||||
);
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
const hookId = hookConfig.name || hookConfig.command || 'unknown';
|
||||
const hookId =
|
||||
hookConfig.name ||
|
||||
(hookConfig.type === HookType.Command ? hookConfig.command : '') ||
|
||||
'unknown';
|
||||
const errorMessage = `Hook execution failed for event '${eventName}' (hook: ${hookId}): ${error}`;
|
||||
debugLogger.warn(`Hook execution error (non-fatal): ${errorMessage}`);
|
||||
|
||||
@@ -230,11 +244,66 @@ export class HookRunner {
|
||||
return modifiedInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a runtime hook
|
||||
*/
|
||||
private async executeRuntimeHook(
|
||||
hookConfig: RuntimeHookConfig,
|
||||
eventName: HookEventName,
|
||||
input: HookInput,
|
||||
startTime: number,
|
||||
): Promise<HookExecutionResult> {
|
||||
const timeout = hookConfig.timeout ?? DEFAULT_HOOK_TIMEOUT;
|
||||
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
// Create a promise that rejects after timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutHandle = setTimeout(
|
||||
() => reject(new Error(`Hook timed out after ${timeout}ms`)),
|
||||
timeout,
|
||||
);
|
||||
});
|
||||
|
||||
// Execute action with timeout race
|
||||
const result = await Promise.race([
|
||||
hookConfig.action(input, { signal: controller.signal }),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
const output =
|
||||
result === null || result === undefined ? undefined : result;
|
||||
|
||||
return {
|
||||
hookConfig,
|
||||
eventName,
|
||||
success: true,
|
||||
output,
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
} catch (error) {
|
||||
// Abort the ongoing hook action if it timed out or errored
|
||||
controller.abort();
|
||||
return {
|
||||
hookConfig,
|
||||
eventName,
|
||||
success: false,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
} finally {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command hook
|
||||
*/
|
||||
private async executeCommandHook(
|
||||
hookConfig: HookConfig,
|
||||
hookConfig: CommandHookConfig,
|
||||
eventName: HookEventName,
|
||||
input: HookInput,
|
||||
startTime: number,
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('HookSystem Integration', () => {
|
||||
matcher: 'TestTool',
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
type: HookType.Command as const,
|
||||
command: 'echo',
|
||||
timeout: 5000,
|
||||
},
|
||||
@@ -164,7 +164,8 @@ describe('HookSystem Integration', () => {
|
||||
{
|
||||
type: 'invalid-type' as HookType, // Invalid hook type for testing
|
||||
command: './test.sh',
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -279,12 +280,12 @@ describe('HookSystem Integration', () => {
|
||||
matcher: 'TestTool',
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
type: HookType.Command as const,
|
||||
command: 'echo "enabled-hook"',
|
||||
timeout: 5000,
|
||||
},
|
||||
{
|
||||
type: HookType.Command,
|
||||
type: HookType.Command as const,
|
||||
command: 'echo "disabled-hook"',
|
||||
timeout: 5000,
|
||||
},
|
||||
@@ -350,7 +351,7 @@ describe('HookSystem Integration', () => {
|
||||
matcher: 'TestTool',
|
||||
hooks: [
|
||||
{
|
||||
type: HookType.Command,
|
||||
type: HookType.Command as const,
|
||||
command: 'echo "will-be-disabled"',
|
||||
timeout: 5000,
|
||||
},
|
||||
|
||||
@@ -21,6 +21,9 @@ import type {
|
||||
AfterModelHookOutput,
|
||||
BeforeToolSelectionHookOutput,
|
||||
McpToolContext,
|
||||
HookConfig,
|
||||
HookEventName,
|
||||
ConfigSource,
|
||||
} from './types.js';
|
||||
import { NotificationType } from './types.js';
|
||||
import type { AggregatedHookResult } from './hookAggregator.js';
|
||||
@@ -202,6 +205,17 @@ export class HookSystem {
|
||||
return this.hookRegistry.getAllHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new hook programmatically
|
||||
*/
|
||||
registerHook(
|
||||
config: HookConfig,
|
||||
eventName: HookEventName,
|
||||
options?: { matcher?: string; sequential?: boolean; source?: ConfigSource },
|
||||
): void {
|
||||
this.hookRegistry.registerHook(config, eventName, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire hook events directly
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { HookSystem } from './hookSystem.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { HookType, HookEventName, ConfigSource } from './types.js';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
// Mock console methods
|
||||
vi.stubGlobal('console', {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
});
|
||||
|
||||
describe('Runtime Hooks', () => {
|
||||
let hookSystem: HookSystem;
|
||||
let config: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
const testDir = path.join(os.tmpdir(), 'test-runtime-hooks');
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
|
||||
config = new Config({
|
||||
model: 'gemini-3-flash-preview',
|
||||
targetDir: testDir,
|
||||
sessionId: 'test-session',
|
||||
debugMode: false,
|
||||
cwd: testDir,
|
||||
});
|
||||
|
||||
// Stub getMessageBus
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(config as any).getMessageBus = () => undefined;
|
||||
|
||||
hookSystem = new HookSystem(config);
|
||||
});
|
||||
|
||||
it('should register a runtime hook', async () => {
|
||||
await hookSystem.initialize();
|
||||
|
||||
const action = vi.fn().mockResolvedValue(undefined);
|
||||
hookSystem.registerHook(
|
||||
{
|
||||
type: HookType.Runtime,
|
||||
name: 'test-hook',
|
||||
action,
|
||||
},
|
||||
HookEventName.BeforeTool,
|
||||
{ matcher: 'TestTool' },
|
||||
);
|
||||
|
||||
const hooks = hookSystem.getAllHooks();
|
||||
expect(hooks).toHaveLength(1);
|
||||
expect(hooks[0].config.name).toBe('test-hook');
|
||||
expect(hooks[0].source).toBe(ConfigSource.Runtime);
|
||||
});
|
||||
|
||||
it('should execute a runtime hook', async () => {
|
||||
await hookSystem.initialize();
|
||||
|
||||
const action = vi.fn().mockImplementation(async () => ({
|
||||
decision: 'allow',
|
||||
systemMessage: 'Hook ran',
|
||||
}));
|
||||
|
||||
hookSystem.registerHook(
|
||||
{
|
||||
type: HookType.Runtime,
|
||||
name: 'test-hook',
|
||||
action,
|
||||
},
|
||||
HookEventName.BeforeTool,
|
||||
{ matcher: 'TestTool' },
|
||||
);
|
||||
|
||||
const result = await hookSystem
|
||||
.getEventHandler()
|
||||
.fireBeforeToolEvent('TestTool', { foo: 'bar' });
|
||||
|
||||
expect(action).toHaveBeenCalled();
|
||||
expect(action.mock.calls[0][0]).toMatchObject({
|
||||
tool_name: 'TestTool',
|
||||
tool_input: { foo: 'bar' },
|
||||
hook_event_name: 'BeforeTool',
|
||||
});
|
||||
|
||||
expect(result.finalOutput?.systemMessage).toBe('Hook ran');
|
||||
});
|
||||
|
||||
it('should handle runtime hook errors', async () => {
|
||||
await hookSystem.initialize();
|
||||
|
||||
const action = vi.fn().mockRejectedValue(new Error('Hook failed'));
|
||||
|
||||
hookSystem.registerHook(
|
||||
{
|
||||
type: HookType.Runtime,
|
||||
name: 'fail-hook',
|
||||
action,
|
||||
},
|
||||
HookEventName.BeforeTool,
|
||||
{ matcher: 'TestTool' },
|
||||
);
|
||||
|
||||
// Should not throw, but handle error gracefully
|
||||
await hookSystem.getEventHandler().fireBeforeToolEvent('TestTool', {});
|
||||
|
||||
expect(action).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should preserve runtime hooks across re-initialization', async () => {
|
||||
await hookSystem.initialize();
|
||||
|
||||
hookSystem.registerHook(
|
||||
{
|
||||
type: HookType.Runtime,
|
||||
name: 'persist-hook',
|
||||
action: async () => {},
|
||||
},
|
||||
HookEventName.BeforeTool,
|
||||
{ matcher: 'TestTool' },
|
||||
);
|
||||
|
||||
expect(hookSystem.getAllHooks()).toHaveLength(1);
|
||||
|
||||
// Re-initialize
|
||||
await hookSystem.initialize();
|
||||
|
||||
expect(hookSystem.getAllHooks()).toHaveLength(1);
|
||||
expect(hookSystem.getAllHooks()[0].config.name).toBe('persist-hook');
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import { TrustedHooksManager } from './trustedHooks.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { HookEventName, HookType } from './types.js';
|
||||
import { HookEventName, HookType, type HookDefinition } from './types.js';
|
||||
|
||||
vi.mock('node:fs');
|
||||
vi.mock('../config/storage.js');
|
||||
@@ -72,8 +72,16 @@ describe('TrustedHooksManager', () => {
|
||||
[HookEventName.BeforeTool]: [
|
||||
{
|
||||
hooks: [
|
||||
{ name: 'trusted-hook', type: HookType.Command, command: 'cmd1' },
|
||||
{ name: 'new-hook', type: HookType.Command, command: 'cmd2' },
|
||||
{
|
||||
name: 'trusted-hook',
|
||||
type: HookType.Command,
|
||||
command: 'cmd1',
|
||||
} as const,
|
||||
{
|
||||
name: 'new-hook',
|
||||
type: HookType.Command,
|
||||
command: 'cmd2',
|
||||
} as const,
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -90,7 +98,11 @@ describe('TrustedHooksManager', () => {
|
||||
[HookEventName.BeforeTool]: [
|
||||
{
|
||||
hooks: [
|
||||
{ name: 'trusted-hook', type: HookType.Command, command: 'cmd1' },
|
||||
{
|
||||
name: 'trusted-hook',
|
||||
type: HookType.Command,
|
||||
command: 'cmd1',
|
||||
} as const,
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -114,9 +126,12 @@ describe('TrustedHooksManager', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(manager.getUntrustedHooks('/project', projectHooks)).toEqual([
|
||||
'./script.sh',
|
||||
]);
|
||||
expect(
|
||||
manager.getUntrustedHooks(
|
||||
'/project',
|
||||
projectHooks as Partial<Record<HookEventName, HookDefinition[]>>,
|
||||
),
|
||||
).toEqual(['./script.sh']);
|
||||
});
|
||||
|
||||
it('should detect change in command as untrusted', () => {
|
||||
@@ -142,11 +157,17 @@ describe('TrustedHooksManager', () => {
|
||||
],
|
||||
};
|
||||
|
||||
manager.trustHooks('/project', originalHook);
|
||||
manager.trustHooks(
|
||||
'/project',
|
||||
originalHook as Partial<Record<HookEventName, HookDefinition[]>>,
|
||||
);
|
||||
|
||||
expect(manager.getUntrustedHooks('/project', updatedHook)).toEqual([
|
||||
'my-hook',
|
||||
]);
|
||||
expect(
|
||||
manager.getUntrustedHooks(
|
||||
'/project',
|
||||
updatedHook as Partial<Record<HookEventName, HookDefinition[]>>,
|
||||
),
|
||||
).toEqual(['my-hook']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as path from 'node:path';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import {
|
||||
getHookKey,
|
||||
HookType,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
} from './types.js';
|
||||
@@ -79,6 +80,7 @@ export class TrustedHooksManager {
|
||||
for (const def of definitions) {
|
||||
if (!def || !Array.isArray(def.hooks)) continue;
|
||||
for (const hook of def.hooks) {
|
||||
if (hook.type === HookType.Runtime) continue;
|
||||
const key = getHookKey(hook);
|
||||
if (!trustedKeys.has(key)) {
|
||||
// Return friendly name or command
|
||||
@@ -108,6 +110,7 @@ export class TrustedHooksManager {
|
||||
for (const def of definitions) {
|
||||
if (!def || !Array.isArray(def.hooks)) continue;
|
||||
for (const hook of def.hooks) {
|
||||
if (hook.type === HookType.Runtime) continue;
|
||||
currentTrusted.add(getHookKey(hook));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { defaultHookTranslator } from './hookTranslator.js';
|
||||
* Configuration source levels in precedence order (highest to lowest)
|
||||
*/
|
||||
export enum ConfigSource {
|
||||
Runtime = 'runtime',
|
||||
Project = 'project',
|
||||
User = 'user',
|
||||
System = 'system',
|
||||
@@ -50,11 +51,43 @@ export enum HookEventName {
|
||||
export const HOOKS_CONFIG_FIELDS = ['enabled', 'disabled', 'notifications'];
|
||||
|
||||
/**
|
||||
* Hook configuration entry
|
||||
* Hook implementation types
|
||||
*/
|
||||
export enum HookType {
|
||||
Command = 'command',
|
||||
Runtime = 'runtime',
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook action function
|
||||
*/
|
||||
export type HookAction = (
|
||||
input: HookInput,
|
||||
options?: { signal: AbortSignal },
|
||||
) => Promise<HookOutput | void | null>;
|
||||
|
||||
/**
|
||||
* Runtime hook configuration
|
||||
*/
|
||||
export interface RuntimeHookConfig {
|
||||
type: HookType.Runtime;
|
||||
/** Unique name for the runtime hook */
|
||||
name: string;
|
||||
/** Function to execute when the hook is triggered */
|
||||
action: HookAction;
|
||||
command?: never;
|
||||
source?: ConfigSource;
|
||||
/** Maximum time allowed for hook execution in milliseconds */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command hook configuration entry
|
||||
*/
|
||||
export interface CommandHookConfig {
|
||||
type: HookType.Command;
|
||||
command: string;
|
||||
action?: never;
|
||||
name?: string;
|
||||
description?: string;
|
||||
timeout?: number;
|
||||
@@ -62,7 +95,7 @@ export interface CommandHookConfig {
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type HookConfig = CommandHookConfig;
|
||||
export type HookConfig = CommandHookConfig | RuntimeHookConfig;
|
||||
|
||||
/**
|
||||
* Hook definition with matcher
|
||||
@@ -73,19 +106,12 @@ export interface HookDefinition {
|
||||
hooks: HookConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook implementation types
|
||||
*/
|
||||
export enum HookType {
|
||||
Command = 'command',
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for a hook configuration
|
||||
*/
|
||||
export function getHookKey(hook: HookConfig): string {
|
||||
const name = hook.name || '';
|
||||
const command = hook.command || '';
|
||||
const command = hook.type === HookType.Command ? hook.command : '';
|
||||
return `${name}:${command}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user