<# .SYNOPSIS Safe, idempotent, fully automated Node.js + n8n updater for Windows. .DESCRIPTION Installs or updates Node.js and n8n to the newest safe versions that match n8n's engine requirements. Creates backups of ~/.n8n before making changes, validates installations, and safely repairs both User and Machine PATHs. Features: • Automatic Node.js version selection based on n8n metadata • Automatic n8n installation/update • Reliable PATH repair (no npm calls required) • Full backup system with timestamped .zip output • Safe elevation handling • Fully idempotent — safe to run repeatedly • Windows PowerShell 5.1 and PowerShell 7+ compatible .NOTES Author: J2897 Refined: ChatGPT 5.1 (OpenAI) Version: 2025 Polished Edition #> $ErrorActionPreference = "Stop" # --------------------------------------------------------------- # ADMIN ELEVATION # --------------------------------------------------------------- $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) $IsAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin) { Write-Host "Re-launching with administrative privileges..." Start-Process powershell.exe -Verb RunAs -ArgumentList ( "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" ) exit } Write-Host "=== n8n Safe Updater for Windows ===" # --------------------------------------------------------------- # UTILITIES # --------------------------------------------------------------- function Try-Command { param([string]$Cmd) try { Invoke-Expression $Cmd 2>$null } catch { $null } } function Get-Json { param([string]$Url) try { (Invoke-WebRequest -Uri $Url -UseBasicParsing).Content | ConvertFrom-Json } catch { Write-Host "ERROR: Failed to fetch JSON from $Url" exit 1 } } # --------------------------------------------------------------- # NODE UNINSTALL # --------------------------------------------------------------- function Uninstall-Node { Write-Host "Removing existing Node.js..." $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) { $uninst = $p.UninstallString if ($uninst -match '{[0-9A-Fa-f\-]+}') { $guid = [regex]::Match($uninst, '{[0-9A-Fa-f\-]+}').Value Start-Process msiexec.exe -Wait -ArgumentList "/x $guid /quiet /norestart" } elseif ($uninst) { Start-Process cmd.exe -Wait -ArgumentList "/c `"$uninst`"" } } Start-Sleep -Seconds 1 } # --------------------------------------------------------------- # NODE INSTALL # --------------------------------------------------------------- function Install-Node { param([string]$Version) $clean = $Version.TrimStart('v') $msiUrl = "https://nodejs.org/dist/v$clean/node-v$clean-x64.msi" $msiPath = Join-Path $env:TEMP "node-v$clean-x64.msi" Write-Host "Installing Node $Version..." Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing Start-Process msiexec.exe -Wait -ArgumentList "/i `"$msiPath`" /quiet /norestart" Remove-Item $msiPath -Force -ErrorAction SilentlyContinue } # --------------------------------------------------------------- # NODE PATH RESOLUTION # --------------------------------------------------------------- function Resolve-NodeInstallPath { foreach ($loc in @( "C:\Program Files\nodejs", "$env:LOCALAPPDATA\Programs\nodejs" )) { if (Test-Path (Join-Path $loc "node.exe")) { return $loc } } return $null } # --------------------------------------------------------------- # REPAIR PATH (PATCH 1 APPLIED) # --------------------------------------------------------------- function Repair-Path { param([string]$NodeDir) # Expected npm global locations $npmBin = Join-Path $env:APPDATA "npm" $npmCache = Join-Path $env:APPDATA "npm-cache" # Ensure both directories exist if (-not (Test-Path $npmBin)) { New-Item -ItemType Directory -Path $npmBin -Force | Out-Null } if (-not (Test-Path $npmCache)) { New-Item -ItemType Directory -Path $npmCache -Force | Out-Null } # Helper to append new path elements uniquely $addUnique = { param($existing, $new) $parts = $existing -split ';' | Where-Object { $_ -ne '' } if ($parts -notcontains $new) { ($parts + $new) -join ';' } else { $existing } } # User PATH $pUser = [Environment]::GetEnvironmentVariable("Path","User") $pUserNew = $addUnique.Invoke($pUser, $npmBin) [Environment]::SetEnvironmentVariable("Path", $pUserNew, "User") # Machine PATH $pMach = [Environment]::GetEnvironmentVariable("Path","Machine") $pMachNew = $addUnique.Invoke($pMach, $NodeDir) [Environment]::SetEnvironmentVariable("Path", $pMachNew, "Machine") # npm bin FIRST (fixes shim creation) $env:PATH = "$npmBin;$pUserNew;$pMachNew" } # --------------------------------------------------------------- # INSTALL n8n (PATCH 3 APPLIED) # --------------------------------------------------------------- function Install-N8N { param([string]$Version) Write-Host "Installing n8n@$Version..." $npmRoot = Join-Path $env:APPDATA "npm" $npmCache = Join-Path $env:APPDATA "npm-cache" if (-not (Test-Path $npmRoot)) { New-Item -ItemType Directory -Path $npmRoot -Force | Out-Null } if (-not (Test-Path $npmCache)) { New-Item -ItemType Directory -Path $npmCache -Force | Out-Null } Write-Host "Running: npm install -g n8n@$Version (raw mode)" $npmOutput = cmd.exe /c "npm install -g n8n@$Version 2>&1" Write-Host $npmOutput if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: npm failed to install n8n." exit 1 } Write-Host "Rebuilding npm shims..." $rebuild = cmd.exe /c "npm rebuild -g 2>&1" Write-Host $rebuild $exe = Join-Path $npmRoot "n8n.cmd" if (-not (Test-Path $exe)) { Write-Host "ERROR: n8n executable not found after installation." exit 1 } $resolved = Try-Command "& `"$exe`" --version" if ($resolved.Trim() -ne $Version) { Write-Host "ERROR: n8n version mismatch (expected $Version, got $resolved)." exit 1 } Write-Host "Installed n8n $resolved" } # --------------------------------------------------------------- # BACKUP (unchanged) # --------------------------------------------------------------- function Backup-N8N { $n8nDir = Join-Path $env:USERPROFILE ".n8n" if (-not (Test-Path $n8nDir)) { Write-Host "No n8n data directory found – nothing to back up." return } Write-Host "Creating backup of n8n data..." $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" } # --------------------------------------------------------------- # MEMORY CHECK # --------------------------------------------------------------- $ramFree = (Get-Counter '\Memory\Available MBytes').CounterSamples[0].CookedValue Write-Host "Free RAM: $([math]::Round($ramFree)) MB" if ($ramFree -lt 800) { Write-Host "Applying safe Node heap limit..." $env:NODE_OPTIONS = "--max-old-space-size=1536" } # --------------------------------------------------------------- # BACKUP BEFORE ANY CHANGES # --------------------------------------------------------------- Backup-N8N # --------------------------------------------------------------- # BLOCK nvm4w # --------------------------------------------------------------- if (Test-Path "C:\nvm4w") { Write-Host "ERROR: C:\nvm4w interferes with Node MSI installs." exit 1 } # --------------------------------------------------------------- # FETCH n8n METADATA # --------------------------------------------------------------- $n8nMeta = Get-Json "https://registry.npmjs.org/n8n/latest" $latestN8n = $n8nMeta.version $nodeReq = $n8nMeta.engines.node Write-Host "Latest n8n: $latestN8n" Write-Host "Node requirement: $nodeReq" # --------------------------------------------------------------- # PARSE NODE ENGINE requirement # --------------------------------------------------------------- $minMatch = [regex]::Match($nodeReq, '(>=|>)\s*(\d+(\.\d+)*)') $maxMatch = [regex]::Match($nodeReq, '(<=|<)\s*(\d+(?:\.\d+){0,2}(?:\.x)?)') $minOp = $minMatch.Groups[1].Value $minNode = $minMatch.Groups[2].Value $maxOp = $maxMatch.Groups[1].Value $maxNode = $maxMatch.Groups[2].Value if ($maxNode -match '\.x$') { $maxNode = $maxNode -replace '\.x$', '.99.99' } # --------------------------------------------------------------- # FETCH NODE INDEX & SELECT SAFE VERSION # --------------------------------------------------------------- $nodeIndex = Get-Json "https://nodejs.org/dist/index.json" $candidates = $nodeIndex | Where-Object { $v = $_.version.TrimStart('v') if ($v -match '-') { return $false } if (-not ($_.files -contains "win-x64-msi")) { return $false } try { $ver = [version]$v $minOK = if ($minOp -eq '>=') { $ver -ge [version]$minNode } else { $ver -gt [version]$minNode } $maxOK = if ($maxOp -eq '<=') { $ver -le [version]$maxNode } else { $ver -lt [version]$maxNode } $minOK -and $maxOK } catch { $false } } $lts = $candidates | Where-Object { $_.lts } $chosen = if ($lts) { $lts | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1 } else { $candidates | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1 } $targetNode = $chosen.version Write-Host "Selected Node: $targetNode" # --------------------------------------------------------------- # INSTALL OR UPDATE NODE # --------------------------------------------------------------- $currentNode = Try-Command "node -v" $needNode = $true if ($currentNode) { try { $verCurrent = [version]$currentNode.TrimStart('v') $verTarget = [version]$targetNode.TrimStart('v') if ($verCurrent -eq $verTarget) { $needNode = $false } } catch { $needNode = $true } } if ($needNode) { Uninstall-Node Install-Node $targetNode } else { Write-Host "Node.js is already the required version ($currentNode) — skipping reinstall." } # --------------------------------------------------------------- # VALIDATE NODE INSTALL & FIX PATH BEFORE npm CALLS (PATCH 2) # --------------------------------------------------------------- $nodeDir = Resolve-NodeInstallPath if (-not $nodeDir) { Write-Host "ERROR: Node installation not found after MSI install." exit 1 } # Make node.exe immediately available $env:PATH = "$nodeDir;$env:PATH" # Full PATH repair Repair-Path $nodeDir $installedNode = Try-Command "node -v" $installedNpm = Try-Command "npm -v" if (-not $installedNode) { Write-Host "ERROR: Node not visible after PATH repair." exit 1 } if (-not $installedNpm) { Write-Host "ERROR: npm not visible after PATH repair." exit 1 } Write-Host "Node: $installedNode" Write-Host "npm: $installedNpm" # --------------------------------------------------------------- # INSTALL / UPDATE n8n # --------------------------------------------------------------- $currentN8n = Try-Command "n8n --version" if ($currentN8n) { if ($currentN8n.Trim() -ne $latestN8n) { Install-N8N $latestN8n } else { Write-Host "n8n is already up-to-date." } } else { Install-N8N $latestN8n } Write-Host "" Write-Host "SUCCESS: n8n $latestN8n installed on Node $installedNode" Write-Host "Use launch-n8n.ps1 to start n8n."