Files
ccs/src/utils/websearch/hook-installer.ts
T
Tam Nhu Tran 4f6e61739c refactor(config): adopt config-loader-facade across the codebase
Issue #1161. Sweeps 127 files to import from
src/config/config-loader-facade.ts instead of unified-config-loader or
utils/config-manager directly.

WRITE callers (32 files): replaced raw saveUnifiedConfig /
mutateUnifiedConfig / updateUnifiedConfig calls with the facade's
cache-coherent wrappers saveConfig / mutateConfig / updateConfig. This
fixes a latent stale-cache window where direct writes through the
underlying loader bypassed the facade's memoization.

READ callers (95 files): mechanical import-path migration only —
function names unchanged because the facade re-exports them. No
behavior change.

Also updated:
- tests/unit/utils/browser/browser-setup.test.ts (DI interface rename)
- src/management/checks/image-analysis-check.ts (dynamic import rename)
- src/web-server/health-service.ts (dynamic require rename)
- src/ccs.ts (path prefix fix from sweep script)

After sweep: zero raw write callers remain outside src/config/. Direct
imports of config-manager remain only for symbols not in the facade
(getConfigPath, getCcsDirSource, etc). Behavior unchanged; full suite
passes 1824/1824.

Out of scope: switching loadOrCreateUnifiedConfig() callers to
getCachedConfig() — needs per-callsite cache-safety analysis. Tracked
as follow-up.

Refs #1161
2026-05-03 01:42:53 -04:00

203 lines
5.7 KiB
TypeScript

/**
* WebSearch Hook Installer
*
* Manages installation and uninstallation of the legacy WebSearch hook runtime.
*
* @module utils/websearch/hook-installer
*/
import * as fs from 'fs';
import * as path from 'path';
import { info, warn } from '../ui';
import { getCcsDir, getCcsHooksDir } from '../config-manager';
import { getHookPath } from './hook-config';
import { getWebSearchConfig } from '../../config/config-loader-facade';
// Re-export from hook-config for backward compatibility
export { getHookPath, getWebSearchHookConfig } from './hook-config';
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
function hasMatchingHookContents(sourcePath: string, destinationPath: string): boolean {
if (!fs.existsSync(destinationPath)) {
return false;
}
const source = fs.readFileSync(sourcePath);
try {
const destination = fs.readFileSync(destinationPath);
return source.equals(destination);
} catch (error) {
if (process.env.CCS_DEBUG) {
console.error(
warn(`Existing WebSearch hook is unreadable; reinstalling: ${(error as Error).message}`)
);
}
return false;
}
}
function getTempHookPath(hookPath: string): string {
const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `${hookPath}.${uniqueSuffix}.tmp`;
}
export 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
*/
export function hasWebSearchHook(): boolean {
return fs.existsSync(getHookPath());
}
/**
* Install WebSearch hook to ~/.ccs/hooks/
*
* This hook now serves as a compatibility fallback and a shared provider runtime
* for the first-class CCS WebSearch MCP server.
*
* @returns true if hook installed successfully
*/
export function installWebSearchHook(): boolean {
try {
const wsConfig = getWebSearchConfig();
// Skip if disabled
if (!wsConfig.enabled) {
if (process.env.CCS_DEBUG) {
console.error(info('WebSearch disabled - skipping hook install'));
}
return false;
}
// Ensure hooks directory exists
const hooksDir = getCcsHooksDir();
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, { recursive: true, mode: 0o700 });
}
const hookPath = getHookPath();
// Find the bundled hook script
// In npm package: node_modules/ccs/lib/hooks/
// In development: lib/hooks/
const possiblePaths = [
path.join(__dirname, '..', '..', '..', 'lib', 'hooks', WEBSEARCH_HOOK),
path.join(__dirname, '..', '..', 'lib', 'hooks', WEBSEARCH_HOOK),
path.join(__dirname, '..', 'lib', 'hooks', WEBSEARCH_HOOK),
];
let sourcePath: string | null = null;
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
sourcePath = p;
break;
}
}
if (!sourcePath) {
if (process.env.CCS_DEBUG) {
console.error(warn(`WebSearch hook source not found: ${WEBSEARCH_HOOK}`));
}
return false;
}
// Avoid rewriting the shared hook binary when the bundled script is unchanged.
if (hasMatchingHookContents(sourcePath, hookPath)) {
return true;
}
// Copy hook to ~/.ccs/hooks/ via a unique temp path so concurrent installers
// do not contend on the same file.
const tempHookPath = getTempHookPath(hookPath);
try {
fs.copyFileSync(sourcePath, tempHookPath);
fs.chmodSync(tempHookPath, 0o755);
try {
fs.renameSync(tempHookPath, hookPath);
} catch (renameError) {
const errorCode = (renameError as NodeJS.ErrnoException).code;
if (errorCode !== 'EEXIST' && errorCode !== 'EPERM') {
throw renameError;
}
fs.copyFileSync(tempHookPath, hookPath);
fs.chmodSync(hookPath, 0o755);
fs.unlinkSync(tempHookPath);
}
} finally {
if (fs.existsSync(tempHookPath)) {
fs.unlinkSync(tempHookPath);
}
}
if (process.env.CCS_DEBUG) {
console.error(info(`Installed WebSearch hook: ${hookPath}`));
}
// Note: Hook registration is handled by ensureProfileHooks() in profile-hook-injector.ts
// which writes to per-profile settings (~/.ccs/<profile>.settings.json)
// Global settings (~/.claude/settings.json) are NOT modified here
return true;
} catch (error) {
if (process.env.CCS_DEBUG) {
console.error(warn(`Failed to install WebSearch hook: ${(error as Error).message}`));
}
return false;
}
}
/**
* Uninstall WebSearch hook from ~/.ccs/hooks/
*
* Note: Does NOT touch global ~/.claude/settings.json.
* Profile-specific hooks are removed when ~/.ccs/ is deleted.
*
* @returns true if hook uninstalled successfully
*/
export function uninstallWebSearchHook(): boolean {
try {
const hookPath = getHookPath();
if (fs.existsSync(hookPath)) {
fs.unlinkSync(hookPath);
if (process.env.CCS_DEBUG) {
console.error(info(`Uninstalled WebSearch hook: ${hookPath}`));
}
}
// Remove migration marker (so fresh install re-runs migration)
removeMigrationMarker();
// Note: Do NOT call removeHookConfig() - global settings should not be touched.
// Per-profile hooks in ~/.ccs/*.settings.json are cleaned up when ~/.ccs/ is deleted.
return true;
} catch (error) {
if (process.env.CCS_DEBUG) {
console.error(warn(`Failed to uninstall WebSearch hook: ${(error as Error).message}`));
}
return false;
}
}