#!/usr/bin/env sh # install-nightly.sh — install the latest nightly build of Skald Circle # # Usage: # curl -fsSL https://builds.skaldagent.net/install-nightly.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}" CHANNEL="${SKALD_CHANNEL:-nightly}" # ── Detect interactive stdin ────────────────────────────────────────────────── 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) ──────────────────────────────── 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 &2 read -r ans || ans="" elif (: /dev/null; then printf "%s" "$msg" >/dev/tty IFS= read -r ans /dev/null 2>&1 && \ systemctl --user stop skald-circle.service 2>/dev/null || true ;; darwin) command -v launchctl >/dev/null 2>&1 && \ launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true ;; esac if command -v pgrep >/dev/null 2>&1; then local i=0 while pgrep -f "${INSTALL_DIR}/bin/skald" >/dev/null 2>&1; do i=$((i + 1)) [ "$i" -gt 20 ] && break sleep 1 done else sleep 2 fi } # ── Docker install helper ───────────────────────────────────────────────────── install_docker() { if [ "$OS" = "linux" ]; then info "▶ Installing Docker via get.docker.com …" curl -fsSL https://get.docker.com | sh info "✔ Docker installed" if command -v usermod >/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 ──────────────────────────────────────────────── 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 — the TTS plugins and host-run connectors 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 nightly ───────────────────────────────────────────── banner "╔══════════════════════════════════════════╗" banner "║ Skald Circle — Nightly Installer ║" banner "╚══════════════════════════════════════════╝" echo "" info "🔍 Looking up latest nightly build …" BASE_URL="https://builds.skaldagent.net" case "$CHANNEL" in nightly) TARBALL_URL="${BASE_URL}/nightly/skald-circle-nightly-${OS}-${ARCH}.tar.gz" VERSION="nightly-$(date -u +%Y%m%d)" DISPLAY_VERSION="nightly" ;; *) err "Unknown channel: $CHANNEL. Use SKALD_CHANNEL=nightly (default)." exit 1 ;; esac # ── Summary + confirmation ──────────────────────────────────────────────────── echo "" header "Installation summary" echo "" echo " What : Skald Circle (${DISPLAY_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 # ── Existing installation? ──────────────────────────────────────────────────── # This installer targets a fresh install; the supported in-place upgrade path is # update.sh (keeps your data, restarts the service safely). If an install is # already here, point the user there and only reinstall over it on request. if [ -x "$INSTALL_DIR/bin/skald" ]; then echo "" warn "An existing installation was found at ${INSTALL_DIR}." echo " To upgrade in place (keeping your database and config), use:" echo " ${INSTALL_DIR}/update.sh" echo "" if prompt_yes_no_default_no " Reinstall over it instead? Data is kept, the service restarts. [y/N] "; then info "⏹️ Stopping the running instance before reinstalling …" stop_existing_service else info "Aborted — run ${INSTALL_DIR}/update.sh to upgrade." exit 0 fi echo "" fi # ── Download & extract ──────────────────────────────────────────────────────── info "↓ Downloading Skald Circle (${DISPLAY_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 — the TTS plugins and host-run connectors 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 — the TTS plugins and host-run connectors will be unavailable." else warn "python3 not found — the TTS plugins and host-run connectors 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 (${DISPLAY_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 # Idempotent: unload any previously-loaded agent first, so a re-install # reloads the freshly written plist instead of failing on "already loaded". launchctl unload "$PLIST" 2>/dev/null || true 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" "$INSTALL_DIR/.release-channel" # ── Hint about optional deps ────────────────────────────────────────────────── if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then echo "" info "💡 Optional GPU/ML dependencies available:" info " ${INSTALL_DIR}/requirements-optional.txt" echo " Install them manually if you use the Orpheus TTS plugin:" echo " cd ${INSTALL_DIR} && .venv/bin/pip install -r requirements-optional.txt" fi echo " Server : ${INSTALL_DIR}/run.sh" echo " Binary : ${INSTALL_DIR}/bin/skald" echo " Setup : ${INSTALL_DIR}/bin/skald-setup" NET_IP="$(get_network_ip)" ADMIN_URL="${NET_IP:+http://${NET_IP}:9000}" echo " Admin console: ${ADMIN_URL:-http://localhost:9000} (on the machine, or use the IP above from another device)" echo " Server IP: ${NET_IP:-localhost} (network address if available)" echo "" echo "" echo " Start: $( [ "$OS" = "linux" ] && echo "systemctl --user start skald-circle" || echo "launchctl start com.skald.circle" )" echo " Stop: $( [ "$OS" = "linux" ] && echo "systemctl --user stop skald-circle" || echo "launchctl stop com.skald.circle" )" echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )" echo ""