#!/bin/sh set -e REPO="nimbus-solution/nimbus" INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" BINARY="nimbus" # Anonymous install ping (see public.installs in supabase). Schema and # privacy posture documented in the matching migration. Opt out with # NIMBUS_NO_TRACKING=1, NIMBUS_TELEMETRY=false, or NIMBUS_NO_TELEMETRY=1. SUPABASE_URL="https://fmqylpjoqnfamdexitbu.supabase.co" SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtcXlscGpvcW5mYW1kZXhpdGJ1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUzNTc1NDQsImV4cCI6MjA5MDkzMzU0NH0.2Y2jKwvmWLsS17unE5JjOoKdbVe7LDzax0oQpD_sc_A" INSTALLER_VERSION="2026-09-20" NIMBUS_DIR="${HOME}/.nimbus" INSTALL_ID_FILE="${NIMBUS_DIR}/install.id" # Detect OS OS=$(uname -s | tr '[:upper:]' '[:lower:]') case "$OS" in linux|darwin) ;; *) echo "Unsupported OS: $OS" >&2; exit 1 ;; esac # Detect architecture ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH="amd64" ;; arm64|aarch64) ARCH="arm64" ;; *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac # Resolve version. Precedence: # 1. NIMBUS_VERSION env var (pin for CI / reproducible installs) # 2. Follow the releases/latest 302 redirect (no auth, no rate limit) # 3. Fall back to the GitHub API, optionally using GITHUB_TOKEN # (the anonymous API limit is 60/hr/IP and shared GHA runners hit it) VERSION="${NIMBUS_VERSION:-}" VERSION="${VERSION#v}" if [ -z "$VERSION" ]; then VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ "https://github.com/${REPO}/releases/latest" 2>/dev/null \ | sed -n 's|.*/tag/v\([^/?#]*\).*|\1|p') fi if [ -z "$VERSION" ]; then AUTH_HEADER="" if [ -n "${GITHUB_TOKEN:-}" ]; then AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}" fi VERSION=$(curl -fsSL ${AUTH_HEADER:+-H "$AUTH_HEADER"} \ "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' \ | sed 's/.*"tag_name": *"v\([^"]*\)".*/\1/') fi if [ -z "$VERSION" ]; then echo "error: could not determine latest version" >&2 echo "hint: set NIMBUS_VERSION=x.y.z to pin a release" >&2 exit 1 fi FILENAME="${BINARY}_${VERSION}_${OS}_${ARCH}.tar.gz" URL="https://github.com/${REPO}/releases/download/v${VERSION}/${FILENAME}" echo "Installing nimbus v${VERSION} (${OS}/${ARCH})..." # Download and extract to a temp dir TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT curl -fsSL "$URL" -o "$TMP/$FILENAME" # Verify the asset against the release's own checksums.txt, the same check the # CI action runs. This refuses an asset that was swapped in transit by anything # that could not also rewrite checksums.txt; it is not a signature. A short or # corrupted download already fails in tar below, so this is about authenticity # rather than truncation. CHECKSUMS_URL="https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt" if ! curl -fsSL "$CHECKSUMS_URL" -o "$TMP/checksums.txt"; then echo "error: could not download ${CHECKSUMS_URL}" >&2 echo "hint: the release is incomplete or the network path rewrote it; refusing to install unverified" >&2 exit 1 fi CHECKSUM_LINE=$(grep " ${FILENAME}\$" "$TMP/checksums.txt" || true) if [ -z "$CHECKSUM_LINE" ]; then echo "error: ${FILENAME} is not listed in the release checksums.txt — refusing to install" >&2 exit 1 fi if command -v sha256sum >/dev/null 2>&1; then (cd "$TMP" && printf '%s\n' "$CHECKSUM_LINE" | sha256sum -c - >/dev/null) || { echo "error: checksum mismatch for ${FILENAME} — refusing to install" >&2; exit 1; } elif command -v shasum >/dev/null 2>&1; then (cd "$TMP" && printf '%s\n' "$CHECKSUM_LINE" | shasum -a 256 -c - >/dev/null) || { echo "error: checksum mismatch for ${FILENAME} — refusing to install" >&2; exit 1; } else echo "error: neither sha256sum nor shasum is available to verify ${FILENAME} — refusing to install" >&2 exit 1 fi tar -xzf "$TMP/$FILENAME" -C "$TMP" # Ensure install dir exists; fall back to ~/.local/bin if not writable mkdir -p "$INSTALL_DIR" 2>/dev/null || true if [ ! -w "$INSTALL_DIR" ]; then INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" fi install -m 755 "$TMP/$BINARY" "$INSTALL_DIR/$BINARY" echo "nimbus v${VERSION} installed to $INSTALL_DIR/$BINARY" # --- make sure the terminal can find it --- # /usr/local/bin is on every PATH. A directory this script was aimed at is # usually not: ~/.local/bin when /usr/local/bin is not writable, or the # ~/.nimbus/bin an editor extension passes as INSTALL_DIR so it can verify # what landed there. A binary the terminal cannot find is the same as no # install, so the login shell's profile gets one guarded export line, once. # NIMBUS_NO_MODIFY_PATH=1 leaves every dotfile alone and prints the line # to add instead. # A path shown with ~ where it can be, so the message reads the way the # reader would type it. home_relative() { case "$1" in # shellcheck disable=SC2088 # a literal ~ is the point: this is display text "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac } case ":${PATH}:" in *":${INSTALL_DIR}:"*) ON_PATH=1 ;; *) ON_PATH=0 ;; esac case "${NIMBUS_NO_MODIFY_PATH:-}" in 1|true|TRUE|yes) MODIFY_PATH=0 ;; *) MODIFY_PATH=1 ;; esac if [ "$ON_PATH" = "0" ]; then # The entry as it should read in a dotfile: $HOME-relative when it can be, # so the line survives a home directory that moves. case "$INSTALL_DIR" in "$HOME"/*) PATH_ENTRY="\$HOME/${INSTALL_DIR#"$HOME"/}" ;; *) PATH_ENTRY="$INSTALL_DIR" ;; esac # One file per login shell, the one an interactive terminal reads. bash on # macOS opens login shells (Terminal, iTerm, the IDE terminal), which read # .bash_profile and not .bashrc; everywhere else bash reads .bashrc. case "$(basename "${SHELL:-sh}")" in zsh) PROFILE="${ZDOTDIR:-$HOME}/.zshrc" ;; bash) if [ "$OS" = "darwin" ]; then PROFILE="$HOME/.bash_profile"; else PROFILE="$HOME/.bashrc"; fi ;; fish) PROFILE="$HOME/.config/fish/config.fish" ;; *) PROFILE="$HOME/.profile" ;; esac if [ "$MODIFY_PATH" = "0" ]; then echo "" echo "$(home_relative "$INSTALL_DIR") is not on your PATH (NIMBUS_NO_MODIFY_PATH is set, so nothing was written). Add it with:" echo " export PATH=\"$PATH_ENTRY:\$PATH\"" elif grep -qsF -- "$PATH_ENTRY" "$PROFILE" || grep -qsF -- "$INSTALL_DIR" "$PROFILE"; then # Already there — ours from an earlier run, or the reader's own line. # Open terminals predate it either way. echo "$(home_relative "$PROFILE") already puts $(home_relative "$INSTALL_DIR") on PATH (open a new terminal to use the nimbus command)." else mkdir -p "$(dirname "$PROFILE")" 2>/dev/null || true case "$PROFILE" in *config.fish) LINE="set -gx PATH \"$PATH_ENTRY\" \$PATH" ;; *) LINE="export PATH=\"$PATH_ENTRY:\$PATH\"" ;; esac if { printf '\n# Added by the Nimbus installer (https://testnimbus.dev)\n%s\n' "$LINE" >> "$PROFILE"; } 2>/dev/null; then echo "Added $(home_relative "$INSTALL_DIR") to PATH in $(home_relative "$PROFILE") (open a new terminal to use the nimbus command)." else echo "" echo "Could not write $(home_relative "$PROFILE"). Add $(home_relative "$INSTALL_DIR") to your PATH with:" echo " $LINE" fi fi fi # --- what to run next --- # `set -e` above means a failed download, extract or install has already # exited, so these lines print only after the binary is in place. `nimbus # setup` assesses the project it is run in and does what is still missing. echo "" echo "Next:" echo " cd " echo " nimbus setup" # --- anonymous install ping --- # Runs after the binary is in place so a tracking failure can never block # the install. All errors are swallowed; the user never sees them. # Honor opt-outs (must mirror internal/telemetry/firstrun.go). TRACKING_DISABLED=0 [ "${NIMBUS_NO_TRACKING:-}" = "1" ] && TRACKING_DISABLED=1 [ "${NIMBUS_NO_TELEMETRY:-}" = "1" ] && TRACKING_DISABLED=1 [ "${NIMBUS_NO_TELEMETRY:-}" = "true" ] && TRACKING_DISABLED=1 case "${NIMBUS_TELEMETRY:-}" in false|0|off|FALSE|OFF) TRACKING_DISABLED=1 ;; esac if [ "$TRACKING_DISABLED" = "0" ]; then # KIND tells us "first install on this machine" vs "re-install / upgrade". # If install.id already exists, the user has run nimbus before (or run # this script before) — so this is an upgrade event regardless of # whether they're moving versions or just re-running the installer. if [ -f "$INSTALL_ID_FILE" ]; then KIND="upgrade" ANON_ID=$(tr -d '[:space:]' < "$INSTALL_ID_FILE" 2>/dev/null || true) else KIND="install" ANON_ID="" fi # Generate a fresh anon_id if needed. Try sources in order of how # reliably they exist across darwin and major linux distros. if [ -z "$ANON_ID" ]; then if [ -r /proc/sys/kernel/random/uuid ]; then ANON_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || true) elif command -v uuidgen >/dev/null 2>&1; then ANON_ID=$(uuidgen 2>/dev/null | tr 'A-Z' 'a-z' || true) fi fi # Persist so the binary's first-run telemetry can correlate this install # with the activation event it'll fire later. if [ -n "$ANON_ID" ]; then mkdir -p "$NIMBUS_DIR" 2>/dev/null || true printf '%s\n' "$ANON_ID" > "$INSTALL_ID_FILE" 2>/dev/null || true chmod 600 "$INSTALL_ID_FILE" 2>/dev/null || true BODY=$(printf '{"anon_id":"%s","kind":"%s","version":"%s","os":"%s","arch":"%s","installer_version":"%s"}' \ "$ANON_ID" "$KIND" "$VERSION" "$OS" "$ARCH" "$INSTALLER_VERSION") # 3-second timeout, no retries, output discarded. The install must # finish whether or not the ping succeeds. curl -fsS \ --max-time 3 \ -X POST \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $SUPABASE_ANON_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: return=minimal" \ -d "$BODY" \ "$SUPABASE_URL/rest/v1/installs" >/dev/null 2>&1 || true fi fi