Newer
Older
navi-1 / deploy / install.sh
#!/usr/bin/env bash
# Navi — one-command deployment for a server module.
#
#   ssh server
#   git clone -b deploy <repo-url> navi-1 && cd navi-1
#   bash deploy/install.sh
#
# What this does (idempotent — safe to re-run):
#   1. checks prerequisites (python 3.11+, docker, docker compose)
#   2. provisions .env from deploy/env.template (generates a DB password)
#   3. starts dockerized PostgreSQL (pgvector, restart: always, 127.0.0.1 only)
#      and installs the vector + pg_trgm extensions
#   4. builds the venv and installs navi (navi-server + navi-code entry points)
#   5. writes + enables + starts the systemd unit (Restart=always — the
#      server lives from installation and survives reboots)
#   6. waits for /health, symlinks navi-code/navi-server into PATH
#
# Default deployed state: web UI off, auth off, terminal client only.
# Re-enable the web panel later: set NAVI_WEBCLIENT_ENABLED=true in .env
# and `sudo systemctl restart navi`.

set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"

say()  { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
err()  { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; }

SUDO=""
[ "$(id -u)" -eq 0 ] || SUDO="sudo"

# ── 1. prerequisites ────────────────────────────────────────────────
say "Checking prerequisites"

if ! command -v python3 >/dev/null; then
    err "python3 not found — install Python 3.11+ first"; exit 1
fi
PYOK=$(python3 - <<'EOF'
import sys
print(0 if sys.version_info >= (3, 11) else 1)
EOF
)
if [ "$PYOK" != "0" ]; then
    err "python3 is older than 3.11 — upgrade and re-run"; exit 1
fi

if ! command -v docker >/dev/null; then
    err "docker not found — install Docker (https://docs.docker.com/engine/install/) first"; exit 1
fi
if ! $SUDO docker compose version >/dev/null 2>&1; then
    err "docker compose (v2 plugin) not available"; exit 1
fi
echo "OK: python3 $(python3 --version | cut -d' ' -f2), docker + compose"

# ── 2. .env ────────────────────────────────────────────────────────
say "Provisioning .env"
NEED_PASS=0
if [ ! -f .env ]; then
    cp deploy/env.template .env
    NEED_PASS=1
    echo "Created .env from deploy/env.template."
elif grep -q "CHANGEME" .env; then
    # The deploy branch ships a committed .env with placeholder secrets —
    # regenerate the DB password on first install.
    NEED_PASS=1
    echo ".env carries placeholders — generating real secrets."
else
    echo ".env already exists — keeping it."
fi
if [ "$NEED_PASS" = "1" ]; then
    DB_PASS="$(head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 32)"
    sed -i "s|^NAVI_DB_PASSWORD=.*|NAVI_DB_PASSWORD=${DB_PASS}|" .env
    sed -i "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://navi:${DB_PASS}@127.0.0.1:5432/navi|" .env
    echo "Generated DB password."
fi
grep -qE "^OLLAMA_API_KEY=.\+" .env || echo "TODO left in .env: OLLAMA_API_KEY (Ollama Cloud key)."

# ── 3. PostgreSQL ───────────────────────────────────────────────────
say "Starting dockerized PostgreSQL (pgvector)"
$SUDO docker compose -f deploy/docker-compose.yml --env-file .env up -d
for i in $(seq 1 30); do
    if $SUDO docker exec navi-postgres pg_isready -U navi -d navi >/dev/null 2>&1; then break; fi
    sleep 2
done
$SUDO docker exec navi-postgres psql -U navi -d navi -q \
    -c "CREATE EXTENSION IF NOT EXISTS vector;" \
    -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
echo "PostgreSQL ready (127.0.0.1:5432, volume navi-pgdata)."

# ── 4. venv + install ───────────────────────────────────────────────
say "Building venv and installing navi"
if [ ! -d .venv ]; then
    python3 -m venv .venv
fi
./.venv/bin/pip install --upgrade pip -q
./.venv/bin/pip install -e .
echo "Installed entry points: navi-server, navi-code"

# ── 5. systemd unit ─────────────────────────────────────────────────
say "Installing systemd unit (navi.service)"
RUN_USER="$(id -un)"
cat > /tmp/navi.service <<EOF
[Unit]
Description=Navi server (agent system API)
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
User=${RUN_USER}
WorkingDirectory=${REPO_DIR}
ExecStart=${REPO_DIR}/.venv/bin/navi-server
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
$SUDO mv /tmp/navi.service /etc/systemd/system/navi.service
$SUDO systemctl daemon-reload
$SUDO systemctl enable --now navi
echo "navi.service enabled and started (restarts on failure, starts on boot)."

# ── 6. health check + PATH symlinks ────────────────────────────────
say "Waiting for the server to become healthy"
NAVI_PORT_CFG="$(grep -E '^NAVI_PORT=' .env | tail -1 | cut -d= -f2 || true)"
NAVI_PORT_CFG="${NAVI_PORT_CFG:-8000}"
HEALTH="ok"
for i in $(seq 1 60); do
    if curl -sf -o /dev/null "http://127.0.0.1:${NAVI_PORT_CFG}/health" 2>/dev/null; then break; fi
    if [ "$i" = "60" ]; then HEALTH="fail"; fi
    sleep 2
done
if [ "$HEALTH" != "ok" ]; then
    err "server did not become healthy — check: journalctl -u navi -e"
    exit 1
fi
echo "Server healthy at http://127.0.0.1:${NAVI_PORT_CFG}/health"

say "Symlinking navi-code and navi-server into /usr/local/bin"
for bin in navi-code navi-server; do
    $SUDO ln -sf "$REPO_DIR/.venv/bin/$bin" "/usr/local/bin/$bin"
done

cat <<'EOF'

Deployment complete.
  • navi-code        — terminal client (run it from any shell)
  • systemctl status navi — server supervision
  • journalctl -u navi -f   — live logs

Still TODO (if not filled yet): OLLAMA_API_KEY in .env, then
    sudo systemctl restart navi

Re-enable the web panel later: set NAVI_WEBCLIENT_ENABLED=true in .env
and restart the unit. Nothing else is needed — the code is all here.
EOF