# Vloopex installer for Windows. It provides Node.js, then runs setup.mjs, # which installs the rest: # irm https://vloopex.com/install.ps1 | iex # & ([scriptblock]::Create((irm https://vloopex.com/install.ps1))) --yes --harnesses claude # # `irm | iex` runs this in the caller's session, so the script uses a function # and `return`: `exit` would close the person's PowerShell window. function Install-Vloopex { param([string[]]$SetupArgs) $ErrorActionPreference = 'Stop' # Windows PowerShell draws a progress bar so slowly that it slows downloads. $ProgressPreference = 'SilentlyContinue' $nodeMin = [version]'24.17.0' $nodeLine = 24 $runtimeDir = Join-Path $env:USERPROFILE '.vloopex\runtime' $setupUrl = if ($env:VLOOPEX_SETUP_URL) { $env:VLOOPEX_SETUP_URL } else { 'https://vloopex.com/setup.mjs' } $assumeYes = ($SetupArgs -contains '--yes') -or ($SetupArgs -contains '-y') $checkOnly = $SetupArgs -contains '--check' $dryRun = $SetupArgs -contains '--dry-run' # Windows PowerShell 5.1 does not enable TLS 1.2 by default. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Fail([string]$message) { Write-Host "Vloopex install: $message" -ForegroundColor Red $global:LASTEXITCODE = 1 } function Get-NodeVersion([string]$exe) { if (-not $exe -or -not (Test-Path $exe)) { return $null } $text = & $exe --version if ($text -match '^v?(\d+\.\d+\.\d+)') { return [version]$Matches[1] } return $null } $tmp = Join-Path ([IO.Path]::GetTempPath()) "vloopex-setup-$PID" New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { # Node.js on PATH first, then a copy that setup downloaded before. $pathNode = (Get-Command node.exe -ErrorAction SilentlyContinue).Source $current = Get-NodeVersion $pathNode $privateNode = $null if (-not ($current -ge $nodeMin)) { $privateNode = Get-ChildItem -Directory -Path $runtimeDir -Filter 'node-v*' -ErrorAction SilentlyContinue | ForEach-Object { Join-Path $_.FullName 'node.exe' } | Where-Object { (Get-NodeVersion $_) -ge $nodeMin } | Select-Object -First 1 } if (-not ($current -ge $nodeMin) -and -not $privateNode) { $found = if ($current) { "Node.js $current is too old" } else { 'Node.js is not installed' } if ($checkOnly) { return Fail "$found. Vloopex needs $nodeMin or newer." } if ($dryRun) { Write-Host "Dry run: $found. Setup would download Node.js $nodeLine from nodejs.org into $runtimeDir. The other steps need Node.js, so the dry run stops here." $global:LASTEXITCODE = 0 return } if (-not $assumeYes) { $reply = Read-Host "$found. Vloopex needs $nodeMin or newer. Download Node.js $nodeLine from nodejs.org into $runtimeDir? It needs no admin rights and does not change other Node.js installs. [Y/n]" if ($reply -and $reply -notmatch '^(y|yes)$') { return Fail "Vloopex needs Node.js $nodeMin or newer." } } $machine = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } $arch = switch ($machine) { 'AMD64' { 'x64' } 'ARM64' { 'arm64' } default { $null } } if (-not $arch) { return Fail "nodejs.org has no Node.js build for $machine. Install Node.js $nodeMin or newer, then run this again." } $base = "https://nodejs.org/dist/latest-v$nodeLine.x" $file = $null $hash = $null foreach ($line in ((Invoke-WebRequest -UseBasicParsing -Uri "$base/SHASUMS256.txt").Content -split "`n")) { if ($line -match "^([0-9a-f]{64})\s+(node-v[\d.]+-win-$arch\.zip)\s*$") { $hash = $Matches[1] $file = $Matches[2] break } } if (-not $file) { return Fail "nodejs.org lists no Node.js $nodeLine build for win-$arch." } Write-Host "Downloading $file from nodejs.org" $zip = Join-Path $tmp $file Invoke-WebRequest -UseBasicParsing -Uri "$base/$file" -OutFile $zip if ((Get-FileHash -Algorithm SHA256 $zip).Hash -ne $hash) { return Fail 'The Node.js download does not match its checksum. Nothing was installed.' } # tar.exe unpacks the zip in seconds; Expand-Archive takes minutes. $tar = Join-Path $env:SystemRoot 'System32\tar.exe' if (Test-Path $tar) { & $tar -xf $zip -C $tmp if ($LASTEXITCODE -ne 0) { return Fail "Could not unpack $file." } } else { Expand-Archive -LiteralPath $zip -DestinationPath $tmp } $name = [IO.Path]::GetFileNameWithoutExtension($file) New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null $target = Join-Path $runtimeDir $name if (Test-Path $target) { Remove-Item -Recurse -Force $target } Move-Item -LiteralPath (Join-Path $tmp $name) -Destination $target $privateNode = Join-Path $target 'node.exe' if (-not ((Get-NodeVersion $privateNode) -ge $nodeMin)) { return Fail 'The downloaded Node.js does not run on this computer.' } } if ($privateNode) { $env:Path = "$(Split-Path $privateNode);$env:Path" } $setup = if ($PSScriptRoot) { Join-Path $PSScriptRoot 'setup.mjs' } else { $null } if (-not $setup -or -not (Test-Path $setup)) { $setup = Join-Path $tmp 'setup.mjs' try { Invoke-WebRequest -UseBasicParsing -Uri $setupUrl -OutFile $setup } catch { return Fail "Could not download ${setupUrl}: $($_.Exception.Message)" } } & node $setup @SetupArgs } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } } Install-Vloopex -SetupArgs $args # Only a script file passes on the exit code; under `irm | iex` it stays in $LASTEXITCODE. if ($PSCommandPath) { exit $LASTEXITCODE }