From 234c76e68656717f513b7f97e9dc74e16c247f61 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 5 Apr 2026 22:09:24 -0700 Subject: [PATCH] feat: add no-Docker install path (conda/venv + uvicorn + npm build) --- install.sh | 195 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 164 insertions(+), 31 deletions(-) diff --git a/install.sh b/install.sh index 7d4a2c6..3a24ad3 100755 --- a/install.sh +++ b/install.sh @@ -1,93 +1,226 @@ #!/usr/bin/env bash # Snipe — self-hosted install script -# Clones Snipe and its shared library (circuitforge-core) into a workspace, -# then starts the Docker stack. +# +# Supports two install paths: +# Docker (recommended) — everything in containers, no system Python deps required +# No-Docker — conda or venv + direct uvicorn, for machines without Docker # # Usage: # bash install.sh # installs to ~/snipe # bash install.sh /opt/snipe # custom install directory +# bash install.sh ~/snipe --no-docker # force no-Docker path even if Docker present # -# Requirements: Docker with Compose plugin, Git +# Requirements (Docker path): Docker with Compose plugin, Git +# Requirements (no-Docker path): Python 3.11+, Node.js 20+, Git, xvfb (system) set -euo pipefail INSTALL_DIR="${1:-$HOME/snipe}" +FORCE_NO_DOCKER="${2:-}" FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge" +CONDA_ENV="cf" info() { echo " [snipe] $*"; } ok() { echo "✓ $*"; } +warn() { echo "! $*"; } fail() { echo "✗ $*" >&2; exit 1; } +hr() { echo "────────────────────────────────────────────────────────"; } echo "" echo " Snipe — self-hosted installer" echo " Install directory: $INSTALL_DIR" echo "" -# ── Pre-flight checks ──────────────────────────────────────────────────────── +# ── Detect capabilities ────────────────────────────────────────────────────── -command -v docker >/dev/null 2>&1 || fail "Docker is required. Install from https://docs.docker.com/get-docker/" -docker compose version >/dev/null 2>&1 || fail "Docker Compose plugin is required (docker compose, not docker-compose)." -command -v git >/dev/null 2>&1 || fail "Git is required." -ok "Docker $(docker --version | awk '{print $3}' | tr -d ,) and Git found." +HAS_DOCKER=false +HAS_CONDA=false +HAS_PYTHON=false +HAS_NODE=false + +docker compose version >/dev/null 2>&1 && HAS_DOCKER=true +conda --version >/dev/null 2>&1 && HAS_CONDA=true +python3 --version >/dev/null 2>&1 && HAS_PYTHON=true +node --version >/dev/null 2>&1 && HAS_NODE=true +command -v git >/dev/null 2>&1 || fail "Git is required. Install with: sudo apt-get install git" + +# Honour --no-docker flag +[[ "$FORCE_NO_DOCKER" == "--no-docker" ]] && HAS_DOCKER=false + +if $HAS_DOCKER; then + INSTALL_PATH="docker" + ok "Docker found — using Docker install path (recommended)" +elif $HAS_PYTHON; then + INSTALL_PATH="python" + warn "Docker not found — using no-Docker path (conda or venv)" +else + fail "Docker or Python 3.11+ is required. Install Docker: https://docs.docker.com/get-docker/" +fi # ── Clone repos ────────────────────────────────────────────────────────────── -# compose.yml builds with context: .. so both repos must be siblings. +# compose.yml and the Dockerfile both use context: .. (parent directory), so +# snipe/ and circuitforge-core/ must be siblings inside INSTALL_DIR. SNIPE_DIR="$INSTALL_DIR/snipe" CORE_DIR="$INSTALL_DIR/circuitforge-core" if [[ -d "$SNIPE_DIR" ]]; then - info "Snipe already exists at $SNIPE_DIR — pulling latest..." + info "Snipe already cloned — pulling latest..." git -C "$SNIPE_DIR" pull --ff-only else info "Cloning Snipe..." mkdir -p "$INSTALL_DIR" git clone "$FORGEJO/snipe.git" "$SNIPE_DIR" fi -ok "Snipe cloned to $SNIPE_DIR" +ok "Snipe → $SNIPE_DIR" if [[ -d "$CORE_DIR" ]]; then - info "circuitforge-core already exists — pulling latest..." + info "circuitforge-core already cloned — pulling latest..." git -C "$CORE_DIR" pull --ff-only else info "Cloning circuitforge-core (shared library)..." git clone "$FORGEJO/circuitforge-core.git" "$CORE_DIR" fi -ok "circuitforge-core cloned to $CORE_DIR" +ok "circuitforge-core → $CORE_DIR" # ── Configure environment ──────────────────────────────────────────────────── ENV_FILE="$SNIPE_DIR/.env" if [[ ! -f "$ENV_FILE" ]]; then cp "$SNIPE_DIR/.env.example" "$ENV_FILE" + # Safe defaults for local installs — no eBay registration, no Heimdall + sed -i 's/^EBAY_WEBHOOK_VERIFY_SIGNATURES=true/EBAY_WEBHOOK_VERIFY_SIGNATURES=false/' "$ENV_FILE" ok ".env created from .env.example" echo "" - echo " ┌────────────────────────────────────────────────────────┐" - echo " │ Next step: edit $ENV_FILE │" - echo " │ │" - echo " │ Snipe works out of the box with no API keys. │" - echo " │ Add EBAY_APP_ID / EBAY_CERT_ID for faster searches │" - echo " │ and full seller account age data (optional). │" - echo " └────────────────────────────────────────────────────────┘" + info "Snipe works out of the box with no API keys." + info "Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)." echo "" else info ".env already exists — skipping (delete it to reset)" fi -# ── Build and start ────────────────────────────────────────────────────────── - -info "Building Docker images (first run downloads ~1 GB of dependencies)..." cd "$SNIPE_DIR" -docker compose build -info "Starting Snipe..." -docker compose up -d +# ── Docker install path ─────────────────────────────────────────────────────── + +if [[ "$INSTALL_PATH" == "docker" ]]; then + info "Building Docker images (~1 GB download on first run)..." + docker compose build + + info "Starting Snipe..." + docker compose up -d + + echo "" + ok "Snipe is running!" + hr + echo " Web UI: http://localhost:8509" + echo " API: http://localhost:8510/docs" + echo "" + echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}" + hr + echo "" + exit 0 +fi + +# ── No-Docker install path ─────────────────────────────────────────────────── + +# System deps: Xvfb is required for Playwright (Kasada bypass via headed Chromium) +if ! command -v Xvfb >/dev/null 2>&1; then + info "Installing Xvfb (required for eBay scraper)..." + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get install -y --no-install-recommends xvfb + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y xorg-x11-server-Xvfb + elif command -v brew >/dev/null 2>&1; then + warn "macOS: Xvfb not available. The scraper fallback may fail." + warn "Add eBay API credentials to .env to use the API adapter instead." + else + warn "Could not install Xvfb automatically. Install it with your package manager." + fi +fi + +# ── Python environment setup ───────────────────────────────────────────────── + +if $HAS_CONDA; then + info "Setting up conda environment '$CONDA_ENV'..." + if conda env list | grep -q "^$CONDA_ENV "; then + info "Conda env '$CONDA_ENV' already exists — updating..." + conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR" + conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR" + else + conda create -n "$CONDA_ENV" python=3.11 -y + conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR" + conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR" + fi + conda run -n "$CONDA_ENV" playwright install chromium + conda run -n "$CONDA_ENV" playwright install-deps chromium + PYTHON_RUN="conda run -n $CONDA_ENV" + ok "Conda environment '$CONDA_ENV' ready" +else + info "Setting up Python venv at $SNIPE_DIR/.venv ..." + python3 -m venv "$SNIPE_DIR/.venv" + "$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$CORE_DIR" + "$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$SNIPE_DIR" + "$SNIPE_DIR/.venv/bin/playwright" install chromium + "$SNIPE_DIR/.venv/bin/playwright" install-deps chromium + PYTHON_RUN="$SNIPE_DIR/.venv/bin" + ok "Python venv ready at $SNIPE_DIR/.venv" +fi + +# ── Frontend ───────────────────────────────────────────────────────────────── + +if $HAS_NODE; then + info "Building Vue frontend..." + cd "$SNIPE_DIR/web" + npm ci --prefer-offline --silent + npm run build + cd "$SNIPE_DIR" + ok "Frontend built → web/dist/" +else + warn "Node.js not found — skipping frontend build." + warn "Install Node.js 20+ from https://nodejs.org and re-run install.sh to build the UI." + warn "Until then, you can access the API directly at http://localhost:8510/docs" +fi + +# ── Write start/stop scripts ───────────────────────────────────────────────── + +cat > "$SNIPE_DIR/start-local.sh" << 'STARTSCRIPT' +#!/usr/bin/env bash +# Start Snipe without Docker (API only — run from the snipe/ directory) +set -euo pipefail +cd "$(dirname "$0")" + +if [[ -f .venv/bin/uvicorn ]]; then + UVICORN=".venv/bin/uvicorn" +elif command -v conda >/dev/null 2>&1 && conda env list | grep -q "^cf "; then + UVICORN="conda run -n cf uvicorn" +else + echo "No Python env found. Run install.sh first." >&2; exit 1 +fi + +mkdir -p data +echo "Starting Snipe API on http://localhost:8510 ..." +$UVICORN api.main:app --host 0.0.0.0 --port 8510 "${@}" +STARTSCRIPT +chmod +x "$SNIPE_DIR/start-local.sh" + +# Frontend serving (if built) +cat > "$SNIPE_DIR/serve-ui.sh" << 'UISCRIPT' +#!/usr/bin/env bash +# Serve the pre-built Vue frontend on port 8509 (dev only — use nginx for production) +cd "$(dirname "$0")/web/dist" +python3 -m http.server 8509 +UISCRIPT +chmod +x "$SNIPE_DIR/serve-ui.sh" echo "" -ok "Snipe is running!" +ok "Snipe installed (no-Docker mode)" +hr +echo " Start API: cd $SNIPE_DIR && ./start-local.sh" +echo " Serve UI: cd $SNIPE_DIR && ./serve-ui.sh (separate terminal)" +echo " API docs: http://localhost:8510/docs" +echo " Web UI: http://localhost:8509 (after ./serve-ui.sh)" echo "" -echo " Web UI: http://localhost:8509" -echo " API: http://localhost:8510/docs" -echo "" -echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}" +echo " For production, point nginx at web/dist/ and proxy /api/ to localhost:8510" +hr echo ""