Jesús Pérez 2f6089caaf # Summary
feat: bootstrap installer with robust archive extraction, version mismatch handling, and improved PATH messaging

## Detailed Description

This commit implements a production-ready bootstrap installer with comprehensive error handling, version-agnostic archive extraction, and clear user messaging. All improvements follow DRY principles using symlink-based architecture for single-source-of-truth maintenance.

## Major Changes (Session 2025-10-19)

### 0. Bootstrap Installer & Distribution Improvements (NEW)

#### 0a. Install Script Architecture - DRY Design

**Issue**: Code duplication across installation paths
- `./install.sh`, `./scripts/templates/install.sh`, `installers/bootstrap/install.sh` were separate copies
- Changes required updating multiple files
- Risk of divergence and inconsistency

**Solution**: Implemented symlink-based DRY architecture
- Single source of truth: `installers/bootstrap/install.sh` (1,247 lines)
- Symlinks created:
  - `./install.sh` → `installers/bootstrap/install.sh`
  - `./scripts/templates/install.sh` → `installers/bootstrap/install.sh`
- All changes automatically propagate through symlinks
- No code duplication

**Impact**:
-  Single maintenance point for all install scripts
-  Consistent behavior across all paths
-  Reduced maintenance burden
-  No divergence risk

#### 0b. Archive Extraction - Version-Agnostic Binary Detection

**Issue**: "No Nushell binary found in extracted archive"
- Script failed when extracting from `nushell-full-0.108.0-darwin-arm64.tar.gz`
- Error occurred even though binaries were present at `nushell-full-0.108.0/bin/nu`

**Root Cause**: `find` command returning parent directory itself in results
```bash
#  OLD - find returns parent directory first
local extracted=$(find "$extract_dir" -maxdepth 1 -type d -name "nushell-*" | head -1)
# Returns: /tmp/nushell-install-22919/nushell-extract (parent!)
# Should return: /tmp/nushell-install-22919/nushell-extract/nushell-full-0.108.0 (subdirectory)
```

**Solution**: Added `-not -path` to exclude starting directory
```bash
#  NEW - exclude parent directory
local extracted=$(find "$extract_dir" -maxdepth 1 -type d -name "nushell-*" -not -path "$extract_dir" | head -1)
# Now correctly returns the subdirectory
```

**Additional Improvements**:
- 4-level fallback detection strategy:
  1. Check `$extracted/bin/nu` (subdirectory structure)
  2. Check `$extracted/nu` (flat structure)
  3. Fallback search for any `nushell-*` subdirectory with `bin/nu`
  4. Last resort recursive search for any executable named `nu`
- Validates binaries exist before using them
- Clear error reporting with all search locations

**Version-Agnostic**:
- Uses `nushell-*` pattern (not hardcoded version numbers)
- Works with any Nushell version: 0.107, 0.108, 0.109, etc.
- Supports both `.tar.gz` and `.zip` archive formats

**Impact**:
-  Archive extraction works reliably
-  Works with any Nushell version
-  Clear error messages guide users
-  Multiple archive structure support

#### 0c. Plugin Registration - Version Mismatch Handling

**Issue**: Plugin registration failed with version incompatibility errors
```
Error: nu:🐚:plugin_failed_to_load
× Plugin `polars` is compiled for nushell version 0.107.1, which is not compatible with version 0.108.0
```

**Solution**: Implemented intelligent error classification
- Captures both stdout and stderr from plugin add commands
- Detects version incompatibility: "is not compatible with version" or "is compiled for nushell version"
- Classifies errors into three categories:
  1. **Success**: Plugin registered successfully 
  2. **Incompatible**: Version mismatch (skipped gracefully) ⚠️
  3. **Failed**: Other registration failures 
- Reports summary with counts of each category
- Installation continues successfully even with version mismatches

**New Error Reporting**:
```
 Registered nu_plugin_auth
⚠️  Skipping nu_plugin_polars: Version mismatch (built for different Nushell version)
 Successfully registered 13 plugins
⚠️  Skipped 1 incompatible plugins (version mismatch):
  - nu_plugin_polars
```

**Impact**:
-  No installation failures due to version mismatches
-  Users informed of incompatible plugins
-  Clear distinction between error types
-  Installation completes successfully

#### 0d. Shell Configuration PATH Update - Clear Messaging

**Issue**: Confusing PATH update messages
- User sees: "PATH already updated" for all files
- Then sees: "No shell configuration files were updated" warning
- Then sees: "Please manually add to your PATH" error
- **Problem**: Contradictory messages when PATH is already configured everywhere

**Root Cause**: Script conflated two states
- State 1: "Was PATH found in files?" (skips updating if found)
- State 2: "Did we add PATH to any file?" (used for messaging)
- Both states ignored means no update was made, but PATH might already exist

**Solution**: Track two separate states
```bash
local updated=false        # Was PATH ADDED to any file?
local path_found=false     # Was PATH FOUND in any file?

# In loop:
if grep -q "$install_dir" "$config_file"; then
    path_found=true        # Found it! Mark as true
    continue
fi

# After loop:
if [ "$updated" = "true" ]; then
    log_success "Shell configuration updated"
elif [ "$path_found" = "true" ]; then
    log_success "PATH is already configured in your shell configuration files"
else
    log_warn "Could not find or update shell configuration"
fi
```

**New Clear Messages**:
-  "PATH is already configured in your shell configuration files" (when found everywhere)
-  "Shell configuration updated" (when just added)
- ⚠️ "Could not find or update shell configuration" (when file missing)

**Impact**:
-  Non-contradictory messages
-  Users understand what happened
-  No false warnings when PATH already configured
-  Clear guidance when manual action needed

#### 0e. Installation Features

**`--source-path` Option** (Local Installation):
- Install from local archive: `./install.sh --source-path archive.tar.gz`
- Install from local directory: `./install.sh --source-path /path/to/binaries`
- Default behavior: `./install.sh --source-path` uses `./bin_archives`
- Works with custom `--install-dir` paths
- No download needed, offline installation support

**`--uninstall` with Configuration Management**:
- Prompts user: "Remove ~/.config/nushell? [y/N]"
- Removes all installed binaries and plugins
- Preserves user choice for configuration
- Clean uninstall before fresh reinstall

#### 0f. Documentation Updates

**CLAUDE.md**:
- Added "Install Script Architecture (DRY Design)" section
- Documents source of truth and symlink structure
- Explains `--source-path` feature
- Shows version-agnostic archive detection
- Lists DRY architecture benefits

**README.md**:
- Added "Install Script Architecture (DRY Design)" subsection
- Shows symlink structure with arrows
- Provides `--source-path` usage examples
- Explains version-agnostic detection
- "How DRY Works" 3-step explanation

#### 0g. Files Modified

Core Changes:
- `installers/bootstrap/install.sh` (+3 lines for PATH messaging fix)
  - Archive extraction fix with `-not -path`
  - Plugin registration error classification
  - Clear PATH update messaging
  - Total: 1,247 lines (unified)

Auto-Updated via Symlinks:
- `./install.sh` - Auto-updated (1,247 lines)
- `./scripts/templates/install.sh` - Auto-updated (1,247 lines)

Documentation:
- `CLAUDE.md` - Added install architecture section
- `README.md` - Added install architecture subsection
- `CHANGELOG.md` - Added comprehensive entry (+100 lines)

#### 0h. Testing & Verification

All scenarios tested and verified:
- [x] Archive extraction works with version-agnostic detection
- [x] Installation to `~/.local` successful (16 binaries)
- [x] Installation to `~/.local/bin` successful (21 plugins loaded)
- [x] Plugin registration handles version mismatches gracefully
- [x] PATH messaging is clear and non-contradictory
- [x] Clean uninstall followed by fresh reinstall works perfectly

#### 0i. Impact

User-Facing Benefits:
-  Users can install from any version of nushell-full archive
-  Version mismatch plugins skipped without breaking installation
-  Clear, honest error messages
-  Non-confusing PATH update messages
-  Offline installation support via `--source-path`
-  Clean uninstall/reinstall workflow

Developer Benefits:
-  Single source of truth eliminates code duplication
-  Changes propagate automatically through symlinks
-  Reduced maintenance burden
-  Consistent behavior across all paths
-  Production-ready installation process

---

### 1. Help System Integration (New Feature)

**Issue**: Version-update module recipes were not discoverable
- Not shown in `just help modules`
- Not referenced in `just help`
- Not included in help navigation system
- Users had to manually run `just --list` to find update commands

**Solution**:
- Added version-update module to all help outputs
- Updated `justfiles/help.just` to document all 30+ version-update recipes
- Created new `just commands` recipe as discoverable alias for `just --list`
- Integrated version-update into help-all workflow

**Impact**:
- Version-update commands now fully discoverable via help system
- Users can find update commands with: `just help modules`, `just help`, `just commands`
- Improved overall help system navigation

**Files Modified**:
- `justfiles/help.just` (+23 lines)
  - Added version-update module to help sections
  - Added to modules list
  - Added to help-all workflow
  - New `commands` recipe showing all recipes by group

### 2. Build Process Fixes (Phase 3: Bin Archives)

#### 2a. Plugin Archive Collection Bug

**Issue**: "No plugins found to package" warning in Phase 3
- Collected 26 plugin binaries but reported 0
- Archive creation skipped because count was wrong

**Root Cause**: `each` command returns null, so `| length` returned 0
```nushell
#  OLD - each returns null
let plugin_count = (ls nu_plugin_*/target/release/nu_plugin_* | each {|p|
    cp $p.name $"($temp_dir)/"
} | length)  # Returns 0!
```

**Solution**: Separated counting from copying with proper filtering
```nushell
#  NEW - count before operations
let plugins_to_copy = (ls nu_plugin_*/target/release/nu_plugin_* | where type == "file")
let plugin_count = ($plugins_to_copy | length)
```

**Impact**:
- Now correctly collects and reports 26 plugins
- Filters out .d dependency files automatically
- Warning eliminated

#### 2b. Tar Archive Path Handling

**Issue**: Tar command failing silently with relative paths in subshell
- `cd $temp_dir` changes context unpredictably
- Relative path `../$archive_name` fails in subshell
- Archive file not created despite exit code 0

**Root Cause**: Shell context and relative path issues in Nushell `do` block

**Solution**: Used `tar -C` with absolute paths instead of `cd`
```nushell
#  OLD - unreliable context switching
do {
    cd $temp_dir
    tar -czf ../$archive_name .
}

#  NEW - absolute paths, no context switching
tar -C $temp_dir -czf $archive_path .
```

**Additional Improvements**:
- Absolute path construction using `pwd | path join`
- Better error diagnostics with exit code and stderr output
- File verification after creation

**Impact**:
- Tar archives now created successfully
- Robust path handling across platforms
- Clear error messages for debugging

#### 2c. File Size Calculation Type Error

**Issue**: Runtime error when calculating archive size
```
Error: The '/' operator does not work on values of type 'list<filesize>'
```

**Root Cause**: `ls` returns list of records, so `.size` was a list
```nushell
#  OLD - returns list<filesize>
(ls $archive_path).size / 1024 / 1024

#  NEW - returns filesize
(ls $archive_path | get 0.size) / 1024 / 1024
```

**Impact**:
- Proper file size calculation in MB
- No more type errors

**Files Modified**:
- `scripts/create_full_distribution.nu` (+58 lines, refactored plugin collection)
  - Fixed plugin counting logic
  - Improved path handling with absolute paths
  - Enhanced error diagnostics

### 3. Plugin Rebuild Optimization

**Issue**: All plugins marked for rebuild even when dependencies unchanged
- Step 4 (`update_all_plugins.nu`) touched all Cargo.toml files at 01:00:32
- Step 5 saw all files as "newer" than binaries
- Marked ALL plugins for rebuild, though cargo only rebuilt changed ones

**Root Cause**: Script always saved files, even when no changes made
```nushell
#  OLD - always saves, touching file timestamp
$updated_content | to toml | save -f $cargo_toml
```

**Solution**: Only save if content actually changed
```nushell
#  NEW - compare before writing
let original_toml = $content | to toml
let new_toml = $updated_content | to toml

if $original_toml != $new_toml {
    $updated_content | to toml | save -f $cargo_toml
}
```

**Impact**:
- Unchanged files preserve original timestamps
- Only plugins with actual dependency changes are rebuilt
- Efficient rebuild process with accurate file modification detection

**Files Modified**:
- `scripts/update_all_plugins.nu` (+12 lines, added content comparison)
  - Only touches files with real changes
  - Preserves timestamps for efficiency
  - Clearer logic and comments

### 4. Documentation

**Files Modified**:
- `CHANGELOG.md` (+56 lines)
  - Added comprehensive 2025-10-19 entry
  - Documented all fixes with root causes
  - Listed files modified and impact summary

## Technical Details

### Nushell Patterns Used

1. **Proper List Handling**:
   - `ls` returns list of records, access with `| get 0.size`
   - Filter with `where type == "file"` to exclude metadata

2. **Absolute Path Construction**:
   - `pwd | append "path" | path join` for cross-platform paths
   - Safer than string concatenation with `/`

3. **Content Comparison**:
   - Compare TOML string representation before saving
   - Preserves file timestamps for efficiency

4. **Error Diagnostics**:
   - Capture `stderr` from commands
   - Report exit codes and error messages separately

## Testing

- [x] Help system shows version-update module
- [x] `just commands` displays all recipes by group
- [x] Phase 3 bin archive creation works
- [x] Plugin collection reports correct count (26)
- [x] Tar archives created successfully
- [x] File size calculated correctly
- [x] Plugin rebuild only touches changed files
- [x] CHANGELOG updated with all changes

## Files Changed

```
38 files changed, 2721 insertions(+), 2548 deletions(-)

Core Changes:
- justfiles/help.just                  (+23)  Help system integration
- scripts/create_full_distribution.nu  (+58)  Build process fixes
- scripts/update_all_plugins.nu        (+12)  Rebuild optimization
- CHANGELOG.md                         (+56)  Documentation

Dependency Updates:
- All plugin Cargo.toml and Cargo.lock files (version consistency)
```

## Breaking Changes

None. These are bug fixes and optimizations that maintain backward compatibility.

## Migration Notes

No migration needed. Improvements are transparent to users.

## Related Issues

- Help system discoverability
- Build process Phase 3 failures
- Unnecessary plugin rebuilds
- Build process reliability

## Checklist

- [x] Changes follow Rust/Nushell idioms
- [x] Code is well-commented
- [x] Error handling is comprehensive
- [x] Documentation is updated
- [x] All changes tested
- [x] No breaking changes introduced
2025-10-19 02:39:31 +01:00

559 lines
17 KiB
PowerShell
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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,
[string]$UninstallDir = "",
[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 config directory)
-BackupConfig Backup configuration before removal
-System Remove from system location (Program Files)
Requires administrator privileges (run as admin)
-User Remove from user location (~\.local\bin) [default]
No admin required - recommended
-UninstallDir PATH Remove from custom directory (directory must be writable)
Bypasses interactive prompts when supplied explicitly
Example: -UninstallDir "C:\Tools\nushell"
-DryRun Show what would be removed without actually removing
-Debug Enable debug output
EXAMPLES:
# Interactive removal (from user location)
.\uninstall.ps1
# Non-interactive removal (from user location)
.\uninstall.ps1 -Yes
# Remove with config backup
.\uninstall.ps1 -BackupConfig -Yes
# Remove from custom directory
.\uninstall.ps1 -UninstallDir "C:\Tools\nushell" -Yes
# Remove system installation (requires admin - right-click and 'Run as administrator')
.\uninstall.ps1 -System -Yes
# Preview what would be removed
.\uninstall.ps1 -DryRun
TROUBLESHOOTING:
Access denied to Program Files?
Default uses %USERPROFILE%\.local\bin (no admin needed)
Or: -UninstallDir "%USERPROFILE%\.local\bin" (explicit custom path)
Unsure what to do?
Run with -DryRun first to see what would be removed
Default removal is safest: .\uninstall.ps1 -Yes
"@
}
# Show usage and exit if help requested
if ($Help) {
Show-Usage
exit 0
}
# Determine installation directory
$Interactive = -not $Yes
$IsSystemInstall = $System
if ($UninstallDir) {
# Custom uninstall directory provided - use it directly (bypass all checks)
$InstallDir = $UninstallDir
Write-Info "Using custom uninstall directory (via -UninstallDir): $InstallDir"
} elseif ($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