install: polish install/uninstall scripts with Docker setup, dep checks, and first-run flow
Nightly Build / build (push) Failing after 2m58s
Nightly Build / build (push) Failing after 2m58s
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
Rust async web app (Tokio + Axum). Runs as a local chat server with LLM tool-calling and a sub-agent system.
|
||||
|
||||
> **Never `git commit` unless explicitly asked.** Staging, building, running and testing are fine on your own initiative; creating a commit is not. Do the work, leave it in the working tree, and let the user commit — or ask them to — even when a commit looks like the obvious next step.
|
||||
>
|
||||
> **Commit messages must be in English.**
|
||||
|
||||
## What this repository is
|
||||
|
||||
|
||||
@@ -1,9 +1,101 @@
|
||||
# Skald Circle — SKALD
|
||||
|
||||
## Installation
|
||||
|
||||
### Stable release
|
||||
|
||||
```sh
|
||||
curl -fsSL https://builds.skaldagent.net/install.sh | bash
|
||||
```
|
||||
|
||||
### Nightly (latest automatic build)
|
||||
|
||||
```sh
|
||||
curl -fsSL https://builds.skaldagent.net/install-nightly.sh | bash
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
| Required | Notes |
|
||||
|----------|-------|
|
||||
| **Docker** | User container sandbox. The installer can install it |
|
||||
| **Linux (amd64/arm64)** or **macOS ARM64 (Apple Silicon)** | Intel Mac not supported |
|
||||
| **systemd** (Linux) or **launchd** (macOS) | For running as a service |
|
||||
| **Python 3** (optional) | For Python MCP servers (Gmail, GCal, GMaps, weather, SSH) and local TTS plugins |
|
||||
| **Node.js ≥ 18** (optional) | For WhatsApp MCP server |
|
||||
|
||||
The installer checks each requirement and offers to install Docker if missing.
|
||||
Python and Node.js are optional — the server starts regardless, but certain MCP servers won't work.
|
||||
|
||||
### What it does
|
||||
|
||||
1. Downloads the tarball from `builds.skaldagent.net`
|
||||
2. Extracts to `~/.local/share/skald-circle/` (or `$SKALD_DIR`)
|
||||
3. Configures the service (systemd user service / launchd agent)
|
||||
4. Runs `skald-setup` to create the admin account
|
||||
5. The server starts at `https://localhost:8443`
|
||||
|
||||
### Uninstallation
|
||||
|
||||
```sh
|
||||
curl -fsSL https://builds.skaldagent.net/install.sh | bash
|
||||
# The tarball contains uninstall.sh:
|
||||
~/.local/share/skald-circle/uninstall.sh
|
||||
```
|
||||
|
||||
Or, after installation: `~/.local/share/skald-circle/uninstall.sh`
|
||||
|
||||
### From source
|
||||
|
||||
```sh
|
||||
git clone https://github.com/.../skald-circle.git
|
||||
cd skald-circle
|
||||
cargo build --release
|
||||
./run.sh
|
||||
```
|
||||
|
||||
## Current status
|
||||
|
||||
New application with agents and chatbots to help families and small groups collaborate, with supervised chat for children and vulnerable people.
|
||||
|
||||
## Installer & startup architecture
|
||||
|
||||
```
|
||||
install.sh
|
||||
├── extracts tarball
|
||||
├── creates .venv (inline — does NOT call run.sh)
|
||||
└── runs skald-setup for interactive config
|
||||
|
||||
systemd service → ExecStart=run.sh
|
||||
run.sh (supervisor)
|
||||
├── creates .venv if it doesn't exist (for local dev)
|
||||
├── runs skald-setup (first run only)
|
||||
└── loop: executes skald binary, restart on exit 255
|
||||
```
|
||||
|
||||
**Rule**: `install.sh` must NEVER call `run.sh`. The venv is created inline in the installer.
|
||||
`run.sh` is only for the service supervisor or local development.
|
||||
`skald-setup` is the only setup executable called by install.sh.
|
||||
|
||||
## Bug fix: install.sh stuck on "Setting up Python virtual environment" ✅
|
||||
|
||||
**Problem**: the installer called `"$INSTALL_DIR/run.sh"` to create the venv. But `run.sh` after the venv runs `skald-setup` and then the server in a loop, hanging the installer forever.
|
||||
|
||||
**Fix**: the venv is now created *inline* in `install.sh` and `install-nightly.sh`, using the same logic as `run.sh` (uv > python3) but without starting the server.
|
||||
|
||||
## Bug fix: "Unit docker.service not found" in user service ✅
|
||||
|
||||
**Problem**: the systemd user unit had `Requires=docker.service`, but `docker.service` is a system-level unit (not a user unit). `systemctl --user` couldn't find it and refused to start Skald.
|
||||
|
||||
**Fix**: removed `Requires=docker.service` from the user unit template in both install scripts. Kept `After=docker.service` (advisory, doesn't block if the unit isn't found).
|
||||
|
||||
## Bug fix: skald-setup non interattivo con curl | bash ✅
|
||||
|
||||
**Problem**: `skald-setup` controlla `isatty(0)`, ma con `curl ... | bash` stdin è un pipe, quindi saltava senza chiedere username/password. L'installer arrivava fino in fondo ma senza aver creato l'admin.
|
||||
|
||||
**Fix**: se `IS_INTERACTIVE=false` ma `/dev/tty` esiste, l'installer chiama `skald-setup </dev/tty`.
|
||||
|
||||
|
||||
### Agent icons — completed ✅
|
||||
|
||||
All 11 agents now have **Vector Paintings** icons (painterly vector, warm and family-friendly), generated via ComfyUI:
|
||||
@@ -29,7 +121,7 @@ All 11 agents now have **Vector Paintings** icons (painterly vector, warm and fa
|
||||
- New i18n system (core-api + plugin-mobile-connector + web)
|
||||
- Configuration system refactoring
|
||||
|
||||
### Auto-build CI/CD ✅
|
||||
## Auto-build CI/CD (NiPoGi)
|
||||
|
||||
Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
|
||||
|
||||
@@ -63,7 +155,7 @@ Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
|
||||
- Test release workflow with a PR
|
||||
- Build first macOS ARM64 binary on MacBook, upload to `builds.skaldagent.net`
|
||||
|
||||
### macOS support
|
||||
## macOS support
|
||||
|
||||
**Supported**: macOS ARM64 (Apple Silicon M1+), Intel not supported.
|
||||
|
||||
@@ -75,7 +167,7 @@ Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
|
||||
| **Package script** (`ci/package.sh`) | ✅ | Accepts `--os darwin`, strips best-effort |
|
||||
| **Binary** | ⏳ Not yet built | Build natively on MacBook, deploy to builds.skaldagent.net |
|
||||
|
||||
#### How to build for macOS (on MacBook)
|
||||
### How to build for macOS (on MacBook)
|
||||
|
||||
```sh
|
||||
cargo build --release -p skald-setup -p skald # includes whisper
|
||||
@@ -85,7 +177,7 @@ cargo build --release -p skald-setup -p skald # includes whisper
|
||||
|
||||
Upload the resulting `dist/skald-circle-v0.1.0-darwin-arm64.tar.gz` to the NiPoGi's `builds.skaldagent.net/releases/v0.1.0/` directory.
|
||||
|
||||
#### Cross-compilation from NiPoGi (research notes 🧪)
|
||||
### Cross-compilation from NiPoGi (research notes 🧪)
|
||||
|
||||
Cross-compiling for `aarch64-apple-darwin` from the NiPoGi using **zig** + **cargo-zigbuild** was attempted but hit blockers.
|
||||
|
||||
|
||||
@@ -86,10 +86,21 @@ fn usage() -> String {
|
||||
SKALD_SKIP_SETUP=1 never prompt, even on a terminal"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn run(mode: Mode) -> Result<std::process::ExitCode> {
|
||||
// Opening the pool creates `database/system.db` and its schema if absent —
|
||||
// the same call the server makes, so setup and server agree on the layout.
|
||||
let pool = std::sync::Arc::new(
|
||||
db::init_system_pool(SYSTEM_DB_PATH)
|
||||
.await
|
||||
.context("opening the system database")?,
|
||||
let db_path = exe_dir.join(SYSTEM_DB_PATH);
|
||||
|
||||
// Opening the pool creates the database and its schema if absent — the same
|
||||
// call the server makes, so setup and server agree on the layout.
|
||||
let pool = std::sync::Arc::new(
|
||||
db::init_system_pool(&db_path)
|
||||
.await
|
||||
.context("opening the system database")?,
|
||||
let pool = std::sync::Arc::new(
|
||||
db::init_system_pool(SYSTEM_DB_PATH)
|
||||
.await
|
||||
|
||||
+230
-21
@@ -7,28 +7,161 @@
|
||||
# Supports Linux (systemd) and macOS ARM64 (launchd).
|
||||
# Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR).
|
||||
#
|
||||
# Inspired by: https://hermes-agent.nousresearch.com/install.sh
|
||||
# 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=''; BOLD=''; NC=''
|
||||
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
|
||||
}
|
||||
|
||||
# ── 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 — 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)"
|
||||
@@ -63,18 +196,49 @@ elif [ "$OS" = "darwin" ]; then
|
||||
command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; }
|
||||
fi
|
||||
|
||||
# ── Download & extract ────────────────────────────────────────────────────────
|
||||
# ── 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
|
||||
|
||||
header "📦 Skald Circle — Nightly Installer"
|
||||
# ── Summary + confirmation ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
header "Installation summary"
|
||||
echo ""
|
||||
echo " What : Skald Circle (${DISPLAY_VERSION})"
|
||||
echo " Platform : ${OS}/${ARCH}"
|
||||
echo " Install dir : ${INSTALL_DIR}"
|
||||
echo " Download : ${TARBALL_URL}"
|
||||
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 ""
|
||||
|
||||
info "↓ Downloading Skald Circle nightly …"
|
||||
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 (${DISPLAY_VERSION}) …"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
curl -fsSL "$TARBALL_URL" | tar xz -C "$INSTALL_DIR" --strip-components=1
|
||||
|
||||
@@ -86,16 +250,26 @@ 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 …"
|
||||
"$INSTALL_DIR/run.sh" >/dev/null 2>&1 || true
|
||||
VENV_DIR="${INSTALL_DIR}/.venv"
|
||||
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
||||
|
||||
# ── First-run setup (interactive) ─────────────────────────────────────────────
|
||||
if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then
|
||||
header "⚙️ First-time setup"
|
||||
echo " You will be asked to configure your LLM provider and create an admin user."
|
||||
echo ""
|
||||
"$INSTALL_DIR/bin/skald-setup"
|
||||
echo ""
|
||||
if [ ! -f "$VENV_DIR/bin/python3" ]; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
uv venv "$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 ────────────────────────────────────────────────────────────
|
||||
@@ -106,9 +280,9 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
|
||||
|
||||
cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE
|
||||
[Unit]
|
||||
Description=Skald Circle (nightly)
|
||||
Description=Skald Circle (${DISPLAY_VERSION})
|
||||
Documentation=https://skaldagent.net
|
||||
After=network.target
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -187,9 +361,44 @@ elif [ -n "${NOSYSTEMD:-}" ]; then
|
||||
warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh"
|
||||
fi
|
||||
|
||||
# ── Welcome + first-run setup ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
info "✅ Skald Circle (nightly) installed successfully!"
|
||||
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 ""
|
||||
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 ""
|
||||
echo " ${INSTALL_DIR}/run.sh"
|
||||
echo " ${INSTALL_DIR}/bin/skald"
|
||||
echo " ${INSTALL_DIR}/bin/skald-setup"
|
||||
|
||||
+221
-22
@@ -7,29 +7,168 @@
|
||||
# Supports Linux (systemd) and macOS ARM64 (launchd).
|
||||
# Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR).
|
||||
#
|
||||
# Inspired by: https://hermes-agent.nousresearch.com/install.sh
|
||||
# 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=''; BOLD=''; NC=''
|
||||
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/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
|
||||
}
|
||||
|
||||
# ── 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"
|
||||
|
||||
# Add current user to docker group so they don't need sudo for every command
|
||||
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 ────────────────────────────────────────────────
|
||||
# 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)"
|
||||
@@ -63,14 +202,15 @@ elif [ "$OS" = "darwin" ]; then
|
||||
command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; }
|
||||
fi
|
||||
|
||||
# ── Fetch latest version ──────────────────────────────────────────────────────
|
||||
BASE_URL="https://builds.skaldagent.net"
|
||||
LATEST_URL="${BASE_URL}/releases/LATEST"
|
||||
|
||||
header "📦 Skald Circle — Installer"
|
||||
# ── 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
|
||||
@@ -81,12 +221,26 @@ fi
|
||||
|
||||
TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz"
|
||||
|
||||
# ── Summary + confirmation ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo " Version : ${VERSION}"
|
||||
header "Installation summary"
|
||||
echo ""
|
||||
echo " What : Skald Circle ${VERSION}"
|
||||
echo " Platform : ${OS}/${ARCH}"
|
||||
echo " Install dir : ${INSTALL_DIR}"
|
||||
echo " Download : ${TARBALL_URL}"
|
||||
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} …"
|
||||
@@ -101,16 +255,26 @@ 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 …"
|
||||
"$INSTALL_DIR/run.sh" >/dev/null 2>&1 || true
|
||||
VENV_DIR="${INSTALL_DIR}/.venv"
|
||||
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"
|
||||
|
||||
# ── First-run setup (interactive) ─────────────────────────────────────────────
|
||||
if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then
|
||||
header "⚙️ First-time setup"
|
||||
echo " You will be asked to configure your LLM provider and create an admin user."
|
||||
echo ""
|
||||
"$INSTALL_DIR/bin/skald-setup"
|
||||
echo ""
|
||||
if [ ! -f "$VENV_DIR/bin/python3" ]; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
uv venv "$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 ────────────────────────────────────────────────────────────
|
||||
@@ -123,7 +287,7 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
|
||||
[Unit]
|
||||
Description=Skald Circle (release ${VERSION})
|
||||
Documentation=https://skaldagent.net
|
||||
After=network.target
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -202,9 +366,44 @@ 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 ${VERSION} installed successfully!"
|
||||
echo ""
|
||||
echo " ${INSTALL_DIR}/run.sh"
|
||||
echo " ${INSTALL_DIR}/bin/skald"
|
||||
echo " ${INSTALL_DIR}/bin/skald-setup"
|
||||
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 ""
|
||||
|
||||
Reference in New Issue
Block a user