#!/bin/sh # Pilothouse installer — https://pilothouse.dev # # curl -fsSL https://pilothouse.dev/install.sh | sh # # Downloads the latest Pilothouse release for your OS and CPU from GitHub, # verifies its SHA-256 checksum, and installs the `pilothouse` binary. # It never runs sudo. # # Environment: # PILOTHOUSE_VERSION version to install, e.g. 0.1.0 or v0.1.0 (default: latest) # PILOTHOUSE_INSTALL_DIR where to put the binary (default: /usr/local/bin if # writable, else ~/.local/bin) # PILOTHOUSE_API_BASE GitHub API base URL (default: https://api.github.com) # PILOTHOUSE_DOWNLOAD_BASE GitHub web base URL for the no-API fallback (default: https://github.com) # GITHUB_TOKEN optional, used for the GitHub API to avoid rate limits # # Source: https://github.com/xasdta/pilothouse (Apache-2.0) set -eu REPO="xasdta/pilothouse" API_BASE="${PILOTHOUSE_API_BASE:-https://api.github.com}" GITHUB_BASE="${PILOTHOUSE_DOWNLOAD_BASE:-https://github.com}" if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then BOLD=$(printf '\033[1m') DIM=$(printf '\033[2m') RED=$(printf '\033[31m') GREEN=$(printf '\033[32m') YELLOW=$(printf '\033[33m') RESET=$(printf '\033[0m') else BOLD="" DIM="" RED="" GREEN="" YELLOW="" RESET="" fi info() { printf '%s\n' "${DIM}pilothouse:${RESET} $*"; } warn() { printf '%s\n' "${YELLOW}pilothouse: $*${RESET}" >&2; } fail() { printf '%s\n' "${RED}pilothouse: error: $*${RESET}" >&2 exit 1 } have() { command -v "$1" >/dev/null 2>&1; } # fetch URL [OUTFILE] — prints to stdout when OUTFILE is omitted. fetch() { _url=$1 _out=${2:-} _auth="" case $_url in "$API_BASE"/*) [ -n "${GITHUB_TOKEN:-}" ] && _auth="Authorization: Bearer $GITHUB_TOKEN" ;; esac if have curl; then if [ -n "$_auth" ]; then set -- -fsSL --proto '=https,http' --retry 3 --connect-timeout 15 -H "$_auth" "$_url" else set -- -fsSL --proto '=https,http' --retry 3 --connect-timeout 15 "$_url" fi if [ -n "$_out" ]; then curl "$@" -o "$_out"; else curl "$@"; fi elif have wget; then if [ -n "$_auth" ]; then set -- -q --tries=3 --timeout=15 --header="$_auth" "$_url" else set -- -q --tries=3 --timeout=15 "$_url" fi if [ -n "$_out" ]; then wget "$@" -O "$_out"; else wget "$@" -O -; fi else fail "need curl or wget to download Pilothouse" fi } # resolve_redirect URL — prints the final URL after redirects (used when the API is unavailable). resolve_redirect() { if have curl; then curl -fsSLI -o /dev/null -w '%{url_effective}' "$1" else wget -q --max-redirect=5 --server-response --spider "$1" 2>&1 | awk '/^ Location: /{loc=$2} END{print loc}' fi } sha256_of() { if have sha256sum; then sha256sum "$1" | awk '{print $1}' elif have shasum; then shasum -a 256 "$1" | awk '{print $1}' elif have openssl; then openssl dgst -sha256 "$1" | awk '{print $NF}' else fail "need sha256sum, shasum or openssl to verify the download" fi } detect_os() { case $(uname -s) in Darwin) echo darwin ;; Linux) echo linux ;; MINGW* | MSYS* | CYGWIN* | Windows_NT) fail "Windows isn't supported yet — see https://pilothouse.dev/docs/faq#roadmap" ;; *) fail "unsupported operating system: $(uname -s) (Pilothouse supports macOS and Linux)" ;; esac } detect_arch() { _m=$(uname -m) case $_m in x86_64 | amd64) # An x86_64 shell on Apple Silicon (Rosetta) should still get the native build. if [ "$(uname -s)" = Darwin ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then echo arm64 else echo amd64 fi ;; arm64 | aarch64 | armv8*) echo arm64 ;; *) fail "unsupported CPU architecture: $_m (Pilothouse supports amd64 and arm64)" ;; esac } # json_string KEY — first "KEY": "value" from stdin (good enough for GitHub's release JSON). json_string() { sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n 1 } choose_install_dir() { if [ -n "${PILOTHOUSE_INSTALL_DIR:-}" ]; then mkdir -p "$PILOTHOUSE_INSTALL_DIR" 2>/dev/null || fail "can't create $PILOTHOUSE_INSTALL_DIR" [ -w "$PILOTHOUSE_INSTALL_DIR" ] || fail "$PILOTHOUSE_INSTALL_DIR isn't writable. Pick another PILOTHOUSE_INSTALL_DIR, or run the installer as a user who can write there." echo "$PILOTHOUSE_INSTALL_DIR" elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then echo /usr/local/bin else [ -n "${HOME:-}" ] || fail "HOME is not set; set PILOTHOUSE_INSTALL_DIR" mkdir -p "$HOME/.local/bin" || fail "can't create $HOME/.local/bin" echo "$HOME/.local/bin" fi } main() { os=$(detect_os) arch=$(detect_arch) version=${PILOTHOUSE_VERSION:-} version=${version#v} archive_url="" checksums_url="" # 1. Find the release. if [ -n "$version" ]; then release_api="$API_BASE/repos/$REPO/releases/tags/v$version" else release_api="$API_BASE/repos/$REPO/releases/latest" fi info "looking up ${version:+v$version}${version:-the latest release} for $os/$arch" if release_json=$(fetch "$release_api" 2>/dev/null); then tag=$(printf '%s\n' "$release_json" | json_string tag_name) [ -n "$tag" ] || fail "couldn't read the release tag from $release_api" version=${tag#v} archive="pilothouse_${version}_${os}_${arch}.tar.gz" archive_url=$(printf '%s\n' "$release_json" | grep -o "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*/$archive\"" | sed 's/.*"\(http[^"]*\)"$/\1/' | head -n 1) checksums_url=$(printf '%s\n' "$release_json" | grep -o "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*/checksums.txt\"" | sed 's/.*"\(http[^"]*\)"$/\1/' | head -n 1) [ -n "$archive_url" ] || fail "release $tag has no build for $os/$arch ($archive)" [ -n "$checksums_url" ] || fail "release $tag has no checksums.txt" else # GitHub API unavailable (rate limit, proxy): fall back to the plain release URLs. warn "GitHub API unavailable, falling back to release URLs" if [ -z "$version" ]; then latest=$(resolve_redirect "$GITHUB_BASE/$REPO/releases/latest") || latest="" tag=${latest##*/} case $tag in v[0-9]*) version=${tag#v} ;; *) fail "couldn't find the latest release. Set PILOTHOUSE_VERSION, or try again later." ;; esac fi tag="v$version" archive="pilothouse_${version}_${os}_${arch}.tar.gz" archive_url="$GITHUB_BASE/$REPO/releases/download/$tag/$archive" checksums_url="$GITHUB_BASE/$REPO/releases/download/$tag/checksums.txt" fi # 2. Download into a private temp dir. tmp=$(mktemp -d 2>/dev/null || mktemp -d -t pilothouse) trap 'rm -rf "$tmp"' EXIT INT TERM HUP info "downloading $archive" fetch "$archive_url" "$tmp/$archive" || fail "download failed: $archive_url" fetch "$checksums_url" "$tmp/checksums.txt" || fail "download failed: $checksums_url" # 3. Verify the checksum before touching anything else. expected=$(awk -v f="$archive" '$2 == f || $2 == "*" f {print $1; exit}' "$tmp/checksums.txt") [ -n "$expected" ] || fail "$archive is not listed in checksums.txt" actual=$(sha256_of "$tmp/$archive") if [ "$expected" != "$actual" ]; then fail "checksum mismatch for $archive expected $expected got $actual Nothing was installed." fi info "checksum ${GREEN}ok${RESET} (sha256 $actual)" # 4. Unpack and install. mkdir "$tmp/x" tar -xzf "$tmp/$archive" -C "$tmp/x" || fail "couldn't unpack $archive" bin="$tmp/x/pilothouse" [ -f "$bin" ] || bin=$(find "$tmp/x" -type f -name pilothouse | head -n 1) [ -n "$bin" ] && [ -f "$bin" ] || fail "no pilothouse binary inside $archive" dir=$(choose_install_dir) dest="$dir/pilothouse" cp "$bin" "$dir/.pilothouse.tmp.$$" || fail "couldn't write to $dir" chmod 0755 "$dir/.pilothouse.tmp.$$" mv -f "$dir/.pilothouse.tmp.$$" "$dest" || fail "couldn't install to $dest" printf '\n%s\n' "${GREEN}✓${RESET} ${BOLD}pilothouse $version${RESET} installed to $dest" case ":${PATH:-}:" in *":$dir:"*) ;; *) printf '\n%s\n' "${YELLOW}$dir is not on your PATH.${RESET} Add it, e.g.:" printf ' %s\n' "echo 'export PATH=\"$dir:\$PATH\"' >> ~/.profile # or ~/.zshrc, ~/.bashrc" ;; esac if [ "$dir" != /usr/local/bin ] && [ -z "${PILOTHOUSE_INSTALL_DIR:-}" ]; then printf '%s\n' "${DIM}(/usr/local/bin wasn't writable, so nothing was installed system-wide and sudo was not used.)${RESET}" fi printf '\n%s\n' "Next, run: ${BOLD}pilothouse setup${RESET}" printf '%s\n' "${DIM}Docs: https://pilothouse.dev/docs${RESET}" } main "$@"