The CYBER skin left a class of defects that unit tests could not see: Arco components kept their own light-theme appearance because the token bridge never covered the neutral, gray, tooltip, mask and border-radius families, and four override rules targeted class names that do not exist in Arco, so they silently matched nothing. Bridge the missing families and point the dead selectors at the real ones (.arco-switch-handle, .arco-checkbox-icon, .arco-select-dropdown, .arco-slider-bar), so switches stop rendering a white knob and checkboxes finally glow. Add start.sh so a dev run can be started without a second terminal: it frees the ports left over from a previous run, refusing to kill processes that do not belong to this project, then starts the API server and the Vite client in the foreground.
204 lines
6.3 KiB
Bash
Executable File
204 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# Local development launcher for hermes-ui.
|
||
#
|
||
# Starts the Koa API server and the Vite client together in the foreground,
|
||
# so you can watch the logs and Ctrl-C to stop everything.
|
||
#
|
||
# ./start.sh
|
||
#
|
||
# Then open http://localhost:8649/ and sign in with admin / 123456.
|
||
#
|
||
# The API server listens on 8647 and the Vite dev server proxies /api and
|
||
# /socket.io to it. Both ports are overridable:
|
||
#
|
||
# API_PORT=9000 CLIENT_PORT=9001 ./start.sh
|
||
#
|
||
# Ports left over from a previous run are cleaned up automatically.
|
||
#
|
||
set -uo pipefail
|
||
|
||
cd "$(dirname "$0")"
|
||
|
||
API_PORT="${API_PORT:-8647}"
|
||
CLIENT_PORT="${CLIENT_PORT:-8649}"
|
||
|
||
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
|
||
info() { printf '\033[36m›\033[0m %s\n' "$1"; }
|
||
warn() { printf '\033[33m!\033[0m %s\n' "$1"; }
|
||
die() { printf '\033[31m✗\033[0m %s\n' "$1" >&2; exit 1; }
|
||
|
||
# --- Preflight -----------------------------------------------------------
|
||
|
||
command -v node >/dev/null 2>&1 || die "node not found. Install Node.js >= 23."
|
||
|
||
node_version="$(node -v)"
|
||
node_major="${node_version#v}"
|
||
node_major="${node_major%%.*}"
|
||
if [ "$node_major" -lt 23 ]; then
|
||
die "Node.js >= 23 required, found $node_version."
|
||
fi
|
||
info "node $node_version"
|
||
|
||
if [ ! -d node_modules ]; then
|
||
warn "node_modules missing — installing (this takes a few minutes)."
|
||
npm ci --ignore-scripts || die "npm ci failed."
|
||
# node-pty's install script is what places the loadable native module.
|
||
# `npm ci --ignore-scripts` skips it, which silently disables the terminal.
|
||
info "rebuilding node-pty"
|
||
npm rebuild node-pty || warn "node-pty rebuild failed — the terminal may not work."
|
||
elif ! node -e "require('node-pty')" >/dev/null 2>&1; then
|
||
warn "node-pty native module missing — rebuilding (needed for the terminal)."
|
||
npm rebuild node-pty || warn "node-pty rebuild failed — the terminal may not work."
|
||
fi
|
||
|
||
# --- Port cleanup --------------------------------------------------------
|
||
#
|
||
# A previous run can leave vite, the ts-node server, and their supervisor
|
||
# behind. The Vite client runs with --strictPort, so it refuses to fall back
|
||
# to another port and dies instead. Clean all of it before starting.
|
||
|
||
# PIDs listening on a TCP port, one per line. Always exits 0 — `lsof` returns
|
||
# non-zero when nothing matches, which under `set -e` would abort the script.
|
||
pids_on_port() {
|
||
lsof -nP -iTCP:"$1" -sTCP:LISTEN -t 2>/dev/null | sort -u || true
|
||
}
|
||
|
||
port_in_use() {
|
||
local pids
|
||
pids="$(pids_on_port "$1")"
|
||
[ -n "$pids" ]
|
||
}
|
||
|
||
# Whether a PID belongs to this project. Guards against killing an unrelated
|
||
# process that merely happens to hold the port.
|
||
pid_is_ours() {
|
||
local pid="$1" cmd cwd
|
||
cmd="$(ps -o command= -p "$pid" 2>/dev/null || true)"
|
||
case "$cmd" in
|
||
*hermes-ui*|*"packages/server/src/index.ts"*|*node_modules/.bin/vite*) return 0 ;;
|
||
esac
|
||
# A plain `node` process running from inside the project directory is a
|
||
# leftover dev server too — e.g. one started by hand from this repo.
|
||
case "$cmd" in
|
||
node*) ;;
|
||
*) return 1 ;;
|
||
esac
|
||
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)"
|
||
case "$cwd" in
|
||
"$PWD"/*|"$PWD") return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# Walk up from a PID to the topmost ancestor that still belongs to this
|
||
# project. Killing only the listener leaves its npm/nodemon parent alive to
|
||
# respawn it, which is how stale servers come back.
|
||
top_of_tree() {
|
||
local top="$1" parent parent_cmd
|
||
while true; do
|
||
parent="$(ps -o ppid= -p "$top" 2>/dev/null | tr -d ' ' || true)"
|
||
[ -n "$parent" ] || break
|
||
[ "$parent" = "1" ] && break
|
||
[ "$parent" = "$$" ] && break
|
||
parent_cmd="$(ps -o command= -p "$parent" 2>/dev/null || true)"
|
||
case "$parent_cmd" in
|
||
*hermes-ui*|*concurrently*|*cross-env*|*nodemon*|*"npm run dev"*) top="$parent" ;;
|
||
*) break ;;
|
||
esac
|
||
done
|
||
echo "$top"
|
||
}
|
||
|
||
# Kill a PID once. The supervisor is reachable from several ports, so the
|
||
# same PID can be offered more than once — remember what we already killed.
|
||
# A plain string, not an associative array: macOS ships bash 3.2, which has
|
||
# no `declare -A`.
|
||
KILLED_PIDS=" "
|
||
|
||
kill_pid() {
|
||
local pid="$1" signal="${2:-TERM}" cmd
|
||
cmd="$(ps -o command= -p "$pid" 2>/dev/null || true)"
|
||
[ -n "$cmd" ] || return 0
|
||
case "$KILLED_PIDS" in
|
||
*" $pid "*) return 0 ;;
|
||
esac
|
||
KILLED_PIDS="$KILLED_PIDS$pid "
|
||
warn "killing pid $pid ($cmd)"
|
||
kill "-$signal" "$pid" 2>/dev/null || true
|
||
}
|
||
|
||
free_port() {
|
||
local port="$1" pids pid top waited
|
||
|
||
pids="$(pids_on_port "$port")"
|
||
[ -n "$pids" ] || return 0
|
||
|
||
for pid in $pids; do
|
||
if ! pid_is_ours "$pid"; then
|
||
die "port $port is held by a process that is not this project (pid $pid). Stop it yourself, or set $port to a free port."
|
||
fi
|
||
done
|
||
|
||
for pid in $pids; do
|
||
top="$(top_of_tree "$pid")"
|
||
kill_pid "$top"
|
||
done
|
||
|
||
# Give them a moment to release the socket.
|
||
waited=0
|
||
while [ "$waited" -lt 30 ]; do
|
||
port_in_use "$port" || return 0
|
||
sleep 0.2
|
||
waited=$((waited + 1))
|
||
done
|
||
|
||
# Still held — SIGKILL the stragglers.
|
||
for pid in $(pids_on_port "$port"); do
|
||
kill_pid "$pid" KILL
|
||
done
|
||
sleep 0.5
|
||
}
|
||
|
||
# Kill a leftover `npm run dev` supervisor before taking the ports. If it
|
||
# stays alive it will respawn the children we just killed.
|
||
kill_stale_supervisor() {
|
||
local pid
|
||
for pid in $(pgrep -f 'npm run dev:server' 2>/dev/null || true); do
|
||
[ "$pid" = "$$" ] && continue
|
||
pid_is_ours "$pid" || continue
|
||
kill_pid "$pid"
|
||
done
|
||
for pid in $(pgrep -f 'npm run dev:client' 2>/dev/null || true); do
|
||
[ "$pid" = "$$" ] && continue
|
||
pid_is_ours "$pid" || continue
|
||
kill_pid "$pid"
|
||
done
|
||
for pid in $(pgrep -f 'npm run dev$' 2>/dev/null || true); do
|
||
[ "$pid" = "$$" ] && continue
|
||
pid_is_ours "$pid" || continue
|
||
kill_pid "$pid"
|
||
done
|
||
sleep 0.5
|
||
}
|
||
|
||
kill_stale_supervisor
|
||
free_port "$API_PORT"
|
||
free_port "$CLIENT_PORT"
|
||
|
||
# --- Launch --------------------------------------------------------------
|
||
|
||
bold "hermes-ui — starting"
|
||
info "api http://localhost:$API_PORT"
|
||
info "client http://localhost:$CLIENT_PORT (login: admin / 123456)"
|
||
echo
|
||
|
||
# `npm run dev` runs both halves under concurrently: the server through
|
||
# nodemon (hot restart on server edits) and the client through Vite.
|
||
# HERMES_WEB_UI_BACKEND_PORT tells Vite which port to proxy /api and
|
||
# /socket.io to; PORT tells the Koa server where to listen.
|
||
export PORT="$API_PORT"
|
||
export HERMES_WEB_UI_BACKEND_PORT="$API_PORT"
|
||
|
||
npm run dev
|