#!/usr/bin/env bash set -euo pipefail # ╔══════════════════════════════════════════════════════════════════╗ # ║ ManyHands Installer ║ # ║ ║ # ║ Installs the CLI + orchestrator, registers the MCP server, ║ # ║ configures your API key, and logs you in — all in one step. ║ # ║ ║ # ║ Usage: ║ # ║ curl -fsSL https://api.manyhands.dev/cli/install.sh | bash ║ # ╚══════════════════════════════════════════════════════════════════╝ VERSION="${MANYHANDS_VERSION:-latest}" BASE_URL="${MANYHANDS_DOWNLOAD_URL:-https://api.manyhands.dev/cli/download}" INSTALL_DIR="${MANYHANDS_INSTALL_DIR:-$HOME/.local/bin}" # When piped via curl | bash, stdin is the curl stream. # We CANNOT use exec < /dev/tty here — it would break the pipe that bash # is reading the script from. Instead, individual interactive commands # (read, manyhands login) redirect from /dev/tty explicitly. # ── Theme ─────────────────────────────────────────────────────────── BOLD='\033[1m' DIM='\033[2m' GREEN='\033[0;32m' CYAN='\033[0;36m' YELLOW='\033[0;33m' RED='\033[0;31m' NC='\033[0m' DIAMOND="${GREEN}◆${NC}" DIAMOND_DIM="${DIM}◇${NC}" CHECK="${GREEN}✓${NC}" ARROW="${GREEN}▸${NC}" # ── Output helpers ────────────────────────────────────────────────── header() { echo "" echo -e " ${DIAMOND} ${BOLD}$*${NC}" echo -e " ${DIM}$(printf '%.0s─' {1..56})${NC}" } info() { echo -e " ${DIAMOND_DIM} $*"; } ok() { echo -e " ${CHECK} $*"; } warn() { echo -e " ${YELLOW}!${NC} $*"; } error() { echo -e "\n ${RED}✗ Error:${NC} $*\n" >&2; exit 1; } dim() { echo -e " ${DIM}$*${NC}"; } # Progress bar for downloads progress_label() { echo -ne "\r ${DIAMOND_DIM} ${DIM}$*${NC}" } # ── Step tracking ────────────────────────────────────────────────── TOTAL_STEPS=6 CURRENT_STEP=0 step() { CURRENT_STEP=$((CURRENT_STEP + 1)) echo "" local bar="" for i in $(seq 1 $TOTAL_STEPS); do if [ "$i" -le "$CURRENT_STEP" ]; then bar="${bar}${GREEN}━${NC}" else bar="${bar}${DIM}━${NC}" fi done echo -e " ${bar} ${DIM}${CURRENT_STEP}/${TOTAL_STEPS}${NC}" echo -e " ${ARROW} ${BOLD}$*${NC}" } # ── Banner ────────────────────────────────────────────────────────── echo "" echo -e " ${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e " ${GREEN}║${NC} ${GREEN}║${NC}" echo -e " ${GREEN}║${NC} ${BOLD}ManyHands${NC} ${DIM}— parallel AI orchestration${NC} ${GREEN}║${NC}" echo -e " ${GREEN}║${NC} ${GREEN}║${NC}" echo -e " ${GREEN}╚══════════════════════════════════════════════╝${NC}" # ═══════════════════════════════════════════════════════════════════ # STEP 1: Detect platform # ═══════════════════════════════════════════════════════════════════ detect_os() { case "$(uname -s)" in Linux*) echo "linux";; Darwin*) echo "darwin";; MINGW*|MSYS*|CYGWIN*) echo "windows";; *) error "Unsupported OS: $(uname -s)";; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "amd64";; aarch64|arm64) echo "arm64";; *) error "Unsupported architecture: $(uname -m)";; esac } OS=$(detect_os) ARCH=$(detect_arch) step "Detecting platform" ok "Platform: ${BOLD}${OS}/${ARCH}${NC}" # ═══════════════════════════════════════════════════════════════════ # STEP 2: Download binaries # ═══════════════════════════════════════════════════════════════════ DOWNLOAD_URL="${BASE_URL}/manyhands-${OS}-${ARCH}" ORCH_DOWNLOAD_URL="${BASE_URL}/manyhands-orchestrator-${OS}-${ARCH}" if [ "$OS" = "windows" ]; then DOWNLOAD_URL="${DOWNLOAD_URL}.exe" ORCH_DOWNLOAD_URL="${ORCH_DOWNLOAD_URL}.exe" BINARY_NAME="manyhands.exe" ORCH_BINARY_NAME="manyhands-orchestrator.exe" else BINARY_NAME="manyhands" ORCH_BINARY_NAME="manyhands-orchestrator" fi mkdir -p "$INSTALL_DIR" step "Downloading binaries" info "Downloading CLI..." if command -v curl &>/dev/null; then curl -fL --progress-bar "$DOWNLOAD_URL" -o "${INSTALL_DIR}/${BINARY_NAME}" echo "" info "Downloading orchestrator..." curl -fL --progress-bar "$ORCH_DOWNLOAD_URL" -o "${INSTALL_DIR}/${ORCH_BINARY_NAME}" elif command -v wget &>/dev/null; then wget --show-progress -q "$DOWNLOAD_URL" -O "${INSTALL_DIR}/${BINARY_NAME}" echo "" info "Downloading orchestrator..." wget --show-progress -q "$ORCH_DOWNLOAD_URL" -O "${INSTALL_DIR}/${ORCH_BINARY_NAME}" else error "Neither curl nor wget found. Please install one and try again." fi chmod +x "${INSTALL_DIR}/${BINARY_NAME}" chmod +x "${INSTALL_DIR}/${ORCH_BINARY_NAME}" if [ "$OS" = "darwin" ]; then dim "Signing binaries for macOS Gatekeeper..." codesign --force --sign - "${INSTALL_DIR}/${BINARY_NAME}" 2>/dev/null || true codesign --force --sign - "${INSTALL_DIR}/${ORCH_BINARY_NAME}" 2>/dev/null || true fi # Verify if "${INSTALL_DIR}/${BINARY_NAME}" --version &>/dev/null; then CLI_VERSION=$("${INSTALL_DIR}/${BINARY_NAME}" --version 2>/dev/null || echo "unknown") ok "CLI installed: ${BOLD}${INSTALL_DIR}/${BINARY_NAME}${NC} ${DIM}(${CLI_VERSION})${NC}" else warn "CLI installed but version check failed — it may still work." fi ok "Orchestrator installed: ${BOLD}${INSTALL_DIR}/${ORCH_BINARY_NAME}${NC}" # Ensure PATH includes install dir if ! echo "$PATH" | tr ':' '\n' | grep -q "^${INSTALL_DIR}$"; then export PATH="${INSTALL_DIR}:${PATH}" warn "${INSTALL_DIR} is not in your PATH" SHELL_NAME="$(basename "$SHELL")" PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' case "$SHELL_NAME" in zsh) if ! grep -qF '.local/bin' ~/.zshrc 2>/dev/null; then echo "$PATH_LINE" >> ~/.zshrc dim "Added to ~/.zshrc — restart your shell or run: source ~/.zshrc" else dim "PATH entry already in ~/.zshrc" fi ;; bash) RC_FILE="${HOME}/.bashrc" [ -f "${HOME}/.bash_profile" ] && RC_FILE="${HOME}/.bash_profile" if ! grep -qF '.local/bin' "$RC_FILE" 2>/dev/null; then echo "$PATH_LINE" >> "$RC_FILE" dim "Added to ${RC_FILE} — restart your shell or run: source ${RC_FILE}" else dim "PATH entry already in ${RC_FILE}" fi ;; fish) fish_add_path ~/.local/bin 2>/dev/null || true dim "Added via fish_add_path" ;; *) dim "Add this to your shell config: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;; esac fi # ═══════════════════════════════════════════════════════════════════ # STEP 3: Register MCP server with Claude Code # ═══════════════════════════════════════════════════════════════════ step "Registering MCP server with Claude Code" if command -v claude &>/dev/null; then # Add MCP server globally so it's available in all projects if claude mcp add manyhands -s user -- manyhands mcp 2>/dev/null; then ok "MCP server registered globally with Claude Code" dim "Claude Code can now use ManyHands tools in any project" else warn "Could not register MCP server automatically" dim "Run manually: claude mcp add manyhands -s user -- manyhands mcp" fi else warn "Claude Code CLI not found — skipping MCP registration" dim "After installing Claude Code, run:" dim " claude mcp add manyhands -s user -- manyhands mcp" fi # ═══════════════════════════════════════════════════════════════════ # STEP 4: API key configuration # ═══════════════════════════════════════════════════════════════════ step "Configuring API credentials" echo "" dim "ManyHands needs a Claude API key to run workers on your machine." dim "Your key is stored locally at ~/.manyhands/key.json and is NEVER" dim "sent to our servers. All AI calls happen directly from your machine." echo "" echo -e " ${BOLD}Choose your setup:${NC}" echo "" echo -e " ${ARROW} ${BOLD}Option 1${NC} — Claude Code OAuth ${DIM}(most users)${NC}" echo -e " ${DIM}If you log in to Claude Code with ${NC}/login${DIM}, you're already set.${NC}" echo -e " ${DIM}ManyHands will use your existing Claude Code session.${NC}" echo "" echo -e " ${ARROW} ${BOLD}Option 2${NC} — Anthropic API key" echo -e " ${DIM}Paste your sk-ant-... key below. It stays on your machine.${NC}" echo "" # Check if user already has a key configured EXISTING_KEY="" if [ -f "$HOME/.manyhands/key.json" ]; then EXISTING_KEY="yes" ok "API key already configured" fi if [ -z "$EXISTING_KEY" ]; then echo -ne " ${ARROW} Enter API key ${DIM}(or press Enter to use Claude Code OAuth)${NC}: " read -r API_KEY_INPUT /dev/null || API_KEY_INPUT="" if [ -n "$API_KEY_INPUT" ]; then if manyhands --set-key="$API_KEY_INPUT" 2>&1; then ok "API key saved to ~/.manyhands/key.json" else warn "Could not save key — run: manyhands --set-key=\"your-key\"" fi else ok "Using Claude Code OAuth — no API key needed" dim "Make sure you're logged in to Claude Code (run /login if not)" fi fi # ═══════════════════════════════════════════════════════════════════ # STEP 5: Authenticate with ManyHands # ═══════════════════════════════════════════════════════════════════ step "Logging in to ManyHands" dim "Opening your browser to authenticate..." dim "This connects your CLI to manyhands.dev for run tracking." echo "" if manyhands login