#!/usr/bin/env bash # Hivekeep installer # Usage: curl -fsSL https://raw.githubusercontent.com/MarlBurroW/hivekeep/main/install.sh | bash # Or: HIVEKEEP_PORT=8080 bash install.sh # Non-interactive: HIVEKEEP_NO_PROMPT=true bash install.sh set -euo pipefail # ─── Root detection ────────────────────────────────────────────────────────── IS_ROOT=false [ "$(id -u)" -eq 0 ] && IS_ROOT=true # ─── Configurable via env vars ─────────────────────────────────────────────── if [ "$IS_ROOT" = true ]; then HIVEKEEP_DIR="${HIVEKEEP_DIR:-/opt/hivekeep}" HIVEKEEP_DATA_DIR="${HIVEKEEP_DATA_DIR:-/var/lib/hivekeep}" HIVEKEEP_USER="${HIVEKEEP_USER:-hivekeep}" else HIVEKEEP_DIR="${HIVEKEEP_DIR:-$HOME/hivekeep}" HIVEKEEP_DATA_DIR="${HIVEKEEP_DATA_DIR:-$HOME/.local/share/hivekeep}" fi HIVEKEEP_PORT="${HIVEKEEP_PORT:-3000}" HIVEKEEP_PUBLIC_URL="${HIVEKEEP_PUBLIC_URL:-}" HIVEKEEP_REPO="MarlBurroW/hivekeep" # Explicitly requesting a branch implies the edge channel (tracking a branch # head instead of release tags). HIVEKEEP_BRANCH_EXPLICIT=false [ -n "${HIVEKEEP_BRANCH:-}" ] && HIVEKEEP_BRANCH_EXPLICIT=true HIVEKEEP_BRANCH="${HIVEKEEP_BRANCH:-main}" # Update channel: stable (release tags, default) | edge (HEAD of main). # Empty = auto-detect (existing checkout state, HIVEKEEP_BRANCH, else stable). HIVEKEEP_CHANNEL="${HIVEKEEP_CHANNEL:-}" HIVEKEEP_DRY_RUN=false HIVEKEEP_QUIET="${HIVEKEEP_QUIET:-false}" HIVEKEEP_START_TIME="" HIVEKEEP_YES="${HIVEKEEP_YES:-false}" HIVEKEEP_SYSTEM_BUN_INSTALL="/usr/local/lib/hivekeep/bun" HIVEKEEP_SYSTEM_CONFIG_DIR="/etc/hivekeep" HIVEKEEP_SYSTEMD_UNIT="/etc/systemd/system/hivekeep.service" HIVEKEEP_LEGACY_BUN_LINK="/usr/local/bin/bun" HIVEKEEP_LEGACY_BUN_ROOT="/root/.bun" # ─── Colors (auto-detect terminal support) ─────────────────────────────────── setup_colors() { if [ "${NO_COLOR:-}" = "1" ] || [ "${HIVEKEEP_NO_COLOR:-}" = "true" ]; then RED='' GREEN='' YELLOW='' CYAN='' DIM='' BOLD='' NC='' elif [ -t 1 ] && [ -t 2 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' DIM='\033[2m' BOLD='\033[1m' NC='\033[0m' else # Not a terminal (piped or redirected) — no colors RED='' GREEN='' YELLOW='' CYAN='' DIM='' BOLD='' NC='' fi } setup_colors info() { [ "$HIVEKEEP_QUIET" = true ] && return; echo -e "${CYAN}▸${NC} $*"; } success() { [ "$HIVEKEEP_QUIET" = true ] && return; echo -e "${GREEN}✓${NC} $*"; } warn() { echo -e "${YELLOW}⚠${NC} $*" >&2; } error() { echo -e "${RED}✗ ERROR:${NC} $*" >&2; exit 1; } header() { [ "$HIVEKEEP_QUIET" = true ] && return; echo -e "\n${BOLD}$*${NC}"; } # ─── Elapsed time tracking ────────────────────────────────────────────────── start_timer() { HIVEKEEP_START_TIME="$(date +%s)"; } # Returns human-readable elapsed time since start_timer() was called format_elapsed() { [ -z "$HIVEKEEP_START_TIME" ] && return local now elapsed now="$(date +%s)" elapsed=$((now - HIVEKEEP_START_TIME)) if [ "$elapsed" -lt 5 ] 2>/dev/null; then echo "< 5s" elif [ "$elapsed" -lt 60 ] 2>/dev/null; then echo "${elapsed}s" elif [ "$elapsed" -lt 3600 ] 2>/dev/null; then local m=$((elapsed / 60)) s=$((elapsed % 60)) if [ "$s" -gt 0 ]; then echo "${m}m ${s}s" else echo "${m}m" fi else local h=$((elapsed / 3600)) m=$(( (elapsed % 3600) / 60 )) echo "${h}h ${m}m" fi } # ─── Step progress (for main install flow) ─────────────────────────────────── STEP_CURRENT=0 STEP_TOTAL=0 step() { STEP_CURRENT=$((STEP_CURRENT + 1)) [ "$HIVEKEEP_QUIET" = true ] && return local progress="" if [ "$STEP_TOTAL" -gt 0 ] 2>/dev/null; then progress="${DIM}[${STEP_CURRENT}/${STEP_TOTAL}]${NC} " fi echo -e "\n${progress}${BOLD}$*${NC}" } # ─── Installer self-update check ───────────────────────────────────────────── # When running from a local file (not piped via curl | bash), check if the # installer itself is outdated compared to the remote version on GitHub. # This prevents users from running stale install logic against a newer codebase. check_installer_update() { # Skip if piped (no local file to update), quiet mode, CI, or no-prompt [ ! -t 0 ] && return 0 [ "$HIVEKEEP_QUIET" = true ] && return 0 [ "${HIVEKEEP_NO_PROMPT:-}" = "true" ] && return 0 [ "${CI:-}" = "true" ] && return 0 [ "${HIVEKEEP_SKIP_SELF_UPDATE:-}" = "true" ] && return 0 # Only check if we can identify the running script file local self_path="${BASH_SOURCE[0]:-}" [ -z "$self_path" ] && return 0 [ ! -f "$self_path" ] && return 0 # Compute local checksum local local_hash="" if command -v sha256sum &>/dev/null; then local_hash="$(sha256sum "$self_path" 2>/dev/null | awk '{print $1}')" elif command -v shasum &>/dev/null; then local_hash="$(shasum -a 256 "$self_path" 2>/dev/null | awk '{print $1}')" else return 0 # can't compare without a hash tool fi [ -z "$local_hash" ] && return 0 # Fetch remote installer (lightweight: just the hash via a temp file) local remote_url="https://raw.githubusercontent.com/$HIVEKEEP_REPO/$HIVEKEEP_BRANCH/install.sh" local tmp_remote tmp_remote="$(mktemp)" if ! curl -fsSL --max-time 8 "$remote_url" -o "$tmp_remote" 2>/dev/null; then rm -f "$tmp_remote" return 0 # network issue, skip silently fi local remote_hash="" if command -v sha256sum &>/dev/null; then remote_hash="$(sha256sum "$tmp_remote" 2>/dev/null | awk '{print $1}')" elif command -v shasum &>/dev/null; then remote_hash="$(shasum -a 256 "$tmp_remote" 2>/dev/null | awk '{print $1}')" fi if [ -z "$remote_hash" ] || [ "$local_hash" = "$remote_hash" ]; then rm -f "$tmp_remote" return 0 # up to date or can't compare fi # Installer is outdated echo -e "${YELLOW}⚠${NC} A newer version of the installer is available." echo -en " ${CYAN}?${NC} ${BOLD}Update installer and restart?${NC} ${DIM}[Y/n]${NC}: " >/dev/tty local answer read -r answer &2 # Kill any running spinner background command if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then kill "$SPINNER_PID" 2>/dev/null wait "$SPINNER_PID" 2>/dev/null || true fi SPINNER_PID="" # Clean spinner line printf "\r\033[K" >&2 # Remove temp log file [ -n "$SPINNER_LOG" ] && rm -f "$SPINNER_LOG" SPINNER_LOG="" warn "Interrupted by user" release_lock # EXIT trap will fire next and handle rollback exit 130 } trap cleanup_on_signal INT TERM # ─── Spinner for long-running commands ─────────────────────────────────────── # Usage: run_with_spinner "Installing dependencies..." command arg1 arg2 run_with_spinner() { local label="$1" shift # If not a terminal or quiet mode, just run silently if [ "$HIVEKEEP_QUIET" = true ]; then "$@" >/dev/null 2>&1 return fi if [ ! -t 1 ] && [ ! -t 2 ]; then info "$label" "$@" return fi local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") local frame_count=${#frames[@]} local i=0 local spin_start spin_start="$(date +%s)" SPINNER_LOG="$(mktemp)" # Start the command in background, capturing output "$@" > "$SPINNER_LOG" 2>&1 & local cmd_pid=$! SPINNER_PID=$cmd_pid # Animate spinner while command runs (show elapsed time after 3s) while kill -0 "$cmd_pid" 2>/dev/null; do local elapsed_str="" local now_s now_s="$(date +%s)" local elapsed_s=$((now_s - spin_start)) if [ "$elapsed_s" -ge 3 ]; then if [ "$elapsed_s" -lt 60 ]; then elapsed_str=" ${DIM}(${elapsed_s}s)${NC}" else local em=$((elapsed_s / 60)) es=$((elapsed_s % 60)) elapsed_str=" ${DIM}(${em}m ${es}s)${NC}" fi fi printf "\r ${CYAN}%s${NC} %s%b" "${frames[$((i % frame_count))]}" "$label" "$elapsed_str" >&2 i=$((i + 1)) sleep 0.1 done # Get exit code wait "$cmd_pid" local exit_code=$? SPINNER_PID="" # Compute final elapsed time local final_elapsed="" local end_s end_s="$(date +%s)" local total_s=$((end_s - spin_start)) if [ "$total_s" -ge 3 ]; then if [ "$total_s" -lt 60 ]; then final_elapsed=" ${DIM}(${total_s}s)${NC}" else local fm=$((total_s / 60)) fs=$((total_s % 60)) if [ "$fs" -gt 0 ]; then final_elapsed=" ${DIM}(${fm}m ${fs}s)${NC}" else final_elapsed=" ${DIM}(${fm}m)${NC}" fi fi fi # Clear spinner line printf "\r\033[K" >&2 if [ $exit_code -eq 0 ]; then if [ -n "$final_elapsed" ]; then [ "$HIVEKEEP_QUIET" = true ] && return echo -e "${GREEN}✓${NC} ${label}${final_elapsed}" >&2 else success "$label" fi else echo -e "${RED}✗${NC} ${label}${final_elapsed}" >&2 echo "" >&2 echo -e "${DIM}Command output:${NC}" >&2 tail -20 "$SPINNER_LOG" >&2 rm -f "$SPINNER_LOG" SPINNER_LOG="" return $exit_code fi rm -f "$SPINNER_LOG" SPINNER_LOG="" return 0 } # ─── Retry wrapper for flaky network operations ───────────────────────────── # Usage: retry