#!/bin/bash # ───────────────────────────────────────────── # SPB Git — Personal Git Platform # ───────────────────────────────────────────── # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : deploy/setup-m3u96a.sh # Purpose : Idempotent bootstrap of the full stack on node m3u96a # License : MIT © Simon-Pierre Boucher # ───────────────────────────────────────────── # # Run FROM the app directory on m3u96a: # bash deploy/setup-m3u96a.sh # # What it does (all idempotent): # 1. Verifies node ≥ 20, git, ngrok (installs ngrok via brew when missing). # 2. Creates the data prefix: /srv on Linux, ~/srv on macOS (SIP-safe). # 3. npm ci --omit=dev + fonts. # 4. Writes .env with resolved paths. # 5. Generates the bootstrap PAT — printed ONCE, never stored in clear. # 6. Starts pm2 apps (server + ngrok tunnel) and saves the process list. # set -euo pipefail say() { printf '\033[1;34m▸ %s\033[0m\n' "$*"; } ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; } fail() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; } APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$APP_DIR" # ── 1. prerequisites ───────────────────────────────────────────── command -v git >/dev/null || fail "git is required" command -v node >/dev/null || fail "node ≥ 20 is required (brew install node@20 or nvm install 20)" NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" [ "$NODE_MAJOR" -ge 20 ] || fail "node ≥ 20 required, found $(node --version)" ok "node $(node --version), git $(git --version | awk '{print $3}')" if ! command -v ngrok >/dev/null; then if command -v brew >/dev/null; then say "installing ngrok via homebrew" brew install ngrok >/dev/null else fail "ngrok not found — install it from https://ngrok.com/download" fi fi ok "ngrok $(ngrok version 2>/dev/null | head -1)" if ! command -v pm2 >/dev/null; then say "installing pm2 globally" npm install -g pm2 >/dev/null fi ok "pm2 $(pm2 --version)" # ── 2. directories ─────────────────────────────────────────────── # /srv is read-only on macOS system volumes → use ~/srv there. if [ "$(uname)" = "Darwin" ]; then PREFIX="$HOME/srv" else PREFIX="/srv" [ -w "$PREFIX" ] || sudo mkdir -p "$PREFIX" && sudo chown "$(whoami)" "$PREFIX" 2>/dev/null || true fi GIT_ROOT="$PREFIX/git" DATA_DIR="$PREFIX/spbgit/data" CACHE_DIR="$PREFIX/spbgit/cache" LOG_DIR="$PREFIX/spbgit/logs" BACKUP_DIR="$PREFIX/spbgit/backups" mkdir -p "$GIT_ROOT" "$DATA_DIR" "$CACHE_DIR" "$LOG_DIR" "$BACKUP_DIR" ok "directories ready under $PREFIX" # ── 3. dependencies ────────────────────────────────────────────── say "installing production dependencies" npm ci --omit=dev --no-audit --no-fund >/dev/null ok "npm dependencies installed" # ── 4. environment ─────────────────────────────────────────────── if [ ! -f .env ]; then cat > .env < "$RESOLVED_CFG" < (or export NGROK_AUTHTOKEN)\n' fi say "starting pm2 apps" SPBGIT_LOG_DIR="$LOG_DIR" SPBGIT_NGROK_CONFIG="${SPBGIT_NGROK_CONFIG:-}" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null pm2 save >/dev/null ok "pm2 apps started (spbgit-server, spbgit-tunnel) and saved" printf ' To survive reboots, run once: \033[1mpm2 startup\033[0m (follow its instructions)\n' # ── 7. health check ────────────────────────────────────────────── say "waiting for /healthz" for _ in $(seq 1 20); do if curl -sf http://127.0.0.1:7420/healthz >/dev/null 2>&1; then ok "server healthy: $(curl -s http://127.0.0.1:7420/healthz)" ok "public URL: https://git.spboucher.ai (once DNS/ngrok domain is configured)" exit 0 fi sleep 1 done fail "server did not become healthy — check: pm2 logs spbgit-server"