mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 09:30:41 -07:00
feat(cli): Hooks enable-all/disable-all feature with dynamic status (#15552)
This commit is contained in:
@@ -21,12 +21,16 @@ describe('hooksCommand', () => {
|
||||
};
|
||||
let mockConfig: {
|
||||
getHookSystem: ReturnType<typeof vi.fn>;
|
||||
getEnableHooks: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockSettings: {
|
||||
merged: {
|
||||
hooks?: {
|
||||
disabled?: string[];
|
||||
};
|
||||
tools?: {
|
||||
enableHooks?: boolean;
|
||||
};
|
||||
};
|
||||
setValue: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
@@ -46,6 +50,7 @@ describe('hooksCommand', () => {
|
||||
// Create mock config
|
||||
mockConfig = {
|
||||
getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
|
||||
getEnableHooks: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
// Create mock settings
|
||||
@@ -79,12 +84,14 @@ describe('hooksCommand', () => {
|
||||
|
||||
it('should have all expected subcommands', () => {
|
||||
expect(hooksCommand.subCommands).toBeDefined();
|
||||
expect(hooksCommand.subCommands).toHaveLength(3);
|
||||
expect(hooksCommand.subCommands).toHaveLength(5);
|
||||
|
||||
const subCommandNames = hooksCommand.subCommands!.map((cmd) => cmd.name);
|
||||
expect(subCommandNames).toContain('panel');
|
||||
expect(subCommandNames).toContain('enable');
|
||||
expect(subCommandNames).toContain('disable');
|
||||
expect(subCommandNames).toContain('enable-all');
|
||||
expect(subCommandNames).toContain('disable-all');
|
||||
});
|
||||
|
||||
it('should delegate to panel action when invoked without subcommand', async () => {
|
||||
@@ -131,7 +138,7 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info message when hook system is not enabled', async () => {
|
||||
it('should display panel even when hook system is not enabled', async () => {
|
||||
mockConfig.getHookSystem.mockReturnValue(null);
|
||||
|
||||
const panelCmd = hooksCommand.subCommands!.find(
|
||||
@@ -141,18 +148,22 @@ describe('hooksCommand', () => {
|
||||
throw new Error('panel command must have an action');
|
||||
}
|
||||
|
||||
const result = await panelCmd.action(mockContext, '');
|
||||
await panelCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'Hook system is not enabled. Enable it in settings with hooks.enabled.',
|
||||
});
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.HOOKS_LIST,
|
||||
hooks: [],
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return info message when no hooks are configured', async () => {
|
||||
it('should display panel when no hooks are configured', async () => {
|
||||
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||
'tools'
|
||||
] = { enableHooks: true };
|
||||
|
||||
const panelCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'panel',
|
||||
@@ -161,14 +172,15 @@ describe('hooksCommand', () => {
|
||||
throw new Error('panel command must have an action');
|
||||
}
|
||||
|
||||
const result = await panelCmd.action(mockContext, '');
|
||||
await panelCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'No hooks configured. Add hooks to your settings to get started.',
|
||||
});
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.HOOKS_LIST,
|
||||
hooks: [],
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should display hooks list when hooks are configured', async () => {
|
||||
@@ -178,6 +190,9 @@ describe('hooksCommand', () => {
|
||||
];
|
||||
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
(mockContext.services.settings.merged as Record<string, unknown>)[
|
||||
'tools'
|
||||
] = { enableHooks: true };
|
||||
|
||||
const panelCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'panel',
|
||||
@@ -562,6 +577,254 @@ describe('hooksCommand', () => {
|
||||
expect(result).toEqual(['test-hook']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enable-all subcommand', () => {
|
||||
it('should return error when config is not loaded', async () => {
|
||||
const contextWithoutConfig = createMockCommandContext({
|
||||
services: {
|
||||
config: null,
|
||||
},
|
||||
});
|
||||
|
||||
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable-all',
|
||||
);
|
||||
if (!enableAllCmd?.action) {
|
||||
throw new Error('enable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableAllCmd.action(contextWithoutConfig, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when hook system is not enabled', async () => {
|
||||
mockConfig.getHookSystem.mockReturnValue(null);
|
||||
|
||||
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable-all',
|
||||
);
|
||||
if (!enableAllCmd?.action) {
|
||||
throw new Error('enable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Hook system is not enabled.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable all disabled hooks', async () => {
|
||||
const mockHooks = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, false),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, false),
|
||||
createMockHook('hook-3', HookEventName.BeforeAgent, true), // already enabled
|
||||
];
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable-all',
|
||||
);
|
||||
if (!enableAllCmd?.action) {
|
||||
throw new Error('enable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'hooks.disabled',
|
||||
[],
|
||||
);
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'hook-1',
|
||||
true,
|
||||
);
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'hook-2',
|
||||
true,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Enabled 2 hook(s) successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info when no hooks are configured', async () => {
|
||||
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||
|
||||
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable-all',
|
||||
);
|
||||
if (!enableAllCmd?.action) {
|
||||
throw new Error('enable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No hooks configured.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info when all hooks are already enabled', async () => {
|
||||
const mockHooks = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||
];
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const enableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable-all',
|
||||
);
|
||||
if (!enableAllCmd?.action) {
|
||||
throw new Error('enable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await enableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'All hooks are already enabled.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable-all subcommand', () => {
|
||||
it('should return error when config is not loaded', async () => {
|
||||
const contextWithoutConfig = createMockCommandContext({
|
||||
services: {
|
||||
config: null,
|
||||
},
|
||||
});
|
||||
|
||||
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable-all',
|
||||
);
|
||||
if (!disableAllCmd?.action) {
|
||||
throw new Error('disable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await disableAllCmd.action(contextWithoutConfig, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when hook system is not enabled', async () => {
|
||||
mockConfig.getHookSystem.mockReturnValue(null);
|
||||
|
||||
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable-all',
|
||||
);
|
||||
if (!disableAllCmd?.action) {
|
||||
throw new Error('disable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await disableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Hook system is not enabled.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable all enabled hooks', async () => {
|
||||
const mockHooks = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, true),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, true),
|
||||
createMockHook('hook-3', HookEventName.BeforeAgent, false), // already disabled
|
||||
];
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable-all',
|
||||
);
|
||||
if (!disableAllCmd?.action) {
|
||||
throw new Error('disable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await disableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'hooks.disabled',
|
||||
['hook-1', 'hook-2', 'hook-3'],
|
||||
);
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'hook-1',
|
||||
false,
|
||||
);
|
||||
expect(mockHookSystem.setHookEnabled).toHaveBeenCalledWith(
|
||||
'hook-2',
|
||||
false,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Disabled 2 hook(s) successfully.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info when no hooks are configured', async () => {
|
||||
mockHookSystem.getAllHooks.mockReturnValue([]);
|
||||
|
||||
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable-all',
|
||||
);
|
||||
if (!disableAllCmd?.action) {
|
||||
throw new Error('disable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await disableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No hooks configured.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return info when all hooks are already disabled', async () => {
|
||||
const mockHooks = [
|
||||
createMockHook('hook-1', HookEventName.BeforeTool, false),
|
||||
createMockHook('hook-2', HookEventName.AfterTool, false),
|
||||
];
|
||||
mockHookSystem.getAllHooks.mockReturnValue(mockHooks);
|
||||
|
||||
const disableAllCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable-all',
|
||||
);
|
||||
if (!disableAllCmd?.action) {
|
||||
throw new Error('disable-all command must have an action');
|
||||
}
|
||||
|
||||
const result = await disableAllCmd.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.settings.setValue).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'All hooks are already disabled.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,24 +30,7 @@ async function panelAction(
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'Hook system is not enabled. Enable it in settings with hooks.enabled.',
|
||||
};
|
||||
}
|
||||
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
if (allHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content:
|
||||
'No hooks configured. Add hooks to your settings to get started.',
|
||||
};
|
||||
}
|
||||
const allHooks = hookSystem?.getAllHooks() || [];
|
||||
|
||||
const hooksListItem: HistoryItemHooksList = {
|
||||
type: MessageType.HOOKS_LIST,
|
||||
@@ -102,7 +85,10 @@ async function enableAction(
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
try {
|
||||
settings.setValue(SettingScope.User, 'hooks.disabled', newDisabledHooks);
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooks.disabled', newDisabledHooks);
|
||||
|
||||
// Enable in hook system
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
@@ -165,7 +151,10 @@ async function disableAction(
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
try {
|
||||
settings.setValue(SettingScope.User, 'hooks.disabled', newDisabledHooks);
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooks.disabled', newDisabledHooks);
|
||||
|
||||
// Disable in hook system
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
@@ -216,6 +205,145 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
|
||||
return hook.config.name || hook.config.command || 'unknown-hook';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable all hooks by clearing the disabled list
|
||||
*/
|
||||
async function enableAllAction(
|
||||
context: CommandContext,
|
||||
): Promise<void | MessageActionReturn> {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
};
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Hook system is not enabled.',
|
||||
};
|
||||
}
|
||||
|
||||
const settings = context.services.settings;
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
|
||||
if (allHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No hooks configured.',
|
||||
};
|
||||
}
|
||||
|
||||
const disabledHooks = allHooks.filter((hook) => !hook.enabled);
|
||||
if (disabledHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'All hooks are already enabled.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooks.disabled', []);
|
||||
|
||||
for (const hook of disabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
hookSystem.setHookEnabled(hookName, true);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Enabled ${disabledHooks.length} hook(s) successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to enable hooks: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all hooks by adding all hooks to the disabled list
|
||||
*/
|
||||
async function disableAllAction(
|
||||
context: CommandContext,
|
||||
): Promise<void | MessageActionReturn> {
|
||||
const { config } = context.services;
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Config not loaded.',
|
||||
};
|
||||
}
|
||||
|
||||
const hookSystem = config.getHookSystem();
|
||||
if (!hookSystem) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Hook system is not enabled.',
|
||||
};
|
||||
}
|
||||
|
||||
const settings = context.services.settings;
|
||||
const allHooks = hookSystem.getAllHooks();
|
||||
|
||||
if (allHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No hooks configured.',
|
||||
};
|
||||
}
|
||||
|
||||
const enabledHooks = allHooks.filter((hook) => hook.enabled);
|
||||
if (enabledHooks.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'All hooks are already disabled.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const allHookNames = allHooks.map((hook) => getHookDisplayName(hook));
|
||||
const scope = settings.workspace
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
settings.setValue(scope, 'hooks.disabled', allHookNames);
|
||||
|
||||
for (const hook of enabledHooks) {
|
||||
const hookName = getHookDisplayName(hook);
|
||||
hookSystem.setHookEnabled(hookName, false);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Disabled ${enabledHooks.length} hook(s) successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to disable hooks: ${getErrorMessage(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const panelCommand: SlashCommand = {
|
||||
name: 'panel',
|
||||
altNames: ['list', 'show'],
|
||||
@@ -242,10 +370,34 @@ const disableCommand: SlashCommand = {
|
||||
completion: completeHookNames,
|
||||
};
|
||||
|
||||
const enableAllCommand: SlashCommand = {
|
||||
name: 'enable-all',
|
||||
altNames: ['enableall'],
|
||||
description: 'Enable all disabled hooks',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: enableAllAction,
|
||||
};
|
||||
|
||||
const disableAllCommand: SlashCommand = {
|
||||
name: 'disable-all',
|
||||
altNames: ['disableall'],
|
||||
description: 'Disable all enabled hooks',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: disableAllAction,
|
||||
};
|
||||
|
||||
export const hooksCommand: SlashCommand = {
|
||||
name: 'hooks',
|
||||
description: 'Manage hooks',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [panelCommand, enableCommand, disableCommand],
|
||||
subCommands: [
|
||||
panelCommand,
|
||||
enableCommand,
|
||||
disableCommand,
|
||||
enableAllCommand,
|
||||
disableAllCommand,
|
||||
],
|
||||
action: async (context: CommandContext) => panelCommand.action!(context, ''),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user