[CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$InstallRoot = (Join-Path $env:LOCALAPPDATA 'MercuryToolbox'), [string]$CodexHome ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest . (Join-Path $PSScriptRoot 'toolbox-commands.ps1') function Normalize-PathValue { param( [AllowNull()] [string]$PathValue ) if ([string]::IsNullOrWhiteSpace($PathValue)) { return '' } return $PathValue.Replace('/', '\').Trim().TrimEnd('\').ToLowerInvariant() } function Split-PathEntries { param( [AllowNull()] [string]$PathValue ) if ([string]::IsNullOrWhiteSpace($PathValue)) { return @() } return $PathValue.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) } function Remove-PathEntry { param( [AllowNull()] [string]$ExistingPath, [Parameter(Mandatory = $true)] [string]$Entry ) $normalizedEntry = Normalize-PathValue -PathValue $Entry $remaining = [System.Collections.Generic.List[string]]::new() $changed = $false foreach ($existing in (Split-PathEntries -PathValue $ExistingPath)) { if ((Normalize-PathValue -PathValue $existing) -eq $normalizedEntry) { $changed = $true continue } $remaining.Add($existing) } return @{ Changed = $changed Value = ($remaining -join ';') } } function Resolve-CodexHomePath { param( [AllowNull()] [string]$ExplicitCodexHome ) if (-not [string]::IsNullOrWhiteSpace($ExplicitCodexHome)) { return $ExplicitCodexHome } if (-not [string]::IsNullOrWhiteSpace($env:CODEX_HOME)) { return $env:CODEX_HOME } return (Join-Path $env:USERPROFILE '.codex') } function Test-CodexSkillOwnedByMercury { param( [Parameter(Mandatory = $true)] [string]$SkillRoot ) $markerPath = Join-Path $SkillRoot '.mercury-toolbox-owner.json' if (-not (Test-Path -LiteralPath $markerPath)) { return $false } try { $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json return [string]$marker.owner -eq 'mercury-toolbox' } catch { return $false } } function Assert-ExistingPathNotReparsePoint { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Description ) $resolvedPath = [System.IO.Path]::GetFullPath($Path) if (-not (Test-Path -LiteralPath $resolvedPath)) { return } $item = Get-Item -LiteralPath $resolvedPath -Force if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw "$Description must not be a reparse point: $resolvedPath" } } function Assert-ExistingTreeHasNoReparsePoints { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Description ) $resolvedPath = [System.IO.Path]::GetFullPath($Path) if (-not (Test-Path -LiteralPath $resolvedPath)) { return } $reparsePoints = @(Get-ChildItem -LiteralPath $resolvedPath -Force -Recurse -Attributes ReparsePoint) if ($reparsePoints.Count -gt 0) { throw "$Description must not contain reparse points: $($reparsePoints[0].FullName)" } } $legacyBinDir = Join-Path $InstallRoot 'bin' $currentRoot = Join-Path $InstallRoot 'current' $activeBinDir = Join-Path $currentRoot 'bin' $versionsRoot = Join-Path $InstallRoot 'versions' $shareRoot = Join-Path $InstallRoot 'share\mercury-toolbox' $codexSkillRoot = Join-Path (Resolve-CodexHomePath -ExplicitCodexHome $CodexHome) 'skills\mercury-toolbox' $legacyCommandNames = @('context', 'waitfor') Assert-ExistingPathNotReparsePoint -Path $InstallRoot -Description 'InstallRoot' Assert-ExistingPathNotReparsePoint -Path $versionsRoot -Description 'versions root' Assert-ExistingPathNotReparsePoint -Path $shareRoot -Description 'share root' Assert-ExistingPathNotReparsePoint -Path $codexSkillRoot -Description 'Codex skill root' Assert-ExistingTreeHasNoReparsePoints -Path $shareRoot -Description 'share root' Assert-ExistingTreeHasNoReparsePoints -Path $codexSkillRoot -Description 'Codex skill root' if (Test-Path -LiteralPath $legacyBinDir) { foreach ($commandName in Get-ToolboxCommandNames) { $commandPath = Join-Path $legacyBinDir "$commandName.exe" if (Test-Path -LiteralPath $commandPath) { Remove-Item -LiteralPath $commandPath -Force } } foreach ($legacyName in $legacyCommandNames) { $legacyPath = Join-Path $legacyBinDir "$legacyName.exe" if (Test-Path -LiteralPath $legacyPath) { Remove-Item -LiteralPath $legacyPath -Force } } $remainingBinItems = @(Get-ChildItem -LiteralPath $legacyBinDir -Force) if ($remainingBinItems.Count -eq 0) { Remove-Item -LiteralPath $legacyBinDir -Force } } if (Test-Path -LiteralPath $currentRoot) { Remove-Item -LiteralPath $currentRoot -Force } if (Test-Path -LiteralPath $versionsRoot) { Remove-Item -LiteralPath $versionsRoot -Recurse -Force } if (Test-Path -LiteralPath $shareRoot) { Remove-Item -LiteralPath $shareRoot -Recurse -Force } if (Test-Path -LiteralPath $codexSkillRoot) { if (Test-CodexSkillOwnedByMercury -SkillRoot $codexSkillRoot) { Remove-Item -LiteralPath $codexSkillRoot -Recurse -Force } else { Write-Warning "Skipping Codex skill removal because Mercury Toolbox does not own this skill copy: $codexSkillRoot" } } if (Test-Path -LiteralPath $InstallRoot) { $remainingItems = @(Get-ChildItem -LiteralPath $InstallRoot -Force) if ($remainingItems.Count -eq 0) { Remove-Item -LiteralPath $InstallRoot -Force } } $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') foreach ($entry in @($legacyBinDir, $activeBinDir)) { $pathUpdate = Remove-PathEntry -ExistingPath $userPath -Entry $entry if ($pathUpdate.Changed) { $userPath = $pathUpdate.Value } } [Environment]::SetEnvironmentVariable('Path', $userPath, 'User') foreach ($entry in @($legacyBinDir, $activeBinDir)) { $sessionUpdate = Remove-PathEntry -ExistingPath $env:Path -Entry $entry if ($sessionUpdate.Changed) { $env:Path = $sessionUpdate.Value } } Write-Host "Mercury Toolbox package removed from $InstallRoot" Write-Host "Removed Codex skill root: $codexSkillRoot"