Ctrl K
ComunidadGuías…

setup.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -euo pipefail

# olympus - Zero-Cost Coding Agent Platform Setup
# Usage: chmod +x setup.sh && ./setup.sh
# Run-time: Local CPU only - all heavy inference routed to free cloud providers
#   - Frontend: Hermes 3 (70B) via OpenRouter Free
#   - Backend: Codex via OmniRoute (auto/coding, Responses API)

ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
CORE_DIR="$ROOT_DIR/core-modules"
FACTORY_ENV="$ROOT_DIR/.factory-env"
ARCHON_CONFIG="$ROOT_DIR/.archon/config.yaml"

# ──────────────────────────────────────────────
# Colors & Helpers
# ──────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

info()    { echo -e "${CYAN}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die()     { error "$*"; exit 1; }

header() {
  echo ""
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
  echo -e "${BOLD}  $*${NC}"
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
}

# ──────────────────────────────────────────────
# 1. Validate System Requirements
# ──────────────────────────────────────────────
check_command() {
  local cmd="$1"
  local install_hint="$2"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    error "Missing required command: $cmd"
    echo -e "       Install hint: $install_hint"
    return 1
  fi
  local ver
  ver=$("$cmd" --version 2>&1 | head -n1 || echo "unknown")
  success "$cmd found — $ver"
  return 0
}

validate_requirements() {
  header "Validating System Requirements"

  local missing=0

  check_command "git"     "https://git-scm.com/downloads  |  sudo apt install git  |  brew install git" || missing=1
  check_command "bun"     "curl -fsSL https://bun.sh/install | bash  &&  source ~/.bashrc" || missing=1
  check_command "python3" "https://www.python.org/downloads  |  sudo apt install python3 python3-venv  |  brew install python3" || missing=1
  check_command "gh"      "https://cli.github.com/  |  sudo apt install gh  |  brew install gh  — then run: gh auth login" || missing=1
  check_command "npm"     "https://nodejs.org/  |  sudo apt install nodejs npm  |  brew install node (npm ships with node)" || missing=1
  # The app-manufacturing workflow's nodes all declare `runtime: uv`, and Archon runs
  # them through this binary. Nothing else installs it, and its absence used to surface
  # much later as a script node failing before it had read the spec, so it is required
  # here beside the rest.
  check_command "uv"      "curl -LsSf https://astral.sh/uv/install.sh | sh  |  brew install uv  |  https://docs.astral.sh/uv/getting-started/installation/" || missing=1

  # Extra version floors
  if command -v python3 >/dev/null 2>&1; then
    if ! python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then
      warn "python3 version < 3.10 detected — 3.10+ recommended for factory consumer"
    fi
  fi

  if command -v bun >/dev/null 2>&1; then
    local bun_major
    bun_major=$(bun --version 2>/dev/null | cut -d. -f1 || echo "0")
    if [[ "$bun_major" -lt 1 ]]; then
      warn "bun version < 1.0 detected — please upgrade: bun upgrade"
    fi
  fi

  # pip / venv check (non-fatal but warned)
  if ! python3 -m pip --version >/dev/null 2>&1; then
    warn "pip not available for python3 — factory Python deps may fail. Install with: python3 -m ensurepip --upgrade"
  fi

  if [[ $missing -ne 0 ]]; then
    echo ""
    die "One or more required tools are missing. Install them and re-run: chmod +x setup.sh && ./setup.sh"
  fi

  success "All required tools present"
}

# ──────────────────────────────────────────────
# 2. Create core-modules folder
# ──────────────────────────────────────────────
prepare_core_dir() {
  header "Preparing core-modules"
  mkdir -p "$CORE_DIR"
  success "Created $CORE_DIR"
  mkdir -p "$ROOT_DIR/.archon/cache"
  mkdir -p "$ROOT_DIR/factory"
  # Local dev: build-requests is the trigger, builds is the factory output (both documented, builds/ is gitignored)
  mkdir -p "$ROOT_DIR/build-requests"
  mkdir -p "$ROOT_DIR/builds"
  if [[ ! -f "$ROOT_DIR/build-requests/.gitkeep" && -z "$(ls -A "$ROOT_DIR/build-requests" 2>/dev/null | grep -v README)" ]]; then
    touch "$ROOT_DIR/build-requests/.gitkeep" 2>/dev/null || true
  fi
}

# ──────────────────────────────────────────────
# 3. Clone vendor tools if they do not exist
# ──────────────────────────────────────────────
clone_if_missing() {
  local name="$1"
  local repo="$2"
  local fallback="$3"
  local dest="$CORE_DIR/$name"

  if [[ -d "$dest/.git" ]]; then
    info "$name already cloned at $dest — pulling latest..."
    git -C "$dest" pull --ff-only || warn "git pull failed for $name — continuing with existing checkout"
  elif [[ -d "$dest" && -n "$(ls -A "$dest" 2>/dev/null)" ]]; then
    warn "$dest exists but is not a git repo — skipping clone for $name"
  else
    info "Cloning $name from $repo ..."
    if git clone --depth 1 "$repo" "$dest" 2>/dev/null; then
      success "$name cloned (mirror)"
    elif [[ -n "$fallback" ]] && git clone --depth 1 "$fallback" "$dest" 2>/dev/null; then
      success "$name cloned (upstream fallback: $fallback)"
    else
      error "Failed to clone $name from $repo ${fallback:+or $fallback}"
      return 1
    fi
  fi
}

clone_vendor_tools() {
  header "Cloning Vendor Tools"

  # Vendored mirrors under innotelinc (full-history copies of the upstreams, see
  # UPSTREAMS.md) so the factory survives upstream deletion/moves. The original
  # upstream is kept as fallback for updates. Override via env vars:
  #   OMNIROUTE_REPO, ARCHON_REPO, FACTORY_REPO
  local OMNIROUTE_REPO="${OMNIROUTE_REPO:-https://github.com/innotelinc/omniroute.git}"
  local OMNIROUTE_UPSTREAM="https://github.com/diegosouzapw/OmniRoute.git"
  local ARCHON_REPO="${ARCHON_REPO:-https://github.com/innotelinc/Archon.git}"
  local ARCHON_UPSTREAM="https://github.com/coleam00/Archon.git"
  local FACTORY_REPO="${FACTORY_REPO:-https://github.com/innotelinc/ai-software-factory.git}"
  local FACTORY_UPSTREAM="https://github.com/coleam00/ai-software-factory.git"

  clone_if_missing "omniroute"            "$OMNIROUTE_REPO"   "$OMNIROUTE_UPSTREAM"
  clone_if_missing "archon"               "$ARCHON_REPO"      "$ARCHON_UPSTREAM"
  clone_if_missing "ai-software-factory"  "$FACTORY_REPO"     "$FACTORY_UPSTREAM"

  success "All vendor tools ready under $CORE_DIR/"
}

# ──────────────────────────────────────────────
# 4. Run bun install inside respective directories
# ──────────────────────────────────────────────
bun_install_dir() {
  local dir="$1"
  if [[ -f "$dir/package.json" ]]; then
    info "Running bun install in $dir ..."
    (cd "$dir" && bun install)
    success "bun install complete: $dir"
  elif [[ -f "$dir/bun.lockb" || -f "$dir/bun.lock" ]]; then
    info "Running bun install in $dir (lockfile present) ..."
    (cd "$dir" && bun install)
    success "bun install complete: $dir"
  else
    warn "No package.json in $dir — skipping bun install"
  fi
}

install_dependencies() {
  header "Installing Dependencies (bun)"

  bun_install_dir "$CORE_DIR/omniroute"
  bun_install_dir "$CORE_DIR/archon"
  bun_install_dir "$CORE_DIR/ai-software-factory"

  # Root package.json if present (optional monorepo wrapper)
  if [[ -f "$ROOT_DIR/package.json" ]]; then
    bun_install_dir "$ROOT_DIR"
  fi
}

# ──────────────────────────────────────────────
# 5. Initialize Software Factory setup wizard programmatically
# ──────────────────────────────────────────────
init_factory_wizard() {
  header "Initializing AI Software Factory"

  local factory_dir="$CORE_DIR/ai-software-factory"

  # Mirror factory runner into top-level factory/ for the documented path:
  #   python3 factory/consumer.py tick
  if [[ -f "$factory_dir/consumer.py" && ! -f "$ROOT_DIR/factory/consumer.py" ]]; then
    cp "$factory_dir/consumer.py" "$ROOT_DIR/factory/consumer.py" 2>/dev/null || true
  fi
  if [[ -d "$factory_dir/factory" && ! -f "$ROOT_DIR/factory/consumer.py" ]]; then
    # Some forks nest under factory/factory
    cp "$factory_dir/factory/consumer.py" "$ROOT_DIR/factory/consumer.py" 2>/dev/null || true
  fi

  # Preferred: factory's own setup wizard
  if [[ -f "$factory_dir/setup.py" ]]; then
    info "Running factory setup wizard: python3 setup.py --non-interactive"
    (cd "$factory_dir" && python3 setup.py --non-interactive 2>/dev/null) \
      || (cd "$factory_dir" && python3 setup.py --yes 2>/dev/null) \
      || (cd "$factory_dir" && python3 -m factory.setup --init 2>/dev/null) \
      || warn "Factory setup wizard exited non-zero — you can re-run manually: cd $factory_dir && python3 setup.py"
    success "Factory wizard invoked"
  elif [[ -f "$factory_dir/scripts/setup.sh" ]]; then
    info "Running factory scripts/setup.sh"
    bash "$factory_dir/scripts/setup.sh" --non-interactive 2>/dev/null || bash "$factory_dir/scripts/setup.sh" || warn "factory scripts/setup.sh failed"
  elif [[ -f "$factory_dir/install.py" ]]; then
    info "Running factory install.py"
    (cd "$factory_dir" && python3 install.py --non-interactive 2>/dev/null || python3 install.py)
  else
    warn "No recognized factory wizard found — creating minimal factory skeleton"
    mkdir -p "$ROOT_DIR/factory"
    if [[ ! -f "$ROOT_DIR/factory/consumer.py" ]]; then
      cat > "$ROOT_DIR/factory/consumer.py" <<'PYEOF'
#!/usr/bin/env python3
"""Olympus factory consumer - placeholder.

This checkout ships the deployment surface only. The SDLC scheduler and the
workflow engine live in the AI Software Factory source repo (see README and
factory/doctor.py), so there is no DAG poller here and nothing that dispatches
to the gateway. This file does not pretend otherwise.

The real consumer is driven by `.factory/loop.sh`, which runs
`python factory/consumer.py tick` until a `.factory/STOP` marker appears.

Usage:
    python3 factory/consumer.py status
"""
import json
import os
import sys


def status():
    return {
        "state": "NOT_INSTALLED",
        "reason": "no factory consumer in this checkout - it lives in the source repo",
        "gateway": os.environ.get("OMNIROUTE_BASE_URL", "http://192.168.1.71:20128/v1"),
        "model": os.environ.get("OMNIROUTE_MODEL", "auto/coding"),
    }


if __name__ == "__main__":
    print(json.dumps(status(), indent=2))
    sys.exit(2)
PYEOF
      chmod +x "$ROOT_DIR/factory/consumer.py"
      success "Created stub $ROOT_DIR/factory/consumer.py"
    fi
  fi

  # Python deps if factory exposes requirements
  for req in "$factory_dir/requirements.txt" "$factory_dir/factory/requirements.txt" "$ROOT_DIR/factory/requirements.txt"; do
    if [[ -f "$req" ]]; then
      info "Installing Python deps from $req"
      python3 -m pip install -r "$req" --quiet || warn "pip install -r $req failed — try: python3 -m pip install -r $req"
    fi
  done

  # Ensure archon binary is executable if present
  if [[ -f "$CORE_DIR/archon/bin/archon" ]]; then
    chmod +x "$CORE_DIR/archon/bin/archon" || true
  fi

  success "Factory initialization done"
}

# ──────────────────────────────────────────────
# 6. Securely request Telegram Bot Token -> .factory-env
# ──────────────────────────────────────────────
configure_telegram_token() {
  header "Telegram Bot Token Setup"

  echo -e "${BOLD}This platform uses Hermes 3 (70B) via OpenRouter Free for the Telegram interface.${NC}"
  echo "Create a bot with @BotFather on Telegram to get your token:"
  echo "  1. Open Telegram -> search @BotFather -> /newbot"
  echo "  2. Follow prompts, copy the HTTP API token (e.g. 123456:ABC-...) "
  echo ""

  local existing_token=""
  if [[ -f "$FACTORY_ENV" ]]; then
    existing_token=$(grep -E "^TELEGRAM_BOT_TOKEN=" "$FACTORY_ENV" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'" || true)
    if [[ -n "$existing_token" && "$existing_token" != "ENV_VAR" ]]; then
      info "Existing token found in .factory-env (masked: ${existing_token:0:6}...)"
      read -r -p "Keep existing token? [Y/n]: " keep
      if [[ "$keep" =~ ^[nN] ]]; then
        existing_token=""
      else
        success "Keeping existing Telegram token"
        return 0
      fi
    fi
  fi

  # Allow non-interactive injection: TELEGRAM_BOT_TOKEN env var
  if [[ -n "${TELEGRAM_BOT_TOKEN:-}" && -z "$existing_token" ]]; then
    info "Using TELEGRAM_BOT_TOKEN from environment"
    existing_token="$TELEGRAM_BOT_TOKEN"
  fi

  local token="$existing_token"
  if [[ -z "$token" ]]; then
    # Secure prompt (no echo) with visible fallback
    if [[ -t 0 ]]; then
      echo -n "Paste your Telegram Bot Token (input hidden) : "
      read -r -s token || true
      echo ""
      if [[ -z "$token" ]]; then
        echo -n "Paste your Telegram Bot Token (visible)    : "
        read -r token || true
      fi
    else
      warn "Non-interactive shell — set TELEGRAM_BOT_TOKEN env var or re-run interactively"
    fi
  fi

  if [[ -z "$token" ]]; then
    warn "No token provided — writing placeholder. Edit $FACTORY_ENV later."
    token="PASTE_YOUR_TELEGRAM_BOT_TOKEN_HERE"
  fi

  # Basic format validation (Telegram tokens are <digits>:<35-char>)
  if [[ "$token" != "PASTE_YOUR_TELEGRAM_BOT_TOKEN_HERE" && ! "$token" =~ ^[0-9]+:[A-Za-z0-9_-]{30,}$ ]]; then
    warn "Token format looks unusual — Telegram tokens are like 123456789:AAH... — continuing anyway"
  fi

  # Write .factory-env atomically with 0600 perms
  local tmp_env
  tmp_env=$(mktemp)
  {
    echo "# Olympus Factory Environment — DO NOT COMMIT"
    echo "# Generated by setup.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)"
    echo "TELEGRAM_BOT_TOKEN=$token"
    echo "OPENROUTER_MODEL=nousresearch/hermes-3-llama-3-70b:free"
    # The platform's single gateway runs in Group 2, on its own host; callers
    # dial the SSO proxy in front of it (:20128), which exempts /v1. A caller on
    # the gateway's own host writes http://host.docker.internal:20128/v1 instead.
    echo "OMNIROUTE_BASE_URL=${OMNIROUTE_BASE_URL:-http://192.168.1.71:20128/v1}"
    # OMNIROUTE_MODEL is the key the factory and Studio actually read
    # (factory/doctor.py, web/studio/lib/omniroute.ts). QWEN_MODEL was written
    # here and consumed by nothing, so it is retired.
    echo "OMNIROUTE_MODEL=auto/coding"
    echo "ARCHON_BINARY=./core-modules/archon/bin/archon"
    # Rewrite every managed key, and drop the retired QWEN_MODEL from older files.
    if [[ -f "$FACTORY_ENV" ]]; then
      grep -v -E "^(TELEGRAM_BOT_TOKEN|OPENROUTER_MODEL|OMNIROUTE_BASE_URL|OMNIROUTE_MODEL|QWEN_MODEL|ARCHON_BINARY)=" "$FACTORY_ENV" 2>/dev/null || true
    fi
  } > "$tmp_env"
  mv "$tmp_env" "$FACTORY_ENV"
  chmod 600 "$FACTORY_ENV"
  success "Wrote $FACTORY_ENV (0600)"

  # Also ensure .archon/config.yaml points to ENV_VAR pattern (token injected at runtime via env)
  if [[ -f "$ARCHON_CONFIG" ]]; then
    info "Archon config present at $ARCHON_CONFIG — Telegram provider will read TELEGRAM_BOT_TOKEN from environment"
  fi

  echo ""
  echo -e "${YELLOW}Next:${NC} source .factory-env  or  export \$(cat .factory-env | xargs)  before launching the bot"
}

# ──────────────────────────────────────────────
# 7. Final summary
# ──────────────────────────────────────────────
print_summary() {
  header "Setup Complete — Olympus Ready"
  cat <<EOF
${GREEN}✔ Core modules:${NC}  $CORE_DIR/{omniroute,archon,ai-software-factory}
${GREEN}✔ Archon config:${NC} $ARCHON_CONFIG
${GREEN}✔ Factory env:${NC}   $FACTORY_ENV  (chmod 600)

${BOLD}Quickstart:${NC}
  1. Point at the platform gateway's door (http://192.168.1.71:20128/v1 —
     the SSO proxy in front of the gateway, which exempts /v1 for API clients;
     the gateway's own :20128 answers on that host's loopback and bridge alone,
     so it is not an address to copy into .env). That is what this script wrote
     to OMNIROUTE_BASE_URL, and it is what factory/doctor.py, the build runner
     and Studio all read. This stack does NOT run a gateway: the
     `omniroute` service and its volume were removed when the shared one took
     over (docs/stack.md), so there is no local :20128 to toggle — provider
     credentials and Free Tier Mode belong to that shared gateway's dashboard,
     which is itself behind Cerulean Authentik (`make gateway-sso-check`).
     On the gateway's own host, write http://host.docker.internal:20128/v1
     instead of the LAN address.

  2. Keep the SDLC loop alive (new terminal, from repo root):
       source .factory-env
       while [ ! -f .factory/STOP ]; do
         python3 factory/consumer.py tick || break
         sleep \${FACTORY_INTERVAL_SECONDS:-300}
       done
     (The factory ships this as .factory/loop.sh. `${BOLD}tick${NC}` submits one
      whole workflow; an empty .factory/STOP file halts the loop.)

  3. Message your bot on Telegram — Hermes 3 (70B) via OpenRouter Free
     will handle the chat, and Codex via OmniRoute (${BOLD}auto/coding${NC}) will
     perform codebase edits at ${BOLD}zero local CPU cost${NC}.

${YELLOW}Tip:${NC} Re-run this script any time:  chmod +x setup.sh && ./setup.sh
Docs: cat README.md
EOF
}

# ──────────────────────────────────────────────
# 8. Studio's writable directories
# ──────────────────────────────────────────────
# Two directories Studio writes into from inside its container, which runs as
# uid 1001: build-requests/ for "Export to factory", and .factory/build-queue/
# for "Build it". The bind mounts keep host ownership, so a root-owned directory
# answers 503 on every write. Settled here so a fresh install never needs the
# manual chown — and reported rather than fatal when the operator is not root,
# because the rest of setup is still useful to them.
#
# The queue is created here as well as chowned: if it does not exist, the first
# `docker compose up` creates the bind-mount source itself, as root, and no
# amount of documenting that afterwards makes the failure obvious.
prepare_studio_export_dir() {
  local script="$ROOT_DIR/scripts/studio-export-dir.sh"

  if [[ ! -x "$script" ]]; then
    info "scripts/studio-export-dir.sh not found — skipping Studio's writable directories"
    return 0
  fi

  local label target ok=true

  for spec in "build-requests:$ROOT_DIR/build-requests" "build-queue:$ROOT_DIR/.factory/build-queue"; do
    label="${spec%%:*}"
    target="${spec#*:}"

    header "Studio Writable Directory — $label"
    if STUDIO_DIR_LABEL="$label" "$script" "$target"; then
      success "$label is writable by the Studio container"
    else
      warn "$label is not writable by the Studio container yet — see above;"
      warn "writes will answer 503 until it is"
      ok=false
    fi
  done

  $ok
}

# ──────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────
main() {
  echo -e "${BOLD}"
  echo "   ___  _                              "
  echo "  / _ \| |_   _ _ __ ___  _ __  _   _ ___ "
  echo " | | | | | | | | '_ \` _ \| '_ \| | | / __|"
  echo " | |_| | | |_| | | | | | | |_) | |_| \__ \\"
  echo "  \___/|_|\__, |_| |_| |_| .__/ \__,_|___/"
  echo "          |___/          |_|              "
  echo -e "${NC}"
  echo -e "  Zero-Cost Coding Agent Platform — Local CPU, Free Cloud Brains"
  echo ""

  validate_requirements
  prepare_core_dir
  clone_vendor_tools
  install_dependencies
  init_factory_wizard
  configure_telegram_token
  prepare_studio_export_dir
  print_summary
}

main "$@"