diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index bdd0daab..7ae9410a 100755 --- a/bin/omarchy-install-hermes-cli +++ b/bin/omarchy-install-hermes-cli @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin -# omarchy:args=[--check|--now|--owns] +# omarchy:args=[--check|--now|--owns|--remove] # omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now # Hermes pins every one of its dependencies exactly and declares @@ -112,6 +112,25 @@ if [[ $mode == "--owns" ]]; then if ours; then exit 0; else exit 1; fi fi +# --remove tears down a Hermes CLI this installer put in place -- the mise tool +# it installs and the stub it marks -- so Remove Hermes clears a CLI the app +# never superseded (an interrupted install, or the terminal CLI from before the +# app existed) rather than leaving it stranded on PATH. Scoped to what is ours: +# the tool spec is this installer's, and the stub goes only when it carries the +# marker, so a Hermes the user installed themselves is untouched. Idempotent -- +# nothing installed means nothing to do -- and it is the same teardown the +# desktop takeover above performs, kept in one place so the two cannot drift. +if [[ $mode == "--remove" ]]; then + mise rm -g "$tool" >/dev/null 2>&1 || true + mise uninstall --all "$tool" >/dev/null 2>&1 || true + + if ours; then + rm -f "$HOME/.local/bin/hermes" + fi + + exit 0 +fi + # --check lets callers tell a cold stub from a working one before they commit # to a path that assumes Hermes is ready. if [[ $mode == "--check" ]]; then diff --git a/bin/omarchy-remove-ai-hermes b/bin/omarchy-remove-ai-hermes index f67b1d6d..f332dc4b 100755 --- a/bin/omarchy-remove-ai-hermes +++ b/bin/omarchy-remove-ai-hermes @@ -8,6 +8,13 @@ set -euo pipefail omarchy-pkg-drop hermes-desktop +# The mise CLI is the app's predecessor, not the app itself: Hermes Desktop takes +# it over on install and runs its own runtime instead, so a copy still here is one +# the app never superseded -- an interrupted install, or the terminal CLI from +# before the app existed. Remove Hermes clears that too, scoped by the installer +# to what Omarchy owns so a hermes the user set up themselves is left alone. +omarchy-install-hermes-cli --remove + # The app writes this when the runtime it provisions under ~/.hermes has landed, # 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 diff --git a/test/shell.d/hermes-cli-test.sh b/test/shell.d/hermes-cli-test.sh index fa5c1db9..a70589ef 100755 --- a/test/shell.d/hermes-cli-test.sh +++ b/test/shell.d/hermes-cli-test.sh @@ -402,6 +402,45 @@ marker_copies=$(grep -rl "Written by omarchy-install-hermes-cli" \ fail "only omarchy-install-hermes-cli spells out the ownership marker" pass "the ownership marker is written down once" +# --remove tears down a Hermes CLI this installer owns, so Remove Hermes can +# clear one the desktop app never superseded. It turns on the same ownership as +# the rest of the file, so its cases mirror that split. +remove_home="$test_tmp/remove-home" +mkdir -p "$remove_home/.local/bin" + +run_remove() { + OMARCHY_TEST_DESKTOP_INSTALLED=0 \ + OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \ + OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \ + OMARCHY_TEST_MISE_LOG="$mise_log" \ + HOME="$remove_home" \ + PATH="$mock_bin:$PATH" \ + bash "$ROOT/bin/omarchy-install-hermes-cli" --remove +} + +rm -f "$remove_home/.local/bin/hermes" +: >"$mise_log" +run_remove || fail "--remove succeeds when there is nothing to remove" +pass "--remove is idempotent when no Hermes CLI is present" + +printf '%s\n' "#!/bin/bash" "$stub_marker" >"$remove_home/.local/bin/hermes" +chmod +x "$remove_home/.local/bin/hermes" +: >"$mise_log" +OMARCHY_TEST_MISE_WHERE_OK=1 run_remove || fail "--remove succeeds tearing down an owned CLI" +tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "--remove drops the mise tool from config" +tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "--remove uninstalls the mise tool" +[[ ! -e $remove_home/.local/bin/hermes ]] || fail "--remove takes the stub it owns" +pass "--remove tears down the mise CLI and the stub this installer owns" + +foreign_remove_body="#!/bin/bash +exec /usr/local/bin/my-own-hermes \"\$@\"" +printf '%s\n' "$foreign_remove_body" >"$remove_home/.local/bin/hermes" +chmod +x "$remove_home/.local/bin/hermes" +run_remove || fail "--remove succeeds with a foreign hermes present" +[[ -f $remove_home/.local/bin/hermes && $(cat "$remove_home/.local/bin/hermes") == "$foreign_remove_body" ]] || + fail "--remove leaves a hermes it does not own untouched" +pass "--remove leaves a Hermes the user installed themselves" + # The app's marker says its install once landed, not that it is still there. A # wrapper whose runtime has since gone answers for nothing, so readiness runs # the command, exactly as it does for a hermes the user installed themselves. diff --git a/test/shell.d/hermes-remove-test.sh b/test/shell.d/hermes-remove-test.sh index bc80859f..c590f0d3 100755 --- a/test/shell.d/hermes-remove-test.sh +++ b/test/shell.d/hermes-remove-test.sh @@ -15,6 +15,14 @@ cat >"$mock_bin/omarchy-pkg-drop" <<'SH' #!/bin/bash printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG" SH + +# The CLI teardown is the installer's own, exercised in hermes-cli-test.sh; here +# it is mocked to a logger so this test stays about what Remove Hermes does with +# ~/.hermes, and to keep real mise out of a run with HOME pointed at a fixture. +cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH' +#!/bin/bash +printf '%s\0' "$@" >>"$OMARCHY_TEST_INSTALLER_LOG" +SH chmod +x "$mock_bin"/* seed_install() { @@ -35,7 +43,10 @@ seed_install() { } remove() { - OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" HOME="$test_home" PATH="$mock_bin:$PATH" \ + : >"$test_tmp/installer-log" + OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \ + OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \ + HOME="$test_home" PATH="$mock_bin:$PATH" \ bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1 } @@ -67,6 +78,12 @@ 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" +# Removal also asks the installer to tear down a mise CLI the app superseded, so +# a copy left from before the app took over does not linger once Hermes is gone. +tr '\0' '\n' <"$test_tmp/installer-log" | grep -qx -- '--remove' || + fail "removal asks the installer to tear down its own CLI" +pass "removal tears down the mise CLI through the installer" + # A hermes command the app did not write survives even when the app did install # a runtime of its own. seed_install