File size: 16,026 Bytes
b6ecafa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303c344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6ecafa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
#!/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

  # 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 "$@"