# 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
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
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')
$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