diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100755 index 000000000..8a140ea50 --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,578 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# Moltbot Unified Installer +# ============================================================================= +# Supports: +# - Modes: gateway (Caddy/Nginx + Moltbot) | node (Moltbot only) +# - OS: macOS, Debian/Ubuntu, Rocky Linux +# - Install methods: npm | git +# ============================================================================= + +MODE="${MODE:-gateway}" +PROXY="${PROXY:-caddy}" +INSTALL_METHOD="${INSTALL_METHOD:-npm}" +GIT_REPO="${GIT_REPO:-https://github.com/cloud-neutral-toolkit/clawdbot.svc.plus.git}" +CLAWDBOT_VERSION="${CLAWDBOT_VERSION:-latest}" +CERTBOT_EMAIL="${CERTBOT_EMAIL:-}" +PUBLIC_SCHEME="https" + +# ============================================================================= +# Detect OS +# ============================================================================= +OS_NAME="$(uname -s 2>/dev/null || true)" +OS_FAMILY="unknown" +OS_DISTRO="" + +case "$OS_NAME" in + Darwin) + OS_FAMILY="darwin" + ;; + Linux) + OS_FAMILY="linux" + if [[ -f /etc/os-release ]]; then + . /etc/os-release + OS_DISTRO="${ID:-unknown}" + else + echo "❌ Unsupported Linux (missing /etc/os-release)." + exit 1 + fi + ;; + *) + echo "❌ Unsupported OS: ${OS_NAME:-unknown}" + exit 1 + ;; +esac + +# ============================================================================= +# Usage +# ============================================================================= +usage() { + cat <<'EOF' +Usage: + init.sh [domain] + +Environment Variables: + MODE - Deployment mode: "gateway" (default) or "node" + gateway: Caddy/Nginx + Moltbot + Node.js 24 + node: Moltbot + Node.js 24 only + PROXY - Proxy type: "caddy" (default) or "nginx" (gateway mode only) + INSTALL_METHOD - "npm" (default) or "git" + CLAWDBOT_VERSION - Version to install (default: "latest") + CERTBOT_EMAIL - Email for Certbot (nginx mode only) + GIT_REPO - Git repository URL (git install method only) + +Examples: + # Gateway mode with Caddy (default) + ./init.sh clawdbot.svc.plus + + # Node mode (no proxy) + MODE=node ./init.sh clawdbot.svc.plus + + # Gateway mode with Nginx + PROXY=nginx CERTBOT_EMAIL=admin@example.com ./init.sh clawdbot.svc.plus + + # Install from Git + INSTALL_METHOD=git ./init.sh clawdbot.svc.plus + +Supported OS: + - macOS (Homebrew required for gateway mode) + - Debian/Ubuntu + - Rocky Linux / RHEL-based + +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +# ============================================================================= +# Validate Parameters +# ============================================================================= +MODE="$(tr '[:upper:]' '[:lower:]' <<< "$MODE")" +if [[ "$MODE" != "gateway" && "$MODE" != "node" ]]; then + echo "❌ Unsupported MODE '$MODE'. Use 'gateway' or 'node'." + exit 1 +fi + +PROXY="$(tr '[:upper:]' '[:lower:]' <<< "$PROXY")" +if [[ "$PROXY" != "caddy" && "$PROXY" != "nginx" ]]; then + echo "❌ Unsupported PROXY '$PROXY'. Use 'caddy' or 'nginx'." + exit 1 +fi + +if [[ "$MODE" == "node" && "$PROXY" != "caddy" ]]; then + echo "⚠️ Node mode does not use a proxy. Ignoring PROXY setting." +fi + +INSTALL_METHOD="$(tr '[:upper:]' '[:lower:]' <<< "$INSTALL_METHOD")" +if [[ "$INSTALL_METHOD" != "npm" && "$INSTALL_METHOD" != "git" ]]; then + echo "❌ Unsupported INSTALL_METHOD '$INSTALL_METHOD'. Use 'npm' or 'git'." + exit 1 +fi + +# ============================================================================= +# Determine Domain +# ============================================================================= +DOMAIN="${1:-}" +if [[ -z "$DOMAIN" ]]; then + DOMAIN="$(hostname -f 2>/dev/null || true)" + if [[ -z "$DOMAIN" ]]; then + DOMAIN="$(hostname 2>/dev/null || true)" + fi +fi + +if [[ -z "$DOMAIN" ]]; then + echo "❌ Failed to determine domain (hostname). Pass one explicitly." + exit 1 +fi + +# ============================================================================= +# Helper Functions +# ============================================================================= +as_root() { + if [[ "$(id -u)" -eq 0 ]]; then + if [[ "${1:-}" == "-E" ]]; then + shift + fi + "$@" + else + sudo "$@" + fi +} + +run_as_user() { + local user="${SUDO_USER:-$USER}" + if [[ "$user" == "root" ]]; then + echo "❌ Run this installer as a non-root user (with sudo available)." + exit 1 + fi + sudo -u "$user" -H "$@" +} + +# ============================================================================= +# Package Installation +# ============================================================================= +ensure_packages_debian() { + local packages=(git curl ca-certificates) + if [[ "$MODE" == "gateway" ]]; then + if [[ "$PROXY" == "nginx" ]]; then + packages+=(nginx certbot python3-certbot-nginx ufw) + else + packages+=(caddy ufw) + fi + fi + as_root apt-get update + as_root apt-get install -y "${packages[@]}" +} + +ensure_packages_rocky() { + local packages=(git curl ca-certificates) + if [[ "$MODE" == "gateway" ]]; then + if [[ "$PROXY" == "nginx" ]]; then + packages+=(nginx certbot python3-certbot-nginx firewalld) + else + # Caddy requires EPEL or manual installation on Rocky + echo "⚠️ Caddy installation on Rocky Linux requires EPEL or manual setup." + echo " Installing dependencies only. You may need to install Caddy manually." + packages+=(firewalld) + fi + fi + as_root dnf install -y epel-release || true + as_root dnf install -y "${packages[@]}" +} + +ensure_packages_darwin() { + if [[ "$MODE" == "gateway" && "$PROXY" == "nginx" ]]; then + echo "❌ nginx + Certbot is not supported on macOS. Use PROXY=caddy." + exit 1 + fi + if ! command -v brew >/dev/null 2>&1; then + echo "❌ Homebrew is required on macOS. Install from https://brew.sh" + exit 1 + fi + local packages=(git curl) + if [[ "$MODE" == "gateway" ]]; then + packages+=(caddy) + fi + brew install "${packages[@]}" +} + +ensure_packages() { + case "$OS_FAMILY" in + darwin) + ensure_packages_darwin + ;; + linux) + case "$OS_DISTRO" in + debian|ubuntu) + ensure_packages_debian + ;; + rocky|rhel|centos|fedora) + ensure_packages_rocky + ;; + *) + echo "❌ Unsupported Linux distribution: $OS_DISTRO" + exit 1 + ;; + esac + ;; + esac +} + +# ============================================================================= +# Node.js 24 Installation +# ============================================================================= +ensure_node24_debian() { + local need_install=1 + if command -v node >/dev/null 2>&1; then + local major + major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')" + if [[ "${major:-0}" -ge 24 ]]; then + need_install=0 + fi + fi + if [[ "$need_install" -eq 1 ]]; then + as_root apt-get update + as_root apt-get install -y curl ca-certificates + if [[ $(id -u) -eq 0 ]]; then + curl -fsSL https://deb.nodesource.com/setup_24.x | bash - + else + curl -fsSL https://deb.nodesource.com/setup_24.x | as_root -E bash - + fi + as_root apt-get install -y nodejs + fi +} + +ensure_node24_rocky() { + local need_install=1 + if command -v node >/dev/null 2>&1; then + local major + major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')" + if [[ "${major:-0}" -ge 24 ]]; then + need_install=0 + fi + fi + if [[ "$need_install" -eq 1 ]]; then + if [[ $(id -u) -eq 0 ]]; then + curl -fsSL https://rpm.nodesource.com/setup_24.x | bash - + else + curl -fsSL https://rpm.nodesource.com/setup_24.x | as_root -E bash - + fi + as_root dnf install -y nodejs + fi +} + +ensure_node24_darwin() { + local need_install=1 + if command -v node >/dev/null 2>&1; then + local major + major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')" + if [[ "${major:-0}" -ge 24 ]]; then + need_install=0 + fi + fi + if [[ "$need_install" -eq 1 ]]; then + if command -v brew >/dev/null 2>&1; then + brew install node@24 || brew install node + if brew list node@24 >/dev/null 2>&1; then + brew link --overwrite --force node@24 + fi + else + local arch pkg_name pkg_url pkg_path + arch="$(uname -m)" + case "$arch" in + arm64) arch="arm64" ;; + x86_64) arch="x64" ;; + *) + echo "❌ Unsupported macOS architecture: ${arch}" + exit 1 + ;; + esac + pkg_name="$(curl -fsSL https://nodejs.org/dist/latest-v24.x/ \ + | awk -F\" -v arch="$arch" '/node-v24.*-darwin-/{if ($2 ~ ("-darwin-" arch "\\.pkg$")) {print $2; exit}}')" + if [[ -z "$pkg_name" ]]; then + echo "❌ Failed to find Node.js v24 macOS installer." + exit 1 + fi + pkg_url="https://nodejs.org/dist/latest-v24.x/${pkg_name}" + pkg_path="/tmp/${pkg_name}" + curl -fsSL "$pkg_url" -o "$pkg_path" + as_root installer -pkg "$pkg_path" -target / + fi + fi +} + +ensure_node24() { + case "$OS_FAMILY" in + darwin) + ensure_node24_darwin + ;; + linux) + case "$OS_DISTRO" in + debian|ubuntu) + ensure_node24_debian + ;; + rocky|rhel|centos|fedora) + ensure_node24_rocky + ;; + esac + ;; + esac +} + +# ============================================================================= +# pnpm Installation +# ============================================================================= +ensure_pnpm() { + run_as_user corepack enable + run_as_user corepack prepare pnpm@latest --activate +} + +# ============================================================================= +# Firewall Configuration +# ============================================================================= +configure_firewall_debian() { + local ports=(22/tcp 80/tcp 443/tcp 18789/tcp) + for port in "${ports[@]}"; do + as_root ufw allow "${port}" >/dev/null + done + as_root ufw default allow outgoing >/dev/null + as_root ufw default deny incoming >/dev/null + if as_root ufw status | grep -q "Status: inactive"; then + as_root ufw --force enable >/dev/null + fi +} + +configure_firewall_rocky() { + local ports=(22 80 443 18789) + as_root systemctl enable --now firewalld + for port in "${ports[@]}"; do + as_root firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null + done + as_root firewall-cmd --reload >/dev/null +} + +configure_firewall_darwin() { + # macOS uses application-level firewall + return 0 +} + +configure_firewall() { + if [[ "$MODE" != "gateway" ]]; then + return 0 + fi + case "$OS_FAMILY" in + darwin) + configure_firewall_darwin + ;; + linux) + case "$OS_DISTRO" in + debian|ubuntu) + configure_firewall_debian + ;; + rocky|rhel|centos|fedora) + configure_firewall_rocky + ;; + esac + ;; + esac +} + +# ============================================================================= +# Moltbot Installation +# ============================================================================= +install_clawdbot_npm() { + as_root npm install -g "clawdbot@${CLAWDBOT_VERSION}" +} + +install_clawdbot_git() { + local install_dir="/opt/clawdbot-svc-plus" + if [[ ! -d "$install_dir" ]]; then + as_root mkdir -p "$install_dir" + run_as_user git clone "$GIT_REPO" "$install_dir" + else + run_as_user git -C "$install_dir" fetch --all --prune + run_as_user git -C "$install_dir" checkout main + run_as_user git -C "$install_dir" reset --hard origin/main + fi + run_as_user bash -c "cd $install_dir && pnpm install && pnpm build" + as_root npm install -g "$install_dir" +} + +install_clawdbot() { + if [[ "$INSTALL_METHOD" == "git" ]]; then + install_clawdbot_git + else + install_clawdbot_npm + fi +} + +# ============================================================================= +# Moltbot Configuration +# ============================================================================= +configure_clawdbot() { + run_as_user clawdbot onboard --install-daemon + if [[ "$MODE" == "gateway" ]]; then + run_as_user clawdbot config set gateway.trustedProxies.0 127.0.0.1 + fi +} + +# ============================================================================= +# Proxy Configuration +# ============================================================================= +configure_nginx() { + local vhost="/etc/nginx/sites-available/clawdbot-${DOMAIN}.conf" + if [[ ! -f "$vhost" ]]; then + cat </dev/null +server { + listen 80; + server_name ${DOMAIN}; + + location / { + proxy_pass http://127.0.0.1:18789; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +EOF + fi + as_root ln -sf "$vhost" "/etc/nginx/sites-enabled/$(basename "$vhost")" + as_root nginx -t + as_root systemctl enable --now nginx + as_root systemctl reload nginx +} + +configure_certbot() { + local email_args=("--register-unsafely-without-email") + if [[ -n "$CERTBOT_EMAIL" ]]; then + email_args=("--email" "$CERTBOT_EMAIL" "--agree-tos" "--no-eff-email") + fi + as_root certbot --nginx "${email_args[@]}" --redirect -d "$DOMAIN" || true +} + +configure_caddy() { + local service="/etc/caddy/Caddyfile" + if [[ "$OS_FAMILY" == "darwin" ]]; then + if command -v brew >/dev/null 2>&1; then + service="$(brew --prefix)/etc/Caddyfile" + fi + fi + cat </dev/null +${DOMAIN} { + reverse_proxy 127.0.0.1:18789 +} +EOF + if [[ "$OS_FAMILY" == "darwin" ]]; then + if command -v brew >/dev/null 2>&1; then + brew services start caddy || brew services restart caddy + else + as_root caddy start --config "$service" + fi + else + as_root systemctl enable --now caddy + as_root systemctl reload caddy + fi +} + +configure_proxy() { + if [[ "$MODE" != "gateway" ]]; then + return 0 + fi + if [[ "$PROXY" == "nginx" ]]; then + configure_nginx + configure_certbot + else + configure_caddy + fi +} + +# ============================================================================= +# Health Checks +# ============================================================================= +health_check_url() { + local url="$1" + for i in $(seq 1 5); do + if curl -fsS --max-time 5 --retry 3 --retry-delay 2 "$url" >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + return 1 +} + +run_health_checks() { + if ! health_check_url http://127.0.0.1:18789; then + echo "⚠️ Warning: local gateway health check failed." + fi + if [[ "$MODE" == "gateway" ]]; then + local target="${PUBLIC_SCHEME}://${DOMAIN}" + if ! health_check_url "${target}"; then + echo "⚠️ Warning: public health check failed for ${target}. TLS might not be active yet." + fi + fi +} + +# ============================================================================= +# Main Execution +# ============================================================================= +echo "==> Moltbot Installer" +echo " Mode: ${MODE}" +echo " Domain: ${DOMAIN}" +echo " OS: ${OS_FAMILY} (${OS_DISTRO:-N/A})" +if [[ "$MODE" == "gateway" ]]; then + echo " Proxy: ${PROXY}" +fi +echo "" + +ensure_packages +ensure_node24 +ensure_pnpm +configure_firewall +install_clawdbot +configure_clawdbot +configure_proxy +run_health_checks + +cat </dev/null 2>&1; then + local major + major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')" + if [[ "${major:-0}" -ge 24 ]]; then + need_install=0 + fi + fi + if [[ "$need_install" -eq 1 ]]; then + if command -v brew >/dev/null 2>&1; then + brew install node@24 || brew install node + if brew list node@24 >/dev/null 2>&1; then + brew link --overwrite --force node@24 + fi + else + local arch pkg_name pkg_url pkg_path + arch="$(uname -m)" + case "$arch" in + arm64) arch="arm64" ;; + x86_64) arch="x64" ;; + *) + echo "Unsupported macOS architecture: ${arch}" + exit 1 + ;; + esac + pkg_name="$(curl -fsSL https://nodejs.org/dist/latest-v24.x/ \ + | awk -F\" -v arch="$arch" '/node-v24.*-darwin-/{if ($2 ~ ("-darwin-" arch "\\.pkg$")) {print $2; exit}}')" + if [[ -z "$pkg_name" ]]; then + echo "Failed to find a Node.js v24 macOS installer." + exit 1 + fi + pkg_url="https://nodejs.org/dist/latest-v24.x/${pkg_name}" + pkg_path="/tmp/${pkg_name}" + curl -fsSL "$pkg_url" -o "$pkg_path" + as_root installer -pkg "$pkg_path" -target / + fi + fi +} + ensure_packages() { local packages=(git curl ca-certificates ufw) if [[ "$PROXY" == "nginx" ]]; then @@ -121,6 +180,18 @@ ensure_packages() { as_root apt-get install -y "${packages[@]}" } +ensure_packages_darwin() { + if [[ "$PROXY" == "nginx" ]]; then + echo "nginx + Certbot is not supported on macOS in this installer. Use PROXY=caddy." + exit 1 + fi + if ! command -v brew >/dev/null 2>&1; then + echo "Homebrew is required on macOS. Install it from https://brew.sh and re-run." + exit 1 + fi + brew install git caddy curl +} + ensure_pnpm() { run_as_user corepack enable run_as_user corepack prepare pnpm@latest --activate @@ -138,6 +209,11 @@ configure_firewall() { fi } +configure_firewall_darwin() { + # macOS uses application-level firewall; leave port management to the operator. + return 0 +} + install_clawdbot_npm() { as_root npm install -g "clawdbot@${CLAWDBOT_VERSION}" } @@ -206,13 +282,26 @@ configure_certbot() { configure_caddy() { local service="/etc/caddy/Caddyfile" + if [[ "$OS_FAMILY" == "darwin" ]]; then + if command -v brew >/dev/null 2>&1; then + service="$(brew --prefix)/etc/Caddyfile" + fi + fi cat </dev/null ${DOMAIN} { reverse_proxy 127.0.0.1:18789 } EOF - as_root systemctl enable --now caddy - as_root systemctl reload caddy + if [[ "$OS_FAMILY" == "darwin" ]]; then + if command -v brew >/dev/null 2>&1; then + brew services start caddy || brew services restart caddy + else + as_root caddy start --config "$service" + fi + else + as_root systemctl enable --now caddy + as_root systemctl reload caddy + fi } configure_proxy() { @@ -246,10 +335,19 @@ run_health_checks() { } echo "==> Domain: ${DOMAIN}" -ensure_packages -ensure_node24 +if [[ "$OS_FAMILY" == "darwin" ]]; then + ensure_packages_darwin + ensure_node24_darwin +else + ensure_packages + ensure_node24 +fi ensure_pnpm -configure_firewall +if [[ "$OS_FAMILY" == "darwin" ]]; then + configure_firewall_darwin +else + configure_firewall +fi install_clawdbot configure_clawdbot configure_proxy @@ -263,5 +361,13 @@ Access control and TLS are handled by ${PROXY^^}. If you need to tweak config later: - \`clawdbot config get gateway.trustedProxies\` - - \`journalctl --user -u clawdbot-gateway --no-pager\` +EOF + +if [[ "$OS_FAMILY" == "darwin" ]]; then + cat <<'EOF' + - `tail -f /tmp/clawdbot/clawdbot-gateway.log` +EOF +else + cat <<'EOF' + - `journalctl --user -u clawdbot-gateway --no-pager` EOF