Next.js app for mapping a customer's competitive tool landscape onto our portfolio and building the consolidation business case: assessment workbench, customer report and Word export, product catalog, competitive knowledge base, dossier import, shareable landscape questionnaires with a review inbox, tool-footprint and business-unit value analysis, and an implementation timeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
6.2 KiB
Bash
Executable File
208 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Converge — start / stop / status
|
|
#
|
|
# Safe to run on a host with other Next.js apps. It never matches processes by
|
|
# name: it only ever stops a PID it has confirmed is a Next server whose working
|
|
# directory is this project. Another app on port 3000 (or anywhere else) is
|
|
# never touched, and if this app's port is already taken by something else the
|
|
# script reports it and stops rather than clearing the way.
|
|
#
|
|
# ./start.sh start in the background, wait until it answers
|
|
# ./start.sh stop stop only this project's server
|
|
# ./start.sh restart stop, then start
|
|
# ./start.sh status is it running, and on what PID
|
|
# ./start.sh logs follow the log
|
|
# ./start.sh fg run in the foreground (Ctrl-C to quit)
|
|
# ./start.sh build production build, then serve it
|
|
#
|
|
# PORT=3600 ./start.sh use a different port
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$ROOT"
|
|
|
|
PORT="${PORT:-3500}"
|
|
RUN_DIR="$ROOT/.run"
|
|
PID_FILE="$RUN_DIR/app-$PORT.pid"
|
|
LOG_FILE="$RUN_DIR/app-$PORT.log"
|
|
URL="http://localhost:$PORT"
|
|
|
|
bold=$'\033[1m'; dim=$'\033[2m'; red=$'\033[31m'; green=$'\033[32m'; yellow=$'\033[33m'; off=$'\033[0m'
|
|
say() { printf '%s\n' "$*"; }
|
|
ok() { printf '%s✓%s %s\n' "$green" "$off" "$*"; }
|
|
warn() { printf '%s!%s %s\n' "$yellow" "$off" "$*"; }
|
|
die() { printf '%s✗%s %s\n' "$red" "$off" "$*" >&2; exit 1; }
|
|
|
|
# --- identity checks -------------------------------------------------------
|
|
# Everything below refuses to act on a process it cannot prove is ours.
|
|
|
|
# These helpers must succeed even when they find nothing: `lsof` and `ps` exit
|
|
# non-zero on no-match, which under `set -euo pipefail` would abort the script
|
|
# in the ordinary case of a free port.
|
|
|
|
# Working directory of a PID, or empty.
|
|
proc_cwd() {
|
|
{ lsof -a -p "$1" -d cwd -Fn 2>/dev/null || true; } | sed -n 's/^n//p' | head -1
|
|
return 0
|
|
}
|
|
|
|
# Full command of a PID, or empty.
|
|
proc_cmd() {
|
|
ps -o command= -p "$1" 2>/dev/null || true
|
|
return 0
|
|
}
|
|
|
|
# True when the PID is a Next server running out of this project directory.
|
|
is_our_app() {
|
|
local pid="${1:-}"
|
|
[ -n "$pid" ] || return 1
|
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
case "$(proc_cmd "$pid")" in
|
|
*next*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
[ "$(proc_cwd "$pid")" = "$ROOT" ]
|
|
}
|
|
|
|
# PID listening on the port, or empty.
|
|
port_pid() {
|
|
{ lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true; } | head -1
|
|
return 0
|
|
}
|
|
|
|
# PID of our running server, adopting an already-running one if it is ours.
|
|
running_pid() {
|
|
local pid=""
|
|
if [ -f "$PID_FILE" ]; then pid="$(cat "$PID_FILE" 2>/dev/null || true)"; fi
|
|
if is_our_app "$pid"; then printf '%s' "$pid"; return 0; fi
|
|
# No usable pid file — fall back to whoever holds the port, but only claim it
|
|
# if it is demonstrably this project's server.
|
|
pid="$(port_pid)"
|
|
if is_our_app "$pid"; then printf '%s' "$pid"; return 0; fi
|
|
return 1
|
|
}
|
|
|
|
# --- actions ---------------------------------------------------------------
|
|
|
|
ensure_deps() {
|
|
command -v node >/dev/null || die "Node.js is not installed."
|
|
command -v npm >/dev/null || die "npm is not installed."
|
|
if [ ! -d node_modules ]; then
|
|
say "${dim}Installing dependencies…${off}"
|
|
npm install --no-audit --no-fund
|
|
fi
|
|
}
|
|
|
|
# Refuse to start if the port belongs to something that is not this app.
|
|
check_port_free() {
|
|
local pid; pid="$(port_pid)"
|
|
[ -n "$pid" ] || return 0
|
|
if is_our_app "$pid"; then return 0; fi
|
|
printf '%s\n' "${red}✗${off} Port $PORT is in use by another program (PID $pid):" >&2
|
|
printf ' %s\n' "$(proc_cmd "$pid")" >&2
|
|
printf ' Leaving it alone. Start this app on a different port instead:\n' >&2
|
|
printf ' %sPORT=3600 ./start.sh%s\n' "$bold" "$off" >&2
|
|
exit 1
|
|
}
|
|
|
|
wait_until_up() {
|
|
local pid="$1" tries=90
|
|
while [ $tries -gt 0 ]; do
|
|
if curl -sf -o /dev/null "$URL/api/catalog"; then return 0; fi
|
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
sleep 1
|
|
tries=$((tries - 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
do_start() {
|
|
if pid="$(running_pid)"; then
|
|
ok "Already running on $URL ${dim}(PID $pid)${off}"
|
|
return 0
|
|
fi
|
|
ensure_deps
|
|
check_port_free
|
|
mkdir -p "$RUN_DIR"
|
|
|
|
say "${dim}Starting on port ${PORT}…${off}"
|
|
# setsid-less background start; the PID we record is the one we may later stop.
|
|
PORT="$PORT" nohup npx next dev -p "$PORT" > "$LOG_FILE" 2>&1 &
|
|
local pid=$!
|
|
echo "$pid" > "$PID_FILE"
|
|
|
|
if wait_until_up "$pid"; then
|
|
ok "Converge is running"
|
|
say " ${bold}$URL${off}"
|
|
say " ${dim}logs: ./start.sh logs · stop: ./start.sh stop${off}"
|
|
else
|
|
rm -f "$PID_FILE"
|
|
say ""
|
|
tail -20 "$LOG_FILE" >&2 || true
|
|
die "It did not come up. Full log: $LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
do_fg() {
|
|
ensure_deps
|
|
check_port_free
|
|
say "${dim}Starting on port $PORT — Ctrl-C to stop.${off}"
|
|
exec npx next dev -p "$PORT"
|
|
}
|
|
|
|
do_build() {
|
|
ensure_deps
|
|
check_port_free
|
|
npx next build
|
|
ok "Build complete — serving on $URL"
|
|
exec npx next start -p "$PORT"
|
|
}
|
|
|
|
do_stop() {
|
|
local pid
|
|
if ! pid="$(running_pid)"; then
|
|
rm -f "$PID_FILE"
|
|
warn "Not running (nothing on port $PORT belonging to this project)."
|
|
return 0
|
|
fi
|
|
# Stop the tree we started, without ever matching on process name.
|
|
kill "$pid" 2>/dev/null || true
|
|
local tries=15
|
|
while [ $tries -gt 0 ] && kill -0 "$pid" 2>/dev/null; do sleep 0.4; tries=$((tries - 1)); done
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
warn "Did not exit cleanly; sending SIGKILL to PID $pid."
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
fi
|
|
rm -f "$PID_FILE"
|
|
ok "Stopped."
|
|
}
|
|
|
|
do_status() {
|
|
local pid
|
|
if pid="$(running_pid)"; then
|
|
ok "Running on $URL ${dim}(PID $pid)${off}"
|
|
else
|
|
local other; other="$(port_pid)"
|
|
if [ -n "$other" ]; then
|
|
warn "Not running. Port $PORT is held by another program (PID $other):"
|
|
say " $(proc_cmd "$other")"
|
|
else
|
|
say "Not running. Port $PORT is free."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
case "${1:-start}" in
|
|
start) do_start ;;
|
|
fg|dev) do_fg ;;
|
|
build|prod) do_build ;;
|
|
stop) do_stop ;;
|
|
restart) do_stop; do_start ;;
|
|
status) do_status ;;
|
|
logs) [ -f "$LOG_FILE" ] || die "No log yet at $LOG_FILE"; tail -f "$LOG_FILE" ;;
|
|
-h|--help|help) sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
|
|
*) die "Unknown command: $1 (try: start, stop, restart, status, logs, fg, build)" ;;
|
|
esac
|