feat: add no-Docker install path (conda/venv + uvicorn + npm build)
This commit is contained in:
parent
7672dd758a
commit
234c76e686
1 changed files with 164 additions and 31 deletions
195
install.sh
195
install.sh
|
|
@ -1,93 +1,226 @@
|
||||||
#!/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 compose build
|
|
||||||
|
|
||||||
info "Starting Snipe..."
|
# ── Docker install path ───────────────────────────────────────────────────────
|
||||||
docker compose up -d
|
|
||||||
|
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 ""
|
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 ""
|
||||||
echo " Web UI: http://localhost:8509"
|
echo " For production, point nginx at web/dist/ and proxy /api/ to localhost:8510"
|
||||||
echo " API: http://localhost:8510/docs"
|
hr
|
||||||
echo ""
|
|
||||||
echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue