<#
.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."
Comments
0 B
|👍
/👎