install.sh
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -euo pipefail
BIND_PATHS=(
"./data"
"./darkmoon-settings"
"./workflows"
)
OPENCODE_ENV_FILE=".opencode.env"
# Colors
CYAN="\033[1;36m"
BLUE="\033[1;34m"
GREEN="\033[1;32m"
YELLOW="\033[1;33m"
RED="\033[1;31m"
MAGENTA="\033[1;35m"
BOLD="\033[1m"
RESET="\033[0m"
# ─────────────────────────────────────────────────────────────────
# Parse args
# ─────────────────────────────────────────────────────────────────
FORCE_RESET=false
for ARG in "$@"; do
case "${ARG}" in
--help|-h)
echo "Usage: ./install.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --init Force LLM provider reconfiguration (ignores existing .opencode.env)"
echo " --help Show this help"
echo ""
echo "Examples:"
echo " ./install.sh # rebuild, skip provider form if already configured"
echo " ./install.sh --init # rebuild and reconfigure LLM provider"
exit 0 ;;
--reset|--init) FORCE_RESET=true ;;
esac
done
SCRIPT_DIR="$(pwd)"
echo -e "${CYAN}"
cat << "EOF"
____ _
| _ \ __ _ _ __| | ___ __ ___ ___ ___ _ __
| | | |/ _` | '__| |/ / '_ ` _ \ / _ \ / _ \| '_ \
| |_| | (_| | | | <| | | | | | (_) | (_) | | | |
|____/ \__,_|_| |_|\_\_| |_| |_|\___/ \___/|_| |_|
EOF
echo -e "${RESET}"
echo -e "${BLUE}🔎 Checking prerequisites...${RESET}"
if ! command -v docker >/dev/null 2>&1; then
echo -e "${RED}❌ Docker is not installed.${RESET}"
echo -e "${YELLOW}Please install Docker before running this script.${RESET}"
echo ""
echo "Install guide: https://docs.docker.com/engine/install/"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo -e "${RED}❌ Docker daemon is not running.${RESET}"
echo -e "${YELLOW}Please start Docker and retry.${RESET}"
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo -e "${RED}❌ Docker Compose (v2) is not available.${RESET}"
echo -e "${YELLOW}Install Docker Compose plugin:${RESET}"
echo "https://docs.docker.com/compose/install/"
exit 1
fi
echo -e "${GREEN}✔ Docker and Docker Compose detected${RESET}"
# ─────────────────────────────────────────────────────────────────
# Save .opencode.env BEFORE purge (unless --init)
# ─────────────────────────────────────────────────────────────────
SAVED_OPENCODE_ENV=""
if [ "${FORCE_RESET}" = false ] && [ -f "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}" ]; then
SAVED_OPENCODE_ENV="$(cat "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}")"
fi
# ─────────────────────────────────────────────────────────────────
# Detect if provider already configured
# ─────────────────────────────────────────────────────────────────
SKIP_PROVIDER_FORM=false
if [ -n "${SAVED_OPENCODE_ENV}" ]; then
set +u
eval "$(echo "${SAVED_OPENCODE_ENV}" | sed 's/:[[:space:]]\+/=/g' | sed 's/\r//' | grep -E '^[A-Z_]+=.+')" 2>/dev/null || true
set -u
if [ -n "${OPENROUTER_PROVIDER:-}" ] && \
[ -n "${OPENROUTER_API_KEY:-}" ] && \
[ -n "${OPENCODE_MODEL:-}" ]; then
SKIP_PROVIDER_FORM=true
elif [ "${OPENCODE_LOCAL_MODE:-}" = "true" ] && \
[ -n "${OPENCODE_LOCAL_PROVIDER_ID:-}" ] && \
[ -n "${OPENCODE_LOCAL_BASE_URL:-}" ] && \
[ -n "${OPENCODE_LOCAL_MODEL:-}" ]; then
SKIP_PROVIDER_FORM=true
elif [ -n "${ANTHROPIC_BASE_URL:-}" ] && [ -n "${ANTHROPIC_MODEL:-}" ]; then
SKIP_PROVIDER_FORM=true
fi
fi
# ─────────────────────────────────────────────────────────────────
# LLM Provider configuration
# ─────────────────────────────────────────────────────────────────
if [ "${SKIP_PROVIDER_FORM}" = true ]; then
echo -e "${GREEN}✔ LLM provider already configured — skipping${RESET}"
printf '%s\n' "${SAVED_OPENCODE_ENV}" > "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}"
else
echo ""
echo -e "${BOLD}${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e " 🤖 LLM PROVIDER CONFIGURATION"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo ""
echo -e " ${CYAN}[1]${RESET} ${BOLD}Cloud provider${RESET} (Anthropic, OpenRouter, OpenAI, etc.)"
echo -e " ${CYAN}[2]${RESET} ${BOLD}Local model${RESET} (Ollama, llama.cpp / llama-server)"
echo -e " ${CYAN}[3]${RESET} ${BOLD}On-prem Anthropic-compatible${RESET} (custom ANTHROPIC_BASE_URL)"
echo ""
while true; do
read -r -p "$(echo -e "${YELLOW}Your choice [1/2/3]: ${RESET}")" PROVIDER_TYPE
case "${PROVIDER_TYPE}" in
1|cloud|Cloud) PROVIDER_TYPE="cloud"; break ;;
2|local|Local) PROVIDER_TYPE="local"; break ;;
3|anthropic|Anthropic) PROVIDER_TYPE="anthropic"; break ;;
*) echo -e "${RED} Please enter 1, 2 or 3.${RESET}" ;;
esac
done
# ── Cloud ────────────────────────────────────────────────────────
if [ "${PROVIDER_TYPE}" = "cloud" ]; then
echo ""
echo -e "${CYAN}Available cloud providers (examples):${RESET}"
echo -e " ${YELLOW}anthropic${RESET} → claude-opus-4-6, claude-sonnet-4-5"
echo -e " ${YELLOW}openai${RESET} → gpt-4o, o3"
echo -e " ${YELLOW}openrouter${RESET} → any model via openrouter.ai"
echo ""
read -r -p "$(echo -e "${YELLOW}Provider name (e.g. anthropic): ${RESET}")" CLOUD_PROVIDER
while [ -z "${CLOUD_PROVIDER}" ]; do
echo -e "${RED} Provider name cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Provider name: ${RESET}")" CLOUD_PROVIDER
done
read -r -p "$(echo -e "${YELLOW}Model name (e.g. claude-opus-4-6): ${RESET}")" CLOUD_MODEL
while [ -z "${CLOUD_MODEL}" ]; do
echo -e "${RED} Model name cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Model name: ${RESET}")" CLOUD_MODEL
done
read -r -s -p "$(echo -e "${YELLOW}API key: ${RESET}")" CLOUD_API_KEY
echo ""
while [ -z "${CLOUD_API_KEY}" ]; do
echo -e "${RED} API key cannot be empty.${RESET}"
read -r -s -p "$(echo -e "${YELLOW}API key: ${RESET}")" CLOUD_API_KEY
echo ""
done
cat > "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}" <<EOF
# Darkmoon — LLM cloud provider config
# Generated by install.sh on $(date '+%Y-%m-%d %H:%M:%S')
OPENROUTER_PROVIDER=${CLOUD_PROVIDER}
OPENCODE_MODEL=${CLOUD_MODEL}
OPENROUTER_API_KEY=${CLOUD_API_KEY}
EOF
# ── On-prem Anthropic-compatible ──────────────────────────────────
elif [ "${PROVIDER_TYPE}" = "anthropic" ]; then
echo ""
echo -e "${CYAN}On-prem / custom Anthropic-compatible endpoint${RESET}"
echo -e "${CYAN}(opencode talks to it via ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY)${RESET}"
echo ""
read -r -p "$(echo -e "${YELLOW}Base URL (e.g. https://llm.corp.tld/my-model/v1): ${RESET}")" ANTHROPIC_BASE_URL
while [ -z "${ANTHROPIC_BASE_URL}" ]; do
echo -e "${RED} Base URL cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Base URL: ${RESET}")" ANTHROPIC_BASE_URL
done
read -r -p "$(echo -e "${YELLOW}Model name (e.g. claude-opus-4-6): ${RESET}")" ANTHROPIC_MODEL
while [ -z "${ANTHROPIC_MODEL}" ]; do
echo -e "${RED} Model name cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Model name: ${RESET}")" ANTHROPIC_MODEL
done
# API key is OPTIONAL — leave empty if the endpoint needs none.
read -r -s -p "$(echo -e "${YELLOW}API key (optional — leave empty if none): ${RESET}")" ANTHROPIC_API_KEY
echo ""
cat > "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}" <<EOF
# Darkmoon — on-prem Anthropic-compatible LLM config
# Generated by install.sh on $(date '+%Y-%m-%d %H:%M:%S')
ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL}
ANTHROPIC_MODEL=${ANTHROPIC_MODEL}
EOF
# opencode's Anthropic provider needs a key string to register the model.
# Use the provided key, or a harmless placeholder for keyless on-prem endpoints
# (the endpoint ignores it; a key-checking endpoint gets the real key the user typed).
printf 'ANTHROPIC_API_KEY=%s\n' "${ANTHROPIC_API_KEY:-darkmoon-local}" >> "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}"
# ── Local ─────────────────────────────────────────────────────────
else
echo ""
echo -e "${CYAN}Local provider options:${RESET}"
echo -e " ${YELLOW}[1]${RESET} Ollama (default: http://localhost:11434/v1)"
echo -e " ${YELLOW}[2]${RESET} llama.cpp (llama-server, default: http://localhost:8080/v1)"
echo -e " ${YELLOW}[3]${RESET} Custom URL (any OpenAI-compatible endpoint)"
echo ""
while true; do
read -r -p "$(echo -e "${YELLOW}Local engine [1/2/3]: ${RESET}")" LOCAL_ENGINE
case "${LOCAL_ENGINE}" in
1|ollama|Ollama)
LOCAL_PROVIDER_ID="ollama"
LOCAL_PROVIDER_NAME="Ollama (local)"
DEFAULT_BASE_URL="http://localhost:11434/v1"
break ;;
2|llama*|llamacpp)
LOCAL_PROVIDER_ID="llama.cpp"
LOCAL_PROVIDER_NAME="llama-server (local)"
DEFAULT_BASE_URL="http://localhost:8080/v1"
break ;;
3|custom|Custom)
LOCAL_PROVIDER_ID="local"
LOCAL_PROVIDER_NAME="Local model"
DEFAULT_BASE_URL=""
break ;;
*) echo -e "${RED} Please enter 1, 2 or 3.${RESET}" ;;
esac
done
read -r -p "$(echo -e "${YELLOW}Base URL [${DEFAULT_BASE_URL}]: ${RESET}")" LOCAL_BASE_URL
LOCAL_BASE_URL="${LOCAL_BASE_URL:-${DEFAULT_BASE_URL}}"
while [ -z "${LOCAL_BASE_URL}" ]; do
echo -e "${RED} Base URL cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Base URL: ${RESET}")" LOCAL_BASE_URL
done
read -r -p "$(echo -e "${YELLOW}Model name (e.g. qwen2.5-coder:7b): ${RESET}")" LOCAL_MODEL
while [ -z "${LOCAL_MODEL}" ]; do
echo -e "${RED} Model name cannot be empty.${RESET}"
read -r -p "$(echo -e "${YELLOW}Model name: ${RESET}")" LOCAL_MODEL
done
# Optional API key for authenticated OpenAI-compatible endpoints (e.g. proxies).
# Leave empty for engines that need no auth (Ollama, local llama.cpp, ...).
read -r -s -p "$(echo -e "${YELLOW}API key (optional — leave empty if your endpoint needs none): ${RESET}")" LOCAL_API_KEY
echo ""
cat > "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}" <<EOF
# Darkmoon — LLM local provider config
# Generated by install.sh on $(date '+%Y-%m-%d %H:%M:%S')
OPENCODE_LOCAL_MODE=true
OPENCODE_LOCAL_PROVIDER_ID=${LOCAL_PROVIDER_ID}
OPENCODE_LOCAL_PROVIDER_NAME=${LOCAL_PROVIDER_NAME}
OPENCODE_LOCAL_BASE_URL=${LOCAL_BASE_URL}
OPENCODE_LOCAL_MODEL=${LOCAL_MODEL}
EOF
if [ -n "${LOCAL_API_KEY:-}" ]; then
printf 'OPENCODE_LOCAL_API_KEY=%s\n' "${LOCAL_API_KEY}" >> "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}"
fi
fi # end PROVIDER_TYPE
fi # end SKIP_PROVIDER_FORM
chmod 644 "${SCRIPT_DIR}/${OPENCODE_ENV_FILE}"
echo -e "${GREEN}✔ ${OPENCODE_ENV_FILE} written${RESET}"
# ─────────────────────────────────────────────────────────────────
# Stop stack + purge + rebuild
# ─────────────────────────────────────────────────────────────────
echo -e "${BLUE}🛑 Stopping stack (containers + networks + volumes + images)...${RESET}"
docker compose down --remove-orphans --volumes --rmi all
echo -e "${BLUE}🧹 Purging bind mounts...${RESET}"
for path in "${BIND_PATHS[@]}"; do
if [ -d "$path" ]; then
echo -e "${YELLOW} - removing ${path}${RESET}"
rm -rf "$path"
else
echo -e "${YELLOW} - ${path} (absent)${RESET}"
fi
done
echo -e "${BLUE}🧽 Purging docker build cache...${RESET}"
docker builder prune -f
COMPOSE_FILE="docker-compose.yml"
ARCH="$(uname -m)"
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
echo -e "${YELLOW}ARM64 architecture detected. Using docker-compose-dev.yml to build images locally...${RESET}"
COMPOSE_FILE="docker-compose-dev.yml"
fi
# GPU passthrough is opt-in at install time. hashcat is the one tool it changes
# (offline cracking: hours on CPU threads, seconds on a GPU); network brute-forcers
# are bound by the target, not by local compute, so they gain nothing. `gpus: all`
# is kept in an overlay because it makes the container refuse to start on a host
# without a GPU runtime, which would break every CPU-only install.
COMPOSE_ARGS="-f $COMPOSE_FILE"
if docker info --format '{{json .Runtimes}}' 2>/dev/null | grep -q nvidia \
|| docker run --rm --gpus all ubuntu:22.04 true >/dev/null 2>&1; then
echo -e "${GREEN}GPU runtime detected. Enabling GPU passthrough for the toolbox.${RESET}"
COMPOSE_ARGS="$COMPOSE_ARGS -f docker-compose.gpu.yml"
else
echo -e "${YELLOW}No GPU runtime detected. The toolbox will run hashcat on CPU.${RESET}"
echo -e "${YELLOW}To enable it later, install the NVIDIA container toolkit and re-run:${RESET}"
echo -e "${YELLOW} docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d${RESET}"
fi
echo -e "${BLUE}🔨 Rebuilding images (no cache)...${RESET}"
docker compose $COMPOSE_ARGS build --no-cache
echo -e "${BLUE}🚀 Recreating containers...${RESET}"
docker compose $COMPOSE_ARGS up -d --force-recreate
echo -e "${GREEN}✅ Darkmoon stack rebuilt CLEAN${RESET}"