Files
mathew/dev.sh
Aran Roig 6df7950161
Some checks failed
Build and Deploy Nuxt / build (push) Failing after 41s
Ai slop
2026-10-03 14:13:08 +02:00

251 lines
7.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# dev.sh — start/stop the frontend (Nuxt), backend (Express) and the Lean
# editor server (lean4web via docker compose) together.
#
# Usage:
# ./dev.sh Start everything in the foreground (Ctrl+C stops all)
# ./dev.sh start Start everything in the background
# ./dev.sh stop Stop everything
# ./dev.sh restart Stop then start
# ./dev.sh status Show what is running
# ./dev.sh logs [service] Follow logs (all, backend, frontend, or lean)
#
# backend and frontend are npm dev servers; lean is the Lean editor server
# from docker-compose.yml (see lean/). It is the heavy one — the image's
# first build fetches Mathlib — so dev.sh only rides along with docker: when
# docker is not available it says so and starts the other two regardless.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_DIR="$ROOT/.pids"
LOG_DIR="$ROOT/.logs"
COMPOSE="$ROOT/docker-compose.yml"
SERVICES=("backend" "frontend")
LEAN_LOG="$LOG_DIR/lean.log"
compose() { docker compose -f "$COMPOSE" "$@"; }
# The Lean server needs docker up, not just installed — probing the daemon
# keeps a dead docker socket from failing backend + frontend.
docker_ready() {
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
}
lean_running() {
docker_ready && [[ -n "$(compose ps --status running --services 2>/dev/null | grep -x lean)" ]]
}
pid_of() {
local pid_file="$PID_DIR/$1.pid"
[[ -f "$pid_file" ]] && cat "$pid_file" || true
}
is_running() {
local pid
pid="$(pid_of "$1")"
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
start_service() {
local name="$1"
if is_running "$name"; then
echo "$name is already running (PID $(pid_of "$name"))"
return
fi
mkdir -p "$PID_DIR" "$LOG_DIR"
# setsid puts each service in its own process group so `stop` can kill
# npm and all of its child processes (node --watch, nuxt, ...) cleanly.
setsid bash -c 'cd "$1" && exec npm run dev' _ "$ROOT/$name" \
> "$LOG_DIR/$name.log" 2>&1 &
local pid=$!
echo "$pid" > "$PID_DIR/$name.pid"
echo "started $name (PID $pid) — log: $LOG_DIR/$name.log"
}
# `up -d` also builds the image the first time it is asked for (heavy:
# Mathlib), so this rides in the background with its output to lean.log
# rather than holding `start` hostage to a cold build.
start_lean() {
if lean_running; then
echo "lean is already running (docker compose)"
return
fi
if ! docker_ready; then
echo "lean: docker is not available — skipping the Lean server (see lean/README.md)" >&2
return
fi
mkdir -p "$LOG_DIR"
setsid bash -c 'cd "$1" && exec docker compose -f "$2" up -d lean' _ "$ROOT" "$COMPOSE" \
> "$LEAN_LOG" 2>&1 &
echo "starting lean (docker compose) — log: $LEAN_LOG"
echo " (the first build fetches Mathlib and takes a while)"
}
stop_service() {
local name="$1"
local pid
pid="$(pid_of "$name")"
if ! is_running "$name"; then
echo "$name is not running"
rm -f "$PID_DIR/$name.pid"
return
fi
# Negative PID kills the whole process group (npm + its children).
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
# Wait up to 5s for a graceful shutdown, then force-kill.
for _ in {1..50}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$pid" 2>/dev/null; then
kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
fi
rm -f "$PID_DIR/$name.pid"
echo "stopped $name (PID $pid)"
}
# `stop`, not `down` — the container is kept (like a stopped dev server),
# and `docker compose down` stays the maintainer's call.
stop_lean() {
if ! lean_running; then
echo "lean is not running"
return
fi
compose stop lean >/dev/null 2>&1 || true
echo "stopped lean (docker compose)"
}
status() {
for name in "${SERVICES[@]}"; do
if is_running "$name"; then
echo "$name: running (PID $(pid_of "$name"))"
else
echo "$name: stopped"
fi
done
if lean_running; then
echo "lean: running (docker compose)"
elif docker_ready; then
echo "lean: stopped"
else
echo "lean: stopped (docker is not available)"
fi
}
FG_PIDS=()
LEAN_PID=""
SHUTTING=0
# Stop all foreground services (and their process groups). A second call
# (e.g. pressing Ctrl+C again while shutting down) escalates to SIGKILL.
fg_stop_all() {
local name pid sig=TERM
if [[ $SHUTTING -eq 1 ]]; then sig=KILL; else
SHUTTING=1
echo
echo "stopping backend + frontend + lean..."
fi
for pid in "${FG_PIDS[@]:-}"; do
kill -"$sig" -- "-$pid" 2>/dev/null || kill -"$sig" "$pid" 2>/dev/null || true
done
if [[ -n "$LEAN_PID" ]]; then
kill -"$sig" -- "-$LEAN_PID" 2>/dev/null || kill -"$sig" "$LEAN_PID" 2>/dev/null || true
# belt and braces: if the compose client died before telling dockerd,
# the container is still up — `stop` is quick and idempotent.
docker_ready && compose stop lean >/dev/null 2>&1 || true
fi
wait 2>/dev/null || true
for name in "${SERVICES[@]}"; do rm -f "$PID_DIR/$name.pid"; done
exit 0
}
foreground() {
local name
for name in "${SERVICES[@]}"; do
if is_running "$name"; then
echo "$name is already running (PID $(pid_of "$name")); run '$0 stop' first" >&2
exit 1
fi
done
mkdir -p "$PID_DIR"
for name in "${SERVICES[@]}"; do
# setsid puts each service in its own process group (PID == PGID) so the
# output prefix pipeline survives the shutdown; sed -u tags every line.
setsid bash -c 'cd "$1" && exec npm run dev' _ "$ROOT/$name" \
> >(sed -u "s/^/[$name] /") 2>&1 &
local pid=$!
FG_PIDS+=("$pid")
echo "$pid" > "$PID_DIR/$name.pid"
done
# The Lean server rides along the same way — compose `up` streams its
# build/run output, tagged. But the pair stays in charge: lean's absence
# (no docker) or its failure (no image, daemon flake) never takes them
# down, so `wait -n` watches only the npm services.
if docker_ready; then
setsid bash -c 'cd "$1" && exec docker compose -f "$2" up lean' _ "$ROOT" "$COMPOSE" \
> >(sed -u "s/^/[lean] /") 2>&1 &
LEAN_PID=$!
else
echo "[lean] docker is not available — running without the Lean server (see lean/README.md)" >&2
fi
trap 'fg_stop_all' INT TERM
echo "backend + frontend running (lean if docker allows) — press Ctrl+C to stop all"
# Exit as soon as either dev server dies, so they always run/stop together.
wait -n "${FG_PIDS[@]}"
echo "a dev server exited — stopping the others..."
fg_stop_all
}
case "${1:-dev}" in
dev)
foreground
;;
start)
for name in "${SERVICES[@]}"; do start_service "$name"; done
start_lean
echo
echo "backend: http://localhost:5000"
echo "frontend: http://localhost:3000"
echo "lean: http://localhost:8080 (docker compose) — the wiki frames it at /lean"
;;
stop)
for name in "${SERVICES[@]}"; do stop_service "$name"; done
stop_lean
;;
restart)
for name in "${SERVICES[@]}"; do stop_service "$name"; done
stop_lean
for name in "${SERVICES[@]}"; do start_service "$name"; done
start_lean
;;
status)
status
;;
logs)
case "${2:-all}" in
# `all` mixes the file logs with the container's — lean lives in
# docker, not in a dev.log, once it is up.
all)
if docker_ready; then
compose logs -f lean &
LEAN_LOG_PID=$!
trap 'kill "$LEAN_LOG_PID" 2>/dev/null || true' INT TERM
fi
tail -f "$LOG_DIR"/*.log
;;
backend|frontend) tail -f "$LOG_DIR/$2.log" ;;
lean)
if docker_ready; then compose logs -f lean; else echo "docker is not available" >&2; exit 1; fi
;;
*) echo "usage: $0 logs [all|backend|frontend|lean]"; exit 1 ;;
esac
;;
*)
echo "usage: $0 [dev|start|stop|restart|status|logs [all|backend|frontend|lean]] (default: dev)"
exit 1
;;
esac