#!/bin/sh # # JetScale installer / updater for Linux and macOS. # # Usage: # curl -fsSL https://jetscale.sh/install.sh | sh # # What it does: # 1. Detects your OS and CPU architecture. # 2. Asks the server for the latest released 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.linux.amd64.2026-06-28-abc1234.abc1234 # # The script is intentionally split into small functions so it is easy to # read and maintain. Execution starts at the bottom in main(). set -eu ############################################################################### # Configuration ############################################################################### # Binary name and where releases are hosted. APP_NAME="jetscale" DOWNLOAD_BASE="https://jetscale.sh" # Endpoint that returns JSON describing the latest release, e.g. # { "version": "2026-06-28-abc1234", "commit": "abc1234" } LATEST_JSON="$DOWNLOAD_BASE/latest.json" ############################################################################### # Output helpers ############################################################################### # say prints a green "==>" status line. say() { printf "\033[1;32m==>\033[0m %s\n" "$*" } # warn prints a yellow warning but does not stop the script. warn() { printf "\033[1;33mWarning:\033[0m %s\n" "$*" >&2 } # err prints a red error message and exits. err() { printf "\033[1;31mError:\033[0m %s\n" "$*" >&2 exit 1 } ############################################################################### # Small utilities ############################################################################### # command_exists reports whether a command is available on the PATH. command_exists() { command -v "$1" >/dev/null 2>&1 } # download fetches a URL into a file using curl or wget, whichever exists. download() { url="$1" output="$2" if command_exists curl; then curl -fsSL "$url" -o "$output" elif command_exists wget; then wget -qO "$output" "$url" else err "Neither curl nor wget is installed." fi } # json_field extracts a string field from a small, flat JSON document. # Usage: json_field json_field() { file="$1" key="$2" grep "\"$key\"" "$file" | sed -E "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/" | head -n1 } # normalize_version strips a leading "v" and surrounding whitespace so that # "v1.2.3" and "1.2.3" compare as equal. normalize_version() { printf "%s" "$1" | sed -E 's/^[[:space:]]*v?//; s/[[:space:]]*$//' } ############################################################################### # Platform detection ############################################################################### # detect_os maps `uname -s` onto the OS token used in release file names. detect_os() { os=$(uname -s) case "$os" in Darwin) printf "darwin" ;; Linux) printf "linux" ;; *) err "Unsupported operating system: $os" ;; esac } # detect_arch maps `uname -m` onto the architecture token used in release # file names. detect_arch() { arch=$(uname -m) case "$arch" in x86_64 | amd64) printf "amd64" ;; arm64 | aarch64) printf "arm64" ;; armv7l) printf "armv7" ;; *) err "Unsupported architecture: $arch" ;; esac } ############################################################################### # Version discovery ############################################################################### # installed_version prints the version of the jetscale binary already on the # PATH, or nothing if it is not installed. The CLI prints its raw version # string (see `jetscale --version`); we take the last token of the first line # so it also works if the output is prefixed (e.g. "jetscale version X"). installed_version() { command_exists "$APP_NAME" || return 0 raw=$("$APP_NAME" --version 2>/dev/null || "$APP_NAME" version 2>/dev/null || true) printf "%s" "$raw" | head -n1 | awk '{print $NF}' } ############################################################################### # Install location ############################################################################### # resolve_install_dir decides where the binary should go. If jetscale is # already installed we reuse its directory so updates land in place; # otherwise we prefer /usr/local/bin and fall back to ~/.local/bin. resolve_install_dir() { if command_exists "$APP_NAME"; then existing=$(command -v "$APP_NAME") dirname "$existing" return 0 fi if [ -w "/usr/local/bin" ] || command_exists sudo; then printf "/usr/local/bin" else mkdir -p "$HOME/.local/bin" printf "%s/.local/bin" "$HOME" fi } ############################################################################### # Download + install ############################################################################### # download_binary fetches the raw release binary into $tmp/$APP_NAME. # Artifacts are single executables named jetscale.... # (no archive), so there is nothing to extract. download_binary() { version="$1" commit="$2" os="$3" arch="$4" tmp="$5" file="${APP_NAME}.${os}.${arch}.${version}.${commit}" url="$DOWNLOAD_BASE/$file" say "Downloading $file..." download "$url" "$tmp/$APP_NAME" chmod +x "$tmp/$APP_NAME" } # install_binary moves the downloaded binary into the install directory, # using sudo when the destination is not writable. install_binary() { tmp="$1" install_dir="$2" say "Installing to $install_dir" if [ -w "$install_dir" ]; then mv "$tmp/$APP_NAME" "$install_dir/" elif command_exists sudo; then sudo mv "$tmp/$APP_NAME" "$install_dir/" else err "No write permission for $install_dir and sudo is unavailable." fi } # ensure_on_path tells the user how to add the install dir to PATH when it is # not already there. ensure_on_path() { install_dir="$1" case ":$PATH:" in *":$install_dir:"*) ;; *) printf "\n" say "Add this to your shell profile:" printf "\n" printf 'export PATH="%s:$PATH"\n' "$install_dir" printf "\n" ;; esac } ############################################################################### # Entry point ############################################################################### main() { os=$(detect_os) arch=$(detect_arch) # Everything downloaded goes into a temp dir that is cleaned up on exit. tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT say "Fetching latest release..." download "$LATEST_JSON" "$tmp/latest.json" latest=$(json_field "$tmp/latest.json" version) commit=$(json_field "$tmp/latest.json" commit) [ -n "$latest" ] || err "Unable to determine the latest version from $LATEST_JSON." [ -n "$commit" ] || err "Unable to determine the latest commit from $LATEST_JSON." say "Latest version: $latest" # Skip the download entirely if the installed version already matches. current=$(installed_version) if [ -n "$current" ]; then say "Installed version: $current" if [ "$(normalize_version "$current")" = "$(normalize_version "$latest")" ]; then say "$APP_NAME is already up to date." exit 0 fi say "Updating $current -> $latest" fi install_dir=$(resolve_install_dir) download_binary "$latest" "$commit" "$os" "$arch" "$tmp" install_binary "$tmp" "$install_dir" ensure_on_path "$install_dir" printf "\n" say "Installation complete!" printf "\n" printf "%s --version\n" "$APP_NAME" } main "$@"