Pular para o conteúdo principal

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ávelSignificado
HERMES_MAIN_CHECKOUTO checkout de dependências — onde o node_modules realmente mora, e cujo .venv/bin/python roda o backend.
HERMES_GUI_DEPS_CHECKOUTOnde 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 exclusivos

HERMES_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ávelPapel no hgui
HERMES_DESKTOP_HERMES_ROOTRoda o backend a partir deste worktree, não do hermes empacotado ou do PATH.
HERMES_DESKTOP_PYTHONReaproveita a venv do checkout de dependências em vez de resolver um Python de novo.
HERMES_DESKTOP_IGNORE_EXISTINGIgnora qualquer hermes no PATH para que ele não sobreponha o worktree.
HERMES_DESKTOP_CWDAbre 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 segundo hgui colide com o servidor Vite do primeiro; o helper mata o antigo antes.
  • Filhos órfãos. O Electron frequentemente sobrevive ao Ctrl+C através do concurrently sem matar o backend efêmero do dashboard --port 0 nem o processo do Vite. O trap de EXIT/INT/TERM roda uma limpeza que encerra o shell do Electron, quem estiver escutando na :5174 e qualquer dashboard --port 0 que 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.

Por que só fazer link quando os locks batem

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 --dev e o caminho de bundle pré-construído do HERMES_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ê