|
# Hermes worktree-aware launcher. |
|
export HERMES_MAIN_CHECKOUT="$DEV_ROOT/hermes-agent" |
|
|
|
_hermes_root() { |
|
local root |
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1 |
|
[[ -f "$root/hermes_cli/main.py" && -d "$root/ui-tui" ]] || return 1 |
|
print -r "$root" |
|
} |
|
|
|
hermes() { |
|
local root arg use_tui_dir |
|
use_tui_dir=1 |
|
root="$(_hermes_root)" || { |
|
command hermes "$@" |
|
return |
|
} |
|
|
|
for arg in "$@"; do |
|
case "$arg" in |
|
--dev|--dev=*) |
|
use_tui_dir=0 |
|
break |
|
;; |
|
esac |
|
done |
|
|
|
# ui-tui is a root workspace, so its deps track the root lock. Link main's |
|
# tree only when locks match; a stale link to divergent deps is repaired. |
|
if [[ -L "$root/ui-tui/node_modules" ]] && ! _hermes_locks_match "$root" "$HERMES_MAIN_CHECKOUT"; then |
|
rm -f "$root/ui-tui/node_modules" |
|
fi |
|
if [[ ! -e "$root/ui-tui/node_modules" ]]; then |
|
if _hermes_locks_match "$root" "$HERMES_MAIN_CHECKOUT" && [[ -d "$HERMES_MAIN_CHECKOUT/ui-tui/node_modules" ]]; then |
|
ln -s "$HERMES_MAIN_CHECKOUT/ui-tui/node_modules" "$root/ui-tui/node_modules" |
|
else |
|
_hermes_local_install "$root" || true |
|
fi |
|
fi |
|
|
|
if (( use_tui_dir )); then |
|
PYTHONPATH="$root" \ |
|
HERMES_TUI_DIR="$root/ui-tui" \ |
|
"$HERMES_MAIN_CHECKOUT/.venv/bin/python" -m hermes_cli.main "$@" |
|
else |
|
# Force-disable prebuilt override for --dev flow. |
|
env -u HERMES_TUI_DIR \ |
|
PYTHONPATH="$root" \ |
|
"$HERMES_MAIN_CHECKOUT/.venv/bin/python" -m hermes_cli.main "$@" |
|
fi |
|
} |
|
|
|
htui() { |
|
hermes --tui --dev "$@" |
|
} |
|
|
|
_hermes_npm_root_ok() { |
|
local root="$1" |
|
[[ -f "${root%/}/node_modules/vite/package.json" ]] |
|
} |
|
|
|
# Symlink node_modules from deps checkout when missing or a broken partial install. |
|
_hermes_link_node_modules() { |
|
local target="$1" source="$2" |
|
local dest="${target%/}/node_modules" src="${source%/}/node_modules" |
|
|
|
[[ -d "$src" ]] || return 1 |
|
|
|
if [[ -L "$dest" ]]; then |
|
return 0 |
|
fi |
|
|
|
if [[ -e "$dest" ]]; then |
|
if _hermes_npm_root_ok "$target"; then |
|
return 0 |
|
fi |
|
if _hermes_npm_root_ok "$source"; then |
|
rm -rf "$dest" |
|
else |
|
return 1 |
|
fi |
|
fi |
|
|
|
ln -s "$src" "$dest" |
|
} |
|
|
|
# A deps checkout only stands in for another when their locks are byte-identical. |
|
_hermes_locks_match() { |
|
cmp -s "${1%/}/package-lock.json" "${2%/}/package-lock.json" |
|
} |
|
|
|
# Real (non-symlinked) install in the worktree, stamped against its own lock so |
|
# we reinstall only when the lock actually changes. |
|
_hermes_local_install() { |
|
local root="${1%/}" stamp hash |
|
stamp="$root/node_modules/.hgui-lock" |
|
hash="${$(shasum "$root/package-lock.json" 2>/dev/null)%% *}" |
|
|
|
if [[ -d "$root/node_modules" && ! -L "$root/node_modules" \ |
|
&& -f "$stamp" && "$(<$stamp)" == "$hash" ]]; then |
|
return 0 |
|
fi |
|
|
|
echo "hgui: worktree deps diverge from deps checkout; installing locally (npm ci)…" >&2 |
|
[[ -L "$root/node_modules" ]] && rm -f "$root/node_modules" |
|
[[ -L "$root/apps/desktop/node_modules" ]] && rm -f "$root/apps/desktop/node_modules" |
|
[[ -L "$root/ui-tui/node_modules" ]] && rm -f "$root/ui-tui/node_modules" |
|
|
|
( cd "$root" && npm ci ) || return 1 |
|
print -r -- "$hash" > "$stamp" |
|
} |
|
|
|
_hermes_resolve_checkout() { |
|
local arg="$1" root |
|
|
|
if [[ -n "$arg" ]]; then |
|
root="$(cd "$arg" 2>/dev/null && pwd)" || return 1 |
|
[[ -f "$root/hermes_cli/main.py" ]] || return 1 |
|
print -r "$root" |
|
return 0 |
|
fi |
|
|
|
_hermes_root |
|
} |
|
|
|
# Desktop dev spawns ephemeral `dashboard --port 0` backends; Electron often |
|
# survives a Ctrl+C through concurrently without reaping them. |
|
_hermes_gui_cleanup() { |
|
local root="$1" pid cmdline dashboard_pids vite_pids |
|
|
|
if [[ -n "$root" ]]; then |
|
pkill -TERM -f "${root}/apps/desktop/node_modules/electron/dist/Electron" 2>/dev/null |
|
pkill -TERM -f "wait-on http://127.0.0.1:5174" 2>/dev/null |
|
fi |
|
|
|
dashboard_pids=("${(@f)$(pgrep -f 'hermes_cli\.main.*dashboard' 2>/dev/null)}") |
|
for pid in $dashboard_pids; do |
|
cmdline="$(ps -p "$pid" -o command= 2>/dev/null)" || continue |
|
[[ "$cmdline" == *"--port 0"* || "$cmdline" == *"--port=0"* ]] || continue |
|
kill -TERM "$pid" 2>/dev/null |
|
done |
|
|
|
if lsof -t -i:5174 >/dev/null 2>&1; then |
|
vite_pids=("${(@f)$(lsof -t -i:5174 2>/dev/null)}") |
|
for pid in $vite_pids; do kill -TERM "$pid" 2>/dev/null; done |
|
sleep 0.5 |
|
killport 5174 |
|
fi |
|
|
|
sleep 0.5 |
|
if [[ -n "$root" ]]; then |
|
pkill -KILL -f "${root}/apps/desktop/node_modules/electron/dist/Electron" 2>/dev/null |
|
fi |
|
for pid in $dashboard_pids; do |
|
cmdline="$(ps -p "$pid" -o command= 2>/dev/null)" || continue |
|
[[ "$cmdline" == *"--port 0"* || "$cmdline" == *"--port=0"* ]] || continue |
|
kill -KILL "$pid" 2>/dev/null |
|
done |
|
} |
|
|
|
hgui() { |
|
local root deps desktop deps_desktop py arg cleanup_done=0 |
|
|
|
arg="$1" |
|
root="$(_hermes_resolve_checkout "$arg")" || { |
|
if [[ -n "$arg" ]]; then |
|
echo "hgui: $arg is not a Hermes checkout" >&2 |
|
else |
|
echo "hgui: not inside a Hermes checkout (usage: hgui [path])" >&2 |
|
fi |
|
return 1 |
|
} |
|
|
|
deps="${HERMES_GUI_DEPS_CHECKOUT:-$HERMES_MAIN_CHECKOUT}" |
|
desktop="$root/apps/desktop" |
|
deps_desktop="$deps/apps/desktop" |
|
|
|
if [[ ! -d "$desktop" ]]; then |
|
echo "hgui: $root does not have apps/desktop" >&2 |
|
return 1 |
|
fi |
|
|
|
if [[ ! -d "$deps_desktop" ]]; then |
|
echo "hgui: set HERMES_GUI_DEPS_CHECKOUT to a checkout with apps/desktop deps" >&2 |
|
return 1 |
|
fi |
|
|
|
# Link main's tree only when this worktree's lock matches; otherwise a branch |
|
# that bumps a dep would silently run against stale packages. On divergence, |
|
# install locally instead. |
|
if _hermes_locks_match "$root" "$deps"; then |
|
_hermes_link_node_modules "$desktop" "$deps_desktop" || true |
|
_hermes_link_node_modules "$root" "$deps" || true |
|
elif ! _hermes_local_install "$root"; then |
|
echo "hgui: dependency install failed" >&2 |
|
return 1 |
|
fi |
|
|
|
if ! _hermes_npm_root_ok "$root"; then |
|
echo "hgui: run once: cd $deps && npm ci" >&2 |
|
return 1 |
|
fi |
|
|
|
py="$HERMES_MAIN_CHECKOUT/.venv/bin/python" |
|
[[ -x "$py" ]] || py="$(command -v python3)" |
|
|
|
# Vite dev server is fixed at 5174; clear a stale session from another hgui. |
|
if lsof -t -i:5174 >/dev/null 2>&1; then |
|
echo "hgui: stopping process on port 5174" >&2 |
|
killport 5174 |
|
fi |
|
|
|
_hgui_cleanup_trap() { |
|
(( cleanup_done )) && return |
|
cleanup_done=1 |
|
_hermes_gui_cleanup "$root" |
|
} |
|
|
|
trap '_hgui_cleanup_trap' INT TERM EXIT |
|
|
|
( |
|
cd "$desktop" || exit 1 |
|
export PATH="$root/node_modules/.bin:$PATH" |
|
HERMES_DESKTOP_HERMES_ROOT="$root" \ |
|
HERMES_DESKTOP_PYTHON="$py" \ |
|
HERMES_DESKTOP_IGNORE_EXISTING=1 \ |
|
HERMES_DESKTOP_CWD="$root" \ |
|
npm run dev |
|
) |
|
local rc=$? |
|
|
|
trap - INT TERM EXIT |
|
_hgui_cleanup_trap |
|
return $rc |
|
} |