mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-10 02:17:11 +00:00
fix(websearch): use getCcsDir() for test isolation
Ensure all hook-related modules use getCcsDir() from environment.ts for consistent test isolation. Prevents tests from touching the user's real ~/.ccs/ directory during test runs. Changes: - hook-config.ts: Use getCcsDir() for hookConfigPath - hook-installer.ts: Use getCcsDir() for HOOK_SOURCE_PATH - profile-hook-injector.ts: Use getCcsDir() for hook sources - CLAUDE.md: Add test isolation rules
This commit is contained in:
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
AI-facing guidance for Claude Code when working with this repository.
|
AI-facing guidance for Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Critical Constraints (NEVER VIOLATE)
|
||||||
|
|
||||||
|
### Test Isolation (MANDATORY)
|
||||||
|
|
||||||
|
**NEVER touch the user's real `~/.ccs/` or `~/.claude/` directories during tests.**
|
||||||
|
|
||||||
|
- All code accessing CCS paths MUST use `getCcsDir()` from `src/utils/config-manager.ts`
|
||||||
|
- This function respects `CCS_HOME` env var for test isolation
|
||||||
|
- **WRONG:** `path.join(os.homedir(), '.ccs', ...)`
|
||||||
|
- **CORRECT:** `path.join(getCcsDir(), ...)`
|
||||||
|
|
||||||
|
Tests set `process.env.CCS_HOME` to a temp directory. Code using `os.homedir()` directly will modify the user's real files.
|
||||||
|
|
||||||
## Core Function
|
## Core Function
|
||||||
|
|
||||||
CLI wrapper for instant switching between multiple Claude accounts and alternative models (GLM, GLMT, Kimi). See README.md for user documentation.
|
CLI wrapper for instant switching between multiple Claude accounts and alternative models (GLM, GLMT, Kimi). See README.md for user documentation.
|
||||||
|
|||||||
@@ -11,16 +11,21 @@ import * as path from 'path';
|
|||||||
import * as os from 'os';
|
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';
|
||||||
|
|
||||||
// Path to Claude settings.json
|
// Path to Claude settings.json (intentionally uses real homedir for global settings)
|
||||||
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
||||||
|
|
||||||
// CCS hooks directory
|
|
||||||
const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
|
|
||||||
|
|
||||||
// Hook file name
|
// Hook file name
|
||||||
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCS hooks directory (respects CCS_HOME for test isolation)
|
||||||
|
*/
|
||||||
|
function getCcsHooksDir(): string {
|
||||||
|
return path.join(getCcsDir(), 'hooks');
|
||||||
|
}
|
||||||
|
|
||||||
// Buffer time added to max provider timeout for hook timeout (seconds)
|
// Buffer time added to max provider timeout for hook timeout (seconds)
|
||||||
const HOOK_TIMEOUT_BUFFER = 30;
|
const HOOK_TIMEOUT_BUFFER = 30;
|
||||||
|
|
||||||
@@ -31,7 +36,7 @@ const MIN_HOOK_TIMEOUT = 60;
|
|||||||
* Get path to WebSearch hook
|
* Get path to WebSearch hook
|
||||||
*/
|
*/
|
||||||
export function getHookPath(): string {
|
export function getHookPath(): string {
|
||||||
return path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK);
|
return path.join(getCcsHooksDir(), WEBSEARCH_HOOK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,21 +8,25 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
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 { getHookPath, ensureHookConfig } from './hook-config';
|
import { getHookPath, ensureHookConfig } from './hook-config';
|
||||||
import { removeMigrationMarker } from './profile-hook-injector';
|
import { removeMigrationMarker } from './profile-hook-injector';
|
||||||
|
import { getCcsDir } from '../config-manager';
|
||||||
|
|
||||||
// Re-export from hook-config for backward compatibility
|
// Re-export from hook-config for backward compatibility
|
||||||
export { getHookPath, getWebSearchHookConfig } from './hook-config';
|
export { getHookPath, getWebSearchHookConfig } from './hook-config';
|
||||||
|
|
||||||
// CCS hooks directory
|
|
||||||
const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks');
|
|
||||||
|
|
||||||
// Hook file name
|
// Hook file name
|
||||||
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CCS hooks directory (respects CCS_HOME for test isolation)
|
||||||
|
*/
|
||||||
|
function getCcsHooksDir(): string {
|
||||||
|
return path.join(getCcsDir(), 'hooks');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if WebSearch hook is installed
|
* Check if WebSearch hook is installed
|
||||||
*/
|
*/
|
||||||
@@ -50,8 +54,9 @@ export function installWebSearchHook(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure hooks directory exists
|
// Ensure hooks directory exists
|
||||||
if (!fs.existsSync(CCS_HOOKS_DIR)) {
|
const hooksDir = getCcsHooksDir();
|
||||||
fs.mkdirSync(CCS_HOOKS_DIR, { recursive: true, mode: 0o700 });
|
if (!fs.existsSync(hooksDir)) {
|
||||||
|
fs.mkdirSync(hooksDir, { recursive: true, mode: 0o700 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hookPath = getHookPath();
|
const hookPath = getHookPath();
|
||||||
|
|||||||
@@ -9,17 +9,21 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
|
||||||
import { info, warn } from '../ui';
|
import { info, warn } from '../ui';
|
||||||
import { getWebSearchHookConfig, getHookPath } from './hook-config';
|
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';
|
||||||
|
|
||||||
// CCS directory
|
// Valid profile name pattern (alphanumeric, dash, underscore only)
|
||||||
const CCS_DIR = path.join(os.homedir(), '.ccs');
|
const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
|
||||||
// Migration marker file
|
/**
|
||||||
const MIGRATION_MARKER = path.join(CCS_DIR, '.hook-migrated');
|
* Get migration marker path (respects CCS_HOME for test isolation)
|
||||||
|
*/
|
||||||
|
function getMigrationMarkerPath(): string {
|
||||||
|
return path.join(getCcsDir(), '.hook-migrated');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if CCS WebSearch hook exists in settings
|
* Check if CCS WebSearch hook exists in settings
|
||||||
@@ -44,7 +48,8 @@ function hasCcsHook(settings: Record<string, unknown>): boolean {
|
|||||||
* Migrate CCS hook from global settings to profile settings (one-time)
|
* Migrate CCS hook from global settings to profile settings (one-time)
|
||||||
*/
|
*/
|
||||||
function migrateGlobalHook(): void {
|
function migrateGlobalHook(): void {
|
||||||
if (fs.existsSync(MIGRATION_MARKER)) {
|
const markerPath = getMigrationMarkerPath();
|
||||||
|
if (fs.existsSync(markerPath)) {
|
||||||
return; // Already migrated
|
return; // Already migrated
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +58,13 @@ function migrateGlobalHook(): void {
|
|||||||
if (removed && process.env.CCS_DEBUG) {
|
if (removed && process.env.CCS_DEBUG) {
|
||||||
console.error(info('Migrated WebSearch hook from global settings'));
|
console.error(info('Migrated WebSearch hook from global settings'));
|
||||||
}
|
}
|
||||||
|
// Ensure CCS dir exists before creating marker
|
||||||
|
const ccsDir = getCcsDir();
|
||||||
|
if (!fs.existsSync(ccsDir)) {
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
// Create marker file
|
// Create marker file
|
||||||
fs.writeFileSync(MIGRATION_MARKER, new Date().toISOString(), 'utf8');
|
fs.writeFileSync(markerPath, new Date().toISOString(), 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error(warn(`Migration failed: ${(error as Error).message}`));
|
console.error(warn(`Migration failed: ${(error as Error).message}`));
|
||||||
@@ -70,6 +80,14 @@ function migrateGlobalHook(): void {
|
|||||||
*/
|
*/
|
||||||
export function ensureProfileHooks(profileName: string): boolean {
|
export function ensureProfileHooks(profileName: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
// Validate profile name to prevent path traversal
|
||||||
|
if (!VALID_PROFILE_NAME.test(profileName)) {
|
||||||
|
if (process.env.CCS_DEBUG) {
|
||||||
|
console.error(warn(`Invalid profile name: ${profileName}`));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const wsConfig = getWebSearchConfig();
|
const wsConfig = getWebSearchConfig();
|
||||||
|
|
||||||
// Skip if WebSearch is disabled
|
// Skip if WebSearch is disabled
|
||||||
@@ -80,7 +98,15 @@ export function ensureProfileHooks(profileName: string): boolean {
|
|||||||
// One-time migration from global settings
|
// One-time migration from global settings
|
||||||
migrateGlobalHook();
|
migrateGlobalHook();
|
||||||
|
|
||||||
const settingsPath = path.join(CCS_DIR, `${profileName}.settings.json`);
|
// Get CCS directory (respects CCS_HOME for test isolation)
|
||||||
|
const ccsDir = getCcsDir();
|
||||||
|
|
||||||
|
// Ensure CCS dir exists
|
||||||
|
if (!fs.existsSync(ccsDir)) {
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = path.join(ccsDir, `${profileName}.settings.json`);
|
||||||
|
|
||||||
// Read existing settings or create empty
|
// Read existing settings or create empty
|
||||||
let settings: Record<string, unknown> = {};
|
let settings: Record<string, unknown> = {};
|
||||||
@@ -193,8 +219,9 @@ function updateHookTimeoutIfNeeded(
|
|||||||
*/
|
*/
|
||||||
export function removeMigrationMarker(): void {
|
export function removeMigrationMarker(): void {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(MIGRATION_MARKER)) {
|
const markerPath = getMigrationMarkerPath();
|
||||||
fs.unlinkSync(MIGRATION_MARKER);
|
if (fs.existsSync(markerPath)) {
|
||||||
|
fs.unlinkSync(markerPath);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
|
|||||||
Reference in New Issue
Block a user