CCCPaste

Uninstall-n8n-Nodejs-Thoroughly.ps1

<#
.SYNOPSIS
    Thorough removal of Node.js, npm, global modules, shims, and n8n.

.DESCRIPTION
    Removes all Node.js and npm-related components from Windows.
    Backs up ~/.n8n before removing it.
    Safe to run repeatedly. Fully idempotent.
#>

$ErrorActionPreference = "Stop"

Write-Host "=== Full Uninstall: Node.js, npm, n8n ==="

# ---------------------------------------------------------------
# PRE-CLEANUP STATE REPORT
# ---------------------------------------------------------------
function Get-SystemState {

    Write-Host ""
    Write-Host "=== System State Before Cleanup ==="

    # 1. Node.js MSI entries
    $unKeys = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )

    $msiEntries = Get-ItemProperty -Path $unKeys -ErrorAction SilentlyContinue |
                  Where-Object { $_.DisplayName -like "*Node.js*" }

    Write-Host "Node MSI entries: $($msiEntries.Count)"

    # 2. Node folders
    $nodeFolders = @(
        "C:\Program Files\nodejs",
        "C:\Program Files (x86)\nodejs",
        "$env:LOCALAPPDATA\Programs\nodejs"
    ) | Where-Object { Test-Path $_ }

    Write-Host "Node folders: $($nodeFolders.Count)"

    # 3. npm folders
    $npmFolders = @(
        "$env:APPDATA\npm",
        "$env:APPDATA\npm-cache",
        "$env:USERPROFILE\.npm"
    ) | Where-Object { Test-Path $_ }

    Write-Host "npm folders: $($npmFolders.Count)"

    # 4. Shims in PATH
    $shimNames = @("npm.cmd","npm.ps1","npx.cmd","npx.ps1","corepack.cmd","corepack.ps1","n8n.cmd","n8n.ps1")
    $shimHits = @()

    foreach ($dir in ($env:PATH -split ";")) {
        foreach ($name in $shimNames) {
            $cand = Join-Path $dir $name
            if (Test-Path $cand) { $shimHits += $cand }
        }
    }

    Write-Host "Shims in PATH: $($shimHits.Count)"

    # 5. Stray executables
    $exeNames = @("node","npm","npx","corepack","n8n")
    $exeHits = @()

    foreach ($name in $exeNames) {
        $cmd = Get-Command $name -ErrorAction SilentlyContinue
        if ($cmd) { $exeHits += $cmd.Source }
    }

    Write-Host "Stray executables: $($exeHits.Count)"

    # 6. .n8n folder
    $n8nDir = Join-Path $env:USERPROFILE ".n8n"
    $exists = Test-Path $n8nDir
    Write-Host ".n8n directory: $([bool]$exists)"

    Write-Host "==================================="

    # If nothing requires removal, exit immediately.
    if (
        $msiEntries.Count -eq 0 -and
        $nodeFolders.Count -eq 0 -and
        $npmFolders.Count -eq 0 -and
        $shimHits.Count -eq 0 -and
        $exeHits.Count -eq 0 -and
        -not (Test-Path $n8nDir)
    ) {
        Write-Host "Nothing to remove. System is already clean."
        exit
    }

    # Otherwise ask user before performing destructive cleanup.
    $choice = Read-Host "Proceed with aggressive cleanup? (Y/N)"
    if ($choice -notmatch "^[Yy]$") {
        Write-Host "Aborted by user."
        exit
    }
}

# Run diagnostic before uninstall begins
Get-SystemState

# ---------------------------------------------------------------
# BACKUP .n8n BEFORE REMOVAL
# ---------------------------------------------------------------
function Backup-N8N {
    $n8nDir = Join-Path $env:USERPROFILE ".n8n"
    if (-not (Test-Path $n8nDir)) {
        Write-Host "No .n8n directory found. Skipping backup."
        return
    }

    Write-Host "Backing up .n8n before removal..."

    $timestamp  = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
    $backupRoot = Join-Path $env:USERPROFILE "Desktop\n8n-backups"
    $zipPath    = Join-Path $backupRoot "n8n-backup-$timestamp.zip"
    $tempCopy   = Join-Path $env:TEMP "n8n-backup-$timestamp"

    if (-not (Test-Path $backupRoot)) {
        New-Item -ItemType Directory -Path $backupRoot | Out-Null
    }

    if (Test-Path $tempCopy) {
        Remove-Item $tempCopy -Recurse -Force
    }
    New-Item -ItemType Directory -Path $tempCopy | Out-Null

    try {
        Copy-Item "$n8nDir\*" $tempCopy -Recurse -Force
        Compress-Archive -Path $tempCopy -DestinationPath $zipPath -Force
    }
    finally {
        if (Test-Path $tempCopy) {
            Remove-Item $tempCopy -Recurse -Force
        }
    }

    $sizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
    Write-Host "Backup complete ($sizeMB MB) --> $zipPath"

    Write-Host "Removing .n8n directory..."
    Remove-Item $n8nDir -Recurse -Force -ErrorAction SilentlyContinue
}

# ---------------------------------------------------------------
# REMOVE n8n (GLOBAL MODULE + SHIMS)
# ---------------------------------------------------------------
function Remove-N8N {
    Write-Host ""
    Write-Host "Removing n8n..."

    $npm = Get-Command npm -ErrorAction SilentlyContinue
    if ($npm) {
        try { npm uninstall -g n8n 2>$null } catch {}
    }

    $paths = @(
        (Join-Path $env:APPDATA "npm\node_modules\n8n")
        (Join-Path $env:APPDATA "npm\n8n.cmd")
        (Join-Path $env:APPDATA "npm\n8n.ps1")
    )

    foreach ($p in $paths) {
        if (Test-Path $p) {
            Write-Host "Deleting: $p"
            Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    Write-Host "n8n removal complete."
}

# ---------------------------------------------------------------
# REMOVE NODE.JS MSI INSTALLATIONS
# ---------------------------------------------------------------
function Remove-NodeMSI {
    Write-Host ""
    Write-Host "Removing Node.js MSI installations..."

    $unKeys = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )

    $products = Get-ItemProperty -Path $unKeys -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -like "*Node.js*" }

    foreach ($p in $products) {
        $u = $p.UninstallString
        if ($u -match "{[0-9A-Fa-f\-]+}") {
            $guid = [regex]::Match($u, "{[0-9A-Fa-f\-]+}").Value
            Write-Host "Uninstalling Node.js MSI: $guid"
            Start-Process msiexec.exe -Wait -ArgumentList "/x $guid /quiet /norestart"
        }
        elseif ($u) {
            Write-Host "Running uninstall command: $u"
            Start-Process cmd.exe -Wait -ArgumentList "/c `"$u`""
        }
    }

    Write-Host "Node MSI removal complete."
}

# ---------------------------------------------------------------
# REMOVE ALL NODE AND NPM FOLDERS
# ---------------------------------------------------------------
function Remove-NodeFolders {
    Write-Host ""
    Write-Host "Removing Node.js and npm folders..."

    $folders = @(
        "C:\Program Files\nodejs",
        "C:\Program Files (x86)\nodejs",
        "$env:LOCALAPPDATA\Programs\nodejs",
        "$env:APPDATA\npm",
        "$env:APPDATA\npm-cache",
        "$env:USERPROFILE\.npm",
        "$env:USERPROFILE\.node-gyp",
        "$env:USERPROFILE\.node_repl_history"
    )

    foreach ($f in $folders) {
        if (Test-Path $f) {
            Write-Host "Deleting folder: $f"
            Remove-Item $f -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    Write-Host "Folder cleanup complete."
}

# ---------------------------------------------------------------
# REMOVE SHIMS (npm, npx, corepack, n8n)
# ---------------------------------------------------------------
function Remove-Shims {
    Write-Host ""
    Write-Host "Removing npm, npx, corepack, and n8n shims..."

    $names = @(
        "npm.cmd","npm.ps1",
        "npx.cmd","npx.ps1",
        "corepack.cmd","corepack.ps1",
        "n8n.cmd","n8n.ps1"
    )

    $searchDirs = ($env:PATH -split ";")

    foreach ($dir in $searchDirs) {
        foreach ($name in $names) {
            $cand = Join-Path $dir $name
            if (Test-Path $cand) {
                Write-Host "Removing shim: $cand"
                Remove-Item $cand -Force -ErrorAction SilentlyContinue
            }
        }
    }
}

# ---------------------------------------------------------------
# CLEAN PATH ENTRIES
# ---------------------------------------------------------------
function Remove-PathEntries {
    Write-Host ""
    Write-Host "Cleaning PATH entries..."

    $patterns = @("nodejs","npm","node","corepack","n8n")

    $clean = {
        param($scope)

        $path = [Environment]::GetEnvironmentVariable("Path",$scope)
        if (-not $path) { return }

        $parts = $path -split ";"
        $filtered = $parts | Where-Object {
            $remove = $false
            foreach ($p in $patterns) {
                if ($_ -match $p) { $remove = $true }
            }
            -not $remove
        }

        $new = ($filtered -join ";")
        [Environment]::SetEnvironmentVariable("Path",$new,$scope)
        Write-Host "Cleaned $scope PATH."
    }

    $clean.Invoke("Machine")
    $clean.Invoke("User")

    $env:PATH = (
        [Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
        [Environment]::GetEnvironmentVariable("Path","User")
    )
}

# ---------------------------------------------------------------
# REMOVE STRAY EXECUTABLES ANYWHERE IN PATH
# ---------------------------------------------------------------
function Remove-StrayExecutables {
    Write-Host ""
    Write-Host "Checking for leftover executables..."

    $names = @("node","npm","npx","corepack","n8n")

    foreach ($exe in $names) {
        $cmd = Get-Command $exe -ErrorAction SilentlyContinue
        if ($cmd) {
            Write-Host "Deleting stray executable: $($cmd.Source)"
            try { Remove-Item $cmd.Source -Force -ErrorAction SilentlyContinue } catch {}
        }
    }
}

# ---------------------------------------------------------------
# EXECUTE ALL STEPS
# ---------------------------------------------------------------
Backup-N8N
Remove-N8N
Remove-NodeMSI
Remove-NodeFolders
Remove-Shims
Remove-PathEntries
Remove-StrayExecutables

Write-Host ""
Write-Host "All components removed."
Write-Host "System is clean and ready for fresh installation tests."