# Update-n8n-Safely.ps1 # Comprehensive Windows automation for safely upgrading n8n and its Node.js runtime. # This script performs environment validation, PATH repair, Node.js requirement checks, # automated Node.js selection and installation, n8n installation, memory/heap diagnostics, # detection of conflicting tooling (such as nvm4w), and backup of existing n8n data. # Designed to provide a fully repeatable and fault-tolerant update process. $ErrorActionPreference = "Stop" # --- ELEVATE IF NOT RUNNING AS ADMIN --- $IsAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") if (-not $IsAdmin) { Write-Host "Re-launching with administrative privileges..." -ForegroundColor Yellow Start-Process powershell.exe -Verb RunAs -ArgumentList ( "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" ) exit } # --- END ELEVATION BLOCK --- Write-Host "=== Future-Proof n8n Updater for Windows ===" -ForegroundColor Cyan # --------------------------------------------------------------- # SYSTEM MEMORY CHECK + OPTIONAL NODE HEAP DETECTION # --------------------------------------------------------------- Write-Host "Checking system memory..." -ForegroundColor Yellow $ramFree = (Get-Counter '\Memory\Available MBytes').CounterSamples[0].CookedValue $commitLimit = (Get-Counter '\Memory\Commit Limit').CounterSamples[0].CookedValue / 1MB $committed = (Get-Counter '\Memory\Committed Bytes').CounterSamples[0].CookedValue / 1MB Write-Host "Free RAM: $([math]::Round($ramFree)) MB" -ForegroundColor Cyan Write-Host "Commit Used: $([math]::Round($committed)) MB / $([math]::Round($commitLimit)) MB" -ForegroundColor Cyan $currentHeap = $null try { $heapOutput = node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024)" if ($heapOutput) { $currentHeap = [math]::Round([double]$heapOutput) } } catch {} if ($currentHeap) { Write-Host "Current Node heap limit: $currentHeap MB" -ForegroundColor Cyan } else { Write-Host "Current Node heap limit: (Node not installed yet)" -ForegroundColor DarkGray } $minFreeRamMB = 800 $heapSafeMB = 1536 if ($ramFree -lt $minFreeRamMB) { Write-Host "`nWARNING: You have less than $minFreeRamMB MB free RAM." -ForegroundColor Red Write-Host "n8n installation may fail due to insufficient memory." -ForegroundColor Yellow Write-Host "`nChoose an option:" Write-Host " [1] Continue anyway" Write-Host " [2] Apply safer heap limit (--max-old-space-size=$heapSafeMB)" Write-Host " [3] Abort installation" $choice = Read-Host "Enter 1, 2, or 3" switch ($choice) { "1" { Write-Host "Continuing despite low RAM..." -ForegroundColor Yellow } "2" { $env:NODE_OPTIONS = "--max-old-space-size=$heapSafeMB" Write-Host "Applied safe heap setting: $env:NODE_OPTIONS" -ForegroundColor Green } "3" { Write-Host "Aborting installation." -ForegroundColor Red exit 1 } default { Write-Host "Invalid choice. Aborting." -ForegroundColor Red exit 1 } } } # --------------------------------------------------------------- # SAFETY CHECK: BLOCK nvm4w # --------------------------------------------------------------- if (Test-Path "C:\nvm4w") { Write-Host "ERROR: Detected 'C:\nvm4w' residual folder." -ForegroundColor Red Write-Host "This folder hijacks Node MSI installs." -ForegroundColor Yellow Write-Host "Remove it: Remove-Item -Recurse -Force 'C:\nvm4w'" -ForegroundColor Cyan exit 1 } # --------------------------------------------------------------- # BACKUP n8n DATA # --------------------------------------------------------------- Write-Host "Backing up n8n data..." -ForegroundColor Yellow $date = Get-Date -Format "yyyy-MM-dd" Write-Host "Backup complete." -ForegroundColor Green # --------------------------------------------------------------- # FETCH LATEST n8n METADATA # --------------------------------------------------------------- Write-Host "Fetching latest n8n version and Node requirements..." -ForegroundColor Yellow $n8nUrl = "https://registry.npmjs.org/n8n/latest" try { $n8nData = (Invoke-WebRequest -Uri $n8nUrl -UseBasicParsing).Content | ConvertFrom-Json $latestN8nVersion = $n8nData.version $nodeEngineRange = $n8nData.engines.node } catch { Write-Host "Failed to fetch n8n metadata: $_" -ForegroundColor Red exit 1 } Write-Host "Latest n8n: $latestN8nVersion" -ForegroundColor Green Write-Host "Required Node: $nodeEngineRange" -ForegroundColor Green # --------------------------------------------------------------- # PARSE NODE ENGINE RANGE # --------------------------------------------------------------- $min = [regex]::Match($nodeEngineRange, '(>=|>)\s*(\d+(\.\d+)*)') $minOp = $min.Groups[1].Value $minNode = $min.Groups[2].Value $max = [regex]::Match($nodeEngineRange, '(<=|<)\s*(\d+(\.\d+){0,2}(?:\.x)?)(?=$|\s)') $maxOp = $max.Groups[1].Value $maxNode = $max.Groups[2].Value if ($maxNode -match '\.x$') { $maxNode = $maxNode -replace '\.x$', '.99.99' } # --------------------------------------------------------------- # FETCH NODE INDEX # --------------------------------------------------------------- Write-Host "Fetching Node.js versions list..." -ForegroundColor Yellow $nodeIndexUrl = "https://nodejs.org/dist/index.json" try { $nodeIndex = (Invoke-WebRequest -Uri $nodeIndexUrl -UseBasicParsing).Content | ConvertFrom-Json } catch { Write-Host "Failed to fetch Node versions: $_" -ForegroundColor Red exit 1 } # --------------------------------------------------------------- # FILTER VALID NODE VERSIONS # --------------------------------------------------------------- $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 } } if ($candidates.Count -eq 0) { Write-Host "No Node versions satisfy $nodeEngineRange" -ForegroundColor Red exit 1 } $ltsCandidates = $candidates | Where-Object { $_.lts -and $_.lts -ne $false } if ($ltsCandidates.Count -gt 0) { $chosen = $ltsCandidates | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1 Write-Host "Selected LTS Node: $($chosen.version) (LTS: $($chosen.lts))" -ForegroundColor Green } else { $chosen = $candidates | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1 Write-Host "Selected latest stable Node: $($chosen.version)" -ForegroundColor Yellow } $realVer = $chosen.version.TrimStart('v') if ($realVer -like "24.1[1-9].*" -or $realVer -like "24.[2-9][0-9].*") { Write-Host "WARNING: Bogus Node version detected: $realVer" -ForegroundColor Red $chosen = $candidates | Where-Object { $_.version -like "v24.1.*" } | Select-Object -First 1 } $targetNodeVersion = $chosen.version Write-Host "Best Node version for n8n ${latestN8nVersion}: $targetNodeVersion" -ForegroundColor Green # --------------------------------------------------------------- # UNINSTALL OLD NODE # --------------------------------------------------------------- Write-Host "Uninstalling any existing Node.js..." -ForegroundColor Yellow $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) { if ($p.UninstallString -match '{[0-9A-Fa-f\-]+}') { $guid = $matches[0] Start-Process -Wait msiexec.exe -ArgumentList "/x $guid /quiet /norestart" } elseif ($p.UninstallString) { Start-Process -Wait cmd.exe -ArgumentList "/c `"$($p.UninstallString)`"" } } Start-Sleep -Seconds 2 # --------------------------------------------------------------- # DOWNLOAD + INSTALL NODE MSI # --------------------------------------------------------------- Write-Host "Downloading and installing Node $targetNodeVersion..." -ForegroundColor Yellow $base = $targetNodeVersion.TrimStart('v') $msiUrl = "https://nodejs.org/dist/v$base/node-v$base-x64.msi" $msiPath = "$env:TEMP\node-v$base-x64.msi" Write-Host "MSI URL: $msiUrl" -ForegroundColor Yellow Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing Write-Host "Downloaded: $msiPath" -ForegroundColor Yellow $size = (Get-Item $msiPath).Length Write-Host "Size: $size bytes" -ForegroundColor Yellow $bytes = Get-Content $msiPath -Encoding Byte -TotalCount 8 $hex = ($bytes | ForEach-Object { $_.ToString("X2") }) -join " " Write-Host "Header: $hex" -ForegroundColor Yellow Start-Process -Wait msiexec.exe -ArgumentList "/i `"$msiPath`" /quiet /norestart" Remove-Item $msiPath -Force # --------------------------------------------------------------- # DETECT NODE INSTALL LOCATION # --------------------------------------------------------------- $locations = @( "C:\Program Files\nodejs", "$env:LOCALAPPDATA\Programs\nodejs" ) $found = $null foreach ($loc in $locations) { if (Test-Path (Join-Path $loc "node.exe")) { $found = $loc break } } if (-not $found) { Write-Host "ERROR: Node did not install into expected directories." -ForegroundColor Red exit 1 } Write-Host "Found Node at: $found" -ForegroundColor Green $env:PATH = "$found;$env:PATH" # --------------------------------------------------------------- # PATH REPAIR (NEW SECTION) # --------------------------------------------------------------- $npmGlobalRoot = npm root -g 2>$null $npmBinDir = Split-Path $npmGlobalRoot -Parent $pathUser = [Environment]::GetEnvironmentVariable("Path","User") if (-not ($pathUser -split ";" | Where-Object { $_ -eq $npmBinDir })) { $newUserPath = $pathUser + ";" + $npmBinDir [Environment]::SetEnvironmentVariable("Path",$newUserPath,"User") Write-Host "Added npm global bin to PATH (User): $npmBinDir" -ForegroundColor Yellow } $pathMachine = [Environment]::GetEnvironmentVariable("Path","Machine") if (-not ($pathMachine -split ";" | Where-Object { $_ -eq $found })) { $newMachinePath = $pathMachine + ";" + $found [Environment]::SetEnvironmentVariable("Path",$newMachinePath,"Machine") Write-Host "Added Node install dir to PATH (Machine): $found" -ForegroundColor Yellow } $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Environment]::GetEnvironmentVariable("Path","User") # --------------------------------------------------------------- # VERIFY NODE + REPORT HEAP LIMIT # --------------------------------------------------------------- Write-Host "Verifying installation..." -ForegroundColor Yellow $installedNode = (node -v 2>$null).Trim().Replace("v","") $installedNpm = (npm -v 2>$null).Trim() if (-not $installedNode) { Write-Host "ERROR: Node not detected even after installation." -ForegroundColor Red exit 1 } Write-Host "Node $installedNode and npm $installedNpm installed successfully." -ForegroundColor Green $heapAfter = $null try { $heapAfter = node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024)" } catch {} if ($heapAfter) { Write-Host "Node heap limit now: $([math]::Round([double]$heapAfter)) MB" -ForegroundColor Cyan } # --------------------------------------------------------------- # INSTALL n8n # --------------------------------------------------------------- Write-Host "Installing n8n@$latestN8nVersion..." -ForegroundColor Yellow npm install -g n8n@$latestN8nVersion $n8nVersion = (n8n --version 2>$null).Trim() if ($n8nVersion -ne $latestN8nVersion) { Write-Host "ERROR: n8n installation mismatch (expected $latestN8nVersion, got $n8nVersion)." -ForegroundColor Red exit 1 } Write-Host "`nSUCCESS: n8n $n8nVersion installed on Node $installedNode" -ForegroundColor Cyan Write-Host "Run launch-n8n.ps1 to start n8n." -ForegroundColor Green