#requires -version 5 <# study-server installer for Windows (no Docker required). Pulls the requested binaries straight out of the public GHCR "dist" image using only built-in cmdlets + tar.exe (bundled in Windows 10/11) — no Docker, crane, oras, or jq. Works because the package is public (anonymous registry token). irm https://study.bat.nz | iex # both binaries $env:STUDYSERVER_WHAT='studyctl'; irm https://study.bat.nz | iex # just the client Env overrides: STUDYSERVER_VERSION image tag to pull (default: latest) STUDYSERVER_BINDIR install directory (default: %LOCALAPPDATA%\Microsoft\WindowsApps, already on PATH) STUDYSERVER_WHAT studyctl|studyserver|both (default: both) Source of truth: AndrewSav/study-server, install/install.ps1. This is the deployed copy served by the `study` nginx container. #> $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # IWR is slow rendering progress in 5.1 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $registry = 'ghcr.io' $image = 'andrewsav/study-server-dist' $tag = if ($env:STUDYSERVER_VERSION) { $env:STUDYSERVER_VERSION } else { 'latest' } $dest = if ($env:STUDYSERVER_BINDIR) { $env:STUDYSERVER_BINDIR } else { Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps' } $what = if ($env:STUDYSERVER_WHAT) { $env:STUDYSERVER_WHAT } else { 'both' } if ($what -notin 'studyctl', 'studyserver', 'both') { throw "STUDYSERVER_WHAT must be studyctl, studyserver, or both (got '$what')" } Write-Host "Resolving $registry/${image}:$tag ..." $token = (Invoke-RestMethod "https://$registry/token?scope=repository:${image}:pull&service=$registry").token if (-not $token) { throw "failed to get anonymous pull token (is the package public?)" } # Single-layer scratch image: take its one layer's digest. $headers = @{ Authorization = "Bearer $token" Accept = 'application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' } $manifest = Invoke-RestMethod -Headers $headers "https://$registry/v2/${image}/manifests/$tag" $digest = $manifest.layers[-1].digest if (-not $digest) { throw "could not read image manifest for tag $tag" } $tmp = Join-Path ([IO.Path]::GetTempPath()) ("studyserver-" + [IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $tgz = Join-Path $tmp 'layer.tgz' Write-Host "Downloading binaries ..." Invoke-WebRequest -Headers @{ Authorization = "Bearer $token" } ` "https://$registry/v2/${image}/blobs/$digest" -OutFile $tgz tar -xf $tgz -C $tmp # bundled bsdtar auto-detects gzip if ($LASTEXITCODE -ne 0) { throw "tar extraction failed" } New-Item -ItemType Directory -Force -Path $dest | Out-Null function Install-One($name) { $src = Join-Path $tmp "$name-windows-amd64.exe" if (-not (Test-Path $src)) { throw "binary not found in image: $name-windows-amd64.exe" } $out = Join-Path $dest "$name.exe" Copy-Item -Force $src $out Write-Host "installed $out" } switch ($what) { 'studyctl' { Install-One studyctl } 'studyserver' { Install-One studyserver } 'both' { Install-One studyctl; Install-One studyserver } } } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } if (($env:Path -split ';') -notcontains $dest) { Write-Host "" Write-Host "Note: $dest is not on PATH; run studyctl by full path, or unset" Write-Host "STUDYSERVER_BINDIR to use the default (%LOCALAPPDATA%\Microsoft\WindowsApps, on PATH)." }