# Universal Nushell + Plugins Uninstaller for Windows # PowerShell script that cleanly removes Nushell and plugins installation # # This script: # - Detects installation locations (user or system Program Files) # - Removes Nushell binary and plugin binaries # - Cleans up configuration files (with backup option) # - Removes PATH entries from environment variables # - Unregisters plugins from Nushell # - Provides detailed removal report param( [switch]$Help, [switch]$Yes, [switch]$KeepConfig, [switch]$BackupConfig, [switch]$System, [switch]$User, [switch]$DryRun, [switch]$Debug ) # Configuration $InstallDirUser = "$env:USERPROFILE\.local\bin" $InstallDirSystem = "$env:ProgramFiles\nushell" $ConfigDir = "$env:APPDATA\nushell" $BackupSuffix = "uninstall-backup-$(Get-Date -Format 'yyyyMMdd_HHmmss')" # Logging functions function Write-Info { param([string]$Message) Write-Host "ℹ️ $Message" -ForegroundColor Blue } function Write-Success { param([string]$Message) Write-Host "✅ $Message" -ForegroundColor Green } function Write-Warning { param([string]$Message) Write-Host "⚠️ $Message" -ForegroundColor Yellow } function Write-Error { param([string]$Message) Write-Host "❌ $Message" -ForegroundColor Red } function Write-Debug { param([string]$Message) if ($Debug) { Write-Host "🐛 DEBUG: $Message" -ForegroundColor Magenta } } # Usage information function Show-Usage { Write-Host @" Nushell Full Distribution Uninstaller for Windows USAGE: .\uninstall.ps1 [OPTIONS] OPTIONS: -Help Show this help message -Yes Non-interactive mode (assume yes to prompts) -KeepConfig Keep configuration files (don't remove $ConfigDir) -BackupConfig Backup configuration before removal -System Remove from system location (Program Files) - requires admin -User Remove from user location (~\.local\bin) - default -DryRun Show what would be removed without actually removing -Debug Enable debug output EXAMPLES: .\uninstall.ps1 # Interactive removal from user location .\uninstall.ps1 -Yes # Non-interactive removal .\uninstall.ps1 -BackupConfig -Yes # Remove with config backup .\uninstall.ps1 -System # Remove system installation (needs admin) .\uninstall.ps1 -DryRun # Show what would be removed "@ } # Show usage and exit if help requested if ($Help) { Show-Usage exit 0 } # Determine installation directory $Interactive = -not $Yes $IsSystemInstall = $System if ($IsSystemInstall) { $InstallDir = $InstallDirSystem Write-Info "Targeting system installation: $InstallDir" # Check if we have admin privileges $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin -and -not $DryRun) { Write-Warning "System installation requires administrative privileges" if ($Interactive) { $response = Read-Host "Continue with elevated privileges? [y/N]" if ($response -notmatch '^[yY]([eE][sS])?$') { Write-Info "Uninstallation cancelled" exit 0 } } # Re-run as administrator if (-not $isAdmin) { Write-Info "Re-running with elevated privileges..." $arguments = $MyInvocation.BoundParameters.Keys | ForEach-Object { "-$_" } Start-Process PowerShell.exe -Argument "-File `"$PSCommandPath`" $($arguments -join ' ')" -Verb RunAs exit 0 } } } else { $InstallDir = $InstallDirUser Write-Info "Targeting user installation: $InstallDir" } # Detection functions function Get-NushellInstallation { param([string]$InstallDir) $foundItems = @() Write-Debug "Detecting Nushell installation in $InstallDir" # Check for nu binary $nuPath = Join-Path $InstallDir "nu.exe" if (Test-Path $nuPath) { $foundItems += "nu.exe" Write-Debug "Found nu binary: $nuPath" } # Check for plugin binaries $pluginPattern = Join-Path $InstallDir "nu_plugin_*.exe" $pluginBinaries = Get-ChildItem -Path $pluginPattern -ErrorAction SilentlyContinue foreach ($plugin in $pluginBinaries) { $foundItems += $plugin.Name Write-Debug "Found plugin binary: $($plugin.FullName)" } return $foundItems } function Get-ConfigInstallation { param([string]$ConfigDir) $foundItems = @() Write-Debug "Detecting configuration in $ConfigDir" if (Test-Path $ConfigDir) { # Check for main config files $configFiles = @("config.nu", "env.nu", "distribution_config.toml") foreach ($configFile in $configFiles) { $configPath = Join-Path $ConfigDir $configFile if (Test-Path $configPath) { $foundItems += $configFile Write-Debug "Found config file: $configPath" } } # Check for plugin registration $pluginFiles = @("plugin.nu", "registry.dat") foreach ($pluginFile in $pluginFiles) { $pluginPath = Join-Path $ConfigDir $pluginFile if (Test-Path $pluginPath) { $foundItems += "plugin-registry" Write-Debug "Found plugin registry files" break } } } return $foundItems } function Get-PathEntries { param([string]$InstallDir) Write-Debug "Detecting PATH entries for $InstallDir" $foundIn = @() # Check user PATH $userPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) if ($userPath -and $userPath.Contains($InstallDir)) { $foundIn += "User PATH" Write-Debug "Found in User PATH" } # Check system PATH (if we have access) try { $systemPath = [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::Machine) if ($systemPath -and $systemPath.Contains($InstallDir)) { $foundIn += "System PATH" Write-Debug "Found in System PATH" } } catch { Write-Debug "Cannot access System PATH (insufficient privileges)" } return $foundIn } # Removal functions function Remove-Binaries { param( [string]$InstallDir, [array]$Binaries ) Write-Info "Removing binaries from $InstallDir..." $removedCount = 0 foreach ($binary in $Binaries) { $binaryPath = Join-Path $InstallDir $binary if (Test-Path $binaryPath) { Write-Info "Removing binary: $binary" if (-not $DryRun) { try { Remove-Item -Path $binaryPath -Force Write-Success "Removed: $binary" $removedCount++ } catch { Write-Error "Failed to remove: $binary - $($_.Exception.Message)" } } else { Write-Info "[DRY RUN] Would remove: $binaryPath" $removedCount++ } } } Write-Success "Removed $removedCount binaries" } function Backup-Configuration { param([string]$ConfigDir) if (Test-Path $ConfigDir) { $backupDir = "$ConfigDir.$BackupSuffix" Write-Info "Backing up configuration to: $backupDir" if (-not $DryRun) { try { Copy-Item -Path $ConfigDir -Destination $backupDir -Recurse -Force Write-Success "Configuration backed up successfully" } catch { Write-Error "Failed to backup configuration: $($_.Exception.Message)" return $false } } else { Write-Info "[DRY RUN] Would backup configuration to: $backupDir" } return $true } else { Write-Info "No configuration directory to backup" return $true } } function Remove-Configuration { param( [string]$ConfigDir, [array]$ConfigFiles ) if ($KeepConfig) { Write-Info "Keeping configuration files as requested" return } if ($BackupConfig) { Backup-Configuration $ConfigDir } if (Test-Path $ConfigDir) { Write-Info "Removing configuration directory: $ConfigDir" # Ask for confirmation if interactive and not just removing empty dir if ($Interactive -and $ConfigFiles.Count -gt 0) { $response = Read-Host "Remove configuration directory $ConfigDir? [y/N]" if ($response -notmatch '^[yY]([eE][sS])?$') { Write-Info "Keeping configuration directory" return } } if (-not $DryRun) { try { Remove-Item -Path $ConfigDir -Recurse -Force Write-Success "Configuration directory removed" } catch { Write-Error "Failed to remove configuration directory: $($_.Exception.Message)" } } else { Write-Info "[DRY RUN] Would remove configuration directory: $ConfigDir" } } else { Write-Info "No configuration directory found" } } function Remove-PathEntries { param( [string]$InstallDir, [array]$PathLocations ) if ($PathLocations.Count -eq 0) { Write-Info "No PATH entries found" return } Write-Info "Removing PATH entries..." foreach ($location in $PathLocations) { Write-Info "Cleaning PATH entries from: $location" $target = if ($location -eq "User PATH") { [EnvironmentVariableTarget]::User } else { [EnvironmentVariableTarget]::Machine } if (-not $DryRun) { try { $currentPath = [Environment]::GetEnvironmentVariable("Path", $target) $pathEntries = $currentPath -split ';' $newPathEntries = $pathEntries | Where-Object { $_ -ne $InstallDir } $newPath = $newPathEntries -join ';' [Environment]::SetEnvironmentVariable("Path", $newPath, $target) Write-Success "Cleaned PATH entries from: $location" } catch { Write-Error "Failed to clean PATH entries from: $location - $($_.Exception.Message)" } } else { Write-Info "[DRY RUN] Would remove PATH entries from: $location" } } } function Unregister-Plugins { param([string]$InstallDir) # Only try to unregister if nu is still available somewhere $nuBinary = $null if (Get-Command nu -ErrorAction SilentlyContinue) { $nuBinary = (Get-Command nu).Path } elseif (Test-Path (Join-Path $InstallDir "nu.exe")) { $nuBinary = Join-Path $InstallDir "nu.exe" } else { Write-Info "Nushell not available for plugin unregistration" return } Write-Info "Attempting to unregister plugins..." if (-not $DryRun) { try { # Try to get list of registered plugins $registeredPlugins = & $nuBinary -c "plugin list | get name" 2>$null if ($registeredPlugins) { Write-Info "Unregistering plugins from nushell..." foreach ($plugin in $registeredPlugins) { try { & $nuBinary -c "plugin rm $plugin" 2>$null } catch { Write-Warning "Could not unregister plugin: $plugin" } } Write-Success "Plugin unregistration completed" } else { Write-Info "No registered plugins found" } } catch { Write-Warning "Could not retrieve plugin list" } } else { Write-Info "[DRY RUN] Would attempt to unregister plugins" } } # Main uninstallation process function Main { Write-Info "🗑️ Nushell Full Distribution Uninstaller" Write-Info "========================================" if ($DryRun) { Write-Warning "DRY RUN MODE - No files will be modified" } # Detect current installation Write-Info "" Write-Info "🔍 Detecting current installation..." $userBinaries = Get-NushellInstallation $InstallDirUser $systemBinaries = Get-NushellInstallation $InstallDirSystem $configFiles = Get-ConfigInstallation $ConfigDir $userPathEntries = Get-PathEntries $InstallDirUser $systemPathEntries = Get-PathEntries $InstallDirSystem # Determine what we're removing based on target if ($IsSystemInstall) { $binaries = $systemBinaries $pathEntries = $systemPathEntries } else { $binaries = $userBinaries $pathEntries = $userPathEntries } # Show detection results Write-Info "Installation Status:" if ($userBinaries.Count -gt 0) { Write-Info " 📁 User binaries ($InstallDirUser): $($userBinaries -join ', ')" } else { Write-Info " 📁 User binaries ($InstallDirUser): none found" } if ($systemBinaries.Count -gt 0) { Write-Info " 📁 System binaries ($InstallDirSystem): $($systemBinaries -join ', ')" } else { Write-Info " 📁 System binaries ($InstallDirSystem): none found" } if ($configFiles.Count -gt 0) { Write-Info " ⚙️ Configuration ($ConfigDir): $($configFiles -join ', ')" } else { Write-Info " ⚙️ Configuration ($ConfigDir): none found" } if ($pathEntries.Count -gt 0) { Write-Info " 🛣️ PATH entries: $($pathEntries -join ', ')" } else { Write-Info " 🛣️ PATH entries: none found" } # Check if anything was found if ($binaries.Count -eq 0 -and $configFiles.Count -eq 0 -and $pathEntries.Count -eq 0) { Write-Warning "No Nushell installation detected" if ($Interactive) { $response = Read-Host "Continue anyway? [y/N]" if ($response -notmatch '^[yY]([eE][sS])?$') { Write-Info "Uninstallation cancelled" exit 0 } } else { Write-Info "Nothing to remove" exit 0 } } # Confirmation prompt if ($Interactive) { Write-Info "" Write-Warning "This will remove the detected Nushell installation components." if ($KeepConfig) { Write-Info "Configuration files will be kept as requested." } elseif ($BackupConfig) { Write-Info "Configuration files will be backed up before removal." } $response = Read-Host "Proceed with uninstallation? [y/N]" if ($response -notmatch '^[yY]([eE][sS])?$') { Write-Info "Uninstallation cancelled" exit 0 } } # Perform uninstallation Write-Info "" Write-Info "🗑️ Starting uninstallation..." # Unregister plugins before removing binaries if ($binaries.Count -gt 0) { Unregister-Plugins $InstallDir } # Remove binaries if ($binaries.Count -gt 0) { Remove-Binaries $InstallDir $binaries } # Remove configuration if ($configFiles.Count -gt 0) { Remove-Configuration $ConfigDir $configFiles } # Clean PATH entries if ($pathEntries.Count -gt 0) { Remove-PathEntries $InstallDir $pathEntries } # Final summary Write-Info "" Write-Success "🎉 Uninstallation completed!" Write-Info "Summary:" Write-Info " 🗑️ Removed from: $InstallDir" if ($configFiles.Count -gt 0 -and -not $KeepConfig) { Write-Info " 🗑️ Configuration removed: $ConfigDir" } elseif ($KeepConfig) { Write-Info " 💾 Configuration kept: $ConfigDir" } if ($BackupConfig) { Write-Info " 💾 Configuration backed up with suffix: .$BackupSuffix" } Write-Info "" Write-Info "🔄 To complete removal:" Write-Info "1. Restart your terminal or refresh environment variables" Write-Info "2. Verify removal: Get-Command nu (should return nothing)" if ($KeepConfig -or $BackupConfig) { Write-Info "" Write-Info "📝 Note: Configuration files were preserved as requested" } } # Run main function Main