spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
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