#!/usr/bin/env bash # Coco AI Agent — Linux Installer # Usage: curl -fsSL https://coco.sinoaus.net/install | bash set -euo pipefail COCO_VERSION="0.1.0" COCO_ENGINE_IMAGE="coco-cli-registry.cn-shanghai.cr.aliyuncs.com/coovally/coco-engine:latest" COCO_CLIENT_URL="https://coco.sinoaus.net/releases/coco-client.tar.gz" INSTALL_DIR="${HOME}/.local/share/coco" ARCH="$(uname -m)" STEPS_TOTAL=4 STEP_NUM=0 GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' ok() { printf "\r\033[K${GREEN} ✓${NC} %s\n" "$*"; } warn() { printf "\r\033[K${YELLOW} !${NC} %s\n" "$*"; } fatal() { printf "\r\033[K${RED} ✗${NC} %s\n" "$*" >&2; exit 1; } step() { STEP_NUM=$((STEP_NUM + 1)) printf "\n${BOLD}${CYAN} [${STEP_NUM}/${STEPS_TOTAL}]${NC} ${BOLD}%s${NC}\n" "$*" } # Spinner for long-running tasks spin() { local pid=$1 msg=$2 local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 while kill -0 "$pid" 2>/dev/null; do printf "\r${DIM} ${chars:$i:1}${NC} %s" "$msg" i=$(( (i + 1) % ${#chars} )) sleep 0.1 done wait "$pid" 2>/dev/null return $? } # Progress bar for downloads (reads curl progress from stderr) download_with_progress() { local url=$1 dest=$2 msg=$3 curl -fSL --connect-timeout 15 -o "$dest" --progress-bar "$url" 2>&1 | \ while IFS= read -r line; do if [[ "$line" =~ ([0-9]+\.[0-9])% ]]; then printf "\r ↓ %s ... ${BOLD}%s${NC}" "$msg" "${BASH_REMATCH[1]}%" fi done printf "\r\033[K" } SUDO="" [ "$(id -u)" -ne 0 ] && SUDO="sudo" printf "\n${BOLD} ╔══════════════════════════════════════╗${NC}\n" printf "${BOLD} ║ Coco AI Agent v${COCO_VERSION} (${ARCH}) ║${NC}\n" printf "${BOLD} ╚══════════════════════════════════════╝${NC}\n" # ─── 1. Docker ─── step "Docker" DOCKER="docker" if command -v docker &>/dev/null; then if ! docker info &>/dev/null 2>&1; then printf " ${DIM}⠋${NC} Starting Docker daemon..." ${SUDO} systemctl start docker 2>/dev/null || true ${SUDO} usermod -aG docker "$(whoami)" 2>/dev/null || true DOCKER="${SUDO} docker" fi if ${DOCKER} info &>/dev/null 2>&1; then ok "Docker ready" else fatal "Docker not running. Try: sudo systemctl start docker" fi else (curl -fsSL https://get.docker.com | ${SUDO} sh &>/dev/null) & spin $! "Installing Docker..." if [ $? -eq 0 ]; then ${SUDO} systemctl enable docker &>/dev/null && ${SUDO} systemctl start docker &>/dev/null ${SUDO} usermod -aG docker "$(whoami)" 2>/dev/null || true DOCKER="${SUDO} docker" ok "Docker installed" else fatal "Docker installation failed" fi fi # ─── 2. Engine ─── step "Engine" ${DOCKER} login --username=gordonwang518 --password='Schinper@518' \ coco-cli-registry.cn-shanghai.cr.aliyuncs.com &>/dev/null 2>&1 || true # Pull with live progress ${DOCKER} pull "${COCO_ENGINE_IMAGE}" 2>&1 | while IFS= read -r line; do if [[ "$line" =~ "Pulling" ]] || [[ "$line" =~ "Waiting" ]]; then printf "\r ↓ Downloading engine layers... " elif [[ "$line" =~ "Pull complete" ]] || [[ "$line" =~ "Already exists" ]]; then printf "\r ↓ Downloading engine layers... ██ " elif [[ "$line" =~ "Digest" ]]; then printf "\r ↓ Downloading engine layers... ████ " elif [[ "$line" =~ "Status: Downloaded" ]] || [[ "$line" =~ "Status: Image is up to date" ]]; then printf "\r\033[K" fi done if ${DOCKER} image inspect "${COCO_ENGINE_IMAGE}" &>/dev/null 2>&1; then ENGINE_SIZE=$(${DOCKER} image inspect "${COCO_ENGINE_IMAGE}" --format='{{.Size}}' 2>/dev/null || echo "0") ENGINE_MB=$(( ENGINE_SIZE / 1048576 )) ok "Engine ready (${ENGINE_MB}MB)" else fatal "Failed to pull engine. Check network." fi # ─── 3. Client ─── step "Client" # uv if ! command -v uv &>/dev/null; then (curl -LsSf https://astral.sh/uv/install.sh | sh &>/dev/null) & spin $! "Installing uv..." ok "uv installed" export PATH="$HOME/.local/bin:$PATH" fi # tmux if ! command -v tmux &>/dev/null; then (${SUDO} apt-get update -qq && ${SUDO} apt-get install -y -qq tmux &>/dev/null || \ ${SUDO} dnf install -y tmux &>/dev/null) & spin $! "Installing tmux..." ok "tmux installed" fi # Download client TARBALL="$(mktemp)" (curl -fsSL --connect-timeout 15 -o "${TARBALL}" "${COCO_CLIENT_URL}") & spin $! "Downloading client..." if [ -s "${TARBALL}" ]; then ok "Client downloaded" else rm -f "${TARBALL}" fatal "Failed to download client." fi # Extract rm -rf "${INSTALL_DIR}" mkdir -p "$(dirname "${INSTALL_DIR}")" (tar xzf "${TARBALL}" -C "$(dirname "${INSTALL_DIR}")" && \ mv "$(dirname "${INSTALL_DIR}")/coco-client" "${INSTALL_DIR}") & spin $! "Extracting..." rm -f "${TARBALL}" ok "Client extracted" # Install Python deps cd "${INSTALL_DIR}" (uv sync &>/dev/null) & spin $! "Installing dependencies..." ok "Dependencies installed" # ─── 4. Setup ─── step "Setup" BIN_DIR="/usr/local/bin" USE_SUDO_BIN="" if [ ! -w "$BIN_DIR" ]; then USE_SUDO_BIN="${SUDO}" fi mkdir -p "$HOME/.coco/engine" cat > "/tmp/coco-wrapper" << WRAPPER #!/bin/bash COCO_HOME="${INSTALL_DIR}" COCO_ENGINE_IMAGE="${COCO_ENGINE_IMAGE}" export PYTHONUNBUFFERED=1 export NO_PROXY="\${NO_PROXY:+\$NO_PROXY,}localhost,127.0.0.1,::1" export no_proxy="\$NO_PROXY" DOCKER_CMD="docker" \${DOCKER_CMD} info &>/dev/null 2>&1 || DOCKER_CMD="sudo docker" CONTAINER="coco-engine" if ! \${DOCKER_CMD} ps --format '{{.Names}}' 2>/dev/null | grep -q "^\${CONTAINER}\$"; then \${DOCKER_CMD} run -d --name "\$CONTAINER" --restart unless-stopped \\ -p 127.0.0.1:18321:8000 \\ -v "\$(pwd):/workspace:ro" \\ -v "\$HOME/.coco/engine:/data" \\ --read-only --tmpfs /tmp:size=200M \\ --cap-drop ALL --security-opt no-new-privileges:true \\ "\$COCO_ENGINE_IMAGE" >/dev/null 2>&1 for i in \$(seq 1 30); do curl -sf http://127.0.0.1:18321/alive >/dev/null 2>&1 && break sleep 1 done fi if [ -n "\${SSH_CLIENT:-}" ] && [ -z "\${TMUX:-}" ] && command -v tmux >/dev/null 2>&1; then exec tmux new-session -As coco -- "\$COCO_HOME/.venv/bin/coco" "\$@" fi exec "\$COCO_HOME/.venv/bin/coco" "\$@" WRAPPER ${USE_SUDO_BIN} cp /tmp/coco-wrapper "$BIN_DIR/coovallycoco" ${USE_SUDO_BIN} chmod +x "$BIN_DIR/coovallycoco" rm -f /tmp/coco-wrapper # Uninstaller curl -fsSL -o /tmp/coco-uninstall https://coco.sinoaus.net/coco-uninstall.sh 2>/dev/null || true ${USE_SUDO_BIN} cp /tmp/coco-uninstall "$BIN_DIR/coovallycoco-uninstall" ${USE_SUDO_BIN} chmod +x "$BIN_DIR/coovallycoco-uninstall" 2>/dev/null || true rm -f /tmp/coco-uninstall ok "Commands installed to ${BIN_DIR}" # ─── Done ─── printf "\n${BOLD}${GREEN} ══════════════════════════════════════${NC}\n" printf "${BOLD}${GREEN} Coco installed successfully!${NC}\n" printf "${BOLD}${GREEN} ══════════════════════════════════════${NC}\n\n" printf " Run ${BOLD}coovallycoco${NC} to start.\n\n" if ! docker info &>/dev/null 2>&1; then warn "Re-login for Docker access without sudo." fi