#!/usr/bin/env sh # Usage: curl -fsSL https://getcalmo.com/get | sh # Pin a version: VERSION=0.1.1 curl -fsSL https://getcalmo.com/get | sh set -e # Injected by the server based on the request host so the script always # calls the API on the same origin it was downloaded from (dev or prod). CALMO_API_BASE="https://getcalmo.com" # Injected from the shared release config so fallback behavior stays in sync. CALMO_BRIDGE_FALLBACK_VERSION="0.1.1" # Set to "true" when served from localhost; enables OS/arch override menu. CALMO_DEV_MODE="false" # ── Colors (defined first so every helper can use them) ─────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' info() { printf "${GREEN}==>${NC} %s\n" "$*"; } warn() { printf "${YELLOW}==> Warning:${NC} %s\n" "$*"; } error() { printf "${RED}==> Error:${NC} %s\n" "$*"; exit 1; } # Read a line from the terminal even when stdin is the curl pipe read_tty() { if [ -t 0 ]; then read -r _RTY_REPLY elif [ -e /dev/tty ]; then read -r _RTY_REPLY < /dev/tty else _RTY_REPLY="" fi printf '%s' "$_RTY_REPLY" } # ── PostHog analytics (best-effort; never breaks install) ───────────────────── PH_API_KEY="phc_2P5DdCKkYPdDkm6g8F6n5YO503AFRY9vUhYsfsuR3pb" PH_HOST="https://eu.i.posthog.com" PH_HAS_SUCCEEDED="0" PH_STEP="init" gen_distinct_id() { if command -v uuidgen >/dev/null 2>&1; then uuidgen; return; fi if [ -r /proc/sys/kernel/random/uuid ]; then cat /proc/sys/kernel/random/uuid; return; fi echo "$(date +%s)-$$-${RANDOM:-0}" } PH_DISTINCT_ID="$(gen_distinct_id)" posthog_capture() { EVENT_NAME="$1"; PROPS_INNER="$2" PAYLOAD="{\"api_key\":\"$PH_API_KEY\",\"event\":\"$EVENT_NAME\",\"distinct_id\":\"$PH_DISTINCT_ID\",\"properties\":{$PROPS_INNER}}" curl -sS --connect-timeout 2 --max-time 3 \ -H "Content-Type: application/json" -X POST -d "$PAYLOAD" \ "$PH_HOST/capture/" >/dev/null 2>&1 || true } on_exit() { EXIT_CODE="$?" # In dev mode, preserve the temp dir on failure so you can inspect downloaded files. if [ "$EXIT_CODE" -ne 0 ] && [ "${CALMO_DEV_MODE:-false}" = "true" ] \ && [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then printf "\n${YELLOW}==> Dev mode: temp dir preserved for inspection:${NC}\n %s\n" \ "$TMP_DIR" >/dev/tty 2>/dev/null || true printf "${YELLOW}==> Remove manually with:${NC} rm -rf \"%s\"\n\n" \ "$TMP_DIR" >/dev/tty 2>/dev/null || true else [ -n "${TMP_DIR:-}" ] && rm -rf "$TMP_DIR" fi if [ "$EXIT_CODE" -ne 0 ] && [ "$PH_HAS_SUCCEEDED" != "1" ]; then posthog_capture "bridge_install_failed" "\"source\":\"get\",\"step\":\"$PH_STEP\",\"exit_code\":$EXIT_CODE" fi } trap on_exit EXIT # ── Prerequisites ───────────────────────────────────────────────────────────── command -v curl >/dev/null 2>&1 || error "curl is required but not installed." # ── OS and Arch detection ───────────────────────────────────────────────────── OS="$(uname -s)" case "$OS" in Darwin) PLATFORM="darwin" ;; Linux) PLATFORM="linux" ;; *) error "Unsupported OS: $OS" ;; esac case "$(uname -m)" in x86_64|amd64) ARCH="x64" ;; arm64|aarch64) ARCH="arm64" ;; *) error "Unsupported architecture: $(uname -m)" ;; esac # Dev mode: show an interactive platform/arch menu so you can test any # combination without leaving your machine. if [ "$CALMO_DEV_MODE" = "true" ]; then case "${PLATFORM}-${ARCH}" in linux-x64) AUTO_CHOICE=1 ;; linux-arm64) AUTO_CHOICE=2 ;; darwin-x64) AUTO_CHOICE=3 ;; darwin-arm64) AUTO_CHOICE=4 ;; *) AUTO_CHOICE=1 ;; esac printf "\n${BOLD}Dev mode — select target platform:${NC}\n" printf " ${CYAN}1)${NC} linux x64\n" printf " ${CYAN}2)${NC} linux arm64\n" printf " ${CYAN}3)${NC} darwin x64\n" printf " ${CYAN}4)${NC} darwin arm64\n" printf "\n${BOLD}Select platform${NC} [${AUTO_CHOICE} — auto-detected: ${PLATFORM}-${ARCH}]: " DEV_CHOICE=$(read_tty) DEV_CHOICE="${DEV_CHOICE:-$AUTO_CHOICE}" case "$DEV_CHOICE" in 1) PLATFORM="linux"; ARCH="x64" ;; 2) PLATFORM="linux"; ARCH="arm64" ;; 3) PLATFORM="darwin"; ARCH="x64" ;; 4) PLATFORM="darwin"; ARCH="arm64" ;; *) warn "Invalid selection, keeping auto-detected: ${PLATFORM}-${ARCH}" ;; esac info "Target platform: ${PLATFORM}-${ARCH}" fi # ── Temp dir (used for version list file and download) ──────────────────────── TMP_DIR="$(mktemp -d)" VERSIONS_FILE="$TMP_DIR/versions.txt" # ── Fetch available versions from API ───────────────────────────────────────── PH_STEP="fetch_versions" printf "\n" info "Fetching available versions..." API_JSON=$(curl -fsSL --connect-timeout 5 --max-time 10 \ "${CALMO_API_BASE}/api/bridge-versions" 2>/dev/null || true) LATEST="" if [ -n "$API_JSON" ]; then if command -v jq >/dev/null 2>&1; then VERSION_STRINGS=$(printf '%s' "$API_JSON" | jq -r '.versionStrings[]? // empty' 2>/dev/null || true) if [ -n "$VERSION_STRINGS" ]; then printf '%s\n' "$VERSION_STRINGS" > "$VERSIONS_FILE" fi LATEST=$(printf '%s' "$API_JSON" | jq -r '.latest // empty' 2>/dev/null || true) fi if [ ! -s "$VERSIONS_FILE" ] && command -v python3 >/dev/null 2>&1; then VERSION_STRINGS=$(printf '%s' "$API_JSON" | python3 -c ' import json import sys data = json.load(sys.stdin) for version_string in data.get("versionStrings", []) or []: if isinstance(version_string, str) and version_string: print(version_string) ' 2>/dev/null || true) if [ -n "$VERSION_STRINGS" ]; then printf '%s\n' "$VERSION_STRINGS" > "$VERSIONS_FILE" fi fi if [ -z "$LATEST" ] && command -v python3 >/dev/null 2>&1; then LATEST=$(printf '%s' "$API_JSON" | python3 -c ' import json import sys data = json.load(sys.stdin) latest = data.get("latest", "") if isinstance(latest, str) and latest: print(latest) ' 2>/dev/null || true) fi if [ ! -s "$VERSIONS_FILE" ]; then VERSION_STRINGS=$(printf '%s' "$API_JSON" | sed -n '/"versionStrings"[[:space:]]*:[[:space:]]*\[/,/\][[:space:]]*[,}]/p' | grep -o '"[^"]*"' | cut -d'"' -f2 | sed '1d') if [ -n "$VERSION_STRINGS" ]; then printf '%s\n' "$VERSION_STRINGS" > "$VERSIONS_FILE" fi fi if [ -z "$LATEST" ]; then LATEST=$(printf '%s' "$API_JSON" | sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') fi fi if [ -s "$VERSIONS_FILE" ] && [ -z "$LATEST" ]; then LATEST=$(sed -n '1p' "$VERSIONS_FILE") fi if [ ! -s "$VERSIONS_FILE" ] || [ -z "$LATEST" ]; then warn "Could not fetch versions from API. Using fallback: v${CALMO_BRIDGE_FALLBACK_VERSION}" printf '%s\n' "$CALMO_BRIDGE_FALLBACK_VERSION" > "$VERSIONS_FILE" LATEST="$CALMO_BRIDGE_FALLBACK_VERSION" fi VERSION_COUNT=$(wc -l < "$VERSIONS_FILE" | tr -d ' ') # ── Version selection ───────────────────────────────────────────────────────── if [ -n "${VERSION:-}" ]; then VER="$VERSION" info "Using pinned version: v${VER}" elif [ "$VERSION_COUNT" -le 1 ]; then VER="$LATEST" info "Latest version: v${VER}" else printf "\n${BOLD}Available versions:${NC}\n" i=1 while IFS= read -r v; do if [ "$v" = "$LATEST" ]; then printf " ${CYAN}%d)${NC} v%s ${GREEN}(latest)${NC}\n" "$i" "$v" else printf " ${CYAN}%d)${NC} v%s\n" "$i" "$v" fi i=$((i + 1)) done < "$VERSIONS_FILE" printf "\n${BOLD}Select version${NC} [1]: " CHOICE=$(read_tty) CHOICE="${CHOICE:-1}" case "$CHOICE" in *[!0-9]*|'') warn "Invalid selection, defaulting to latest." VER="$LATEST" ;; *) if [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "$VERSION_COUNT" ]; then warn "Invalid selection, defaulting to latest." VER="$LATEST" else VER=$(sed -n "${CHOICE}p" "$VERSIONS_FILE") fi ;; esac if [ -z "$VER" ]; then warn "Invalid selection, defaulting to latest." VER="$LATEST" fi info "Selected: v${VER}" fi BASE_URL="https://storage.googleapis.com/calmo-bridge/v${VER}" # ── Determine package format ────────────────────────────────────────────────── if [ "$PLATFORM" = "darwin" ]; then EXT="zip" FILENAME="calmo-bridge-v${VER}-${PLATFORM}-${ARCH}.${EXT}" elif [ "$PLATFORM" = "linux" ]; then # Detect distro family for a sensible default SUGGESTED_EXT="zip" SUGGESTED_LABEL="portable ZIP (any distro)" if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release if echo "${ID:-} ${ID_LIKE:-}" | grep -qiE "debian|ubuntu|linuxmint|pop|kali"; then SUGGESTED_EXT="deb" SUGGESTED_LABEL="deb — detected Debian/Ubuntu-based system" elif echo "${ID:-} ${ID_LIKE:-}" | grep -qiE "rhel|centos|fedora|rocky|almalinux|amzn"; then SUGGESTED_EXT="rpm" SUGGESTED_LABEL="rpm — detected Red Hat/Fedora-based system" fi fi case "$SUGGESTED_EXT" in deb) DEFAULT_PKG=1 ;; rpm) DEFAULT_PKG=2 ;; *) DEFAULT_PKG=3 ;; esac printf "\n${BOLD}Available package formats for Linux (${ARCH}):${NC}\n" printf " ${CYAN}1)${NC} .deb — Debian, Ubuntu and derivatives\n" printf " ${CYAN}2)${NC} .rpm — Red Hat, Fedora, CentOS and derivatives\n" printf " ${CYAN}3)${NC} .zip — Portable (any distro)\n" printf "\n${BOLD}Select package format${NC} [${DEFAULT_PKG} — ${SUGGESTED_LABEL}]: " PKG_CHOICE=$(read_tty) PKG_CHOICE="${PKG_CHOICE:-$DEFAULT_PKG}" case "$PKG_CHOICE" in 1) EXT="deb" ;; 2) EXT="rpm" ;; 3) EXT="zip" ;; *) warn "Invalid selection, using suggested: .${SUGGESTED_EXT}"; EXT="$SUGGESTED_EXT" ;; esac # Warn if the chosen format doesn't match the detected distro family MISMATCH=0 case "$EXT" in deb) echo "${ID:-} ${ID_LIKE:-}" | grep -qiE "debian|ubuntu|linuxmint|pop|kali" || MISMATCH=1 ;; rpm) echo "${ID:-} ${ID_LIKE:-}" | grep -qiE "rhel|centos|fedora|rocky|almalinux|amzn" || MISMATCH=1 ;; esac if [ "$MISMATCH" = "1" ]; then printf "\n" warn ".${EXT} was selected but your system appears to be ${PRETTY_NAME:-Linux}." warn "The recommended format for your system is .${SUGGESTED_EXT}." printf "${BOLD}Proceed with .${EXT} anyway?${NC} [y/N]: " CONFIRM=$(read_tty) case "$CONFIRM" in y|Y|yes|Yes|YES) ;; *) EXT="$SUGGESTED_EXT"; info "Switching to .${EXT}." ;; esac fi FILENAME="calmo-bridge-v${VER}-${PLATFORM}-${ARCH}.${EXT}" fi URL="${BASE_URL}/${FILENAME}" DOWNLOAD_PATH="$TMP_DIR/$FILENAME" # ── Track install start ─────────────────────────────────────────────────────── posthog_capture "bridge_install_started" \ "\"source\":\"get\",\"version\":\"$VER\",\"platform\":\"$PLATFORM\",\"arch\":\"$ARCH\",\"ext\":\"$EXT\"" # ── Download ────────────────────────────────────────────────────────────────── PH_STEP="download" printf "\n" info "Downloading Calmo Bridge v${VER} for ${PLATFORM}-${ARCH} (.${EXT})..." info "Source: $URL" printf "\n" # Pre-fetch the exact file size from the GCS object metadata API. # This is a fast JSON call and does NOT require bucket list permissions. # GCS uses chunked transfer for large files so Content-Length is absent # from the download response headers — this is the only reliable way. ENCODED_OBJ=$(printf 'v%s/%s' "$VER" "$FILENAME" | sed 's|/|%2F|g') TOTAL_BYTES=$(curl -fsSL --connect-timeout 4 --max-time 6 \ "https://storage.googleapis.com/storage/v1/b/calmo-bridge/o/${ENCODED_OBJ}" \ 2>/dev/null | sed -n 's/.*"size": "\([0-9]*\)".*/\1/p') curl -fsSL -o "$DOWNLOAD_PATH" "$URL" & CURL_PID=$! if [ -e /dev/tty ]; then BAR_WIDTH=40 while kill -0 "$CURL_PID" 2>/dev/null; do DOWNLOADED=$(ls -ln "$DOWNLOAD_PATH" 2>/dev/null | awk '{print $5}') DOWNLOADED="${DOWNLOADED:-0}" if [ -n "$TOTAL_BYTES" ] && [ "$TOTAL_BYTES" -gt 0 ] 2>/dev/null; then # Full bar: |============> | 34% 121.1 / 356.2 MB printf '\r %s' "$(awk \ -v dl="$DOWNLOADED" \ -v total="$TOTAL_BYTES" \ -v w="$BAR_WIDTH" \ 'BEGIN { pct = int(dl * 100 / total) if (pct > 100) pct = 100 filled = int(pct * w / 100) bar = "" for (i = 0; i < filled; i++) bar = bar "=" if (filled < w) bar = bar ">" for (i = filled + 1; i < w; i++) bar = bar " " printf "|%s| %3d%% %.1f / %.1f MB", bar, pct, dl / 1048576, total / 1048576 }')" > /dev/tty else # Headers not yet received — show raw bytes until total is known printf '\r Connecting... %.1f MB received' \ "$(awk "BEGIN { printf \"%.1f\", $DOWNLOADED / 1048576 }")" > /dev/tty fi sleep 0.2 done wait "$CURL_PID" || error "Download failed. Please check your network connection or the version." if [ -n "$TOTAL_BYTES" ] && [ "$TOTAL_BYTES" -gt 0 ] 2>/dev/null; then TOTAL_MB=$(awk "BEGIN { printf \"%.1f\", $TOTAL_BYTES / 1048576 }") FULL_BAR=$(awk -v w="$BAR_WIDTH" 'BEGIN { for (i = 0; i < w; i++) printf "=" }') # Show actual downloaded size but cap display at TOTAL_MB to avoid "350 / 348 MB" ACTUAL_DL=$(ls -ln "$DOWNLOAD_PATH" 2>/dev/null | awk '{print $5}') SHOW_MB=$(awk -v dl="${ACTUAL_DL:-0}" -v t="$TOTAL_BYTES" \ 'BEGIN { printf "%.1f", (dl < t ? dl : t) / 1048576 }') printf "\r |%s| 100%% %s / %s MB ${GREEN}Done!${NC} \n" \ "$FULL_BAR" "$SHOW_MB" "$TOTAL_MB" > /dev/tty else FINAL_SIZE=$(ls -ln "$DOWNLOAD_PATH" 2>/dev/null | awk '{printf "%.1f MB", $5 / 1048576}') printf "\r ${GREEN}✓${NC} Downloaded %s \n" \ "$FINAL_SIZE" > /dev/tty fi else # No tty (headless CI etc.) — silent download wait "$CURL_PID" || error "Download failed. Please check your network connection or the version." fi printf "\n" info "Download complete." # ── Install ─────────────────────────────────────────────────────────────────── printf "\n" case "$EXT" in zip) if [ "$PLATFORM" = "darwin" ]; then PH_STEP="install_zip_mac" info "Installing to /Applications..." unzip -o -q "$DOWNLOAD_PATH" -d "$TMP_DIR" APP_PATH=$(find "$TMP_DIR" -name "CalmoBridge.app" -type d | head -n 1) if [ -n "$APP_PATH" ]; then STAGING_APP="/Applications/CalmoBridge.app.new" rm -rf "$STAGING_APP" cp -R "$APP_PATH" "$STAGING_APP" # Remove quarantine attribute to prevent "App is damaged" error on macOS xattr -cr "$STAGING_APP" if [ -d "/Applications/CalmoBridge.app" ]; then rm -rf "/Applications/CalmoBridge.app" fi mv "$STAGING_APP" "/Applications/CalmoBridge.app" info "Installed: /Applications/CalmoBridge.app" else warn "Could not find 'CalmoBridge.app' inside the downloaded ZIP." warn "Extracted contents (top 3 levels):" find "$TMP_DIR" -maxdepth 3 | sed "s|$TMP_DIR/||" | sort >/dev/tty 2>/dev/null || true error "Installation aborted. If testing on Linux/dev mode, the darwin .zip must be opened on a Mac." fi else PH_STEP="install_zip_linux" INSTALL_DIR="$HOME/.local/share/calmo-bridge" BIN_DIR="$HOME/.local/bin" info "Installing to $INSTALL_DIR..." mkdir -p "$INSTALL_DIR" "$BIN_DIR" unzip -o -q "$DOWNLOAD_PATH" -d "$INSTALL_DIR" if [ -f "$INSTALL_DIR/calmo-bridge" ]; then chmod +x "$INSTALL_DIR/calmo-bridge" ln -sf "$INSTALL_DIR/calmo-bridge" "$BIN_DIR/calmo-bridge" info "Linked binary: $BIN_DIR/calmo-bridge" else error "Installation failed: expected calmo-bridge binary was not found inside the ZIP." fi fi ;; deb) PH_STEP="install_deb" info "Installing .deb package..." CMD="dpkg -i $DOWNLOAD_PATH" CMD_FIX="apt-get install -f -y" if command -v sudo >/dev/null 2>&1; then CMD="sudo $CMD" CMD_FIX="sudo $CMD_FIX" fi if ! $CMD; then warn "Dependency issues found, running fix..." $CMD_FIX fi info "Installation complete." ;; rpm) PH_STEP="install_rpm" info "Installing .rpm package..." CMD="rpm -U $DOWNLOAD_PATH" if command -v sudo >/dev/null 2>&1; then CMD="sudo $CMD"; fi $CMD info "Installation complete." ;; esac PH_HAS_SUCCEEDED="1" posthog_capture "bridge_install_succeeded" \ "\"source\":\"get\",\"version\":\"$VER\",\"platform\":\"$PLATFORM\",\"arch\":\"$ARCH\",\"ext\":\"$EXT\"" printf "\n${GREEN}${BOLD}Calmo Bridge v${VER} installed successfully!${NC}\n\n"