#!/usr/bin/env sh # install.sh — install the latest release of Skald Circle # # Usage: # curl -fsSL https://builds.skaldagent.net/install.sh | bash # # Supports Linux (systemd) and macOS ARM64 (launchd). # Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR). # # If Docker is missing, the installer can optionally install it. set -eu # ── User overrides ──────────────────────────────────────────────────────────── INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}" # ── Detect interactive stdin ────────────────────────────────────────────────── # We need this early: the confirmation prompt reads from /dev/tty when stdin is # piped (curl | bash), but we only show it when a terminal is actually available. if [ -t 0 ]; then IS_INTERACTIVE=true else IS_INTERACTIVE=false fi # ── Colours (if terminal) ───────────────────────────────────────────────────── if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC='' fi info() { printf "${GREEN}%s${NC}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; } err() { printf "${RED}✖ %s${NC}\n" "$*"; } header(){ printf "\n${BOLD}%s${NC}\n" "$*"; } banner(){ printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; } # ── Network IP helper ────────────────────────────────────────────────────────── # Returns the primary non-loopback network IP, or empty string if unavailable. get_network_ip() { if command -v ip >/dev/null 2>&1; then ip route get 1 2>/dev/null | awk '{print $7}' elif command -v ifconfig >/dev/null 2>&1; then ifconfig 2>/dev/null | grep -E 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -1 elif command -v hostname >/dev/null 2>&1; then hostname -I 2>/dev/null | awk '{print $1}' fi } # ── Prompt helpers (work from piped stdin too) ──────────────────────────────── # Both read from /dev/tty when stdin is piped (curl | bash), so the user can # always see and answer. prompt_enter() { local msg="${1:-Press Enter to continue or Ctrl+C to cancel}" if [ "$IS_INTERACTIVE" = true ]; then printf "%s" "$msg" >&2 read -r _ || true elif (: /dev/null; then printf "%s" "$msg" >/dev/tty IFS= read -r _ &2 read -r ans || ans="y" elif (: /dev/null; then printf "%s" "$msg" >/dev/tty IFS= read -r ans /dev/null 2>&1; then sudo usermod -aG docker "$USER" warn "You may need to log out and back in for the docker group to take effect." fi info "✔ User added to docker group" elif [ "$OS" = "darwin" ]; then if command -v brew >/dev/null 2>&1; then info "▶ Installing Docker Desktop via Homebrew …" brew install --cask docker info "✔ Docker Desktop installed. Open it from Applications to complete setup." else err "Homebrew not found. Please install Docker Desktop manually from:" err " https://docs.docker.com/desktop/setup/install/mac-install/" err "Then re-run this installer." exit 1 fi fi } ask_install_docker() { echo "" header "🐳 Docker" echo "" if command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1; then info "✔ Docker is installed and the daemon is running." return 0 elif command -v docker >/dev/null 2>&1; then echo " Docker CLI is present but the daemon is not running." echo " Please start Docker before starting the server." echo "" return 0 else warn "Docker is required but not found." echo "" echo " Skald uses Docker to run sandboxed user containers." echo " The server will not start without it." echo "" if prompt_yes_no " Install Docker now? [Y/n] "; then install_docker echo "" else warn "Skipping Docker installation." echo " You can install it later: https://docs.docker.com/engine/install/" echo "" fi fi } # ── Optional dependency checks ──────────────────────────────────────────────── # These are just warnings — the server starts without them, but some MCP servers # or plugins won't work. check_optional_deps() { echo "" header "🔧 Optional dependencies" echo "" if command -v python3 >/dev/null 2>&1; then info "✔ Python 3 found ($(python3 --version 2>&1 | head -1))" else warn "Python 3 not found — Python MCP servers (Gmail, GCal, GMaps, ...) will not work." echo " Install it from https://www.python.org/downloads/" echo "" fi if command -v node >/dev/null 2>&1; then info "✔ Node.js found ($(node --version 2>&1 | head -1))" else warn "Node.js not found — WhatsApp MCP server will not work." echo " Install it from https://nodejs.org/ (version 18 or later)" echo "" fi } # ── Platform detection ──────────────────────────────────────────────────────── OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; *) err "Unsupported OS: $OS"; exit 1 ;; esac case "$ARCH" in x86_64) ARCH="amd64" if [ "$OS" = "darwin" ]; then err "Intel Macs are not supported. Apple Silicon (M1+) only." exit 1 fi ;; aarch64|arm64) ARCH="arm64" ;; *) err "Unsupported architecture: $ARCH"; exit 1 ;; esac # ── Dependency checks ───────────────────────────────────────────────────────── command -v curl >/dev/null 2>&1 || { err "curl is required but not installed."; exit 1; } if [ "$OS" = "linux" ]; then command -v systemctl >/dev/null 2>&1 || { warn "systemd not found — service will not be installed automatically."; NOSYSTEMD=1; } elif [ "$OS" = "darwin" ]; then command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; } fi # ── Banner + fetch latest version ───────────────────────────────────────────── banner "╔══════════════════════════════════════════╗" banner "║ Skald Circle — Installer ║" banner "╚══════════════════════════════════════════╝" echo "" info "🔍 Looking up latest release …" BASE_URL="https://builds.skaldagent.net" LATEST_URL="${BASE_URL}/releases/LATEST" VERSION="$(curl -fsSL "$LATEST_URL" | head -1 | tr -d '[:space:]')" if [ -z "$VERSION" ]; then err "Could not determine latest release version." err "Check ${LATEST_URL} or try install-nightly.sh for the latest build." exit 1 fi TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz" # ── Summary + confirmation ──────────────────────────────────────────────────── echo "" header "Installation summary" echo "" echo " What : Skald Circle ${VERSION}" echo " Platform : ${OS}/${ARCH}" echo " Install to : ${INSTALL_DIR}" echo " Service : $( [ "$OS" = "linux" ] && echo "systemd (user)" || echo "launchd" )" echo "" echo " The installer will check for Docker and offer to install it if missing." echo " Python 3 and Node.js are optional — needed for some MCP servers." echo "" prompt_enter "Press Enter to continue or Ctrl+C to cancel " # ── Docker check & install ──────────────────────────────────────────────────── ask_install_docker # ── Optional dependency check ───────────────────────────────────────────────── check_optional_deps # ── Download & extract ──────────────────────────────────────────────────────── info "↓ Downloading Skald Circle ${VERSION} …" mkdir -p "$INSTALL_DIR" curl -fsSL "$TARBALL_URL" | tar xz -C "$INSTALL_DIR" --strip-components=1 if [ ! -x "$INSTALL_DIR/bin/skald" ]; then err "Download or extraction failed — skald binary not found." exit 1 fi info "✔ Extracted to ${INSTALL_DIR}" # ── Python venv (best-effort) ───────────────────────────────────────────────── # Create the venv inline instead of calling run.sh (which also launches the # server and would hang the installer). info "🔧 Setting up Python virtual environment …" VENV_DIR="${INSTALL_DIR}/.venv" REQUIREMENTS="${INSTALL_DIR}/requirements.txt" # A venv is usable only if python3 AND pip both work. Ubuntu's `python3 -m venv` # without the python3-venv package leaves a venv with python3 but no pip — detect # that and recreate, so a broken venv never survives a restart. if [ ! -f "$VENV_DIR/bin/python3" ] || ! "$VENV_DIR/bin/python3" -m pip --version >/dev/null 2>&1; then rm -rf "$VENV_DIR" if command -v uv >/dev/null 2>&1; then uv venv --seed "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \ && info "✔ Python venv ready (uv)" \ || warn "Python venv setup failed — Python MCP servers will be unavailable." elif command -v python3 >/dev/null 2>&1; then python3 -m venv "$VENV_DIR" && "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" \ && info "✔ Python venv ready (pip)" \ || warn "Python venv setup failed — Python MCP servers will be unavailable." else warn "python3 not found — Python MCP servers will be unavailable." fi else info "✔ Python venv already exists" fi # ── Install daemon ──────────────────────────────────────────────────────────── if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then header "⚡ Installing systemd user service …" mkdir -p "$HOME/.config/systemd/user" cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE [Unit] Description=Skald Circle (release ${VERSION}) Documentation=https://skaldagent.net After=network.target docker.service [Service] Type=simple ExecStart=${INSTALL_DIR}/run.sh WorkingDirectory=${INSTALL_DIR} Restart=on-failure RestartSec=5 Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup [Install] WantedBy=default.target SERVICE systemctl --user daemon-reload systemctl --user enable --now skald-circle.service info "✔ Service installed and started" echo "" echo " Status: systemctl --user status skald-circle" echo " Logs: journalctl --user -u skald-circle -f" elif [ "$OS" = "darwin" ]; then header "⚡ Installing launchd agent …" mkdir -p "$HOME/Library/LaunchAgents" "$INSTALL_DIR/logs" PLIST="$HOME/Library/LaunchAgents/com.skald.circle.plist" cat > "$PLIST" <<- PLIST Label com.skald.circle ProgramArguments ${INSTALL_DIR}/run.sh WorkingDirectory ${INSTALL_DIR} RunAtLoad KeepAlive StandardOutPath ${INSTALL_DIR}/logs/stdout.log StandardErrorPath ${INSTALL_DIR}/logs/stderr.log EnvironmentVariables SKALD_BIN ${INSTALL_DIR}/bin/skald SKALD_SETUP_BIN ${INSTALL_DIR}/bin/skald-setup PLIST launchctl load "$PLIST" info "✔ Agent installed and started" echo "" echo " Status: launchctl list com.skald.circle" echo " Logs: tail -f ${INSTALL_DIR}/logs/stdout.log" elif [ -n "${NOSYSTEMD:-}" ]; then warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh" fi # ── Welcome + first-run setup ───────────────────────────────────────────────── echo "" header "👋 Welcome to Skald Circle!" if [ -x "$INSTALL_DIR/bin/skald-setup" ]; then echo "" echo " The server is running. Now let's create your admin account." echo " You'll be asked for a username and password." echo "" # skald-setup uses the relative path `database/system.db`, so we cd to the # install directory first. When running via curl | bash the cwd is ~, which # would create `~/database/system.db` instead of the correct location. cd "$INSTALL_DIR" if [ "$IS_INTERACTIVE" = true ]; then "bin/skald-setup" elif (: /dev/null; then "bin/skald-setup"