SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
7.3 KB · 162 lines shellscript
Raw Blame History
1#!/bin/bash2# ─────────────────────────────────────────────3#  SPB Git — Personal Git Platform4# ─────────────────────────────────────────────5#  Author  : Simon-Pierre Boucher6#  Contact : contact@spboucher.ai7#  File    : deploy/setup-m3u96a.sh8#  Purpose : Idempotent bootstrap of the full stack on node m3u96a9#  License : MIT © Simon-Pierre Boucher10# ─────────────────────────────────────────────11#12# Run FROM the app directory on m3u96a:13#   bash deploy/setup-m3u96a.sh14#15# What it does (all idempotent):16#   1. Verifies node ≥ 20, git, ngrok (installs ngrok via brew when missing).17#   2. Creates the data prefix:  /srv on Linux, ~/srv on macOS (SIP-safe).18#   3. npm ci --omit=dev + fonts.19#   4. Writes .env with resolved paths.20#   5. Generates the bootstrap PAT — printed ONCE, never stored in clear.21#   6. Starts pm2 apps (server + ngrok tunnel) and saves the process list.22#23set -euo pipefail2425say()  { printf '\033[1;34m▸ %s\033[0m\n' "$*"; }26ok()   { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }27fail() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }2829APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"30cd "$APP_DIR"3132# ── 1. prerequisites ─────────────────────────────────────────────33command -v git >/dev/null || fail "git is required"34command -v node >/dev/null || fail "node ≥ 20 is required (brew install node@20 or nvm install 20)"35NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"36[ "$NODE_MAJOR" -ge 20 ] || fail "node ≥ 20 required, found $(node --version)"37ok "node $(node --version), git $(git --version | awk '{print $3}')"3839if ! command -v ngrok >/dev/null; then40  if command -v brew >/dev/null; then41    say "installing ngrok via homebrew"42    brew install ngrok >/dev/null43  else44    fail "ngrok not found — install it from https://ngrok.com/download"45  fi46fi47ok "ngrok $(ngrok version 2>/dev/null | head -1)"4849if ! command -v pm2 >/dev/null; then50  say "installing pm2 globally"51  npm install -g pm2 >/dev/null52fi53ok "pm2 $(pm2 --version)"5455# ── 2. directories ───────────────────────────────────────────────56# /srv is read-only on macOS system volumes → use ~/srv there.57if [ "$(uname)" = "Darwin" ]; then58  PREFIX="$HOME/srv"59else60  PREFIX="/srv"61  [ -w "$PREFIX" ] || sudo mkdir -p "$PREFIX" && sudo chown "$(whoami)" "$PREFIX" 2>/dev/null || true62fi63GIT_ROOT="$PREFIX/git"64DATA_DIR="$PREFIX/spbgit/data"65CACHE_DIR="$PREFIX/spbgit/cache"66LOG_DIR="$PREFIX/spbgit/logs"67BACKUP_DIR="$PREFIX/spbgit/backups"68mkdir -p "$GIT_ROOT" "$DATA_DIR" "$CACHE_DIR" "$LOG_DIR" "$BACKUP_DIR"69ok "directories ready under $PREFIX"7071# ── 3. dependencies ──────────────────────────────────────────────72say "installing production dependencies"73npm ci --omit=dev --no-audit --no-fund >/dev/null74ok "npm dependencies installed"7576# ── 4. environment ───────────────────────────────────────────────77if [ ! -f .env ]; then78  cat > .env <<ENV79# Written by deploy/setup-m3u96a.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)80SPBGIT_PORT=742081SPBGIT_HOST=127.0.0.182SPBGIT_PUBLIC_URL=https://git.spboucher.ai83SPBGIT_GIT_ROOT=$GIT_ROOT84SPBGIT_DATA_DIR=$DATA_DIR85SPBGIT_CACHE_DIR=$CACHE_DIR86SPBGIT_LOG_LEVEL=info87SPBGIT_ENV=production88ENV89  chmod 600 .env90  ok ".env written"91else92  ok ".env already present — left untouched"93fi9495# ── 5. bootstrap PAT (only when no token exists yet) ─────────────96if [ ! -s "$DATA_DIR/tokens.json" ] || [ "$(node -p "try{JSON.parse(require('fs').readFileSync('$DATA_DIR/tokens.json','utf8')).tokens.length}catch(e){0}")" = "0" ]; then97  say "generating bootstrap personal access token"98  BOOTSTRAP_TOKEN="$(node --input-type=module -e "99    import { loadConfig, ensureDirs } from '$APP_DIR/src/config.mjs';100    import { TokenStore } from '$APP_DIR/src/auth/token.mjs';101    const config = loadConfig();102    ensureDirs(config);103    const { token } = await new TokenStore(config.dataDir).create('bootstrap');104    console.log(token);105  ")"106  printf '\n\033[1;33m┌────────────────────────────────────────────────────────────┐\033[0m\n'107  printf '\033[1;33m│ BOOTSTRAP TOKEN — shown once, copy it now:                 │\033[0m\n'108  printf '\033[1;33m└────────────────────────────────────────────────────────────┘\033[0m\n'109  printf '\n  %s\n\n' "$BOOTSTRAP_TOKEN"110  printf 'Configure your laptop with:  spbgit init\n\n'111else112  ok "tokens already exist — no bootstrap token generated"113fi114115# ── 6. ngrok auth + pm2 ──────────────────────────────────────────116# Resolve the authtoken: env first, then the agent's default config file.117# A resolved config (real token, chmod 600) is written OUTSIDE the repo and118# handed to pm2 via SPBGIT_NGROK_CONFIG — deploy/ngrok.yml stays a template.119NGROK_DEFAULT_CFG="$HOME/Library/Application Support/ngrok/ngrok.yml"120[ -f "$NGROK_DEFAULT_CFG" ] || NGROK_DEFAULT_CFG="$HOME/.config/ngrok/ngrok.yml"121AUTHTOKEN="${NGROK_AUTHTOKEN:-}"122if [ -z "$AUTHTOKEN" ] && [ -f "$NGROK_DEFAULT_CFG" ]; then123  AUTHTOKEN="$(grep -E '^[[:space:]]*authtoken:' "$NGROK_DEFAULT_CFG" | head -1 | awk '{print $2}')"124fi125RESOLVED_CFG="$PREFIX/spbgit/ngrok.resolved.yml"126if [ -n "$AUTHTOKEN" ]; then127  cat > "$RESOLVED_CFG" <<NGROK128version: 3129agent:130  authtoken: $AUTHTOKEN131endpoints:132  - name: spbgit133    url: https://git.spboucher.ai134    upstream:135      url: http://127.0.0.1:7420136NGROK137  chmod 600 "$RESOLVED_CFG"138  export SPBGIT_NGROK_CONFIG="$RESOLVED_CFG"139  ok "ngrok config resolved → $RESOLVED_CFG"140else141  printf '\033[1;33m! No ngrok authtoken found (env or agent config).\033[0m\n'142  printf '  Run: ngrok config add-authtoken <token>   (or export NGROK_AUTHTOKEN)\n'143fi144145say "starting pm2 apps"146SPBGIT_LOG_DIR="$LOG_DIR" SPBGIT_NGROK_CONFIG="${SPBGIT_NGROK_CONFIG:-}" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null147pm2 save >/dev/null148ok "pm2 apps started (spbgit-server, spbgit-tunnel) and saved"149printf '  To survive reboots, run once: \033[1mpm2 startup\033[0m (follow its instructions)\n'150151# ── 7. health check ──────────────────────────────────────────────152say "waiting for /healthz"153for _ in $(seq 1 20); do154  if curl -sf http://127.0.0.1:7420/healthz >/dev/null 2>&1; then155    ok "server healthy: $(curl -s http://127.0.0.1:7420/healthz)"156    ok "public URL: https://git.spboucher.ai (once DNS/ngrok domain is configured)"157    exit 0158  fi159  sleep 1160done161fail "server did not become healthy — check: pm2 logs spbgit-server"162