feat: add no-Docker install path (conda/venv + uvicorn + npm build)

This commit is contained in:
pyr0ball 2026-04-05 22:09:24 -07:00
parent 7672dd758a
commit 234c76e686

View file

@ -1,83 +1,110 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Snipe — self-hosted install script # 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: # Usage:
# bash install.sh # installs to ~/snipe # bash install.sh # installs to ~/snipe
# bash install.sh /opt/snipe # custom install directory # 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 set -euo pipefail
INSTALL_DIR="${1:-$HOME/snipe}" INSTALL_DIR="${1:-$HOME/snipe}"
FORCE_NO_DOCKER="${2:-}"
FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge" FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge"
CONDA_ENV="cf"
info() { echo " [snipe] $*"; } info() { echo " [snipe] $*"; }
ok() { echo "$*"; } ok() { echo "$*"; }
warn() { echo "! $*"; }
fail() { echo "$*" >&2; exit 1; } fail() { echo "$*" >&2; exit 1; }
hr() { echo "────────────────────────────────────────────────────────"; }
echo "" echo ""
echo " Snipe — self-hosted installer" echo " Snipe — self-hosted installer"
echo " Install directory: $INSTALL_DIR" echo " Install directory: $INSTALL_DIR"
echo "" 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/" HAS_DOCKER=false
docker compose version >/dev/null 2>&1 || fail "Docker Compose plugin is required (docker compose, not docker-compose)." HAS_CONDA=false
command -v git >/dev/null 2>&1 || fail "Git is required." HAS_PYTHON=false
ok "Docker $(docker --version | awk '{print $3}' | tr -d ,) and Git found." 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 ────────────────────────────────────────────────────────────── # ── 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" SNIPE_DIR="$INSTALL_DIR/snipe"
CORE_DIR="$INSTALL_DIR/circuitforge-core" CORE_DIR="$INSTALL_DIR/circuitforge-core"
if [[ -d "$SNIPE_DIR" ]]; then 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 git -C "$SNIPE_DIR" pull --ff-only
else else
info "Cloning Snipe..." info "Cloning Snipe..."
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
git clone "$FORGEJO/snipe.git" "$SNIPE_DIR" git clone "$FORGEJO/snipe.git" "$SNIPE_DIR"
fi fi
ok "Snipe cloned to $SNIPE_DIR" ok "Snipe $SNIPE_DIR"
if [[ -d "$CORE_DIR" ]]; then 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 git -C "$CORE_DIR" pull --ff-only
else else
info "Cloning circuitforge-core (shared library)..." info "Cloning circuitforge-core (shared library)..."
git clone "$FORGEJO/circuitforge-core.git" "$CORE_DIR" git clone "$FORGEJO/circuitforge-core.git" "$CORE_DIR"
fi fi
ok "circuitforge-core cloned to $CORE_DIR" ok "circuitforge-core $CORE_DIR"
# ── Configure environment ──────────────────────────────────────────────────── # ── Configure environment ────────────────────────────────────────────────────
ENV_FILE="$SNIPE_DIR/.env" ENV_FILE="$SNIPE_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then if [[ ! -f "$ENV_FILE" ]]; then
cp "$SNIPE_DIR/.env.example" "$ENV_FILE" 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" ok ".env created from .env.example"
echo "" echo ""
echo " ┌────────────────────────────────────────────────────────┐" info "Snipe works out of the box with no API keys."
echo " │ Next step: edit $ENV_FILE" info "Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)."
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 " └────────────────────────────────────────────────────────┘"
echo "" echo ""
else else
info ".env already exists — skipping (delete it to reset)" info ".env already exists — skipping (delete it to reset)"
fi fi
# ── Build and start ──────────────────────────────────────────────────────────
info "Building Docker images (first run downloads ~1 GB of dependencies)..."
cd "$SNIPE_DIR" cd "$SNIPE_DIR"
# ── Docker install path ───────────────────────────────────────────────────────
if [[ "$INSTALL_PATH" == "docker" ]]; then
info "Building Docker images (~1 GB download on first run)..."
docker compose build docker compose build
info "Starting Snipe..." info "Starting Snipe..."
@ -85,9 +112,115 @@ docker compose up -d
echo "" echo ""
ok "Snipe is running!" ok "Snipe is running!"
echo "" hr
echo " Web UI: http://localhost:8509" echo " Web UI: http://localhost:8509"
echo " API: http://localhost:8510/docs" echo " API: http://localhost:8510/docs"
echo "" echo ""
echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}" 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 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 " For production, point nginx at web/dist/ and proxy /api/ to localhost:8510"
hr
echo "" echo ""