mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
refactor(websearch): extract shared hook utils to DRY module
- Create hook-utils.ts with isCcsWebSearchHook() and deduplicateCcsHooks() - Update profile-hook-injector.ts and hook-config.ts to use shared module - Combine double writeFileSync into single write in ensureHookConfig() Addresses code review feedback on PR #420
This commit is contained in:
1 parent
847aad00fe
commit
1f8d9b82d5
3 files changed
+58
-101
No files matched your search
@@ -12,6 +12,7 @@ import * as os from 'os';
|
|||||||
import { info, warn } from '../ui';
|
import { info, warn } from '../ui';
|
||||||
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
|
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
|
||||||
|
|
||||||
// Hook file name
|
// Hook file name
|
||||||
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
||||||
@@ -51,47 +52,6 @@ export function getHookPath(): string {
|
|||||||
return path.join(getCcsHooksDir(), WEBSEARCH_HOOK);
|
return path.join(getCcsHooksDir(), WEBSEARCH_HOOK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a hook entry is a CCS WebSearch hook
|
|
||||||
*/
|
|
||||||
function isCcsWebSearchHook(hook: Record<string, unknown>): boolean {
|
|
||||||
if (hook.matcher !== 'WebSearch') return false;
|
|
||||||
|
|
||||||
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
|
|
||||||
if (!hookArray?.[0]?.command) return false;
|
|
||||||
|
|
||||||
const command = hookArray[0].command;
|
|
||||||
if (typeof command !== 'string') return false;
|
|
||||||
// Normalize path separators for cross-platform matching (Windows uses backslashes)
|
|
||||||
const normalizedCommand = command.replace(/\\/g, '/');
|
|
||||||
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove duplicate CCS WebSearch hooks from settings, keeping only the first one
|
|
||||||
* Returns true if duplicates were removed
|
|
||||||
*/
|
|
||||||
function deduplicateCcsHooks(settings: Record<string, unknown>): boolean {
|
|
||||||
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
||||||
if (!hooks?.PreToolUse) return false;
|
|
||||||
|
|
||||||
let foundFirst = false;
|
|
||||||
const originalLength = hooks.PreToolUse.length;
|
|
||||||
|
|
||||||
hooks.PreToolUse = hooks.PreToolUse.filter((h: unknown) => {
|
|
||||||
const hook = h as Record<string, unknown>;
|
|
||||||
if (!isCcsWebSearchHook(hook)) return true; // Keep non-CCS hooks
|
|
||||||
|
|
||||||
if (!foundFirst) {
|
|
||||||
foundFirst = true;
|
|
||||||
return true; // Keep first CCS hook
|
|
||||||
}
|
|
||||||
return false; // Remove subsequent duplicates
|
|
||||||
});
|
|
||||||
|
|
||||||
return hooks.PreToolUse.length < originalLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get WebSearch hook configuration for settings.json
|
* Get WebSearch hook configuration for settings.json
|
||||||
* Timeout is computed from max provider timeout in config.yaml + buffer
|
* Timeout is computed from max provider timeout in config.yaml + buffer
|
||||||
@@ -177,12 +137,6 @@ export function ensureHookConfig(): boolean {
|
|||||||
if (webSearchHookIndex !== -1) {
|
if (webSearchHookIndex !== -1) {
|
||||||
// Hook exists - first clean up any duplicates
|
// Hook exists - first clean up any duplicates
|
||||||
const hadDuplicates = deduplicateCcsHooks(settings);
|
const hadDuplicates = deduplicateCcsHooks(settings);
|
||||||
if (hadDuplicates) {
|
|
||||||
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(info('Removed duplicate WebSearch hooks from settings.json'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check if it needs updating
|
// Then check if it needs updating
|
||||||
const existingHook = hooks.PreToolUse[webSearchHookIndex] as Record<string, unknown>;
|
const existingHook = hooks.PreToolUse[webSearchHookIndex] as Record<string, unknown>;
|
||||||
@@ -204,10 +158,13 @@ export function ensureHookConfig(): boolean {
|
|||||||
needsUpdate = true;
|
needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsUpdate) {
|
// Combine into single write if either changed
|
||||||
|
if (hadDuplicates || needsUpdate) {
|
||||||
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
|
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error(info('Updated WebSearch hook config in settings.json'));
|
if (hadDuplicates)
|
||||||
|
console.error(info('Removed duplicate WebSearch hooks from settings.json'));
|
||||||
|
if (needsUpdate) console.error(info('Updated WebSearch hook config in settings.json'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* WebSearch Hook Utilities
|
||||||
|
*
|
||||||
|
* Shared helper functions for WebSearch hook detection and deduplication.
|
||||||
|
*
|
||||||
|
* @module utils/websearch/hook-utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a hook entry is a CCS WebSearch hook
|
||||||
|
* Normalizes path separators for cross-platform matching (Windows uses backslashes)
|
||||||
|
*/
|
||||||
|
export function isCcsWebSearchHook(hook: Record<string, unknown>): boolean {
|
||||||
|
if (hook.matcher !== 'WebSearch') return false;
|
||||||
|
|
||||||
|
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
|
||||||
|
if (!hookArray?.[0]?.command) return false;
|
||||||
|
|
||||||
|
const command = hookArray[0].command;
|
||||||
|
if (typeof command !== 'string') return false;
|
||||||
|
|
||||||
|
// Normalize path separators for cross-platform matching
|
||||||
|
const normalizedCommand = command.replace(/\\/g, '/');
|
||||||
|
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove duplicate CCS WebSearch hooks from settings, keeping only the first one
|
||||||
|
* Returns true if duplicates were removed
|
||||||
|
*/
|
||||||
|
export function deduplicateCcsHooks(settings: Record<string, unknown>): boolean {
|
||||||
|
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
||||||
|
if (!hooks?.PreToolUse) return false;
|
||||||
|
|
||||||
|
let foundFirst = false;
|
||||||
|
const originalLength = hooks.PreToolUse.length;
|
||||||
|
|
||||||
|
hooks.PreToolUse = hooks.PreToolUse.filter((h: unknown) => {
|
||||||
|
const hook = h as Record<string, unknown>;
|
||||||
|
if (!isCcsWebSearchHook(hook)) return true; // Keep non-CCS hooks
|
||||||
|
|
||||||
|
if (!foundFirst) {
|
||||||
|
foundFirst = true;
|
||||||
|
return true; // Keep first CCS hook
|
||||||
|
}
|
||||||
|
return false; // Remove subsequent duplicates
|
||||||
|
});
|
||||||
|
|
||||||
|
return hooks.PreToolUse.length < originalLength;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { getWebSearchHookConfig, getHookPath } from './hook-config';
|
|||||||
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
import { getWebSearchConfig } from '../../config/unified-config-loader';
|
||||||
import { removeHookConfig } from './hook-config';
|
import { removeHookConfig } from './hook-config';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
|
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
|
||||||
|
|
||||||
// Valid profile name pattern (alphanumeric, dash, underscore only)
|
// Valid profile name pattern (alphanumeric, dash, underscore only)
|
||||||
const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/;
|
const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/;
|
||||||
@@ -33,61 +34,10 @@ function hasCcsHook(settings: Record<string, unknown>): boolean {
|
|||||||
if (!hooks?.PreToolUse) return false;
|
if (!hooks?.PreToolUse) return false;
|
||||||
|
|
||||||
return hooks.PreToolUse.some((h: unknown) => {
|
return hooks.PreToolUse.some((h: unknown) => {
|
||||||
const hook = h as Record<string, unknown>;
|
return isCcsWebSearchHook(h as Record<string, unknown>);
|
||||||
if (hook.matcher !== 'WebSearch') return false;
|
|
||||||
|
|
||||||
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
|
|
||||||
if (!hookArray?.[0]?.command) return false;
|
|
||||||
|
|
||||||
const command = hookArray[0].command;
|
|
||||||
if (typeof command !== 'string') return false;
|
|
||||||
// Normalize path separators for cross-platform matching (Windows uses backslashes)
|
|
||||||
const normalizedCommand = command.replace(/\\/g, '/');
|
|
||||||
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a hook entry is a CCS WebSearch hook
|
|
||||||
*/
|
|
||||||
function isCcsWebSearchHook(hook: Record<string, unknown>): boolean {
|
|
||||||
if (hook.matcher !== 'WebSearch') return false;
|
|
||||||
|
|
||||||
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
|
|
||||||
if (!hookArray?.[0]?.command) return false;
|
|
||||||
|
|
||||||
const command = hookArray[0].command;
|
|
||||||
if (typeof command !== 'string') return false;
|
|
||||||
// Normalize path separators for cross-platform matching (Windows uses backslashes)
|
|
||||||
const normalizedCommand = command.replace(/\\/g, '/');
|
|
||||||
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove duplicate CCS WebSearch hooks from settings, keeping only the first one
|
|
||||||
* Returns true if duplicates were removed
|
|
||||||
*/
|
|
||||||
function deduplicateCcsHooks(settings: Record<string, unknown>): boolean {
|
|
||||||
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
||||||
if (!hooks?.PreToolUse) return false;
|
|
||||||
|
|
||||||
let foundFirst = false;
|
|
||||||
const originalLength = hooks.PreToolUse.length;
|
|
||||||
|
|
||||||
hooks.PreToolUse = hooks.PreToolUse.filter((h: unknown) => {
|
|
||||||
const hook = h as Record<string, unknown>;
|
|
||||||
if (!isCcsWebSearchHook(hook)) return true; // Keep non-CCS hooks
|
|
||||||
|
|
||||||
if (!foundFirst) {
|
|
||||||
foundFirst = true;
|
|
||||||
return true; // Keep first CCS hook
|
|
||||||
}
|
|
||||||
return false; // Remove subsequent duplicates
|
|
||||||
});
|
|
||||||
|
|
||||||
return hooks.PreToolUse.length < originalLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate CCS hook from global settings to profile settings (one-time)
|
* Migrate CCS hook from global settings to profile settings (one-time)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user