From 65c04c0f812faf93e20306adf7f9f13d744c34c3 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Sat, 18 Jul 2026 19:24:56 +0200 Subject: [PATCH 1/3] feat(install): add install.sh and install.ps1 one-line installers Both scripts resolve the latest GitHub release, download the platform archive, verify its SHA-256 against checksums.txt, and install the binary (/usr/local/bin or ~/.local/bin on Unix; %LOCALAPPDATA%\Programs on Windows, with user-PATH registration). Version and install dir are overridable via GOMODEL_VERSION / GOMODEL_INSTALL_DIR. The scripts send no telemetry. Co-Authored-By: Claude Fable 5 --- install/install.ps1 | 74 ++++++++++++++++++++++++++++++++ install/install.sh | 100 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 install/install.ps1 create mode 100644 install/install.sh diff --git a/install/install.ps1 b/install/install.ps1 new file mode 100644 index 00000000..773716ba --- /dev/null +++ b/install/install.ps1 @@ -0,0 +1,74 @@ +# GoModel installer for Windows. +# +# irm https://gomodel.enterpilot.io/install.ps1 | iex +# +# Downloads the latest release from GitHub, verifies its SHA-256 checksum, +# and installs gomodel.exe to %LOCALAPPDATA%\Programs\gomodel (added to the +# user PATH when missing). No telemetry is sent by this script. +# +# Overrides (set before running): +# $env:GOMODEL_VERSION install a specific version (e.g. v0.1.50); default: latest +# $env:GOMODEL_INSTALL_DIR install directory; default: %LOCALAPPDATA%\Programs\gomodel + +$ErrorActionPreference = 'Stop' + +$Repo = 'ENTERPILOT/GoModel' +$Binary = 'gomodel' + +$arch = switch ($env:PROCESSOR_ARCHITECTURE) { + 'AMD64' { 'amd64' } + 'ARM64' { 'arm64' } + default { throw "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" } +} + +$tag = $env:GOMODEL_VERSION +if (-not $tag) { + $tag = (Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest").tag_name +} +if ($tag -notmatch '^v') { throw "unexpected release tag: $tag" } +$version = $tag.TrimStart('v') + +$archive = "${Binary}_${version}_windows_${arch}.zip" +$baseUrl = "https://github.com/$Repo/releases/download/$tag" + +$tmpDir = Join-Path ([IO.Path]::GetTempPath()) "gomodel-install-$([IO.Path]::GetRandomFileName())" +New-Item -ItemType Directory -Path $tmpDir | Out-Null +try { + Write-Host "Downloading $Binary $tag (windows/$arch)..." + Invoke-WebRequest -Uri "$baseUrl/$archive" -OutFile (Join-Path $tmpDir $archive) + Invoke-WebRequest -Uri "$baseUrl/checksums.txt" -OutFile (Join-Path $tmpDir 'checksums.txt') + + $expected = (Get-Content (Join-Path $tmpDir 'checksums.txt') | + Where-Object { $_ -match [regex]::Escape($archive) } | + ForEach-Object { ($_ -split '\s+')[0] }) | Select-Object -First 1 + if (-not $expected) { throw "no checksum for $archive in checksums.txt" } + $actual = (Get-FileHash (Join-Path $tmpDir $archive) -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected.ToLower()) { throw "checksum mismatch for $archive" } + Write-Host 'Checksum verified.' + + Expand-Archive -Path (Join-Path $tmpDir $archive) -DestinationPath $tmpDir -Force + + $installDir = $env:GOMODEL_INSTALL_DIR + if (-not $installDir) { $installDir = Join-Path $env:LOCALAPPDATA 'Programs\gomodel' } + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + Copy-Item (Join-Path $tmpDir "$Binary.exe") (Join-Path $installDir "$Binary.exe") -Force + + Write-Host '' + Write-Host "Installed $Binary $tag to $installDir\$Binary.exe" + + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (($userPath -split ';') -notcontains $installDir) { + [Environment]::SetEnvironmentVariable('Path', "$userPath;$installDir", 'User') + Write-Host "Added $installDir to your user PATH — restart your terminal to pick it up." + } + + Write-Host '' + Write-Host 'Get started:' + Write-Host ' $env:OPENAI_API_KEY = "sk-..." # or any other provider key' + Write-Host " $Binary" + Write-Host '' + Write-Host 'Docs: https://gomodel.enterpilot.io' +} +finally { + Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue +} diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 00000000..251beb4c --- /dev/null +++ b/install/install.sh @@ -0,0 +1,100 @@ +#!/bin/sh +# GoModel installer for macOS and Linux. +# +# curl -fsSL https://gomodel.enterpilot.io/install.sh | sh +# +# Downloads the latest release binary from GitHub, verifies its SHA-256 +# checksum, and installs it to /usr/local/bin (or ~/.local/bin when +# /usr/local/bin is not writable). No telemetry is sent by this script. +# +# Overrides: +# GOMODEL_VERSION install a specific version (e.g. v0.1.50); default: latest +# GOMODEL_INSTALL_DIR install directory; default: /usr/local/bin or ~/.local/bin + +set -eu + +REPO="ENTERPILOT/GoModel" +BINARY="gomodel" + +say() { printf '%s\n' "$*"; } +fail() { printf 'error: %s\n' "$*" >&2; exit 1; } + +command -v curl >/dev/null 2>&1 || fail "curl is required" +command -v tar >/dev/null 2>&1 || fail "tar is required" + +case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + *) fail "unsupported OS: $(uname -s) (Windows: use install.ps1)" ;; +esac + +case "$(uname -m)" in + x86_64 | amd64) arch="amd64" ;; + arm64 | aarch64) arch="arm64" ;; + *) fail "unsupported architecture: $(uname -m)" ;; +esac + +# Resolve the version from the /releases/latest redirect, avoiding the +# GitHub API and its per-IP rate limit. +tag="${GOMODEL_VERSION:-}" +if [ -z "$tag" ]; then + location=$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/$REPO/releases/latest") || + fail "could not resolve the latest release" + tag="${location##*/}" +fi +case "$tag" in + v*) ;; + *) fail "unexpected release tag: $tag" ;; +esac +version="${tag#v}" + +archive="${BINARY}_${version}_${os}_${arch}.tar.gz" +base_url="https://github.com/$REPO/releases/download/$tag" + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT INT TERM + +say "Downloading $BINARY $tag ($os/$arch)..." +curl -fsSL -o "$tmpdir/$archive" "$base_url/$archive" || fail "download failed: $base_url/$archive" +curl -fsSL -o "$tmpdir/checksums.txt" "$base_url/checksums.txt" || fail "download failed: checksums.txt" + +expected=$(awk -v f="$archive" '$2 == f { print $1 }' "$tmpdir/checksums.txt") +[ -n "$expected" ] || fail "no checksum for $archive in checksums.txt" +if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$tmpdir/$archive" | awk '{ print $1 }') +else + actual=$(shasum -a 256 "$tmpdir/$archive" | awk '{ print $1 }') +fi +[ "$actual" = "$expected" ] || fail "checksum mismatch for $archive" +say "Checksum verified." + +tar -xzf "$tmpdir/$archive" -C "$tmpdir" "$BINARY" + +install_dir="${GOMODEL_INSTALL_DIR:-}" +if [ -z "$install_dir" ]; then + if [ -w /usr/local/bin ]; then + install_dir="/usr/local/bin" + else + install_dir="$HOME/.local/bin" + fi +fi +mkdir -p "$install_dir" || fail "cannot create $install_dir" +[ -w "$install_dir" ] || fail "$install_dir is not writable — rerun with GOMODEL_INSTALL_DIR set, or run: sudo install -m 755 $tmpdir/$BINARY /usr/local/bin/" +install -m 755 "$tmpdir/$BINARY" "$install_dir/$BINARY" + +say "" +say "Installed $BINARY $tag to $install_dir/$BINARY" +case ":$PATH:" in + *":$install_dir:"*) ;; + *) + say "" + say "Note: $install_dir is not on your PATH. Add it with:" + say " export PATH=\"$install_dir:\$PATH\"" + ;; +esac +say "" +say "Get started:" +say " export OPENAI_API_KEY=sk-... # or any other provider key" +say " $BINARY" +say "" +say "Docs: https://gomodel.enterpilot.io" From de026bd6ccbb356c082f5ed28b7e1a1eed363264 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Sat, 18 Jul 2026 20:05:35 +0200 Subject: [PATCH 2/3] refactor(install): move installer scripts under docs/ to keep the root lean Co-Authored-By: Claude Fable 5 --- {install => docs/install}/install.ps1 | 0 {install => docs/install}/install.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {install => docs/install}/install.ps1 (100%) rename {install => docs/install}/install.sh (100%) diff --git a/install/install.ps1 b/docs/install/install.ps1 similarity index 100% rename from install/install.ps1 rename to docs/install/install.ps1 diff --git a/install/install.sh b/docs/install/install.sh similarity index 100% rename from install/install.sh rename to docs/install/install.sh From f6f6615bee2ee39640662d7a975866ce79e22915 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Sun, 19 Jul 2026 14:27:11 +0200 Subject: [PATCH 3/3] fix(install): address review findings in installer scripts install.sh: fail clearly when no SHA-256 tool exists instead of reporting a bogus checksum mismatch; drop the unusable sudo hint that referenced the trap-deleted temp directory. install.ps1: resolve the latest tag via the releases/latest redirect instead of the rate-limited API; prefer PROCESSOR_ARCHITEW6432 so 32-bit PowerShell on 64-bit Windows picks the right archive; exact-field checksum matching; no leading semicolon when the user PATH is empty; pure-ASCII output. Co-Authored-By: Claude Fable 5 --- docs/install/install.ps1 | 34 ++++++++++++++++++++++++++-------- docs/install/install.sh | 6 ++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/install/install.ps1 b/docs/install/install.ps1 index 773716ba..3b44f906 100644 --- a/docs/install/install.ps1 +++ b/docs/install/install.ps1 @@ -15,15 +15,30 @@ $ErrorActionPreference = 'Stop' $Repo = 'ENTERPILOT/GoModel' $Binary = 'gomodel' -$arch = switch ($env:PROCESSOR_ARCHITECTURE) { +# PROCESSOR_ARCHITEW6432 reports the real machine architecture when running +# in a 32-bit PowerShell on a 64-bit Windows. +$rawArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +$arch = switch ($rawArch) { 'AMD64' { 'amd64' } 'ARM64' { 'arm64' } - default { throw "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" } + default { throw "unsupported architecture: $rawArch" } } $tag = $env:GOMODEL_VERSION if (-not $tag) { - $tag = (Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest").tag_name + # Resolve the latest tag from the releases/latest redirect, avoiding the + # GitHub API and its per-IP rate limit (same approach as install.sh). + $req = [System.Net.WebRequest]::Create("https://github.com/$Repo/releases/latest") + $req.AllowAutoRedirect = $false + $res = $req.GetResponse() + try { + $location = $res.Headers['Location'] + } + finally { + $res.Close() + } + if (-not $location) { throw 'could not resolve the latest release' } + $tag = ($location -split '/')[-1] } if ($tag -notmatch '^v') { throw "unexpected release tag: $tag" } $version = $tag.TrimStart('v') @@ -38,9 +53,11 @@ try { Invoke-WebRequest -Uri "$baseUrl/$archive" -OutFile (Join-Path $tmpDir $archive) Invoke-WebRequest -Uri "$baseUrl/checksums.txt" -OutFile (Join-Path $tmpDir 'checksums.txt') - $expected = (Get-Content (Join-Path $tmpDir 'checksums.txt') | - Where-Object { $_ -match [regex]::Escape($archive) } | - ForEach-Object { ($_ -split '\s+')[0] }) | Select-Object -First 1 + $expected = $null + foreach ($line in Get-Content (Join-Path $tmpDir 'checksums.txt')) { + $parts = $line.Trim() -split '\s+' + if ($parts.Length -ge 2 -and $parts[1] -eq $archive) { $expected = $parts[0]; break } + } if (-not $expected) { throw "no checksum for $archive in checksums.txt" } $actual = (Get-FileHash (Join-Path $tmpDir $archive) -Algorithm SHA256).Hash.ToLower() if ($actual -ne $expected.ToLower()) { throw "checksum mismatch for $archive" } @@ -58,8 +75,9 @@ try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (($userPath -split ';') -notcontains $installDir) { - [Environment]::SetEnvironmentVariable('Path', "$userPath;$installDir", 'User') - Write-Host "Added $installDir to your user PATH — restart your terminal to pick it up." + $newPath = if ([string]::IsNullOrEmpty($userPath)) { $installDir } else { "$userPath;$installDir" } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + Write-Host "Added $installDir to your user PATH - restart your terminal to pick it up." } Write-Host '' diff --git a/docs/install/install.sh b/docs/install/install.sh index 251beb4c..c48258f5 100644 --- a/docs/install/install.sh +++ b/docs/install/install.sh @@ -62,8 +62,10 @@ expected=$(awk -v f="$archive" '$2 == f { print $1 }' "$tmpdir/checksums.txt") [ -n "$expected" ] || fail "no checksum for $archive in checksums.txt" if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$tmpdir/$archive" | awk '{ print $1 }') -else +elif command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$tmpdir/$archive" | awk '{ print $1 }') +else + fail "sha256sum or shasum is required to verify the download" fi [ "$actual" = "$expected" ] || fail "checksum mismatch for $archive" say "Checksum verified." @@ -79,7 +81,7 @@ if [ -z "$install_dir" ]; then fi fi mkdir -p "$install_dir" || fail "cannot create $install_dir" -[ -w "$install_dir" ] || fail "$install_dir is not writable — rerun with GOMODEL_INSTALL_DIR set, or run: sudo install -m 755 $tmpdir/$BINARY /usr/local/bin/" +[ -w "$install_dir" ] || fail "$install_dir is not writable — set GOMODEL_INSTALL_DIR to a writable directory, or rerun with sudo" install -m 755 "$tmpdir/$BINARY" "$install_dir/$BINARY" say ""