Documentação oficial desta página · v0.20.6
TUI e Desktop a partir de Worktrees
O núcleo em Python roda sem problema a partir de qualquer git worktree — você entra com cd e o hermes simplesmente funciona. As duas superfícies em TypeScript não: ui-tui/ e apps/desktop/ precisam cada uma de um node_modules populado, e um npm ci novo por worktree é lento e duplica gigabytes em cada branch que você tem aberta.
htui e hgui são dois helpers de shell que fecham essa lacuna. Cada um sobe a sua superfície a partir do worktree atual enquanto pega emprestado o node_modules de um checkout canônico — assim uma branch descartável custa um symlink, não uma instalação.
São conveniências de desenvolvedor, não comandos oficiais. Jogue no seu ~/.zshrc; ajuste os caminhos ao seu gosto.
O modelo de dependências compartilhadas
Um checkout é o checkout de dependências — o único lugar onde você de fato roda npm install. Todo outro worktree faz link contra ele, e só reinstala localmente quando o lockfile diverge (uma branch que sobe a versão de uma dependência não pode rodar em silêncio contra pacotes velhos).
flowchart TD
A[htui / hgui num worktree] --> B{package-lock.json<br/>bate com o checkout de deps?}
B -- sim --> C[symlink de node_modules<br/>do checkout de deps]
B -- não --> D[npm ci local<br/>neste worktree]
C --> E[sobe a superfície]
D --> E
Duas variáveis de ambiente apontam o checkout canônico:
| Variável | Significado |
|---|---|
HERMES_MAIN_CHECKOUT | O checkout de dependências — onde o node_modules realmente mora, e cujo .venv/bin/python roda o backend. |
HERMES_GUI_DEPS_CHECKOUT | Onde ficam as dependências do desktop (apps/desktop/node_modules). Por padrão, o mesmo de HERMES_MAIN_CHECKOUT; só sobrescreva se você guarda as dependências do desktop em outro lugar. |
Nenhuma das duas é lida pelo próprio Hermes — são privadas desses helpers. As variáveis que o Hermes lê de verdade estão em Variáveis de Ambiente.
htui — a TUI a partir do worktree
A TUI em Ink já tem um caminho de desenvolvimento: hermes --tui --dev roda os fontes TypeScript via tsx em vez do bundle pré-construído. O htui é uma linha em cima disso que também aponta a execução para o ui-tui/ do worktree atual:
htui() {
local root
root="$(_hermes_root)" || { echo "htui: not in a Hermes checkout" >&2; return 1; }
( cd "$root" && PYTHONPATH="$root" \
"$HERMES_MAIN_CHECKOUT/.venv/bin/python" -m hermes_cli.main --tui --dev "$@" )
}
O --dev compila a partir do fonte, então ele faz link do ui-tui/node_modules a partir de HERMES_MAIN_CHECKOUT quando o lockfile da raiz bate, e instala localmente caso contrário (veja _hermes_root / helpers de link).
--dev e HERMES_TUI_DIR são mutuamente exclusivosHERMES_TUI_DIR aponta o Hermes para um bundle pré-construído (Nix, pacotes do sistema), que não tem fonte para recarregar a quente. Se estiver definida no seu shell, hermes --tui --dev sai com erro. Rode unset HERMES_TUI_DIR antes do htui.
hgui — o app desktop a partir do worktree
O app desktop é mais pesado: precisa de node_modules tanto na raiz do repositório quanto em apps/desktop/, de um servidor de desenvolvimento Vite fixado na porta 5174 e de um backend Python. O hgui liga tudo isso ao worktree atual:
hgui() {
local root deps desktop
root="$(_hermes_root)" || { echo "hgui: not in a Hermes checkout" >&2; return 1; }
deps="${HERMES_GUI_DEPS_CHECKOUT:-$HERMES_MAIN_CHECKOUT}"
desktop="$root/apps/desktop"
# Pega as dependências emprestadas quando os locks batem; senão, instala local no worktree.
if cmp -s "$root/package-lock.json" "$deps/package-lock.json"; then
_hermes_link_deps "$desktop" "$deps/apps/desktop"
_hermes_link_deps "$root" "$deps"
else
( cd "$root" && npm ci ) || return 1
fi
# O Vite é fixo na 5174 — despeja uma sessão velha de outro hgui.
lsof -t -i:5174 >/dev/null 2>&1 && killport 5174
# O Electron costuma sobreviver ao Ctrl+C sem matar seus backends efêmeros.
trap '_hermes_gui_cleanup "$root"' INT TERM EXIT
( cd "$desktop"
export PATH="$root/node_modules/.bin:$PATH"
HERMES_DESKTOP_HERMES_ROOT="$root" \
HERMES_DESKTOP_PYTHON="$HERMES_MAIN_CHECKOUT/.venv/bin/python" \
HERMES_DESKTOP_IGNORE_EXISTING=1 \
HERMES_DESKTOP_CWD="$root" \
npm run dev )
}
As variáveis de ambiente do desktop que ele define são todas botões reais de resolução do backend:
| Variável | Papel no hgui |
|---|---|
HERMES_DESKTOP_HERMES_ROOT | Roda o backend a partir deste worktree, não do hermes empacotado ou do PATH. |
HERMES_DESKTOP_PYTHON | Reaproveita a venv do checkout de dependências em vez de resolver um Python de novo. |
HERMES_DESKTOP_IGNORE_EXISTING | Ignora qualquer hermes no PATH para que ele não sobreponha o worktree. |
HERMES_DESKTOP_CWD | Abre o chat do desktop enraizado no worktree. |
Duas armadilhas que o hgui resolve e um npm run dev cru não:
- A porta
5174é fixa. Um segundohguicolide com o servidor Vite do primeiro; o helper mata o antigo antes. - Filhos órfãos. O Electron frequentemente sobrevive ao
Ctrl+Catravés doconcurrentlysem matar o backend efêmero dodashboard --port 0nem o processo do Vite. O trap deEXIT/INT/TERMroda uma limpeza que encerra o shell do Electron, quem estiver escutando na:5174e qualquer dashboard--port 0que ele tenha subido.
Helpers compartilhados
As duas funções resolvem o checkout que as contém e fazem link das dependências do mesmo jeito:
# O worktree que contém o diretório atual, verificado como um checkout real do Hermes.
_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" ]] && print -r "$root"
}
# Faz symlink do node_modules do checkout de dependências — nunca por cima de uma árvore existente.
_hermes_link_deps() {
local target="${1%/}" source="${2%/}"
[[ -d "$source/node_modules" ]] || return 1
[[ -e "$target/node_modules" ]] || ln -s "$source/node_modules" "$target/node_modules"
}
# Mata os backends efêmeros que o Electron deixa para trás ao sair.
_hermes_gui_cleanup() {
local root="$1"
[[ -n "$root" ]] && pkill -TERM -f "${root}/apps/desktop/node_modules/electron" 2>/dev/null
lsof -t -i:5174 >/dev/null 2>&1 && killport 5174
pgrep -f 'hermes_cli\.main.*dashboard.*--port 0' 2>/dev/null | xargs -r kill -TERM 2>/dev/null
}
O killport é um helper seu (lsof -ti:$1 | xargs kill); troque pela fórmula que você preferir.
Um symlink para um node_modules divergente é pior do que não instalar nada — o worktree ia compilar contra pacotes que o próprio lockfile dele nunca declarou. Comparar package-lock.json byte a byte é a proteção barata e exata: mesmo lock ⇒ pode pegar emprestado; lock diferente ⇒ npm ci local. O Vite resolve o caminho real dos symlinks antes de aplicar server.fs.allow, e é por isso que apps/desktop/vite.config.ts libera o local real do node_modules.
Veja também
- Git Worktrees — o modelo de isolamento em que esses helpers se apoiam
- TUI —
hermes --tui --deve o caminho de bundle pré-construído doHERMES_TUI_DIR - App Desktop — compilar a partir do fonte e a escada de resolução do backend
apps/desktop/README.md— servidor de desenvolvimento, script de sandbox e empacotamento- Variáveis de Ambiente — todas as variáveis
HERMES_*que o Hermes lê