mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
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
203 lines
5.7 KiB
TypeScript
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;
|
|
}
|
|
}
|