J2897

Windows n8n/Node Auto-Updater

Dec 7th, 2025 (edited)
2,006
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2. .SYNOPSIS
  3.     Safe, idempotent, fully automated Node.js + n8n updater for Windows.
  4.  
  5. .DESCRIPTION
  6.     Installs or updates Node.js and n8n to the newest safe versions that match
  7.     n8n's engine requirements. Creates backups of ~/.n8n before making changes,
  8.     validates installations, and safely repairs both User and Machine PATHs.
  9.  
  10.     Features:
  11.       • Automatic Node.js version selection based on n8n metadata
  12.       • Automatic n8n installation/update
  13.       • Reliable PATH repair (no npm calls required)
  14.       • Full backup system with timestamped .zip output
  15.       • Safe elevation handling
  16.       • Fully idempotent — safe to run repeatedly
  17.       • Windows PowerShell 5.1 and PowerShell 7+ compatible
  18.  
  19. .NOTES
  20.     Author:  J2897
  21.     Refined: ChatGPT 5.1 (OpenAI)
  22.     Version: 2025 Polished Edition
  23. #>
  24.  
  25. $ErrorActionPreference = "Stop"
  26.  
  27. # ---------------------------------------------------------------
  28. # ADMIN ELEVATION
  29. # ---------------------------------------------------------------
  30. $identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
  31. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  32. $IsAdmin   = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
  33.  
  34. if (-not $IsAdmin) {
  35.     Write-Host "Re-launching with administrative privileges..."
  36.     Start-Process powershell.exe -Verb RunAs -ArgumentList (
  37.         "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
  38.     )
  39.     exit
  40. }
  41.  
  42. Write-Host "=== n8n Safe Updater for Windows ==="
  43.  
  44. # ---------------------------------------------------------------
  45. # UTILITIES
  46. # ---------------------------------------------------------------
  47. function Try-Command {
  48.     param([string]$Cmd)
  49.     try { Invoke-Expression $Cmd 2>$null } catch { $null }
  50. }
  51.  
  52. function Get-Json {
  53.     param([string]$Url)
  54.     try {
  55.         (Invoke-WebRequest -Uri $Url -UseBasicParsing).Content | ConvertFrom-Json
  56.     } catch {
  57.         Write-Host "ERROR: Failed to fetch JSON from $Url"
  58.         exit 1
  59.     }
  60. }
  61.  
  62. # ---------------------------------------------------------------
  63. # NODE UNINSTALL
  64. # ---------------------------------------------------------------
  65. function Uninstall-Node {
  66.     Write-Host "Removing existing Node.js..."
  67.  
  68.     $unKeys = @(
  69.         "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
  70.         "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
  71.     )
  72.  
  73.     $products = Get-ItemProperty -Path $unKeys -ErrorAction SilentlyContinue |
  74.         Where-Object { $_.DisplayName -like "*Node.js*" }
  75.  
  76.     foreach ($p in $products) {
  77.         $uninst = $p.UninstallString
  78.  
  79.         if ($uninst -match '{[0-9A-Fa-f\-]+}') {
  80.             $guid = [regex]::Match($uninst, '{[0-9A-Fa-f\-]+}').Value
  81.             Start-Process msiexec.exe -Wait -ArgumentList "/x $guid /quiet /norestart"
  82.         }
  83.         elseif ($uninst) {
  84.             Start-Process cmd.exe -Wait -ArgumentList "/c `"$uninst`""
  85.         }
  86.     }
  87.  
  88.     Start-Sleep -Seconds 1
  89. }
  90.  
  91. # ---------------------------------------------------------------
  92. # NODE INSTALL
  93. # ---------------------------------------------------------------
  94. function Install-Node {
  95.     param([string]$Version)
  96.  
  97.     $clean = $Version.TrimStart('v')
  98.     $msiUrl  = "https://nodejs.org/dist/v$clean/node-v$clean-x64.msi"
  99.     $msiPath = Join-Path $env:TEMP "node-v$clean-x64.msi"
  100.  
  101.     Write-Host "Installing Node $Version..."
  102.     Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
  103.  
  104.     Start-Process msiexec.exe -Wait -ArgumentList "/i `"$msiPath`" /quiet /norestart"
  105.     Remove-Item $msiPath -Force -ErrorAction SilentlyContinue
  106. }
  107.  
  108. # ---------------------------------------------------------------
  109. # NODE PATH RESOLUTION
  110. # ---------------------------------------------------------------
  111. function Resolve-NodeInstallPath {
  112.     foreach ($loc in @(
  113.         "C:\Program Files\nodejs",
  114.         "$env:LOCALAPPDATA\Programs\nodejs"
  115.     )) {
  116.         if (Test-Path (Join-Path $loc "node.exe")) {
  117.             return $loc
  118.         }
  119.     }
  120.     return $null
  121. }
  122.  
  123. # ---------------------------------------------------------------
  124. # REPAIR PATH (PATCH 1 APPLIED)
  125. # ---------------------------------------------------------------
  126. function Repair-Path {
  127.     param([string]$NodeDir)
  128.  
  129.     # Expected npm global locations
  130.     $npmBin   = Join-Path $env:APPDATA "npm"
  131.     $npmCache = Join-Path $env:APPDATA "npm-cache"
  132.  
  133.     # Ensure both directories exist
  134.     if (-not (Test-Path $npmBin)) {
  135.         New-Item -ItemType Directory -Path $npmBin -Force | Out-Null
  136.     }
  137.     if (-not (Test-Path $npmCache)) {
  138.         New-Item -ItemType Directory -Path $npmCache -Force | Out-Null
  139.     }
  140.  
  141.     # Helper to append new path elements uniquely
  142.     $addUnique = {
  143.         param($existing, $new)
  144.         $parts = $existing -split ';' | Where-Object { $_ -ne '' }
  145.         if ($parts -notcontains $new) { ($parts + $new) -join ';' } else { $existing }
  146.     }
  147.  
  148.     # User PATH
  149.     $pUser = [Environment]::GetEnvironmentVariable("Path","User")
  150.     $pUserNew = $addUnique.Invoke($pUser, $npmBin)
  151.     [Environment]::SetEnvironmentVariable("Path", $pUserNew, "User")
  152.  
  153.     # Machine PATH
  154.     $pMach = [Environment]::GetEnvironmentVariable("Path","Machine")
  155.     $pMachNew = $addUnique.Invoke($pMach, $NodeDir)
  156.     [Environment]::SetEnvironmentVariable("Path", $pMachNew, "Machine")
  157.  
  158.     # npm bin FIRST (fixes shim creation)
  159.     $env:PATH = "$npmBin;$pUserNew;$pMachNew"
  160. }
  161.  
  162. # ---------------------------------------------------------------
  163. # INSTALL n8n (PATCH 3 APPLIED)
  164. # ---------------------------------------------------------------
  165. function Install-N8N {
  166.     param([string]$Version)
  167.  
  168.     Write-Host "Installing n8n@$Version..."
  169.  
  170.     $npmRoot  = Join-Path $env:APPDATA "npm"
  171.     $npmCache = Join-Path $env:APPDATA "npm-cache"
  172.  
  173.     if (-not (Test-Path $npmRoot))  { New-Item -ItemType Directory -Path $npmRoot  -Force | Out-Null }
  174.     if (-not (Test-Path $npmCache)) { New-Item -ItemType Directory -Path $npmCache -Force | Out-Null }
  175.  
  176.     Write-Host "Running: npm install -g n8n@$Version (raw mode)"
  177.     $npmOutput = cmd.exe /c "npm install -g n8n@$Version 2>&1"
  178.     Write-Host $npmOutput
  179.  
  180.     if ($LASTEXITCODE -ne 0) {
  181.         Write-Host "ERROR: npm failed to install n8n."
  182.         exit 1
  183.     }
  184.  
  185.     Write-Host "Rebuilding npm shims..."
  186.     $rebuild = cmd.exe /c "npm rebuild -g 2>&1"
  187.     Write-Host $rebuild
  188.  
  189.     $exe = Join-Path $npmRoot "n8n.cmd"
  190.     if (-not (Test-Path $exe)) {
  191.         Write-Host "ERROR: n8n executable not found after installation."
  192.         exit 1
  193.     }
  194.  
  195.     $resolved = Try-Command "& `"$exe`" --version"
  196.     if ($resolved.Trim() -ne $Version) {
  197.         Write-Host "ERROR: n8n version mismatch (expected $Version, got $resolved)."
  198.         exit 1
  199.     }
  200.  
  201.     Write-Host "Installed n8n $resolved"
  202. }
  203.  
  204. # ---------------------------------------------------------------
  205. # BACKUP (unchanged)
  206. # ---------------------------------------------------------------
  207. function Backup-N8N {
  208.     $n8nDir = Join-Path $env:USERPROFILE ".n8n"
  209.     if (-not (Test-Path $n8nDir)) {
  210.         Write-Host "No n8n data directory found – nothing to back up."
  211.         return
  212.     }
  213.  
  214.     Write-Host "Creating backup of n8n data..."
  215.  
  216.     $timestamp  = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
  217.     $backupRoot = Join-Path $env:USERPROFILE "Desktop\n8n-backups"
  218.     $zipPath    = Join-Path $backupRoot "n8n-backup-$timestamp.zip"
  219.     $tempCopy   = Join-Path $env:TEMP     "n8n-backup-$timestamp"
  220.  
  221.     if (-not (Test-Path $backupRoot)) {
  222.         New-Item -ItemType Directory -Path $backupRoot | Out-Null
  223.     }
  224.  
  225.     if (Test-Path $tempCopy) {
  226.         Remove-Item $tempCopy -Recurse -Force
  227.     }
  228.     New-Item -ItemType Directory -Path $tempCopy | Out-Null
  229.  
  230.     try {
  231.         Copy-Item "$n8nDir\*" $tempCopy -Recurse -Force
  232.         Compress-Archive -Path $tempCopy -DestinationPath $zipPath -Force
  233.     }
  234.     finally {
  235.         if (Test-Path $tempCopy) {
  236.             Remove-Item $tempCopy -Recurse -Force
  237.         }
  238.     }
  239.  
  240.     $sizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 1)
  241.     Write-Host "Backup complete ($sizeMB MB) -> $zipPath"
  242. }
  243.  
  244. # ---------------------------------------------------------------
  245. # MEMORY CHECK
  246. # ---------------------------------------------------------------
  247. $ramFree = (Get-Counter '\Memory\Available MBytes').CounterSamples[0].CookedValue
  248. Write-Host "Free RAM: $([math]::Round($ramFree)) MB"
  249.  
  250. if ($ramFree -lt 800) {
  251.     Write-Host "Applying safe Node heap limit..."
  252.     $env:NODE_OPTIONS = "--max-old-space-size=1536"
  253. }
  254.  
  255. # ---------------------------------------------------------------
  256. # BACKUP BEFORE ANY CHANGES
  257. # ---------------------------------------------------------------
  258. Backup-N8N
  259.  
  260. # ---------------------------------------------------------------
  261. # BLOCK nvm4w
  262. # ---------------------------------------------------------------
  263. if (Test-Path "C:\nvm4w") {
  264.     Write-Host "ERROR: C:\nvm4w interferes with Node MSI installs."
  265.     exit 1
  266. }
  267.  
  268. # ---------------------------------------------------------------
  269. # FETCH n8n METADATA
  270. # ---------------------------------------------------------------
  271. $n8nMeta   = Get-Json "https://registry.npmjs.org/n8n/latest"
  272. $latestN8n = $n8nMeta.version
  273. $nodeReq   = $n8nMeta.engines.node
  274.  
  275. Write-Host "Latest n8n: $latestN8n"
  276. Write-Host "Node requirement: $nodeReq"
  277.  
  278. # ---------------------------------------------------------------
  279. # PARSE NODE ENGINE requirement
  280. # ---------------------------------------------------------------
  281. $minMatch = [regex]::Match($nodeReq, '(>=|>)\s*(\d+(\.\d+)*)')
  282. $maxMatch = [regex]::Match($nodeReq, '(<=|<)\s*(\d+(?:\.\d+){0,2}(?:\.x)?)')
  283.  
  284. $minOp   = $minMatch.Groups[1].Value
  285. $minNode = $minMatch.Groups[2].Value
  286.  
  287. $maxOp   = $maxMatch.Groups[1].Value
  288. $maxNode = $maxMatch.Groups[2].Value
  289. if ($maxNode -match '\.x$') {
  290.     $maxNode = $maxNode -replace '\.x$', '.99.99'
  291. }
  292.  
  293. # ---------------------------------------------------------------
  294. # FETCH NODE INDEX & SELECT SAFE VERSION
  295. # ---------------------------------------------------------------
  296. $nodeIndex = Get-Json "https://nodejs.org/dist/index.json"
  297.  
  298. $candidates = $nodeIndex | Where-Object {
  299.     $v = $_.version.TrimStart('v')
  300.     if ($v -match '-') { return $false }
  301.     if (-not ($_.files -contains "win-x64-msi")) { return $false }
  302.  
  303.     try {
  304.         $ver   = [version]$v
  305.         $minOK = if ($minOp -eq '>=') { $ver -ge [version]$minNode } else { $ver -gt [version]$minNode }
  306.         $maxOK = if ($maxOp -eq '<=') { $ver -le [version]$maxNode } else { $ver -lt [version]$maxNode }
  307.         $minOK -and $maxOK
  308.     } catch { $false }
  309. }
  310.  
  311. $lts    = $candidates | Where-Object { $_.lts }
  312. $chosen = if ($lts) {
  313.     $lts | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1
  314. } else {
  315.     $candidates | Sort-Object { [version]($_.version.TrimStart('v')) } -Descending | Select-Object -First 1
  316. }
  317.  
  318. $targetNode = $chosen.version
  319. Write-Host "Selected Node: $targetNode"
  320.  
  321. # ---------------------------------------------------------------
  322. # INSTALL OR UPDATE NODE
  323. # ---------------------------------------------------------------
  324. $currentNode = Try-Command "node -v"
  325.  
  326. $needNode = $true
  327. if ($currentNode) {
  328.     try {
  329.         $verCurrent = [version]$currentNode.TrimStart('v')
  330.         $verTarget  = [version]$targetNode.TrimStart('v')
  331.  
  332.         if ($verCurrent -eq $verTarget) {
  333.             $needNode = $false
  334.         }
  335.     } catch {
  336.         $needNode = $true
  337.     }
  338. }
  339.  
  340. if ($needNode) {
  341.     Uninstall-Node
  342.     Install-Node $targetNode
  343. } else {
  344.     Write-Host "Node.js is already the required version ($currentNode) — skipping reinstall."
  345. }
  346.  
  347. # ---------------------------------------------------------------
  348. # VALIDATE NODE INSTALL & FIX PATH BEFORE npm CALLS (PATCH 2)
  349. # ---------------------------------------------------------------
  350. $nodeDir = Resolve-NodeInstallPath
  351. if (-not $nodeDir) {
  352.     Write-Host "ERROR: Node installation not found after MSI install."
  353.     exit 1
  354. }
  355.  
  356. # Make node.exe immediately available
  357. $env:PATH = "$nodeDir;$env:PATH"
  358.  
  359. # Full PATH repair
  360. Repair-Path $nodeDir
  361.  
  362. $installedNode = Try-Command "node -v"
  363. $installedNpm  = Try-Command "npm -v"
  364.  
  365. if (-not $installedNode) {
  366.     Write-Host "ERROR: Node not visible after PATH repair."
  367.     exit 1
  368. }
  369.  
  370. if (-not $installedNpm) {
  371.     Write-Host "ERROR: npm not visible after PATH repair."
  372.     exit 1
  373. }
  374.  
  375. Write-Host "Node: $installedNode"
  376. Write-Host "npm:  $installedNpm"
  377.  
  378. # ---------------------------------------------------------------
  379. # INSTALL / UPDATE n8n
  380. # ---------------------------------------------------------------
  381. $currentN8n = Try-Command "n8n --version"
  382.  
  383. if ($currentN8n) {
  384.     if ($currentN8n.Trim() -ne $latestN8n) {
  385.         Install-N8N $latestN8n
  386.     } else {
  387.         Write-Host "n8n is already up-to-date."
  388.     }
  389. } else {
  390.     Install-N8N $latestN8n
  391. }
  392.  
  393. Write-Host ""
  394. Write-Host "SUCCESS: n8n $latestN8n installed on Node $installedNode"
  395. Write-Host "Use launch-n8n.ps1 to start n8n."
  396.  
Add Comment
Please, Sign In to add comment