diff --git a/bin/omarchy-remove-ai-hermes b/bin/omarchy-remove-ai-hermes index 1145fbd1..924cafab 100755 --- a/bin/omarchy-remove-ai-hermes +++ b/bin/omarchy-remove-ai-hermes @@ -25,11 +25,8 @@ omarchy-install-hermes-cli --remove || cli_removed=false # has whatever was there before, and none of it is ours to delete. if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then # The checkout and venv, its own uv, its own node. None of it is any use once - # the app is gone. Not ~/.config/Hermes, which holds the gateway connections - # and their encrypted tokens, the active profile and the update settings. Not - # the rest of ~/.hermes either: the chats, memories and the skills Hermes - # wrote for itself are the user's, they are small, and finding them still - # there after a reinstall is the better surprise. + # the app is gone, so it goes without asking; what the user made with the app + # is a different question, answered below. rm -rf \ "$HOME/.hermes/hermes-agent" \ "$HOME/.hermes/bootstrap-cache" \ @@ -58,10 +55,35 @@ if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then fi done + # What is left is the user's: the chats, memories and skills in ~/.hermes, + # the connections and their encrypted tokens in ~/.config/Hermes. Keeping + # them stays the default -- they are small, and finding them intact after a + # reinstall is the better surprise -- but a removal meant to be complete + # should not leave credentials behind either, so the choice is put in front + # of the user, default no. Only here, behind the bootstrap marker: without + # it ~/.hermes is an install the app never owned, and offering to delete it + # would put the user's own Hermes on the chopping block. Without a terminal + # to ask in, keeping it is the answer. + data_removed=false + if [[ -t 0 ]] && command -v gum >/dev/null; then + # du answers non-zero when either directory is missing (the app makes + # ~/.config/Hermes, but nothing says it is still there), and pipefail + # would turn that into an aborted removal; the size is worth no such thing. + size=$(du -shc "$HOME/.hermes" "$HOME/.config/Hermes" 2>/dev/null | tail -1 | cut -f1 || true) + if gum confirm --default=false "Also delete your Hermes data ($size: chats, memories, skills, connections and tokens)?"; then + rm -rf "$HOME/.hermes" "$HOME/.config/Hermes" + data_removed=true + fi + fi + echo "" echo "Hermes Desktop has been removed." - echo "Your chats, memories, and skills are still in ~/.hermes," - echo "and your connections and settings in ~/.config/Hermes." + if [[ $data_removed == true ]]; then + echo "Its chats, memories, and settings in ~/.hermes and ~/.config/Hermes are gone too." + else + echo "Your chats, memories, and skills are still in ~/.hermes," + echo "and your connections and settings in ~/.config/Hermes." + fi else echo "" echo "Hermes Desktop has been removed." diff --git a/manual/17-ai.md b/manual/17-ai.md index 78b51430..7a9e7c49 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -46,7 +46,7 @@ Crashes can also be silenced one program at a time, which is what the diagnosis The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, and Hermes Desktop. -Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself. +Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too. ### Local LLMs diff --git a/test/shell.d/hermes-remove-test.sh b/test/shell.d/hermes-remove-test.sh index d3389087..88554894 100755 --- a/test/shell.d/hermes-remove-test.sh +++ b/test/shell.d/hermes-remove-test.sh @@ -24,6 +24,16 @@ cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH' printf '%s\0' "$@" >>"$OMARCHY_TEST_INSTALLER_LOG" exit "${OMARCHY_TEST_INSTALLER_STATUS:-0}" SH + +# The remover asks through gum whether the user's data should go too. The stub +# answers "no" unless a test says otherwise, and logs every call: a real gum +# would hang a test run, and one that answered "yes" on its own would be the +# very data loss the default-no exists to prevent. +cat >"$mock_bin/gum" <<'SH' +#!/bin/bash +printf '%s\0' "$@" >>"$OMARCHY_TEST_GUM_LOG" +exit "${OMARCHY_TEST_GUM_STATUS:-1}" +SH chmod +x "$mock_bin"/* seed_install() { @@ -43,13 +53,30 @@ seed_install() { touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete" } +# "$test_tmp/installer-log" + : >"$test_tmp/gum-log" OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \ OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \ OMARCHY_TEST_INSTALLER_STATUS="${OMARCHY_TEST_INSTALLER_STATUS:-0}" \ + OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \ HOME="$test_home" PATH="$mock_bin:$PATH" \ - bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1 + bash "$ROOT/bin/omarchy-remove-ai-hermes" /dev/null 2>&1 +} + +# script(1) puts the remover on a pty, which is the only way -t 0 answers true +# without a person at a real one; the stubbed gum then supplies the answer. +remove_tty() { + : >"$test_tmp/installer-log" + : >"$test_tmp/gum-log" + OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \ + OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \ + OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \ + OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \ + HOME="$test_home" PATH="$mock_bin:$PATH" \ + script -qec "bash '$ROOT/bin/omarchy-remove-ai-hermes'" /dev/null >/dev/null 2>&1 } # The app brings its own uv and its own node; both are runtime, not data. @@ -77,6 +104,12 @@ pass "removal clears only the managed Node links it stranded" [[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal" pass "removal keeps what belongs to the user" +# Without a terminal there is nobody to ask, so gum must not even be reached: +# a gum that answered "yes" on its own would be a data loss. +[[ ! -s $test_tmp/gum-log ]] || + fail "removal does not ask about the user's data without a terminal" +pass "removal keeps the user's data unasked when there is no terminal" + [[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed" pass "removal takes the command the app installed" @@ -134,6 +167,36 @@ remove || fail "remove succeeds with a wrapper pointing at a sibling directory" fail "a wrapper pointing at ~/xhermes is not mistaken for one pointing into ~/.hermes" pass "removal matches the runtime path as a plain string" +# On a terminal the user is asked, default no: declining leaves every piece of +# data where it was. +seed_install +remove_tty || fail "remove succeeds when the data question is declined" +tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' || + fail "removal asks about the user's data on a terminal" +[[ -f $test_home/.hermes/sessions/one.json && -d $test_home/.config/Hermes ]] || + fail "declining the question keeps the user's data" +pass "removal asks on a terminal and declining keeps the data" + +# An explicit yes is the one path that takes the data too. +seed_install +OMARCHY_TEST_GUM_STATUS=0 remove_tty || fail "remove succeeds when the data goes too" +[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] || + fail "a yes deletes ~/.hermes and ~/.config/Hermes" +pass "removal deletes the user's data only on an explicit yes" + +# Without the bootstrap marker ~/.hermes is an install the app never owned, so +# it must not even be offered for deletion -- not to a terminal, not to a user +# who would say yes. +seed_install +rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete" +OMARCHY_TEST_GUM_STATUS=0 remove_tty || + fail "remove succeeds when the app never installed Hermes" +[[ ! -s $test_tmp/gum-log ]] || + fail "a Hermes the app never installed is not offered for deletion" +[[ -d $test_home/.hermes/hermes-agent && -d $test_home/.config/Hermes ]] || + fail "a Hermes the app never installed survives a would-be yes" +pass "removal never offers a Hermes the app did not install" + # A CLI teardown that fails must not stop the runtime handling, and must not be # papered over either: the data work still happens, and the failure reaches the # caller's exit code.