diff --git a/bin/omarchy-remove-ai-hermes b/bin/omarchy-remove-ai-hermes index 1145fbd1..2465e46c 100755 --- a/bin/omarchy-remove-ai-hermes +++ b/bin/omarchy-remove-ai-hermes @@ -22,14 +22,11 @@ omarchy-install-hermes-cli --remove || cli_removed=false # and it is the only thing that tells that runtime apart from one the user # installed themselves -- the paths are the same either way. Without it the app # never got that far: a machine where it was installed but never launched still -# has whatever was there before, and none of it is ours to delete. +# has whatever was there before, and none of it is ours to delete unasked. 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,14 +55,38 @@ if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then fi done - echo "" - echo "Hermes Desktop has been removed." +fi + +# What survives to here 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 with the size, default no. Asked whenever the directories +# exist, marker or no marker: on a machine where the marker never appeared the +# data came from the terminal CLI or an install the app never finished, and it +# is still what removal is asked to clean up. Naming the paths keeps the +# question honest there too -- ~/.hermes may still carry a runtime the app +# never owned, a yes takes that with it, and saying so is the prompt's job. +# Without a terminal to ask in, keeping everything is the answer. +data_removed=false +if [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]] && [[ -t 0 ]] && command -v gum >/dev/null; then + # du answers non-zero when either directory is missing, 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 ~/.hermes and ~/.config/Hermes ($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." +if [[ $data_removed == true ]]; then + echo "Its chats, memories, and settings in ~/.hermes and ~/.config/Hermes are gone too." +elif [[ -d $HOME/.hermes || -d $HOME/.config/Hermes ]]; then echo "Your chats, memories, and skills are still in ~/.hermes," echo "and your connections and settings in ~/.config/Hermes." -else - echo "" - echo "Hermes Desktop has been removed." - echo "It never finished installing its own Hermes, so nothing in ~/.hermes was touched." fi # The messages above still hold -- the app and its runtime are gone -- but a CLI diff --git a/manual/17-ai.md b/manual/17-ai.md index f94c3546..73e5fedf 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, Hermes Desktop, and OpenClaw. -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. OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it. diff --git a/test/shell.d/hermes-remove-test.sh b/test/shell.d/hermes-remove-test.sh index d3389087..b3ef6862 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,45 @@ 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 the runtime is not the app's to take unasked, +# but the data question is still the user's to answer: declining keeps the +# whole tree -- runtime included -- untouched. +seed_install +rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete" +remove_tty || fail "remove succeeds when the app never installed Hermes" +tr '\0' '\n' <"$test_tmp/gum-log" | grep -qx 'confirm' || + fail "removal still asks about the data without the bootstrap marker" +[[ -d $test_home/.hermes/hermes-agent && -d $test_home/.config/Hermes ]] || + fail "declining keeps a Hermes the app never installed" +pass "removal asks without the marker and declining keeps everything" + +# The prompt names ~/.hermes itself, so a yes takes the whole tree there too, +# unowned runtime and all -- that is what was asked and answered. +seed_install +rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete" +OMARCHY_TEST_GUM_STATUS=0 remove_tty || + fail "remove succeeds when the data goes too without the marker" +[[ ! -e $test_home/.hermes && ! -e $test_home/.config/Hermes ]] || + fail "a yes takes ~/.hermes whole when the marker never appeared" +pass "removal honors a yes on the named paths without the marker" + # 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.