# PlanBefore installer for Windows (PowerShell 5.1 or later). # # irm https://planbefore.kvnlabs.xyz/install.ps1 | iex # # Optional environment variables: # # REPOFLOW_VERSION version to install, e.g. v0.1.0 # (default: the latest stable release) # REPOFLOW_INSTALL_DIR destination directory # (default: %LOCALAPPDATA%\Programs\RepoFlow) # REPOFLOW_NO_MODIFY_PATH set to 1 to leave your user PATH untouched # # What it does, in order: detects the CPU architecture, resolves the version, # downloads the ZIP and checksums.txt from the official PlanBefore GitHub # Releases over HTTPS, verifies the SHA-256 checksum (and stops if it does # not match), extracts pbnode.exe, installs it for the current user and # runs `pbnode version`. If the install directory is not on your user PATH # it adds it and says so (opt out with REPOFLOW_NO_MODIFY_PATH=1). # # It never asks for credentials, never runs `pbnode login` and never # touches your repositories. # # Releases published before the CLI was renamed only contain repoflow_* # archives. This installer installs pbnode and nothing else: for such a # release (pinned with REPOFLOW_VERSION, or because it is still the latest # stable one) it stops with a clear message and installs nothing. It never # downloads a repoflow archive to install it under the pbnode name. # # Upgrading from the previous `repoflow` command: pbnode reuses the same # configuration, Node identity and projects. The old repoflow.exe is left # untouched (it is reported as legacy); it is never deleted and no `repoflow` # alias is created. # # Errors are thrown, never `exit`, so a failure does not close the PowerShell # window that ran `iex`. & { Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $Repo = 'kevincoder91/planbefore' $ReleasesUrl = "https://github.com/$Repo/releases" $DocsUrl = 'https://planbefore.kvnlabs.xyz/install' $VersionPattern = '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' # Windows PowerShell 5.1 may still default to TLS 1.0. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { Write-Verbose "Could not enable TLS 1.2 explicitly: $($_.Exception.Message)" } function Get-RepoFlowArch { # The system-wide PROCESSOR_ARCHITECTURE in the registry is the # native one, even from an emulated x64 or 32-bit PowerShell. $arch = $null try { $arch = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE).PROCESSOR_ARCHITECTURE } catch { Write-Verbose "Could not read the native architecture from the registry: $($_.Exception.Message)" } if (-not $arch) { if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 } else { $arch = $env:PROCESSOR_ARCHITECTURE } } switch -Regex ("$arch".ToUpperInvariant()) { '^(AMD64|X64)$' { return 'amd64' } '^ARM64$' { return 'arm64' } default { throw "Unsupported CPU architecture: '$arch'. PlanBefore is built for amd64 and arm64." } } } function Resolve-RepoFlowVersion { if ($env:REPOFLOW_VERSION) { $v = $env:REPOFLOW_VERSION if (-not $v.StartsWith('v')) { $v = "v$v" } if ($v -notmatch $VersionPattern) { throw "REPOFLOW_VERSION must look like v1.2.3 (got '$($env:REPOFLOW_VERSION)')" } return $v } # /releases/latest redirects to /releases/tag/ of the latest # stable (non pre-release) release. No API call, so no API rate limit. try { $response = Invoke-WebRequest -Uri "$ReleasesUrl/latest" -Method Head -UseBasicParsing } catch { throw "Could not reach $ReleasesUrl ($($_.Exception.Message))" } # Final URL after redirects: HttpWebResponse.ResponseUri in Windows # PowerShell 5.1, HttpResponseMessage.RequestMessage.RequestUri in 7. $baseResponse = $response.BaseResponse $final = $null if ($baseResponse.PSObject.Properties['ResponseUri']) { $final = $baseResponse.ResponseUri } elseif ($baseResponse.PSObject.Properties['RequestMessage']) { $final = $baseResponse.RequestMessage.RequestUri } $v = ("$final" -split '/')[-1] if ($v -notmatch $VersionPattern) { throw "No stable PlanBefore release is published yet. See $ReleasesUrl" } return $v } function Get-RepoFlowFile([string] $Url, [string] $OutFile) { if (-not $Url.StartsWith('https://')) { throw "Refusing to download over a non-HTTPS URL: $Url" } try { Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing } catch { throw "Download failed: $Url ($($_.Exception.Message))" } } function Add-ToUserPath([string] $Dir) { # Read and write the raw registry value so %VARIABLES% already in the # user PATH stay unexpanded. $key = Get-Item -Path 'HKCU:\Environment' $current = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames') $entries = @("$current".Split(';') | Where-Object { $_ -ne '' }) if ($entries | Where-Object { $_.TrimEnd('\') -ieq $Dir.TrimEnd('\') }) { return $false } $updated = (@($entries) + $Dir) -join ';' Set-ItemProperty -Path 'HKCU:\Environment' -Name Path -Value $updated -Type ExpandString # Setting any user variable broadcasts WM_SETTINGCHANGE, so terminals # opened from now on see the new PATH. [Environment]::SetEnvironmentVariable('REPOFLOW_INSTALL_PATH_REFRESH', '1', 'User') [Environment]::SetEnvironmentVariable('REPOFLOW_INSTALL_PATH_REFRESH', $null, 'User') return $true } $arch = Get-RepoFlowArch $version = Resolve-RepoFlowVersion $archive = "pbnode_$($version.Substring(1))_windows_$arch.zip" if ($env:REPOFLOW_INSTALL_DIR) { $installDir = $env:REPOFLOW_INSTALL_DIR } else { $installDir = Join-Path $env:LOCALAPPDATA 'Programs\RepoFlow' } $target = Join-Path $installDir 'pbnode.exe' $tmp = Join-Path ([IO.Path]::GetTempPath()) ("pbnode-install-" + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { # checksums.txt first: it says which archives the release has, so a # release without a pbnode build is reported before any archive download. $sumsPath = Join-Path $tmp 'checksums.txt' $zipPath = Join-Path $tmp $archive Get-RepoFlowFile "$ReleasesUrl/download/$version/checksums.txt" $sumsPath $expected = $null $historical = $false foreach ($line in Get-Content -Path $sumsPath) { $parts = $line.Trim() -split '\s+' if ($parts.Count -ne 2) { continue } $name = $parts[1].TrimStart('*') if ($name -eq $archive) { $expected = $parts[0].ToLowerInvariant() break } if ($name.StartsWith('repoflow_')) { $historical = $true } } if (-not $expected -and $historical) { # Published for the previous `repoflow` CLI: nothing besides # checksums.txt was downloaded and nothing is installed. $why = "PlanBefore $version was published for the previous 'repoflow' CLI and has no pbnode build." if ($env:REPOFLOW_VERSION) { $why += " This installer installs pbnode only; it cannot install $version as pbnode. Set REPOFLOW_VERSION to a PlanBefore Node release published after the rename, or remove it." } else { $why += " It is still the latest stable release: no PlanBefore Node (pbnode) release is published yet. Run this installer again once a pbnode release is out, or pin one with REPOFLOW_VERSION." } throw "$why Releases: $ReleasesUrl. Nothing was installed." } if (-not $expected) { throw "No build of PlanBefore $version for windows/$arch in checksums.txt. See $ReleasesUrl" } Write-Host "Installing PlanBefore $version for windows/$arch" Write-Host "Downloading $archive" Get-RepoFlowFile "$ReleasesUrl/download/$version/$archive" $zipPath $actual = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw "SHA-256 mismatch for $archive (expected $expected, got $actual). Nothing was installed." } Write-Host "SHA-256 verified: $actual" $extractDir = Join-Path $tmp 'x' Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force $exe = Join-Path $extractDir 'pbnode.exe' if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw "$archive does not contain pbnode.exe" } New-Item -ItemType Directory -Path $installDir -Force | Out-Null $replaced = Test-Path -LiteralPath $target if ($replaced) { # A running `pbnode serve` locks the .exe, but Windows allows # renaming it: move it aside and put the new one in place. $old = "$target.old" Remove-Item -LiteralPath $old -Force -ErrorAction SilentlyContinue try { Remove-Item -LiteralPath $target -Force } catch { Move-Item -LiteralPath $target -Destination $old -Force } } Copy-Item -LiteralPath $exe -Destination $target -Force } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } Write-Host "Installed $target" Write-Host '' & $target version if ($LASTEXITCODE -ne 0) { throw "The installed binary did not run: $target" } $onPath = @($env:Path.Split(';') | Where-Object { $_.TrimEnd('\') -ieq $installDir.TrimEnd('\') }).Count -gt 0 if (-not $onPath) { Write-Host '' if ($env:REPOFLOW_NO_MODIFY_PATH -eq '1') { Write-Host "$installDir is not on your PATH (left untouched because REPOFLOW_NO_MODIFY_PATH=1)." Write-Host 'Add it in Settings > System > About > Advanced system settings > Environment Variables,' Write-Host "or run: [Environment]::SetEnvironmentVariable('Path', `"`$([Environment]::GetEnvironmentVariable('Path','User'));$installDir`", 'User')" } else { if (Add-ToUserPath $installDir) { Write-Host "Added $installDir to your user PATH. Open a new terminal to use 'pbnode'." } $env:Path = "$env:Path;$installDir" } } $missing = @() if (-not (Get-Command git -ErrorAction SilentlyContinue)) { $missing += 'git' } if (-not (Get-Command rg -ErrorAction SilentlyContinue)) { $missing += 'ripgrep' } if ($missing.Count -gt 0) { Write-Host '' Write-Warning "PlanBefore Node needs git and ripgrep (rg). Missing: $($missing -join ', ')" if ($missing -contains 'git') { Write-Host ' winget install --id Git.Git -e' } if ($missing -contains 'ripgrep') { Write-Host ' winget install --id BurntSushi.ripgrep.MSVC -e' } } # The previous CLI was called `repoflow`: report it as legacy, never # delete it and never create a `repoflow` alias. $legacy = Get-Command repoflow -ErrorAction SilentlyContinue | Select-Object -First 1 if ($replaced -or $legacy) { Write-Host '' Write-Host 'If a PlanBefore Node was already running, restart it to use this version:' Write-Host ' pbnode stop; pbnode serve --background' } if ($legacy) { Write-Host '' Write-Host "Found the previous PlanBefore Node command (legacy): $($legacy.Source)" Write-Host 'pbnode uses the same configuration, Node identity and projects, so no' Write-Host "'pbnode login' is needed. The legacy repoflow executable was left in" Write-Host "place; once 'pbnode version' and 'pbnode status' look right, you can" Write-Host 'remove it.' } Write-Host '' Write-Host 'Next steps:' Write-Host ' pbnode init' Write-Host ' pbnode login' Write-Host ' pbnode expose ' Write-Host ' pbnode serve --background' Write-Host ' pbnode status' Write-Host '' Write-Host "Guide: $DocsUrl" }