Files
hermes-ui/start.sh
T
toor 8fdec02a1c
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
add a foreground dev launcher and close the Arco token gaps
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.
2026-10-21 10:13:38 +08:00

204 lines
6.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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