<# .SYNOPSIS JetScale installer / updater for Windows. .DESCRIPTION Usage: irm https://jetscale.sh/install.ps1 | iex What it does: 1. Detects your CPU architecture. 2. Asks the server for the latest version + commit (latest.json). 3. Compares it against the version you already have installed (if any). 4. Downloads and installs only when the versions differ. Release artifacts are flat files named: jetscale.... served from the download base alongside latest.json, e.g. https://jetscale.sh/jetscale.windows.amd64.2026-06-28-abc1234.abc1234 The script is split into small functions to keep it easy to maintain. Execution starts at the bottom in Install-Main. #> $ErrorActionPreference = "Stop" ############################################################################### # Configuration ############################################################################### # Binary name and where releases are hosted. $AppName = "jetscale" $DownloadBase = "https://jetscale.sh" # Endpoint that returns JSON describing the latest release, e.g. # { "version": "2026-06-28-abc1234", "commit": "abc1234" } $LatestJson = "$DownloadBase/latest.json" # Where the binary is installed and the matching executable path. $InstallDir = "$env:LOCALAPPDATA\Programs\$AppName" $BinaryPath = Join-Path $InstallDir "$AppName.exe" ############################################################################### # Output helpers ############################################################################### # Write-Status prints a "==>" status line. function Write-Status([string]$Message) { Write-Host "==> $Message" } ############################################################################### # Platform detection ############################################################################### # Get-Arch maps the OS architecture onto the token used in release file names. function Get-Arch { $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture switch ($arch) { "X64" { return "amd64" } "Arm64" { return "arm64" } default { throw "Unsupported architecture: $arch" } } } ############################################################################### # Version discovery ############################################################################### # Normalize-Version strips a leading "v" and whitespace so that "v1.2.3" and # "1.2.3" compare as equal. function Normalize-Version([string]$Version) { if (-not $Version) { return "" } return ($Version.Trim() -replace '^v', '') } # Get-LatestRelease fetches latest.json and returns its parsed object with # .version and .commit fields. function Get-LatestRelease { $latest = Invoke-RestMethod $LatestJson if (-not $latest.version) { throw "Unable to determine the latest version from $LatestJson." } if (-not $latest.commit) { throw "Unable to determine the latest commit from $LatestJson." } return $latest } # Get-InstalledVersion returns the version of the installed binary, or $null # if it is not present. The CLI prints its raw version string; we take the # last token of the first line so it also works if the output is prefixed. function Get-InstalledVersion { if (-not (Test-Path $BinaryPath)) { return $null } try { $raw = & $BinaryPath --version 2>$null if (-not $raw) { $raw = & $BinaryPath version 2>$null } } catch { return $null } if (-not $raw) { return $null } $line = ([string[]]$raw)[0] return ($line -split '\s+' | Where-Object { $_ })[-1] } ############################################################################### # Download + install ############################################################################### # Install-Binary downloads the raw release binary for the given version and # commit and installs it. Artifacts are single executables named # jetscale.... (no archive), so there is nothing # to extract. function Install-Binary([string]$Version, [string]$Commit, [string]$Arch) { $file = "${AppName}.windows.${Arch}.${Version}.${Commit}" $url = "$DownloadBase/$file" # Use a fresh temp directory for the download. $temp = Join-Path $env:TEMP "$AppName-install" Remove-Item $temp -Recurse -Force -ErrorAction Ignore New-Item -ItemType Directory -Path $temp | Out-Null $downloaded = Join-Path $temp "$AppName.exe" Write-Status "Downloading $file..." Invoke-WebRequest $url -OutFile $downloaded Write-Status "Installing to $InstallDir" New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Copy-Item $downloaded $BinaryPath -Force Remove-Item $temp -Recurse -Force -ErrorAction Ignore } # Add-ToPath appends the install directory to the user's PATH if missing. function Add-ToPath { $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if ($userPath -notlike "*$InstallDir*") { [Environment]::SetEnvironmentVariable( "Path", "$userPath;$InstallDir", "User" ) Write-Host "" Write-Host "Added $InstallDir to PATH." Write-Host "Restart your terminal." } } ############################################################################### # Entry point ############################################################################### function Install-Main { Write-Status "Detecting platform..." $arch = Get-Arch Write-Status "Fetching latest release..." $latest = Get-LatestRelease Write-Status "Latest version: $($latest.version)" # Skip the download entirely if the installed version already matches. $current = Get-InstalledVersion if ($current) { Write-Status "Installed version: $current" if ((Normalize-Version $current) -eq (Normalize-Version $latest.version)) { Write-Status "$AppName is already up to date." return } Write-Status "Updating $current -> $($latest.version)" } Install-Binary $latest.version $latest.commit $arch Add-ToPath Write-Host "" Write-Host "Installation complete!" Write-Host "" Write-Host "$AppName --version" } Install-Main