From 33570af961f1733939d15a3a72e896ab19503451 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 22:55:27 +0000 Subject: [PATCH] feat: add all-in-one full-deploy script for Hostinger VPS Single command that handles the entire deployment pipeline via SSH: - Phase 1: SSH connection verification - Phase 2: Server provisioning (Docker, firewall, fail2ban, systemd) - Phase 3: Interactive config (prompts for LLM provider, API keys, chat channel) - Phase 4: Source sync, Docker build, container start - Phase 5: Health check verification and status report Supports both interactive and non-interactive (env vars) modes. Adds `make deploy-hostinger-full` target. https://claude.ai/code/session_019vXaqxGmkdCjM8m3jp6rYj --- Makefile | 12 +- deploy/hostinger/full-deploy.sh | 692 ++++++++++++++++++++++++++++++++ 2 files changed, 703 insertions(+), 1 deletion(-) create mode 100755 deploy/hostinger/full-deploy.sh diff --git a/Makefile b/Makefile index 6e5418de7..fae8027c5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build install uninstall clean help test deploy-hostinger deploy-hostinger-setup deploy-hostinger-status deploy-hostinger-rollback +.PHONY: all build install uninstall clean help test deploy-hostinger deploy-hostinger-full deploy-hostinger-setup deploy-hostinger-status deploy-hostinger-rollback # Build variables BINARY_NAME=picoclaw @@ -155,6 +155,15 @@ HOSTINGER_SSH_KEY?=$(HOME)/.ssh/id_rsa HOSTINGER_SSH_PORT?=22 HOSTINGER_DEPLOY_METHOD?=docker +## deploy-hostinger-full: Full deploy (setup + config + build + start) in one command +deploy-hostinger-full: + @bash deploy/hostinger/full-deploy.sh \ + -h "$(HOSTINGER_HOST)" \ + -u "$(HOSTINGER_USER)" \ + -k "$(HOSTINGER_SSH_KEY)" \ + -p "$(HOSTINGER_SSH_PORT)" \ + -m "$(HOSTINGER_DEPLOY_METHOD)" + ## deploy-hostinger-setup: Run initial server setup on Hostinger VPS deploy-hostinger-setup: @bash deploy/hostinger/setup-server.sh @@ -202,6 +211,7 @@ help: @echo " make uninstall # Remove from /usr/local/bin" @echo "" @echo "Hostinger Deployment:" + @echo " make deploy-hostinger-full HOSTINGER_HOST=1.2.3.4 # Full deploy (all-in-one)" @echo " make deploy-hostinger-setup HOSTINGER_HOST=1.2.3.4 # Initial server setup" @echo " make deploy-hostinger HOSTINGER_HOST=1.2.3.4 # Deploy to VPS" @echo " make deploy-hostinger-status HOSTINGER_HOST=1.2.3.4 # Check status" diff --git a/deploy/hostinger/full-deploy.sh b/deploy/hostinger/full-deploy.sh new file mode 100755 index 000000000..02edad546 --- /dev/null +++ b/deploy/hostinger/full-deploy.sh @@ -0,0 +1,692 @@ +#!/usr/bin/env bash +# ============================================================ +# PicoClaw - Full Hostinger VPS Deploy (All-in-One) +# ============================================================ +# This script does EVERYTHING via SSH in a single run: +# 1. Server setup (packages, Docker, firewall, fail2ban) +# 2. User & directory creation +# 3. Configuration files (prompts for API keys) +# 4. Source code sync & Docker build +# 5. Start service & health check verification +# +# Usage: +# ./deploy/hostinger/full-deploy.sh -h YOUR_VPS_IP [OPTIONS] +# +# Examples: +# # Interactive (prompts for API keys): +# ./deploy/hostinger/full-deploy.sh -h 149.28.10.50 +# +# # Non-interactive with env vars: +# LLM_PROVIDER=anthropic \ +# LLM_API_KEY=sk-ant-xxx \ +# CHAT_CHANNEL=telegram \ +# CHAT_TOKEN=123456:ABC \ +# ./deploy/hostinger/full-deploy.sh -h 149.28.10.50 +# +# Options: +# -h, --host HOST VPS IP or hostname (required) +# -u, --user USER SSH user (default: root) +# -k, --key KEY SSH key path (default: ~/.ssh/id_rsa) +# -p, --port PORT SSH port (default: 22) +# -m, --method METHOD "docker" or "binary" (default: docker) +# --skip-setup Skip server provisioning (if already done) +# --skip-config Skip config prompts (use existing server config) +# --yes Skip confirmation prompts +# --help Show this help +# ============================================================ + +set -euo pipefail + +# ── Colors ─────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +log() { echo -e "${GREEN}>>>${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +info() { echo -e "${BLUE}[i]${NC} $*"; } +step() { echo -e "\n${CYAN}${BOLD}── $1 ──${NC}"; } +success() { echo -e "${GREEN}${BOLD}$*${NC}"; } + +# ── Default Configuration ──────────────────────────── +HOST="" +SSH_USER="root" +SSH_KEY="${HOME}/.ssh/id_rsa" +SSH_PORT="22" +METHOD="docker" +SKIP_SETUP=false +SKIP_CONFIG=false +AUTO_YES=false +REMOTE_DIR="/opt/picoclaw" + +# Pre-set via env (for non-interactive deploys) +LLM_PROVIDER="${LLM_PROVIDER:-}" +LLM_API_KEY="${LLM_API_KEY:-}" +CHAT_CHANNEL="${CHAT_CHANNEL:-}" +CHAT_TOKEN="${CHAT_TOKEN:-}" +CHAT_ALLOW_FROM="${CHAT_ALLOW_FROM:-}" +DEPLOY_TIMEZONE="${DEPLOY_TIMEZONE:-America/Sao_Paulo}" + +# ── Parse Arguments ────────────────────────────────── +while [[ $# -gt 0 ]]; do + case $1 in + -h|--host) HOST="$2"; shift 2 ;; + -u|--user) SSH_USER="$2"; shift 2 ;; + -k|--key) SSH_KEY="$2"; shift 2 ;; + -p|--port) SSH_PORT="$2"; shift 2 ;; + -m|--method) METHOD="$2"; shift 2 ;; + --skip-setup) SKIP_SETUP=true; shift ;; + --skip-config) SKIP_CONFIG=true; shift ;; + --yes) AUTO_YES=true; shift ;; + --help) + sed -n '2,/^# =====/p' "$0" | head -n -1 | sed 's/^# //' | sed 's/^#//' + exit 0 + ;; + *) error "Unknown option: $1. Use --help for usage." ;; + esac +done + +# ── Validate ───────────────────────────────────────── +[ -z "${HOST}" ] && error "Host is required. Usage: $0 -h YOUR_VPS_IP" + +if [ ! -f "${SSH_KEY}" ]; then + warn "SSH key not found at ${SSH_KEY}" + info "Will attempt password-based SSH. For better security, set up SSH keys." + SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT}" +else + SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT} -i ${SSH_KEY}" +fi + +SSH_CMD="ssh ${SSH_OPTS} ${SSH_USER}@${HOST}" +SCP_CMD="scp ${SSH_OPTS}" + +# ── Get project root ───────────────────────────────── +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# ── Banner ─────────────────────────────────────────── +echo "" +echo -e "${CYAN}${BOLD}" +echo " ╔═══════════════════════════════════════════╗" +echo " ║ PicoClaw - Hostinger Full Deploy ║" +echo " ╚═══════════════════════════════════════════╝" +echo -e "${NC}" +echo " Host: ${HOST}" +echo " User: ${SSH_USER}" +echo " Method: ${METHOD}" +echo " SSH Key: ${SSH_KEY}" +echo "" + +if [ "${AUTO_YES}" = false ]; then + read -rp " Continue with deployment? [Y/n] " confirm + case "${confirm}" in + [nN]*) echo "Aborted."; exit 0 ;; + esac +fi + +DEPLOY_START=$(date +%s) + +# ════════════════════════════════════════════════════ +# PHASE 1: Verify SSH Connection +# ════════════════════════════════════════════════════ +step "Phase 1/5: Checking SSH connection" + +if ${SSH_CMD} "echo 'ok'" &>/dev/null; then + log "SSH connection to ${SSH_USER}@${HOST}:${SSH_PORT} OK" +else + error "Cannot connect via SSH to ${SSH_USER}@${HOST}:${SSH_PORT} + + Troubleshooting: + - Verify the VPS IP in Hostinger hPanel + - Enable SSH access in hPanel > Advanced > SSH Access + - Check your SSH key: ssh-keygen -t ed25519 && ssh-copy-id ${SSH_USER}@${HOST} + - Try with password: $0 -h ${HOST} (without -k flag)" +fi + +# Detect remote OS +REMOTE_OS=$(${SSH_CMD} "cat /etc/os-release 2>/dev/null | head -3 || echo 'unknown'" 2>/dev/null) +info "Remote OS: $(echo "${REMOTE_OS}" | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || echo 'detected')" + +# ════════════════════════════════════════════════════ +# PHASE 2: Server Provisioning +# ════════════════════════════════════════════════════ +if [ "${SKIP_SETUP}" = false ]; then + step "Phase 2/5: Server provisioning" + log "Installing packages, Docker, firewall, fail2ban..." + + ${SSH_CMD} "bash -s -- ${METHOD}" <<'SETUP_SCRIPT' +#!/bin/bash +set -euo pipefail + +METHOD="${1:-docker}" + +echo "[REMOTE] Updating system packages..." +export DEBIAN_FRONTEND=noninteractive + +if command -v apt-get &>/dev/null; then + apt-get update -qq + apt-get upgrade -y -qq + apt-get install -y -qq curl wget git ufw fail2ban unzip jq rsync +elif command -v yum &>/dev/null; then + yum update -y -q + yum install -y -q curl wget git firewalld fail2ban unzip jq rsync +elif command -v dnf &>/dev/null; then + dnf update -y -q + dnf install -y -q curl wget git firewalld fail2ban unzip jq rsync +fi + +# Create dedicated user +if ! id picoclaw &>/dev/null; then + echo "[REMOTE] Creating picoclaw user..." + useradd --system --create-home --home-dir /opt/picoclaw --shell /bin/bash picoclaw +fi + +# Directory structure +echo "[REMOTE] Creating directories..." +mkdir -p /opt/picoclaw/{bin,config,workspace,logs,backups,src} +chown -R picoclaw:picoclaw /opt/picoclaw + +# Docker +if [ "${METHOD}" = "docker" ]; then + if ! command -v docker &>/dev/null; then + echo "[REMOTE] Installing Docker..." + curl -fsSL https://get.docker.com | sh + systemctl enable docker + systemctl start docker + usermod -aG docker picoclaw + fi + echo "[REMOTE] Docker: $(docker --version 2>/dev/null)" + + if ! docker compose version &>/dev/null; then + echo "[REMOTE] Installing Docker Compose..." + apt-get install -y -qq docker-compose-plugin 2>/dev/null || true + fi + echo "[REMOTE] Docker Compose: $(docker compose version 2>/dev/null || echo 'not available')" +else + # Binary method: install Go + if ! command -v go &>/dev/null; then + echo "[REMOTE] Installing Go..." + GO_VERSION="1.23.4" + ARCH=$(uname -m) + case "${ARCH}" in + x86_64) GO_ARCH="amd64" ;; + aarch64) GO_ARCH="arm64" ;; + *) GO_ARCH="${ARCH}" ;; + esac + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz + rm -rf /usr/local/go + tar -C /usr/local -xzf /tmp/go.tar.gz + rm /tmp/go.tar.gz + echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh + export PATH=$PATH:/usr/local/go/bin + fi + echo "[REMOTE] Go: $(go version 2>/dev/null || echo 'installed')" + + if ! command -v make &>/dev/null; then + apt-get install -y -qq make 2>/dev/null || yum install -y -q make 2>/dev/null || true + fi + + # Install systemd service + cat > /etc/systemd/system/picoclaw.service <<'SVCEOF' +[Unit] +Description=PicoClaw AI Assistant Gateway +Documentation=https://github.com/agenciaspace/picoclaw +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=300 +StartLimitBurst=5 + +[Service] +Type=simple +User=picoclaw +Group=picoclaw +WorkingDirectory=/opt/picoclaw +ExecStart=/opt/picoclaw/bin/picoclaw gateway +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=10 +TimeoutStopSec=30 + +StandardOutput=append:/opt/picoclaw/logs/picoclaw.log +StandardError=append:/opt/picoclaw/logs/picoclaw-error.log + +EnvironmentFile=-/opt/picoclaw/config/.env +Environment=HOME=/opt/picoclaw +Environment=PICOCLAW_CONFIG=/opt/picoclaw/config/config.json + +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths=/opt/picoclaw +PrivateTmp=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RestrictNamespaces=yes + +[Install] +WantedBy=multi-user.target +SVCEOF + systemctl daemon-reload + systemctl enable picoclaw +fi + +# Firewall +echo "[REMOTE] Configuring firewall..." +if command -v ufw &>/dev/null; then + ufw --force reset >/dev/null 2>&1 + ufw default deny incoming >/dev/null 2>&1 + ufw default allow outgoing >/dev/null 2>&1 + ufw allow ssh >/dev/null 2>&1 + ufw allow 18790/tcp >/dev/null 2>&1 + ufw --force enable >/dev/null 2>&1 + echo "[REMOTE] UFW: enabled (SSH + 18790)" +elif command -v firewall-cmd &>/dev/null; then + systemctl enable firewalld >/dev/null 2>&1 + systemctl start firewalld >/dev/null 2>&1 + firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 + firewall-cmd --permanent --add-port=18790/tcp >/dev/null 2>&1 + firewall-cmd --reload >/dev/null 2>&1 + echo "[REMOTE] firewalld: enabled (SSH + 18790)" +fi + +# fail2ban +systemctl enable fail2ban >/dev/null 2>&1 +systemctl start fail2ban >/dev/null 2>&1 +echo "[REMOTE] fail2ban: active" + +# Logrotate +cat > /etc/logrotate.d/picoclaw <<'LOGEOF' +/opt/picoclaw/logs/*.log { + daily + missingok + rotate 14 + compress + delaycompress + notifempty + create 0640 picoclaw picoclaw +} +LOGEOF + +echo "[REMOTE] Server provisioning complete!" +SETUP_SCRIPT + + log "Server provisioning done" +else + step "Phase 2/5: Server provisioning (SKIPPED)" +fi + +# ════════════════════════════════════════════════════ +# PHASE 3: Configuration +# ════════════════════════════════════════════════════ +if [ "${SKIP_CONFIG}" = false ]; then + step "Phase 3/5: Configuration" + + # Collect config interactively if not provided via env + if [ -z "${LLM_PROVIDER}" ]; then + echo "" + echo " Which LLM provider will you use?" + echo " 1) anthropic (Claude)" + echo " 2) openai (GPT)" + echo " 3) openrouter (Multiple models)" + echo " 4) gemini (Google)" + echo " 5) zhipu (GLM)" + echo " 6) groq (Fast inference)" + echo " 7) ollama (Local/self-hosted)" + echo "" + read -rp " Provider [1-7]: " provider_choice + case "${provider_choice}" in + 1) LLM_PROVIDER="anthropic" ;; + 2) LLM_PROVIDER="openai" ;; + 3) LLM_PROVIDER="openrouter" ;; + 4) LLM_PROVIDER="gemini" ;; + 5) LLM_PROVIDER="zhipu" ;; + 6) LLM_PROVIDER="groq" ;; + 7) LLM_PROVIDER="ollama" ;; + *) LLM_PROVIDER="anthropic" ;; + esac + fi + + if [ -z "${LLM_API_KEY}" ] && [ "${LLM_PROVIDER}" != "ollama" ]; then + read -rp " ${LLM_PROVIDER} API key: " LLM_API_KEY + [ -z "${LLM_API_KEY}" ] && warn "No API key provided. You can set it later on the server." + fi + + if [ -z "${CHAT_CHANNEL}" ]; then + echo "" + echo " Which chat channel? (optional, press Enter to skip)" + echo " 1) telegram" + echo " 2) discord" + echo " 3) slack" + echo " 4) whatsapp" + echo " 5) line" + echo " 6) none (gateway API only)" + echo "" + read -rp " Channel [1-6]: " channel_choice + case "${channel_choice}" in + 1) CHAT_CHANNEL="telegram" ;; + 2) CHAT_CHANNEL="discord" ;; + 3) CHAT_CHANNEL="slack" ;; + 4) CHAT_CHANNEL="whatsapp" ;; + 5) CHAT_CHANNEL="line" ;; + *) CHAT_CHANNEL="" ;; + esac + fi + + if [ -n "${CHAT_CHANNEL}" ] && [ -z "${CHAT_TOKEN}" ]; then + read -rp " ${CHAT_CHANNEL} bot token: " CHAT_TOKEN + fi + + if [ -n "${CHAT_CHANNEL}" ] && [ -z "${CHAT_ALLOW_FROM}" ]; then + read -rp " Allowed user IDs (comma-separated, or * for all): " CHAT_ALLOW_FROM + fi + + # Determine model based on provider + case "${LLM_PROVIDER}" in + anthropic) DEFAULT_MODEL="claude-sonnet-4-20250514" ;; + openai) DEFAULT_MODEL="gpt-4o" ;; + openrouter) DEFAULT_MODEL="anthropic/claude-sonnet-4" ;; + gemini) DEFAULT_MODEL="gemini-2.0-flash" ;; + zhipu) DEFAULT_MODEL="glm-4" ;; + groq) DEFAULT_MODEL="llama-3.3-70b-versatile" ;; + ollama) DEFAULT_MODEL="llama3.2" ;; + *) DEFAULT_MODEL="claude-sonnet-4-20250514" ;; + esac + + # Build env key name + case "${LLM_PROVIDER}" in + anthropic) ENV_KEY_NAME="ANTHROPIC_API_KEY" ;; + openai) ENV_KEY_NAME="OPENAI_API_KEY" ;; + openrouter) ENV_KEY_NAME="OPENROUTER_API_KEY" ;; + gemini) ENV_KEY_NAME="GEMINI_API_KEY" ;; + zhipu) ENV_KEY_NAME="ZHIPU_API_KEY" ;; + groq) ENV_KEY_NAME="GROQ_API_KEY" ;; + ollama) ENV_KEY_NAME="" ;; + *) ENV_KEY_NAME="${LLM_PROVIDER^^}_API_KEY" ;; + esac + + # Build chat token env name + case "${CHAT_CHANNEL}" in + telegram) CHAT_ENV_KEY="TELEGRAM_BOT_TOKEN" ;; + discord) CHAT_ENV_KEY="DISCORD_BOT_TOKEN" ;; + slack) CHAT_ENV_KEY="SLACK_BOT_TOKEN" ;; + *) CHAT_ENV_KEY="" ;; + esac + + # Format allow_from as JSON array + if [ -n "${CHAT_ALLOW_FROM}" ] && [ "${CHAT_ALLOW_FROM}" != "*" ]; then + ALLOW_FROM_JSON=$(echo "${CHAT_ALLOW_FROM}" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | awk '{printf "\"%s\",", $0}' | sed 's/,$//') + ALLOW_FROM_JSON="[${ALLOW_FROM_JSON}]" + else + ALLOW_FROM_JSON="[]" + fi + + log "Writing configuration to server..." + + # Write .env file + ${SSH_CMD} "cat > /opt/picoclaw/config/.env" < /opt/picoclaw/config/config.json" </dev/null || true + +echo "[REMOTE] Starting PicoClaw gateway..." +docker compose up -d picoclaw-gateway + +echo "[REMOTE] Waiting for container to start..." +sleep 5 + +if docker compose ps picoclaw-gateway 2>/dev/null | grep -q "Up\|running"; then + echo "[REMOTE] Container is running!" + docker compose ps +else + echo "[REMOTE] Container status:" + docker compose ps + echo "" + echo "[REMOTE] Container logs:" + docker compose logs --tail=30 picoclaw-gateway +fi + +docker image prune -f >/dev/null 2>&1 +echo "[REMOTE] Docker deploy complete" +DOCKER_DEPLOY + +else + # ── Binary Deploy ──────────────────────────────── + log "Syncing source code to server..." + rsync -az --delete \ + -e "ssh ${SSH_OPTS}" \ + --exclude '.git' \ + --exclude 'build/' \ + --exclude '.env' \ + "${PROJECT_ROOT}/" "${SSH_USER}@${HOST}:${REMOTE_DIR}/src/" + + log "Building and installing binary on server..." + ${SSH_CMD} <<'BINARY_DEPLOY' +set -e +cd /opt/picoclaw/src +export PATH=$PATH:/usr/local/go/bin + +echo "[REMOTE] Building PicoClaw..." +make build + +# Backup current binary +if [ -f /opt/picoclaw/bin/picoclaw ]; then + cp /opt/picoclaw/bin/picoclaw "/opt/picoclaw/backups/picoclaw-$(date +%Y%m%d%H%M%S).bak" +fi + +cp build/picoclaw /opt/picoclaw/bin/picoclaw +chmod +x /opt/picoclaw/bin/picoclaw + +echo "[REMOTE] Restarting picoclaw service..." +systemctl restart picoclaw + +sleep 3 +if systemctl is-active --quiet picoclaw; then + echo "[REMOTE] Service is running" + systemctl status picoclaw --no-pager -l +else + echo "[REMOTE] Service status:" + systemctl status picoclaw --no-pager -l || true + echo "[REMOTE] Recent logs:" + journalctl -u picoclaw --no-pager -n 20 || true +fi + +echo "[REMOTE] Binary deploy complete" +BINARY_DEPLOY +fi + +log "Build & deploy done" + +# ════════════════════════════════════════════════════ +# PHASE 5: Verification +# ════════════════════════════════════════════════════ +step "Phase 5/5: Verification" + +log "Running health checks..." + +HEALTH_OK=false +for i in 1 2 3 4 5; do + sleep 3 + if ${SSH_CMD} "curl -sf http://localhost:18790/health" &>/dev/null; then + HEALTH_OK=true + break + fi + info "Waiting for gateway to start... (attempt ${i}/5)" +done + +# Get final status +${SSH_CMD} <<'VERIFY_SCRIPT' +echo "" +echo "── Server ────────────────────────────────────" +printf " %-12s %s\n" "Hostname:" "$(hostname)" +printf " %-12s %s\n" "Memory:" "$(free -h | awk '/Mem:/ {print $3 "/" $2}')" +printf " %-12s %s\n" "Disk:" "$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}')" +echo "" + +echo "── PicoClaw ──────────────────────────────────" +if docker compose -f /opt/picoclaw/docker-compose.yml ps 2>/dev/null | grep -q picoclaw; then + printf " %-12s %s\n" "Method:" "Docker" + STATUS=$(docker compose -f /opt/picoclaw/docker-compose.yml ps --format '{{.Status}}' picoclaw-gateway 2>/dev/null) + printf " %-12s %s\n" "Container:" "${STATUS}" +elif systemctl is-active --quiet picoclaw 2>/dev/null; then + printf " %-12s %s\n" "Method:" "Binary (systemd)" + printf " %-12s %s\n" "Status:" "running" +else + printf " %-12s %s\n" "Status:" "unknown" +fi + +echo "" +echo "── Health Check ──────────────────────────────" +if curl -sf http://localhost:18790/health >/dev/null 2>&1; then + printf " %-12s %s\n" "Gateway:" "HEALTHY (port 18790)" +else + printf " %-12s %s\n" "Gateway:" "not responding yet (may still be starting)" +fi + +echo "" +echo "── Firewall ──────────────────────────────────" +if command -v ufw &>/dev/null; then + ufw status | grep -E "^(Status|18790|22)" 2>/dev/null || true +fi +VERIFY_SCRIPT + +# ════════════════════════════════════════════════════ +# Summary +# ════════════════════════════════════════════════════ +DEPLOY_END=$(date +%s) +DEPLOY_DURATION=$((DEPLOY_END - DEPLOY_START)) + +echo "" +echo -e "${CYAN}${BOLD}" +echo " ╔═══════════════════════════════════════════╗" +if [ "${HEALTH_OK}" = true ]; then +echo " ║ Deploy completed successfully! ║" +else +echo " ║ Deploy finished (check status) ║" +fi +echo " ╚═══════════════════════════════════════════╝" +echo -e "${NC}" +echo " Host: ${HOST}" +echo " Method: ${METHOD}" +echo " Duration: ${DEPLOY_DURATION}s" +echo " Gateway: http://${HOST}:18790" +if [ "${HEALTH_OK}" = true ]; then +echo -e " Health: ${GREEN}HEALTHY${NC}" +else +echo -e " Health: ${YELLOW}Starting up (check in a few seconds)${NC}" +fi +echo "" +echo " Quick commands:" +echo " Status: ssh ${SSH_USER}@${HOST} 'docker compose -f /opt/picoclaw/docker-compose.yml ps'" +echo " Logs: ssh ${SSH_USER}@${HOST} 'docker compose -f /opt/picoclaw/docker-compose.yml logs -f'" +echo " Health: curl http://${HOST}:18790/health" +echo " Redeploy: $0 -h ${HOST} --skip-setup --skip-config" +echo ""