param( [switch] $DryRun, [switch] $Help, [Parameter(ValueFromRemainingArguments = $true)] [object[]] $RemainingArgs = @() ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $PackageName = "free-claude-code" $FccHomeDirname = ".fcc" $FccCommands = @( "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi", "fcc-init", "free-claude-code" ) $script:UvPath = "" $script:UvToolBin = "" function Show-Usage { @" Usage: uninstall.ps1 [options] Removes the Free Claude Code uv tool and deletes ~/.fcc/ after removal is verified. Does not remove uv, Claude Code, Codex, Pi, the uv-managed Python runtime, or shared PATH entries. Options: -DryRun Print commands without running them. -Help Show this help text. "@ } function Write-Step { param([string] $Message) Write-Host "" Write-Host "==> $Message" } function Format-Argument { param([string] $Value) if ($Value -match '^[A-Za-z0-9_./:@%+=,\[\]\\-]+$') { return $Value } return "'" + ($Value -replace "'", "''") + "'" } function Format-Command { param( [string] $FilePath, [string[]] $Arguments = @() ) $parts = @($FilePath) + $Arguments return ($parts | ForEach-Object { Format-Argument ([string] $_) }) -join " " } function Get-ApplicationCommand { param([string] $Name) $commands = @(Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue) if ($commands.Count -eq 0) { return $null } return $commands[0] } function Invoke-NativeResult { param( [string] $FilePath, [string[]] $Arguments ) $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { $global:LASTEXITCODE = 0 $output = (& $FilePath @Arguments 2>&1 | Out-String).Trim() return [pscustomobject] @{ ExitCode = $LASTEXITCODE Output = $output } } finally { $ErrorActionPreference = $previousErrorActionPreference } } function Test-MissingUvToolError { param([string] $Output) $normalized = $Output.ToLowerInvariant() return $normalized.Contains($PackageName) -and $normalized.Contains("is not installed") } function Add-PathEntry { param([string] $PathEntry) if ([string]::IsNullOrWhiteSpace($PathEntry)) { return } $separator = [IO.Path]::PathSeparator $entries = @() if (-not [string]::IsNullOrEmpty($env:Path)) { $entries = $env:Path -split [regex]::Escape([string] $separator) } if ($entries -notcontains $PathEntry) { $env:Path = "$PathEntry$separator$env:Path" } } function Add-KnownUvPaths { Add-PathEntry (Join-Path $env:USERPROFILE ".local\bin") Add-PathEntry (Join-Path $env:USERPROFILE ".cargo\bin") } function Assert-NoFccProcessesRunning { $running = @() foreach ($commandName in $FccCommands) { $processes = @(Get-Process -Name $commandName -ErrorAction SilentlyContinue) if ($processes.Count -gt 0) { $running += $commandName } } if ($running.Count -gt 0) { throw "Free Claude Code is still running ($($running -join ', ')). Stop those processes, then rerun uninstall." } } function Initialize-UvContext { Add-KnownUvPaths if ($DryRun) { Write-Host "+ uv tool dir --bin" return } $uvCommand = Get-ApplicationCommand "uv" if (-not $uvCommand) { throw "uv is required to remove the Free Claude Code tool. Install uv, then rerun this uninstaller; ~/.fcc was not deleted." } $script:UvPath = $uvCommand.Source $commandText = Format-Command -FilePath $script:UvPath -Arguments @("tool", "dir", "--bin") Write-Host "+ $commandText" $result = Invoke-NativeResult -FilePath $script:UvPath -Arguments @("tool", "dir", "--bin") if ($result.ExitCode -ne 0) { if (-not [string]::IsNullOrWhiteSpace($result.Output)) { [Console]::Error.WriteLine($result.Output) } throw "Could not determine the uv tool bin directory (exit code $($result.ExitCode)); ~/.fcc was not deleted." } $script:UvToolBin = $result.Output.Trim() if ([string]::IsNullOrWhiteSpace($script:UvToolBin)) { throw "uv returned an empty tool bin directory; ~/.fcc was not deleted." } } function Uninstall-FreeClaudeCode { Write-Host "+ uv tool uninstall $PackageName" if ($DryRun) { return } $result = Invoke-NativeResult -FilePath $script:UvPath -Arguments @( "tool", "uninstall", $PackageName ) if ($result.ExitCode -eq 0) { if (-not [string]::IsNullOrWhiteSpace($result.Output)) { Write-Host $result.Output } return } if (Test-MissingUvToolError -Output $result.Output) { Write-Host "Free Claude Code uv tool is already absent; verifying its entry points." return } if (-not [string]::IsNullOrWhiteSpace($result.Output)) { [Console]::Error.WriteLine($result.Output) } throw "uv tool uninstall $PackageName failed with exit code $($result.ExitCode); ~/.fcc was not deleted." } function Confirm-FccCommandsRemoved { if ($DryRun) { Write-Host "+ verify all Free Claude Code entry points are absent from the uv tool bin directory" return } $remaining = @() $extensions = @("", ".exe", ".cmd", ".bat", ".ps1") foreach ($commandName in $FccCommands) { foreach ($extension in $extensions) { $commandPath = Join-Path $script:UvToolBin "$commandName$extension" if (Test-Path -LiteralPath $commandPath) { $remaining += $commandPath } } } if ($remaining.Count -gt 0) { throw "Free Claude Code entry points remain after uv uninstall: $($remaining -join ', '); ~/.fcc was not deleted." } } function Purge-FccHome { $fccHome = Join-Path $env:USERPROFILE $FccHomeDirname if (-not (Test-Path -LiteralPath $fccHome)) { Write-Host "No FCC config directory at $fccHome; skipping purge." return } $commandText = @( "Remove-Item", "-LiteralPath", (Format-Argument $fccHome), "-Recurse", "-Force" ) -join " " Write-Host "+ $commandText" if ($DryRun) { return } Remove-Item -LiteralPath $fccHome -Recurse -Force if (Test-Path -LiteralPath $fccHome) { throw "FCC config directory still exists after deletion: $fccHome" } } if ($Help) { Show-Usage return } if ($RemainingArgs.Count -gt 0) { Show-Usage throw "Unknown option: $($RemainingArgs -join ' ')" } if ([string]::IsNullOrWhiteSpace($env:USERPROFILE)) { throw "USERPROFILE is not set; cannot locate Free Claude Code data." } Write-Step "Checking for running Free Claude Code processes" Assert-NoFccProcessesRunning Write-Step "Locating the uv-managed Free Claude Code installation" Initialize-UvContext Write-Step "Removing the Free Claude Code uv tool" Uninstall-FreeClaudeCode Write-Step "Verifying Free Claude Code entry points were removed" Confirm-FccCommandsRemoved Write-Step "Purging FCC config and data from ~/.fcc" Purge-FccHome Write-Host "" if ($DryRun) { Write-Host "Dry run complete. No changes were made." } else { Write-Host "Free Claude Code has been removed and verified." Write-Host "uv, Claude Code, Codex, Pi, the uv-managed Python runtime, and shared PATH entries were left installed." }