diff --git a/scripts/postuninstall.js b/scripts/postuninstall.js index 5058e4ae..bb7561d6 100644 --- a/scripts/postuninstall.js +++ b/scripts/postuninstall.js @@ -34,8 +34,14 @@ function cleanupCcsFiles() { // Note: Do NOT touch ~/.claude/settings.json // Per-profile hooks in ~/.ccs/*.settings.json will be cleaned up // when the user removes ~/.ccs/ directory. - } catch { - // Silent fail - not critical + } catch (err) { + // Silent fail - not critical, but log for debugging + try { + const logPath = path.join(CCS_DIR, 'uninstall.log'); + fs.appendFileSync(logPath, `${new Date().toISOString()}: ${err.message}\n`); + } catch { + // Ignore logging failures + } } } diff --git a/src/commands/install-command.ts b/src/commands/install-command.ts index 8cc67a19..076e97fb 100644 --- a/src/commands/install-command.ts +++ b/src/commands/install-command.ts @@ -39,15 +39,13 @@ export async function handleUninstallCommand(): Promise { const hookRemoved = uninstallWebSearchHook(); if (hookRemoved) { console.log(ok('Removed WebSearch hook')); - removed++; + removed += 1; // Count as 1 item (the hook file) } // 2. Remove symlinks from ~/.claude/ const symlinkManager = new ClaudeSymlinkManager(); const symlinksRemoved = symlinkManager.uninstall(); - if (symlinksRemoved > 0) { - removed++; - } + removed += symlinksRemoved; // Add actual count of symlinks removed // 3. Summary console.log(''); diff --git a/src/utils/websearch/profile-hook-injector.ts b/src/utils/websearch/profile-hook-injector.ts index 5bdc7ea5..214184da 100644 --- a/src/utils/websearch/profile-hook-injector.ts +++ b/src/utils/websearch/profile-hook-injector.ts @@ -63,8 +63,8 @@ function migrateGlobalHook(): void { if (!fs.existsSync(ccsDir)) { fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 }); } - // Create marker file - fs.writeFileSync(markerPath, new Date().toISOString(), 'utf8'); + // Create marker file atomically (wx = fail if exists, prevents race condition) + fs.writeFileSync(markerPath, new Date().toISOString(), { encoding: 'utf8', flag: 'wx' }); } catch (error) { if (process.env.CCS_DEBUG) { console.error(warn(`Migration failed: ${(error as Error).message}`)); @@ -114,9 +114,11 @@ export function ensureProfileHooks(profileName: string): boolean { try { const content = fs.readFileSync(settingsPath, 'utf8'); settings = JSON.parse(content); - } catch { + } catch (parseError) { if (process.env.CCS_DEBUG) { - console.error(warn(`Malformed ${profileName}.settings.json - creating fresh hooks`)); + console.error( + warn(`Malformed ${profileName}.settings.json: ${(parseError as Error).message}`) + ); } // Continue with empty settings, will add hooks } diff --git a/tests/native/windows/uninstall.ps1 b/tests/native/windows/uninstall.ps1 index b9ac0785..d00e104e 100644 --- a/tests/native/windows/uninstall.ps1 +++ b/tests/native/windows/uninstall.ps1 @@ -428,42 +428,15 @@ Test-Case "Edge case: Missing parent directories" "Handles missing .claude direc } # ============================================================================ -# TEST SECTION 7: SETTINGS.JSON HOOK CLEANUP +# TEST SECTION 7: PER-PROFILE HOOK BEHAVIOR (Global settings untouched) # ============================================================================ Write-Host "" Write-ColorOutput "========================================" "Yellow" -Write-ColorOutput "SECTION 7: SETTINGS.JSON HOOK CLEANUP" "Yellow" +Write-ColorOutput "SECTION 7: PER-PROFILE HOOK BEHAVIOR" "Yellow" Write-ColorOutput "========================================" "Yellow" -Test-Case "Hook cleanup: Removes CCS WebSearch hook from settings.json" "Hook removed" { - # Setup: Create settings.json with CCS hook - New-Item -ItemType Directory -Path $TestClaudeDir -Force | Out-Null - $settingsPath = Join-Path $TestClaudeDir "settings.json" - $settingsContent = @' -{ - "hooks": { - "PreToolUse": [ - { "matcher": "WebSearch", "hooks": [{ "command": "node ~/.ccs/hooks/websearch-transformer.cjs" }] } - ] - } -} -'@ - Set-Content -Path $settingsPath -Value $settingsContent - - # Install then uninstall - $env:HOME = $TestHome - try { - & $CcsPath --install | Out-Null - & $CcsPath --uninstall | Out-Null - } catch { } - - # Check hook removed - $content = Get-Content -Path $settingsPath -Raw - -not ($content -match "websearch-transformer") -} - -Test-Case "Hook cleanup: Preserves user-defined WebSearch hooks" "User hooks kept" { +Test-Case "Per-profile: Uninstall does NOT touch global settings.json" "Global settings unchanged" { # Setup: Create settings.json with user hook New-Item -ItemType Directory -Path $TestClaudeDir -Force | Out-Null $settingsPath = Join-Path $TestClaudeDir "settings.json" @@ -484,13 +457,13 @@ Test-Case "Hook cleanup: Preserves user-defined WebSearch hooks" "User hooks kep & $CcsPath --uninstall | Out-Null } catch { } - # Check user hook preserved + # Verify global settings unchanged $content = Get-Content -Path $settingsPath -Raw $content -match "my-custom-hook" } -Test-Case "Hook cleanup: Handles mixed CCS and user hooks" "Only CCS hook removed" { - # Setup: Create settings.json with both CCS and user hooks +Test-Case "Per-profile: Uninstall preserves CCS hooks in global settings (not touched)" "Both hooks preserved" { + # Setup: Create settings.json with CCS hook (old format - shouldn't be touched) New-Item -ItemType Directory -Path $TestClaudeDir -Force | Out-Null $settingsPath = Join-Path $TestClaudeDir "settings.json" $settingsContent = @' @@ -498,28 +471,41 @@ Test-Case "Hook cleanup: Handles mixed CCS and user hooks" "Only CCS hook remove "hooks": { "PreToolUse": [ { "matcher": "WebSearch", "hooks": [{ "command": "node ~/.ccs/hooks/websearch-transformer.cjs" }] }, - { "matcher": "WebSearch", "hooks": [{ "command": "my-custom-hook.sh" }] }, - { "matcher": "Bash", "hooks": [{ "command": "bash-hook.sh" }] } + { "matcher": "WebSearch", "hooks": [{ "command": "my-custom-hook.sh" }] } ] } } '@ Set-Content -Path $settingsPath -Value $settingsContent + # Uninstall - should NOT modify global settings + $env:HOME = $TestHome + try { + & $CcsPath --uninstall | Out-Null + } catch { } + + # Both hooks should still be in global settings (not touched) + $content = Get-Content -Path $settingsPath -Raw + ($content -match "websearch-transformer") -and ($content -match "my-custom-hook") +} + +Test-Case "Per-profile: Migration marker cleanup on uninstall" "Marker file removed" { + # Setup: Create ~/.ccs/.hook-migrated marker + $ccsDir = Join-Path $TestHome ".ccs" + New-Item -ItemType Directory -Path $ccsDir -Force | Out-Null + Set-Content -Path (Join-Path $ccsDir ".hook-migrated") -Value "2026-01-25T00:00:00.000Z" + # Uninstall $env:HOME = $TestHome try { & $CcsPath --uninstall | Out-Null } catch { } - # Check CCS hook removed, user hooks preserved - $content = Get-Content -Path $settingsPath -Raw - (-not ($content -match "websearch-transformer")) -and - ($content -match "my-custom-hook") -and - ($content -match "bash-hook") + # Verify marker removed + -not (Test-Path (Join-Path $ccsDir ".hook-migrated")) } -Test-Case "Hook cleanup: Handles missing settings.json" "No error when settings.json missing" { +Test-Case "Per-profile: Handles missing settings.json" "No error when settings.json missing" { # Ensure settings.json doesn't exist $settingsPath = Join-Path $TestClaudeDir "settings.json" if (Test-Path $settingsPath) {