#!/usr/bin/env bash # dev.sh — start/stop the frontend (Nuxt) and backend (Express) together. # # Usage: # ./dev.sh Start both in the foreground (Ctrl+C stops both) # ./dev.sh start Start both dev servers in the background # ./dev.sh stop Stop both dev servers # ./dev.sh restart Stop then start # ./dev.sh status Show which services are running # ./dev.sh logs [service] Follow logs (all, backend, or frontend) set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PID_DIR="$ROOT/.pids" LOG_DIR="$ROOT/.logs" SERVICES=("backend" "frontend") 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" } 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)" } status() { for name in "${SERVICES[@]}"; do if is_running "$name"; then echo "$name: running (PID $(pid_of "$name"))" else echo "$name: stopped" fi done } FG_PIDS=() 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 frontend + backend..." fi for pid in "${FG_PIDS[@]:-}"; do kill -"$sig" -- "-$pid" 2>/dev/null || kill -"$sig" "$pid" 2>/dev/null || true done 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 trap 'fg_stop_all' INT TERM echo "frontend + backend running — press Ctrl+C to stop both" # Exit as soon as either service dies, so they always run/stop together. wait -n echo "a dev server exited — stopping the other one..." fg_stop_all } case "${1:-dev}" in dev) foreground ;; start) for name in "${SERVICES[@]}"; do start_service "$name"; done echo echo "backend: http://localhost:5000" echo "frontend: http://localhost:3000" ;; stop) for name in "${SERVICES[@]}"; do stop_service "$name"; done ;; restart) for name in "${SERVICES[@]}"; do stop_service "$name"; done for name in "${SERVICES[@]}"; do start_service "$name"; done ;; status) status ;; logs) case "${2:-all}" in all) tail -f "$LOG_DIR"/*.log ;; backend|frontend) tail -f "$LOG_DIR/$2.log" ;; *) echo "usage: $0 logs [all|backend|frontend]"; exit 1 ;; esac ;; *) echo "usage: $0 [dev|start|stop|restart|status|logs [all|backend|frontend]] (default: dev)" exit 1 ;; esac