Spaces:
Sleeping
Sleeping
HonzysClawdbot
fix(gateway): resolve Docker connectivity β 404 endpoints, EROFS write, missing OPENCLAW_HOME (#343)
303c344 unverified | # Mission Control β One-Command Installer | |
| # The mothership for your OpenClaw fleet. | |
| # | |
| # Usage: | |
| # curl -fsSL https://raw.githubusercontent.com/builderz-labs/mission-control/main/install.sh | bash | |
| # # or | |
| # bash install.sh [--docker|--local] [--port PORT] [--data-dir DIR] | |
| # | |
| # Installs Mission Control and optionally repairs/configures OpenClaw. | |
| set -euo pipefail | |
| # ββ Defaults ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MC_PORT="${MC_PORT:-3000}" | |
| MC_DATA_DIR="" | |
| DEPLOY_MODE="" | |
| SKIP_OPENCLAW=false | |
| REPO_URL="https://github.com/builderz-labs/mission-control.git" | |
| INSTALL_DIR="${MC_INSTALL_DIR:-$(pwd)/mission-control}" | |
| # ββ Parse arguments βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --docker) DEPLOY_MODE="docker"; shift ;; | |
| --local) DEPLOY_MODE="local"; shift ;; | |
| --port) MC_PORT="$2"; shift 2 ;; | |
| --data-dir) MC_DATA_DIR="$2"; shift 2 ;; | |
| --skip-openclaw) SKIP_OPENCLAW=true; shift ;; | |
| --dir) INSTALL_DIR="$2"; shift 2 ;; | |
| -h|--help) | |
| echo "Usage: install.sh [--docker|--local] [--port PORT] [--data-dir DIR] [--dir INSTALL_DIR] [--skip-openclaw]" | |
| exit 0 ;; | |
| *) echo "Unknown option: $1"; exit 1 ;; | |
| esac | |
| done | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| info() { echo -e "\033[1;34m[MC]\033[0m $*"; } | |
| ok() { echo -e "\033[1;32m[OK]\033[0m $*"; } | |
| warn() { echo -e "\033[1;33m[!!]\033[0m $*"; } | |
| err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } | |
| die() { err "$*"; exit 1; } | |
| command_exists() { command -v "$1" &>/dev/null; } | |
| detect_os() { | |
| local os arch | |
| os="$(uname -s)" | |
| arch="$(uname -m)" | |
| case "$os" in | |
| Linux) OS="linux" ;; | |
| Darwin) OS="darwin" ;; | |
| *) die "Unsupported OS: $os" ;; | |
| esac | |
| case "$arch" in | |
| x86_64|amd64) ARCH="x64" ;; | |
| aarch64|arm64) ARCH="arm64" ;; | |
| *) die "Unsupported architecture: $arch" ;; | |
| esac | |
| ok "Detected $OS/$ARCH" | |
| } | |
| check_prerequisites() { | |
| local has_docker=false has_node=false | |
| if command_exists docker && docker info &>/dev/null 2>&1; then | |
| has_docker=true | |
| ok "Docker available ($(docker --version | head -1))" | |
| fi | |
| if command_exists node; then | |
| local node_major | |
| node_major=$(node -v | sed 's/v//' | cut -d. -f1) | |
| if [[ "$node_major" -ge 20 ]]; then | |
| has_node=true | |
| ok "Node.js $(node -v) available" | |
| else | |
| warn "Node.js $(node -v) found but v20+ required" | |
| fi | |
| fi | |
| if ! $has_docker && ! $has_node; then | |
| die "Either Docker or Node.js 20+ is required. Install one and retry." | |
| fi | |
| # Auto-select deploy mode if not specified | |
| if [[ -z "$DEPLOY_MODE" ]]; then | |
| if $has_docker; then | |
| DEPLOY_MODE="docker" | |
| info "Auto-selected Docker deployment (use --local to override)" | |
| else | |
| DEPLOY_MODE="local" | |
| info "Auto-selected local deployment (Docker not available)" | |
| fi | |
| fi | |
| # Validate chosen mode | |
| if [[ "$DEPLOY_MODE" == "docker" ]] && ! $has_docker; then | |
| die "Docker deployment requested but Docker is not available" | |
| fi | |
| if [[ "$DEPLOY_MODE" == "local" ]] && ! $has_node; then | |
| die "Local deployment requested but Node.js 20+ is not available" | |
| fi | |
| if [[ "$DEPLOY_MODE" == "local" ]] && ! command_exists pnpm; then | |
| info "Installing pnpm via corepack..." | |
| corepack enable && corepack prepare pnpm@latest --activate | |
| ok "pnpm installed" | |
| fi | |
| } | |
| # ββ Clone or update repo βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fetch_source() { | |
| if [[ -d "$INSTALL_DIR/.git" ]]; then | |
| info "Updating existing installation at $INSTALL_DIR..." | |
| cd "$INSTALL_DIR" | |
| git fetch --tags | |
| local latest_tag | |
| latest_tag=$(git describe --tags --abbrev=0 origin/main 2>/dev/null || echo "") | |
| if [[ -n "$latest_tag" ]]; then | |
| git checkout "$latest_tag" | |
| ok "Checked out $latest_tag" | |
| else | |
| git pull origin main | |
| ok "Updated to latest main" | |
| fi | |
| else | |
| info "Cloning Mission Control..." | |
| if command_exists git; then | |
| git clone --depth 1 "$REPO_URL" "$INSTALL_DIR" | |
| cd "$INSTALL_DIR" | |
| ok "Cloned to $INSTALL_DIR" | |
| else | |
| die "git is required to clone the repository" | |
| fi | |
| fi | |
| } | |
| # ββ Generate .env βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| setup_env() { | |
| if [[ -f "$INSTALL_DIR/.env" ]]; then | |
| info "Existing .env found β keeping current configuration" | |
| return | |
| fi | |
| info "Generating secure .env configuration..." | |
| bash "$INSTALL_DIR/scripts/generate-env.sh" "$INSTALL_DIR/.env" | |
| # Set the port if non-default | |
| if [[ "$MC_PORT" != "3000" ]]; then | |
| if [[ "$(uname)" == "Darwin" ]]; then | |
| sed -i '' "s|^# PORT=3000|PORT=$MC_PORT|" "$INSTALL_DIR/.env" | |
| else | |
| sed -i "s|^# PORT=3000|PORT=$MC_PORT|" "$INSTALL_DIR/.env" | |
| fi | |
| fi | |
| # Auto-detect and write OpenClaw home directory into .env | |
| local oc_home="${OPENCLAW_HOME:-$HOME/.openclaw}" | |
| if [[ -d "$oc_home" ]]; then | |
| if [[ "$(uname)" == "Darwin" ]]; then | |
| sed -i '' "s|^OPENCLAW_HOME=.*|OPENCLAW_HOME=$oc_home|" "$INSTALL_DIR/.env" | |
| else | |
| sed -i "s|^OPENCLAW_HOME=.*|OPENCLAW_HOME=$oc_home|" "$INSTALL_DIR/.env" | |
| fi | |
| info "Set OPENCLAW_HOME=$oc_home in .env" | |
| fi | |
| # In Docker mode, the gateway runs on the host, not inside the container. | |
| # Set OPENCLAW_GATEWAY_HOST to the Docker host gateway IP so the container | |
| # can reach the gateway. Users may override this with the gateway container | |
| # name if running OpenClaw in a container on the same network. | |
| if [[ "$DEPLOY_MODE" == "docker" ]]; then | |
| local gw_host="${OPENCLAW_GATEWAY_HOST:-}" | |
| if [[ -z "$gw_host" ]]; then | |
| # Detect Docker host IP (host-gateway alias or default bridge) | |
| if getent hosts host-gateway &>/dev/null 2>&1; then | |
| gw_host="host-gateway" | |
| else | |
| # Fallback: use the default Docker bridge gateway (172.17.0.1) | |
| gw_host=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}' || echo "172.17.0.1") | |
| fi | |
| fi | |
| if [[ -n "$gw_host" && "$gw_host" != "127.0.0.1" ]]; then | |
| if [[ "$(uname)" == "Darwin" ]]; then | |
| sed -i '' "s|^OPENCLAW_GATEWAY_HOST=.*|OPENCLAW_GATEWAY_HOST=$gw_host|" "$INSTALL_DIR/.env" | |
| else | |
| sed -i "s|^OPENCLAW_GATEWAY_HOST=.*|OPENCLAW_GATEWAY_HOST=$gw_host|" "$INSTALL_DIR/.env" | |
| fi | |
| info "Set OPENCLAW_GATEWAY_HOST=$gw_host in .env (Docker host IP)" | |
| info " If your gateway runs in a Docker container, update OPENCLAW_GATEWAY_HOST" | |
| info " to the container name and add it to the mc-net network." | |
| fi | |
| fi | |
| ok "Secure .env generated" | |
| } | |
| # ββ Docker deployment βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| deploy_docker() { | |
| info "Starting Docker deployment..." | |
| export MC_PORT | |
| docker compose up -d --build | |
| # Wait for healthy | |
| info "Waiting for Mission Control to become healthy..." | |
| local retries=30 | |
| while [[ $retries -gt 0 ]]; do | |
| if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then | |
| break | |
| fi | |
| # Fallback: try HTTP check | |
| if curl -sf "http://localhost:$MC_PORT/login" &>/dev/null; then | |
| break | |
| fi | |
| sleep 2 | |
| ((retries--)) | |
| done | |
| if [[ $retries -eq 0 ]]; then | |
| warn "Timeout waiting for health check β container may still be starting" | |
| docker compose logs --tail 20 | |
| else | |
| ok "Mission Control is running in Docker" | |
| fi | |
| } | |
| # ββ Local deployment ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| deploy_local() { | |
| info "Starting local deployment..." | |
| cd "$INSTALL_DIR" | |
| pnpm install --frozen-lockfile 2>/dev/null || pnpm install | |
| ok "Dependencies installed" | |
| info "Building Mission Control..." | |
| pnpm build | |
| ok "Build complete" | |
| # Create systemd service on Linux if systemctl is available | |
| if [[ "$OS" == "linux" ]] && command_exists systemctl; then | |
| setup_systemd | |
| fi | |
| info "Starting Mission Control..." | |
| PORT="$MC_PORT" nohup pnpm start > "$INSTALL_DIR/.data/mc.log" 2>&1 & | |
| local pid=$! | |
| echo "$pid" > "$INSTALL_DIR/.data/mc.pid" | |
| sleep 3 | |
| if kill -0 "$pid" 2>/dev/null; then | |
| ok "Mission Control running (PID $pid)" | |
| else | |
| err "Failed to start. Check logs: $INSTALL_DIR/.data/mc.log" | |
| exit 1 | |
| fi | |
| } | |
| # ββ Systemd service ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| setup_systemd() { | |
| local service_file="/etc/systemd/system/mission-control.service" | |
| if [[ -f "$service_file" ]]; then | |
| info "Systemd service already exists" | |
| return | |
| fi | |
| info "Creating systemd service..." | |
| local user | |
| user="$(whoami)" | |
| local node_path | |
| node_path="$(which node)" | |
| cat > /tmp/mission-control.service <<UNIT | |
| [Unit] | |
| Description=Mission Control - OpenClaw Agent Dashboard | |
| After=network.target | |
| [Service] | |
| Type=simple | |
| User=$user | |
| WorkingDirectory=$INSTALL_DIR | |
| ExecStart=$node_path $INSTALL_DIR/.next/standalone/server.js | |
| Restart=on-failure | |
| RestartSec=5 | |
| Environment=NODE_ENV=production | |
| Environment=PORT=$MC_PORT | |
| EnvironmentFile=$INSTALL_DIR/.env | |
| [Install] | |
| WantedBy=multi-user.target | |
| UNIT | |
| if [[ "$(id -u)" -eq 0 ]]; then | |
| mv /tmp/mission-control.service "$service_file" | |
| systemctl daemon-reload | |
| systemctl enable mission-control | |
| ok "Systemd service installed and enabled" | |
| else | |
| info "Run as root to install systemd service:" | |
| info " sudo mv /tmp/mission-control.service $service_file" | |
| info " sudo systemctl daemon-reload && sudo systemctl enable --now mission-control" | |
| fi | |
| } | |
| # ββ OpenClaw fleet check βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| check_openclaw() { | |
| if $SKIP_OPENCLAW; then | |
| info "Skipping OpenClaw checks (--skip-openclaw)" | |
| return | |
| fi | |
| echo "" | |
| info "=== OpenClaw Fleet Check ===" | |
| # Check if openclaw binary exists | |
| if command_exists openclaw; then | |
| local oc_version | |
| oc_version="$(openclaw --version 2>/dev/null || echo 'unknown')" | |
| ok "OpenClaw binary found: $oc_version" | |
| elif command_exists clawdbot; then | |
| local cb_version | |
| cb_version="$(clawdbot --version 2>/dev/null || echo 'unknown')" | |
| ok "ClawdBot binary found: $cb_version (legacy)" | |
| warn "Consider upgrading to openclaw CLI" | |
| else | |
| info "OpenClaw CLI not found β install it to enable agent orchestration" | |
| info " See: https://github.com/builderz-labs/openclaw" | |
| return | |
| fi | |
| # Check OpenClaw home directory | |
| local oc_home="${OPENCLAW_HOME:-$HOME/.openclaw}" | |
| if [[ -d "$oc_home" ]]; then | |
| ok "OpenClaw home: $oc_home" | |
| # Check config | |
| local oc_config="$oc_home/openclaw.json" | |
| if [[ -f "$oc_config" ]]; then | |
| ok "Config found: $oc_config" | |
| else | |
| warn "No openclaw.json found at $oc_config" | |
| info "Mission Control will create a default config on first gateway connection" | |
| fi | |
| # Check for stale PID files | |
| local stale_count=0 | |
| for pidfile in "$oc_home"/*.pid "$oc_home"/pids/*.pid; do | |
| [[ -f "$pidfile" ]] || continue | |
| local pid | |
| pid="$(cat "$pidfile" 2>/dev/null)" || continue | |
| if ! kill -0 "$pid" 2>/dev/null; then | |
| rm -f "$pidfile" | |
| ((stale_count++)) | |
| fi | |
| done | |
| if [[ $stale_count -gt 0 ]]; then | |
| ok "Cleaned $stale_count stale PID file(s)" | |
| fi | |
| # Check logs directory size | |
| local logs_dir="$oc_home/logs" | |
| if [[ -d "$logs_dir" ]]; then | |
| local logs_size | |
| if [[ "$(uname)" == "Darwin" ]]; then | |
| logs_size="$(du -sh "$logs_dir" 2>/dev/null | cut -f1)" | |
| else | |
| logs_size="$(du -sh "$logs_dir" 2>/dev/null | cut -f1)" | |
| fi | |
| info "Logs directory: $logs_size ($logs_dir)" | |
| # Clean old logs (> 30 days) | |
| local old_logs | |
| old_logs=$(find "$logs_dir" -name "*.log" -mtime +30 2>/dev/null | wc -l | tr -d ' ') | |
| if [[ "$old_logs" -gt 0 ]]; then | |
| find "$logs_dir" -name "*.log" -mtime +30 -delete 2>/dev/null || true | |
| ok "Cleaned $old_logs log file(s) older than 30 days" | |
| fi | |
| fi | |
| # Check workspace directory | |
| local workspace="$oc_home/workspace" | |
| if [[ -d "$workspace" ]]; then | |
| local agent_count | |
| agent_count=$(find "$workspace" -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ') | |
| ((agent_count--)) # subtract the workspace dir itself | |
| info "Workspace: $agent_count agent workspace(s) in $workspace" | |
| fi | |
| else | |
| info "OpenClaw home not found at $oc_home" | |
| info "Set OPENCLAW_HOME in .env to point to your OpenClaw state directory" | |
| fi | |
| # Check gateway port | |
| local gw_host="${OPENCLAW_GATEWAY_HOST:-127.0.0.1}" | |
| local gw_port="${OPENCLAW_GATEWAY_PORT:-18789}" | |
| if nc -z "$gw_host" "$gw_port" 2>/dev/null || (echo > "/dev/tcp/$gw_host/$gw_port") 2>/dev/null; then | |
| ok "Gateway reachable at $gw_host:$gw_port" | |
| else | |
| info "Gateway not reachable at $gw_host:$gw_port (start it with: openclaw gateway start)" | |
| fi | |
| } | |
| # ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| main() { | |
| echo "" | |
| echo " ββββββββββββββββββββββββββββββββββββββββ" | |
| echo " β Mission Control Installer β" | |
| echo " β The mothership for your fleet β" | |
| echo " ββββββββββββββββββββββββββββββββββββββββ" | |
| echo "" | |
| detect_os | |
| check_prerequisites | |
| # If running from within an existing clone, use current dir | |
| if [[ -f "$(pwd)/package.json" ]] && grep -q '"mission-control"' "$(pwd)/package.json" 2>/dev/null; then | |
| INSTALL_DIR="$(pwd)" | |
| info "Running from existing clone at $INSTALL_DIR" | |
| else | |
| fetch_source | |
| fi | |
| # Ensure data directory exists | |
| mkdir -p "$INSTALL_DIR/.data" | |
| setup_env | |
| case "$DEPLOY_MODE" in | |
| docker) deploy_docker ;; | |
| local) deploy_local ;; | |
| *) die "Unknown deploy mode: $DEPLOY_MODE" ;; | |
| esac | |
| check_openclaw | |
| # ββ Print summary ββ | |
| echo "" | |
| echo " ββββββββββββββββββββββββββββββββββββββββ" | |
| echo " β Installation Complete β" | |
| echo " ββββββββββββββββββββββββββββββββββββββββ" | |
| echo "" | |
| info "Dashboard: http://localhost:$MC_PORT" | |
| info "Mode: $DEPLOY_MODE" | |
| info "Data: $INSTALL_DIR/.data/" | |
| echo "" | |
| info "Credentials are in: $INSTALL_DIR/.env" | |
| echo "" | |
| if [[ "$DEPLOY_MODE" == "docker" ]]; then | |
| info "Manage:" | |
| info " docker compose logs -f # view logs" | |
| info " docker compose restart # restart" | |
| info " docker compose down # stop" | |
| else | |
| info "Manage:" | |
| info " cat $INSTALL_DIR/.data/mc.log # view logs" | |
| info " kill \$(cat $INSTALL_DIR/.data/mc.pid) # stop" | |
| fi | |
| echo "" | |
| } | |
| main "$@" | |