diff --git a/src/utils/websearch/hook-installer.ts b/src/utils/websearch/hook-installer.ts index 49bccb8f..77b36f39 100644 --- a/src/utils/websearch/hook-installer.ts +++ b/src/utils/websearch/hook-installer.ts @@ -10,9 +10,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { info, warn } from '../ui'; import { getWebSearchConfig } from '../../config/unified-config-loader'; -import { getCcsHooksDir } from '../config-manager'; +import { getCcsDir, getCcsHooksDir } from '../config-manager'; import { getHookPath } from './hook-config'; -import { removeMigrationMarker } from './profile-hook-injector'; // Re-export from hook-config for backward compatibility export { getHookPath, getWebSearchHookConfig } from './hook-config'; @@ -20,6 +19,23 @@ export { getHookPath, getWebSearchHookConfig } from './hook-config'; // Hook file name const WEBSEARCH_HOOK = 'websearch-transformer.cjs'; +function getMigrationMarkerPath(): string { + return path.join(getCcsDir(), '.hook-migrated'); +} + +export function removeMigrationMarker(): void { + try { + const markerPath = getMigrationMarkerPath(); + if (fs.existsSync(markerPath)) { + fs.unlinkSync(markerPath); + } + } catch (error) { + if (process.env.CCS_DEBUG) { + console.error(warn(`removeMigrationMarker failed: ${(error as Error).message}`)); + } + } +} + /** * Check if WebSearch hook is installed */ diff --git a/src/utils/websearch/profile-hook-injector.ts b/src/utils/websearch/profile-hook-injector.ts index 698f00f6..d2fd3867 100644 --- a/src/utils/websearch/profile-hook-injector.ts +++ b/src/utils/websearch/profile-hook-injector.ts @@ -15,6 +15,7 @@ import { getWebSearchConfig } from '../../config/unified-config-loader'; import { removeHookConfig } from './hook-config'; import { getCcsDir } from '../config-manager'; import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils'; +import { installWebSearchHook } from './hook-installer'; // Valid profile name pattern (alphanumeric, dash, underscore only) const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/; @@ -100,6 +101,14 @@ export function ensureProfileHooks(profileName: string): boolean { fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 }); } + // Keep the injected command target valid for all profile types, not just CLIProxy. + if (!installWebSearchHook() && !fs.existsSync(getHookPath())) { + if (process.env.CCS_DEBUG) { + console.error(warn('WebSearch hook binary is missing and could not be installed')); + } + return false; + } + const settingsPath = path.join(ccsDir, `${profileName}.settings.json`); // Read existing settings or create empty diff --git a/tests/unit/utils/websearch/profile-hook-injector.test.ts b/tests/unit/utils/websearch/profile-hook-injector.test.ts new file mode 100644 index 00000000..0b7b16b4 --- /dev/null +++ b/tests/unit/utils/websearch/profile-hook-injector.test.ts @@ -0,0 +1,41 @@ +import { afterEach, describe, expect, it } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ensureProfileHooks } from '../../../../src/utils/websearch/profile-hook-injector'; +import { getHookPath } from '../../../../src/utils/websearch/hook-config'; + +describe('ensureProfileHooks', () => { + let tempHome: string | undefined; + let originalCcsHome: string | undefined; + + afterEach(() => { + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + + if (tempHome && fs.existsSync(tempHome)) { + fs.rmSync(tempHome, { recursive: true, force: true }); + } + + tempHome = undefined; + originalCcsHome = undefined; + }); + + it('installs the hook binary before writing the profile hook command', () => { + tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-hook-test-')); + originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + + const ensured = ensureProfileHooks('glm'); + const hookPath = getHookPath(); + const settingsPath = path.join(tempHome, '.ccs', 'glm.settings.json'); + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + + expect(ensured).toBe(true); + expect(fs.existsSync(hookPath)).toBe(true); + expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe(`node "${hookPath}"`); + }); +});