Add Hermes as a desktop app and a coding agent
Hermes joins Install > AI as a desktop app, sits beside it under Remove > AI, and becomes a choice in the default-agent list. The CLI installs through omarchy-install-hermes-cli rather than a bare `mise use`, so its interpreter is pinned before mise builds it. Rebased onto quattro. Ori claimed U+E909 in #7709 while this branch was open, so the Hermes mark moves to U+E90A in the icon font, the menu entries, the font README, and the charset the menu test pins. The glyph outline itself is unchanged; it is spliced in beside Ori rather than over it. Co-Authored-By: witcheer <witcheer.eth@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SySdB3RtCA8BNv6Am246BP
This commit is contained in:
1 parent
0ae1694830
commit
a12a21c02f
13 files changed
+434
-6
No files matched your search
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
mise_log="$test_tmp/mise-log"
|
||||
mkdir -p "$mock_bin" "$test_home/.local/bin"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
|
||||
#!/bin/bash
|
||||
! command -v "$1" >/dev/null 2>&1
|
||||
SH
|
||||
|
||||
# `mise where` must fail so the installer sees no Hermes behind the stub.
|
||||
cat >"$mock_bin/mise" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
|
||||
[[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]] && exit 0
|
||||
[[ $1 != "where" ]]
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
run_installer() {
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
|
||||
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$test_home" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stub_marker="omarchy-install-hermes-cli"
|
||||
app_stub_body='#!/bin/bash
|
||||
exec /home/x/.hermes/hermes-agent/venv/bin/hermes "$@"'
|
||||
|
||||
# Writing the stub must not provision anything: user setup calls this on every
|
||||
# machine, including the ones that never run Hermes.
|
||||
: >"$mise_log"
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
run_installer 0 || fail "installer failed with no desktop installed"
|
||||
[[ -x $test_home/.local/bin/hermes ]] || fail "installer writes a hermes stub when the desktop is absent"
|
||||
grep -q "$stub_marker" "$test_home/.local/bin/hermes" || fail "the stub records which command wrote it"
|
||||
tr '\0' ' ' <"$mise_log" | grep -q "use -g --quiet uv" &&
|
||||
fail "writing the stub does not install uv"
|
||||
pass "writing the Hermes stub provisions nothing"
|
||||
|
||||
# The desktop app owns Hermes, so our own stub must go rather than sit there
|
||||
# answering `hermes` until the app's bootstrap replaces it.
|
||||
printf '%s\n' "#!/bin/bash" "# $stub_marker" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 || true
|
||||
[[ ! -e $test_home/.local/bin/hermes ]] ||
|
||||
fail "the desktop taking over removes the stub this command wrote"
|
||||
pass "installing the desktop app removes the CLI stub"
|
||||
|
||||
# ...but the app's own hermes is not ours to delete.
|
||||
printf '%s\n' "$app_stub_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 || true
|
||||
[[ -x $test_home/.local/bin/hermes ]] ||
|
||||
fail "the desktop app's own hermes command survives"
|
||||
pass "the app's own hermes command is left alone"
|
||||
|
||||
# A copy mise cannot vouch for is still a second Hermes.
|
||||
printf '%s\n' "#!/bin/bash" "# $stub_marker" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
: >"$mise_log"
|
||||
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 1 || true
|
||||
tr '\0' '\n' <"$mise_log" | grep -q "uninstall" ||
|
||||
fail "takeover removes a mise copy even when it is not healthy"
|
||||
pass "takeover removes an unhealthy mise copy"
|
||||
|
||||
# --check answers about Hermes being usable, not about the venv appearing. The
|
||||
# venv exists from the python-deps stage, several stages before the command.
|
||||
rm -rf "$test_home/.hermes"
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check && fail "--check reports Hermes missing before the app installs it"
|
||||
mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
|
||||
printf '%s\n' "#!/bin/bash" >"$test_home/.hermes/hermes-agent/venv/bin/hermes"
|
||||
chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes"
|
||||
run_installer 1 --check && fail "--check waits for the install to finish, not just the venv"
|
||||
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check || fail "--check reports Hermes present once the app has finished"
|
||||
pass "--check follows the app's completed install"
|
||||
|
||||
# An executable called hermes that belongs to something else is not this
|
||||
# install being ready.
|
||||
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/somebody-elses-hermes \"\$@\"" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check && fail "--check rejects a hermes command belonging to something else"
|
||||
pass "--check rejects a foreign hermes command"
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
|
||||
SH
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
seed_install() {
|
||||
rm -rf "$test_home"
|
||||
mkdir -p "$test_home/.hermes/hermes-agent" "$test_home/.hermes/bootstrap-cache" \
|
||||
"$test_home/.hermes/bin" "$test_home/.hermes/node/bin" \
|
||||
"$test_home/.hermes/memories" "$test_home/.hermes/sessions" \
|
||||
"$test_home/.config/Hermes" "$test_home/.local/bin"
|
||||
printf 'chat\n' >"$test_home/.hermes/sessions/one.json"
|
||||
printf 'memory\n' >"$test_home/.hermes/memories/one.md"
|
||||
printf 'soul\n' >"$test_home/.hermes/SOUL.md"
|
||||
printf 'uv\n' >"$test_home/.hermes/bin/uv"
|
||||
ln -sf "$test_home/.hermes/node/bin/node" "$test_home/.local/bin/node"
|
||||
ln -sf "$test_home/.hermes/node/bin/npm" "$test_home/.local/bin/npm"
|
||||
ln -sf /usr/bin/npx "$test_home/.local/bin/npx"
|
||||
printf 'node\n' >"$test_home/.hermes/node/bin/node"
|
||||
}
|
||||
|
||||
remove() {
|
||||
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" HOME="$test_home" PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# The app brings its own uv and its own node; both are runtime, not data.
|
||||
seed_install
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
|
||||
>"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds"
|
||||
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime checkout is removed"
|
||||
[[ ! -d $test_home/.hermes/bin ]] || fail "the uv the app installed is removed"
|
||||
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
|
||||
pass "removal takes the whole runtime the app installed"
|
||||
|
||||
[[ -d $test_home/.config/Hermes ]] ||
|
||||
fail "gateway connections, tokens and settings survive removal"
|
||||
pass "removal keeps the app's connections and settings"
|
||||
|
||||
# -L, not -e: a dangling symlink fails -e while very much still being there.
|
||||
[[ ! -L $test_home/.local/bin/node ]] || fail "a node symlink into ~/.hermes is removed"
|
||||
[[ ! -L $test_home/.local/bin/npm ]] || fail "an npm symlink into ~/.hermes is removed"
|
||||
[[ -L $test_home/.local/bin/npx ]] || fail "an npx symlink pointing elsewhere survives"
|
||||
pass "removal clears only the managed Node links it stranded"
|
||||
|
||||
[[ -f $test_home/.hermes/sessions/one.json ]] || fail "chats survive removal"
|
||||
[[ -f $test_home/.hermes/memories/one.md ]] || fail "memories survive removal"
|
||||
[[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal"
|
||||
pass "removal keeps what belongs to the user"
|
||||
|
||||
[[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed"
|
||||
pass "removal takes the command the app installed"
|
||||
|
||||
# Installed but never launched: the app never wrote these, so they are somebody
|
||||
# else's and must survive.
|
||||
seed_install
|
||||
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/my-own-hermes \"\$@\"" \
|
||||
>"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds with a foreign hermes present"
|
||||
[[ -f $test_home/.local/bin/hermes ]] ||
|
||||
fail "a hermes command the app did not write survives removal"
|
||||
pass "removal leaves a hermes it does not own"
|
||||
@@ -224,6 +224,7 @@ const expectedAgents = {
|
||||
claude: { icon: '', label: 'Claude' },
|
||||
codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' },
|
||||
grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' },
|
||||
hermes: { icon: '\ue90a', iconFont: 'omarchy', label: 'Hermes' },
|
||||
copilot: { icon: '', label: 'Copilot' },
|
||||
crush: { icon: '', label: 'Crush' },
|
||||
}
|
||||
@@ -244,7 +245,7 @@ assertDeepEqual(
|
||||
defaultItems
|
||||
.filter(item => item.parent === 'setup.default.agent')
|
||||
.map(item => item.label),
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'omp', 'OpenCode', 'Ori', 'Pi'],
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenCode', 'Ori', 'Pi'],
|
||||
'menu sorts coding agents alphabetically'
|
||||
)
|
||||
const expectedDefaults = {
|
||||
@@ -636,5 +637,5 @@ assert(
|
||||
JS
|
||||
|
||||
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
|
||||
[[ $font_charset == *"e900-e909"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
[[ $font_charset == *"e900-e90a"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
pass "Omarchy icon font includes the official agent marks"
|
||||
Reference in new issue
Block a user