430 lines
14 KiB
Bash
Executable File
430 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 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
|
|
|
|
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 "$@"
|