Initial commit: Converge portfolio rationalization app

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>
This commit is contained in:
2026-09-24 11:48:42 -04:00
commit d5c77543e8
64 changed files with 30861 additions and 0 deletions

207
start.sh Executable file
View File

@@ -0,0 +1,207 @@
#!/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