Nightly Build / build (push) Successful in 4m5s
An MCP server whose `initialize` answer is an error — a broken or version- mismatched connector — starts fine and then never exits. `McpServer::start` returned `Err` correctly, but the `Child` lives in the read-loop task rather than in the returned value, so `kill_on_drop` followed a task nothing ever drops. Every retry therefore left a live process holding three pipes and a pidfd, and the supervisor's retry ceiling is deliberately not permanent. The end state was not a dead connector but a dead instance: the process hit its 1024-descriptor limit, `accept()` began failing with EMFILE, and incoming connections queued on a socket nobody could accept from — while the process, the port and every other connector still looked healthy. Observed in production at ~5h from the first bad handshake to unreachable, with 229 orphaned interpreters. `stop_server`/`stop_all` had the same hole from the other side: they document the dropped handle as killing the process, but the task holds its end of stdin, so the child stayed blocked on a read that would never return. Both close with one seam. `McpServer` now owns a oneshot sender whose receiver the read-loop selects on; nothing ever sends, so the drop is the message. That covers a deliberate stop, the last `Arc` going away, and a `?` in `start()` unwinding past the local binding before it was ever returned — including the caller's `timeout`, which drops the same future. The loop then kills and, as importantly, reaps: an unreaped child trades the orphan for a zombie holding the same pipes. Both leaks are covered by tests that fail without the fix. Also raise LimitNOFILE to 65536: the installers write it, and update.sh heals an existing unit additively, leaving an admin's own value alone. The leak is the bug, but 1024 for a process sharing descriptors between the listener, every user's SQLite handles and three pipes per connector is thin regardless.
561 lines
23 KiB
Bash
Executable File
561 lines
23 KiB
Bash
Executable File
#!/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/tty) 2>/dev/null; then
|
|
printf "%s" "$msg" >/dev/tty
|
|
IFS= read -r _ </dev/tty || true
|
|
fi
|
|
}
|
|
|
|
prompt_yes_no() {
|
|
local msg="${1:-Continue? [Y/n]}"
|
|
local ans
|
|
if [ "$IS_INTERACTIVE" = true ]; then
|
|
printf "%s" "$msg" >&2
|
|
read -r ans || ans="y"
|
|
elif (: </dev/tty) 2>/dev/null; then
|
|
printf "%s" "$msg" >/dev/tty
|
|
IFS= read -r ans </dev/tty || ans="y"
|
|
else
|
|
ans="n"
|
|
fi
|
|
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
|
|
""|"y"|"yes") return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Like prompt_yes_no but defaults to NO — for destructive confirmations.
|
|
prompt_yes_no_default_no() {
|
|
local msg="${1:-Continue? [y/N]}"
|
|
local ans=""
|
|
if [ "$IS_INTERACTIVE" = true ]; then
|
|
printf "%s" "$msg" >&2
|
|
read -r ans || ans=""
|
|
elif (: </dev/tty) 2>/dev/null; then
|
|
printf "%s" "$msg" >/dev/tty
|
|
IFS= read -r ans </dev/tty || ans=""
|
|
fi
|
|
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
|
|
"y"|"yes") return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# ── Stop a running instance (for reinstall over an existing install) ──────────
|
|
# Stops the service and waits, bounded, for the process to exit — so the
|
|
# extraction never overwrites a live binary (ETXTBSY on Linux).
|
|
stop_existing_service() {
|
|
case "$OS" in
|
|
linux)
|
|
command -v systemctl >/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
|
|
}
|
|
|
|
# ── systemd user lingering ────────────────────────────────────────────────────
|
|
# A `systemctl --user` unit runs under the per-user manager (user@UID.service),
|
|
# which systemd starts at first login and STOPS when the user's last session
|
|
# ends — taking every user service down with it. So without lingering the server
|
|
# dies the moment you close the SSH session that started it, and never comes up
|
|
# at boot. Enabling it is the whole difference between "runs while I'm logged
|
|
# in" and "is a daemon".
|
|
enable_linger() {
|
|
local target="${USER:-$(id -un)}"
|
|
|
|
if ! command -v loginctl >/dev/null 2>&1; then
|
|
warn "loginctl not found — cannot enable lingering."
|
|
echo " The server will stop when you log out of this machine."
|
|
return 0
|
|
fi
|
|
|
|
case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
|
|
*=yes) info "✔ Lingering already enabled for ${target}"; return 0 ;;
|
|
esac
|
|
|
|
# Enabling linger for yourself is normally allowed without elevation; fall
|
|
# back to sudo, non-interactive first so `curl | bash` never blocks on a
|
|
# password prompt it has no terminal to answer.
|
|
if loginctl enable-linger "$target" 2>/dev/null \
|
|
|| sudo -n loginctl enable-linger "$target" 2>/dev/null \
|
|
|| { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
|
|
info "✔ Lingering enabled — the server keeps running after you log out"
|
|
else
|
|
warn "Could not enable lingering for ${target}."
|
|
echo " Without it, the server stops as soon as your last session ends"
|
|
echo " and does not start at boot. Run this once, as an administrator:"
|
|
echo ""
|
|
echo " sudo loginctl enable-linger ${target}"
|
|
echo ""
|
|
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 ────────────────────────────────────────────────────────
|
|
# Download to a temp file and verify the archive BEFORE touching the install dir
|
|
# — the same ordering update.sh uses, and for the same reason. Piping curl
|
|
# straight into tar half-extracts a truncated download, which on the
|
|
# reinstall-over-an-existing-install path above leaves a tree mixing old and new
|
|
# files: worse than either version, and with no error to say so.
|
|
info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …"
|
|
|
|
TMP_TARBALL="$(mktemp -t skald-install.XXXXXX.tar.gz)"
|
|
STAGING="$(mktemp -d -t skald-install-staging.XXXXXX)"
|
|
trap 'rm -f "$TMP_TARBALL" 2>/dev/null || true; rm -rf "$STAGING" 2>/dev/null || true' EXIT
|
|
|
|
curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"
|
|
|
|
info "🔎 Verifying archive …"
|
|
tar xzf "$TMP_TARBALL" -C "$STAGING" --strip-components=1
|
|
if [ ! -x "$STAGING/bin/skald" ]; then
|
|
err "Downloaded archive is invalid — skald binary not found."
|
|
err "Nothing was written to ${INSTALL_DIR}."
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1
|
|
|
|
if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
|
|
err "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
|
|
# No After=docker.service here: this is a *user* unit, and docker.service is a
|
|
# system unit the user manager knows nothing about — the dependency would be
|
|
# silently ignored. Docker may therefore still be starting when we do; the
|
|
# server fails fast when the daemon is unreachable and Restart brings it back a
|
|
# few seconds later, so boot ordering settles itself.
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${INSTALL_DIR}/run.sh
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
# always, not on-failure: run.sh exits 0 on any graceful shutdown, including one
|
|
# nobody asked for (a stray SIGTERM to the server), which on-failure would treat
|
|
# as a clean stop and leave the box down. An explicit "systemctl --user stop"
|
|
# is unaffected — systemd never restarts after a requested stop.
|
|
Restart=always
|
|
RestartSec=5
|
|
# The default soft limit is 1024, which one process shares between the HTTP
|
|
# listener, every user's SQLite handles and three pipes per connector process.
|
|
# Running out does not degrade gracefully: accept() starts failing with EMFILE
|
|
# and the whole app stops answering while still looking healthy from outside.
|
|
LimitNOFILE=65536
|
|
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"
|
|
|
|
enable_linger
|
|
|
|
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
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.skald.circle</string>
|
|
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${INSTALL_DIR}/run.sh</string>
|
|
</array>
|
|
|
|
<key>WorkingDirectory</key>
|
|
<string>${INSTALL_DIR}</string>
|
|
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
|
|
<key>StandardOutPath</key>
|
|
<string>${INSTALL_DIR}/logs/stdout.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${INSTALL_DIR}/logs/stderr.log</string>
|
|
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>SKALD_BIN</key>
|
|
<string>${INSTALL_DIR}/bin/skald</string>
|
|
<key>SKALD_SETUP_BIN</key>
|
|
<string>${INSTALL_DIR}/bin/skald-setup</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
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/tty) 2>/dev/null; then
|
|
"bin/skald-setup" </dev/tty
|
|
else
|
|
echo " From a terminal, run:"
|
|
echo " cd ${INSTALL_DIR} && ./bin/skald-setup"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
info "✅ Skald Circle (${DISPLAY_VERSION}) installed successfully!"
|
|
echo ""
|
|
|
|
# Write channel tag for future updates
|
|
echo "$CHANNEL" > "$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 ""
|