setup.sh
Rama main · réplica de lectura del repositorio original.
#!/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 "$@"