diff --git a/bin/omarchy-install-ai-hermes b/bin/omarchy-install-ai-hermes index 536b8ac4..8a87420d 100755 --- a/bin/omarchy-install-ai-hermes +++ b/bin/omarchy-install-ai-hermes @@ -10,11 +10,7 @@ if (( EUID == 0 )); then exit 1 fi -# The package goes in here, before the runtime installer runs, because the -# installer stands aside for a Hermes the user set up themselves only while the -# app is absent. Asked for by name, the app needs the runtime it prepares. -echo "Installing Hermes Desktop..." -omarchy-pkg-add hermes-desktop +# The same install the default agent gets, plus opening the window. omarchy-install-hermes-cli --now echo "Opening Hermes Desktop..." diff --git a/bin/omarchy-install-hermes-cli b/bin/omarchy-install-hermes-cli index 1fabf9be..c45adca4 100755 --- a/bin/omarchy-install-hermes-cli +++ b/bin/omarchy-install-hermes-cli @@ -1,6 +1,6 @@ #!/bin/bash -# omarchy:summary=Install Hermes for the default agent: the desktop app's self-updating runtime +# omarchy:summary=Install Hermes Desktop for the default agent; its runtime provides the hermes command # omarchy:args=<--check|--now|--retire-mise> # omarchy:examples=omarchy install hermes cli --now | omarchy install hermes cli --check # omarchy:requires-sudo=true @@ -10,15 +10,12 @@ # installer makes the only Hermes that can update itself: a checkout under # ~/.hermes with its own venv, which `hermes update` fast-forwards. The mise # build this replaced had no checkout, so nothing an update could move. -# Choosing Hermes as the default agent therefore installs the app the way +# Choosing Hermes as the default agent therefore installs the app, the way # Install > AI does, short of opening its window; see omarchy-install-ai-hermes. -# -# --check asks whether a Hermes omarchy-agent can run, not whose it is: a -# working one the user installed themselves is as good an answer as the app's, -# and without the app --now leaves it be rather than putting a second Hermes -# beside it. Once the app is installed its runtime is the one Hermes, and -# whatever held the command's name is saved aside before upstream's installer -# takes it. +# Hermes is only ever installed through the app: --check asks whether the +# app's Hermes is in and its command is what omarchy-agent would run, and +# --now installs the app whatever else answered to hermes before, saving +# whatever held the command's name aside before upstream's installer takes it. # # The wrapper the retired installer wrote, and the mise environment it built, # are known here and nowhere else: --retire-mise is what the migration runs, @@ -158,11 +155,6 @@ hermes_prompt_ready() { help_defines_flag '--query' "$help" } -# A Hermes omarchy-agent can run, whoever installed it. -usable() { - ! legacy_stub && hermes_prompt_ready -} - # The command upstream's installer writes execs into the runtime. Anything else # at that path -- an official install elsewhere, a hand-rolled wrapper, even a # dangling link -- is the user's. Matched as a plain string with its trailing @@ -200,25 +192,21 @@ native_app_complete() { [[ -f $native_app/Hermes && -x $native_app/Hermes && -f $native_app/resources/app.asar && -f $native_app/resources/install-stamp.json ]] } -# What --check answers, and what --now has nothing left to do about. Without -# the app, a Hermes that runs, whoever installed it. With the app, its own: the -# command upstream's installer wrote, the runtime's completion marker and the -# seeded packaged app, because the app only runs against a runtime it prepared -# and the terminal has to be on the same one. Either way the command PATH finds. -# And nothing of the retired wrapper's left to remove. The two agree on -# purpose: choosing Hermes from the menu asks first and opens a terminal only -# on a no, so a yes here has to mean --now would take no minutes-long step -# where nobody can watch it. +# What --check answers, and what --now has nothing left to do about: the app +# is installed and the Hermes on the machine is its own -- the command +# upstream's installer wrote, the runtime's completion marker and the seeded +# packaged app, because the app only runs against a runtime it prepared and +# the terminal has to be on the same one -- and that command is the one PATH +# finds, with nothing of the retired wrapper's left to remove. The two agree +# on purpose: choosing Hermes from the menu asks first and opens a terminal +# only on a no, so a yes here has to mean --now would take no minutes-long +# step where nobody can watch it. installed() { on_path || return 1 if legacy_owned; then return 1 fi - if omarchy-pkg-present hermes-desktop; then - ours && [[ -f $runtime/.hermes-bootstrap-complete ]] && native_app_complete && hermes_prompt_ready - else - usable - fi + omarchy-pkg-present hermes-desktop && ours && [[ -f $runtime/.hermes-bootstrap-complete ]] && native_app_complete && hermes_prompt_ready } # Once a Hermes answers at the command's path, the environment the retired @@ -365,19 +353,7 @@ if installed; then fi if ! omarchy-pkg-present hermes-desktop; then - # The user's own Hermes is what the default agent will run; one that runs - # but predates seeded sessions is theirs to update, not ours to replace. - # The retired wrapper is neither: it is replaced, and never run to find out. - if usable; then - if settle_path; then exit 0; else exit 1; fi - fi - if ! legacy_stub && hermes_runs; then - echo "$command_path does not support the interactive seeded sessions Omarchy needs." >&2 - echo "Update it to a Hermes Agent release with interactive chat queries, then run omarchy-install-hermes-cli --now again." >&2 - exit 1 - fi - - echo "Installing Hermes..." + echo "Installing Hermes Desktop..." omarchy-pkg-add hermes-desktop fi @@ -392,6 +368,43 @@ if runtime_ready; then runtime_present=true fi +# The packaged app can be seeded only beside a runtime at the release it was +# built from, with its desktop sources untouched, and a seeded app has to be +# whole. Asked before a command is replaced, so that a refusal leaves the +# user's Hermes exactly as it was, and again before seeding. +# Asked through ||, so nothing in here stops on its own: every git answer is +# checked, and one that cannot be read refuses rather than reading as clean. +refuse_unless_seedable() { + local runtime_commit changes + if [[ -e $native_app || -L $native_app ]]; then + if ! native_app_complete; then + echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2 + return 1 + fi + return 0 + fi + if ! runtime_commit=$(git -C "$runtime" rev-parse HEAD 2>/dev/null) || [[ -z $runtime_commit ]]; then + echo "Could not read the Hermes runtime's git state at $runtime. Repair that installation before trying again; existing files have been kept." >&2 + return 1 + fi + if [[ $runtime_commit != "$release_commit" ]]; then + echo "The Hermes runtime has moved beyond the packaged desktop release. Run 'hermes desktop --build-only', then try again." >&2 + return 1 + fi + if ! changes=$(git -C "$runtime" status --porcelain --untracked-files=all -- apps/desktop package.json package-lock.json 2>/dev/null); then + echo "Could not read the Hermes runtime's git state at $runtime. Repair that installation before trying again; existing files have been kept." >&2 + return 1 + fi + if [[ -n $changes ]]; then + echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2 + return 1 + fi +} + +if [[ $runtime_present == "true" ]]; then + refuse_unless_seedable || exit 1 +fi + # Whether this run gave Hermes something new to show the theme to: a runtime # it set up, or the app it seeded. Putting a command back is neither. set_up=false @@ -471,22 +484,8 @@ if [[ $runtime_commit == "$release_commit" ]]; then fi fi -if [[ -e $native_app || -L $native_app ]]; then - if ! native_app_complete; then - echo "The Hermes desktop app at $native_app is incomplete. Repair it with 'hermes desktop --build-only' before trying again." >&2 - exit 1 - fi -else - if [[ $runtime_commit != "$release_commit" ]]; then - echo "The Hermes runtime has moved beyond the packaged desktop release. Run 'hermes desktop --build-only', then try again." >&2 - exit 1 - fi - desktop_changes=$(git -C "$runtime" status --porcelain --untracked-files=all -- apps/desktop package.json package-lock.json) - if [[ -n $desktop_changes ]]; then - echo "Hermes desktop sources have local changes. Run 'hermes desktop --build-only', then try again; existing files have been kept." >&2 - exit 1 - fi - +refuse_unless_seedable || exit 1 +if [[ ! -e $native_app && ! -L $native_app ]]; then mkdir -p -- "${native_app%/*}" staging=$(mktemp -d "${native_app%/*}/.linux-unpacked.XXXXXX") trap 'rm -rf -- "$staging"' EXIT diff --git a/manual/17-ai.md b/manual/17-ai.md index 1a3b61f8..35ba9a11 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -1,6 +1,6 @@ # AI -Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a favorite for you. Instead, every major coding-agent CLI comes pre-wired as a lazy-loaded launcher. The launchers are tiny [mise](https://mise.jdx.dev/)-managed stubs in `~/.local/bin/`, so nothing is downloaded until the first time you actually run one. Hermes is the exception: nothing is stubbed for it, and choosing it installs it, as _Desktop apps_ below explains. Invoke any of these and authenticate when prompted: +Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a favorite for you. Instead, every major coding-agent CLI comes pre-wired as a lazy-loaded launcher. The launchers are tiny [mise](https://mise.jdx.dev/)-managed stubs in `~/.local/bin/`, so nothing is downloaded until the first time you actually run one. Hermes is the exception: nothing is stubbed for it, and choosing it installs Hermes Desktop, as _Desktop apps_ below explains. Invoke any of these and authenticate when prompted: | Command | Agent | |------------|------------------------------------------------------------------| @@ -52,7 +52,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, the Claude desktop app (Anthropic's Linux beta, with Chat, Cowork, and Claude Code tabs), Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app. -Hermes Desktop is the one to know about, because a machine has one Hermes, and once the app is installed it is the app's. Unless a `hermes` you installed yourself already works, choosing Hermes as the default agent installs the same thing the _Install > AI_ entry does: the package, and a Hermes runtime under `~/.hermes` set up by Hermes' own installer, which takes a few minutes the first time. That runtime is the one Hermes the terminal `hermes` command, the default agent and the app all use, and it updates itself with `hermes update` rather than waiting on an Omarchy release. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. 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. +Hermes Desktop is the one to know about, because a machine has one Hermes, and it is the app's: Hermes is only ever installed through the app, and the `hermes` command the default agent runs is the app's. Choosing Hermes as the default agent installs the same thing the _Install > AI_ entry does: the package, and a Hermes runtime under `~/.hermes` set up by Hermes' own installer, which takes a few minutes the first time. That runtime is the one Hermes the terminal `hermes` command, the default agent and the app all use, and it updates itself with `hermes update` rather than waiting on an Omarchy release. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. 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-cli-test.sh b/test/shell.d/hermes-cli-test.sh index 18ed06c1..fceff8e8 100755 --- a/test/shell.d/hermes-cli-test.sh +++ b/test/shell.d/hermes-cli-test.sh @@ -126,47 +126,15 @@ run_installer --check && fail "--check reports a link to the retired wrapper as rm -f "$hermes" pass "--check never runs the retired mise wrapper" +# Hermes is only ever installed through the app: a hermes that works but did +# not come from it is not the app's, so --check says no and --now installs the +# app over it, the command saved aside by the runtime setup further on. write_hermes -run_installer --check || fail "--check follows a hermes that runs and takes seeded sessions" -pass "--check is true for a working hermes" - -# The flags have to be defined by the help, not merely mentioned in it, and -# both of them: omarchy-agent passes --query to seed the session and --tui to -# keep it interactive. -OMARCHY_TEST_HERMES_HELP='Run with --tui for a terminal session; see --query in the docs.' run_installer --check && - fail "--check accepts flags that are only mentioned" -OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query" -OMARCHY_TEST_HERMES_HELP='[-q QUERY, --query QUERY]' run_installer --check && fail "--check accepts a hermes without --tui" -pass "--check needs both flags defined, not mentioned" - -chmod -x "$hermes" -run_installer --check && fail "--check accepts a hermes that is not executable" -rm -f "$hermes" -mkdir "$hermes" -run_installer --check && fail "--check accepts a directory at the command's path" -rmdir "$hermes" -ln -s "$test_tmp/nowhere" "$hermes" -run_installer --check && fail "--check accepts a dangling link" -rm -f "$hermes" -pass "--check rejects what is not a command that runs" - -# A hermes the user set up themselves is what the default agent will run, so -# --now installs nothing beside it. -write_hermes -before=$(cat "$hermes") +run_installer --check && fail "--check calls a hermes without the app installed" : >"$pkg_log" -run_installer --now || fail "--now succeeds over a working hermes of the user's own" "$(cat "$test_tmp/output")" -[[ $(cat "$hermes") == "$before" ]] || fail "--now leaves the user's hermes as it was" -[[ ! -s $pkg_log ]] || fail "--now installs a package beside a working hermes" -pass "--now leaves a working hermes of the user's own alone" - -# One that runs but predates seeded sessions is theirs to update, not ours to -# replace with the app. -: >"$pkg_log" -OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --now && fail "--now replaces a hermes that predates seeded sessions" -grep -q 'Update it' "$test_tmp/output" || fail "an old hermes gets update guidance" "$(cat "$test_tmp/output")" -[[ ! -s $pkg_log ]] || fail "an old hermes has a package installed over it" -pass "--now tells the user to update a hermes that predates seeded sessions" +run_installer --now && fail "--now carries on past the mocked package failure" +grep -qx 'hermes-desktop' "$pkg_log" || fail "--now installs the app over a hermes from elsewhere" "$(cat "$test_tmp/output")" +pass "a hermes that did not come from the app is not the app's; choosing Hermes installs the app" rm -f "$hermes" : >"$pkg_log" @@ -210,6 +178,27 @@ write_hermes ours OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the runtime's own command" pass "--check needs the app's own command once the app is installed" +# The flags have to be defined by the help, not merely mentioned in it, and +# both of them: omarchy-agent passes --query to seed the session and --tui to +# keep it interactive. +OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='Run with --tui for a terminal session; see --query in the docs.' run_installer --check && + fail "--check accepts flags that are only mentioned" +OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='[--tui]' run_installer --check && fail "--check accepts a hermes without --query" +OMARCHY_TEST_DESKTOP_INSTALLED=1 OMARCHY_TEST_HERMES_HELP='[-q QUERY, --query QUERY]' run_installer --check && fail "--check accepts a hermes without --tui" +pass "--check needs both flags defined, not mentioned" + +chmod -x "$hermes" +OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a hermes that is not executable" +rm -f "$hermes" +mkdir "$hermes" +OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a directory at the command's path" +rmdir "$hermes" +ln -s "$test_tmp/nowhere" "$hermes" +OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check accepts a dangling link" +rm -f "$hermes" +write_hermes ours +pass "--check rejects what is not a command that runs" + # Installed and finished: nothing to do, and quickly, because choosing the # agent from the menu runs this. : >"$pkg_log" @@ -238,15 +227,9 @@ pass "--check does not need ~/.local/bin on the caller's PATH" # one the agent gets. cp "$hermes" "$mock_bin/hermes" OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check && fail "--check calls a shadowed command installed" -rm -rf "$test_home/.hermes" "$test_home/.local/bin/.hermes-before-desktop."* -run_installer --check && fail "--check calls a shadowed command of the user's own installed" -: >"$pkg_log" -run_installer --now && fail "--now reports a shadowed command as ready" -grep -qF "$mock_bin/hermes" "$test_tmp/output" || fail "--now names the command in the way" "$(cat "$test_tmp/output")" -[[ ! -s $pkg_log ]] || fail "a shadowed command has the app installed over it" rm -f "$mock_bin/hermes" -run_installer --check || fail "--check follows the command once nothing shadows it" -pass "a hermes ahead of ~/.local/bin on PATH is reported, not installed over" +OMARCHY_TEST_DESKTOP_INSTALLED=1 run_installer --check || fail "--check follows the command once nothing shadows it" +pass "a hermes ahead of ~/.local/bin on PATH is not the one the agent gets" # The retired wrapper's environment goes with the wrapper's proof, wherever # that is. --retire-mise is the migration's whole job; --now does the same once @@ -272,20 +255,5 @@ OMARCHY_TEST_MISE_SIBLING=1 run_installer --retire-mise || fail "--retire-mise s ! grep -q '^mise rm' "$mise_log" || fail "a similarly named tool is taken for the retired one" "$(cat "$mise_log")" pass "a mise tool named like the retired one does not keep the migration pending" -# The shim mise put ahead of ~/.local/bin is what the agent would run until -# the environment goes; it goes with the uninstall, and so does the saved -# wrapper, since once the environment is gone the proof would only keep -# --check saying no and claim an environment the user builds later. -write_hermes -saved="$test_home/.local/bin/.hermes-before-desktop.abc123" -mkdir -p "$saved" -write_legacy_stub "$saved/hermes" -cp "$hermes" "$mock_bin/hermes" -: >"$mise_log"; rm -f "$mise_log.removed" "$mise_log.unrequested" -run_installer --check && fail "--check calls a handover with the shim still ahead finished" -OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$mock_bin/hermes" run_installer --now || fail "--now finishes the handover over a saved wrapper" "$(cat "$test_tmp/output")" -grep -qF "mise uninstall --all pipx:hermes-agent[extras=all]" "$mise_log" || fail "--now retires the environment the saved wrapper proves Omarchy's" "$(cat "$mise_log")" -[[ ! -e $mock_bin/hermes ]] || fail "the shim is gone with the environment" -[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its empty directory go once the environment is gone" -run_installer --check || fail "--check follows a finished handover" "$(cat "$test_tmp/output")" -pass "--now retires the mise Hermes once the runtime installer has saved the wrapper aside" +# --now finishing a handover, mise environment and shim included, needs the +# package fixtures and lives in hermes-desktop-install-test.sh. diff --git a/test/shell.d/hermes-desktop-install-test.sh b/test/shell.d/hermes-desktop-install-test.sh index 01a0d23e..39f0591b 100644 --- a/test/shell.d/hermes-desktop-install-test.sh +++ b/test/shell.d/hermes-desktop-install-test.sh @@ -175,6 +175,26 @@ cat >"$test_tmp/bin/systemd-run" <<'MOCK' #!/bin/bash printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events" MOCK +# The only thing the installer asks mise is to remove what the retired wrapper +# built. `where` finds that environment and `ls -g` lists it while a test says +# it was built and it has not been uninstalled and unrequested; uninstalling +# also takes the shim stand-in named in OMARCHY_TEST_SHIM, as mise's does. +cat >"$test_tmp/bin/mise" <<'MOCK' +#!/bin/bash +printf 'mise %s\n' "$*" >>"$OMARCHY_TEST_ROOT/mise-log" +case "$1" in + where) [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-removed ]] ;; + ls) + if [[ ${OMARCHY_TEST_MISE_BUILT:-0} == 1 && ! -e $OMARCHY_TEST_ROOT/mise-unrequested ]]; then + echo '{"pipx:hermes-agent[extras=all]": [{"version": "latest"}]}' + else + echo '{}' + fi + ;; + rm) touch "$OMARCHY_TEST_ROOT/mise-unrequested" ;; + uninstall) touch "$OMARCHY_TEST_ROOT/mise-removed"; rm -f "${OMARCHY_TEST_SHIM:-}" ;; +esac +MOCK chmod +x "$test_tmp/bin/"* # Substitute only system package paths in scratch copies of the actual scripts. @@ -279,6 +299,28 @@ run_installer && fail "a newer runtime cannot receive an older native app" [[ ! -e $native ]] || fail "no mismatched native app was copied" grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance" assert_stopped "a missing updated app prevents launch and theme setup" +# The refusal comes before anything is touched: a launcher of the user's own +# beside that newer runtime is neither saved aside nor replaced by a run that +# is going to stop anyway. +own_launcher="#!/bin/bash +exec \"$test_home/tools/hermes\" \"\$@\"" +printf '%s\n' "$own_launcher" >"$test_home/.local/bin/hermes" +: >"$test_tmp/events" +run_cli --now && fail "a newer runtime with a launcher of its own still stops" +grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "the stop still carries the build-only guidance" "$(cat "$test_tmp/output")" +[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "a run that stops leaves the user's launcher as it was" +[[ -z $(find "$test_home/.local/bin" -maxdepth 1 -name '.hermes-before-desktop.*' -print) ]] || fail "a run that stops saves nothing aside" +[[ ! -s $test_tmp/events ]] || fail "a run that stops writes no commands" "$(cat "$test_tmp/events")" +# A git state that cannot be read is a refusal too, not a clean tree: back at +# the release with the index unreadable, the launcher is still left alone. +git -C "$runtime" checkout -q --detach "$release_commit" +chmod 000 "$runtime/.git/index" +: >"$test_tmp/events" +run_cli --now && { chmod 644 "$runtime/.git/index"; fail "an unreadable git state is not a clean tree"; } +chmod 644 "$runtime/.git/index" +grep -q 'Could not read' "$test_tmp/output" || fail "an unreadable git state is named" "$(cat "$test_tmp/output")" +[[ $(cat "$test_home/.local/bin/hermes") == "$own_launcher" ]] || fail "an unreadable git state leaves the user's launcher as it was" +[[ ! -s $test_tmp/events ]] || fail "an unreadable git state writes no commands" "$(cat "$test_tmp/events")" pass "updated runtimes are preserved and never seeded with the old packaged app" new_home dirty-desktop @@ -350,7 +392,7 @@ printf 'local edit\n' >"$runtime/runtime.txt" : >"$test_tmp/events" run_installer || fail "a finished install with local edits is accepted" "$(cat "$test_tmp/output")" [[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "local edits to a finished runtime are preserved" -[[ $(cat "$test_tmp/events") == $'package hermes-desktop\nlaunch' ]] || fail "a finished install is only opened" "$(cat "$test_tmp/events")" +[[ $(cat "$test_tmp/events") == launch ]] || fail "a finished install is only opened" "$(cat "$test_tmp/events")" pass "a finished install is left as the user has it" new_home full-history-retry @@ -516,14 +558,14 @@ fi SH chmod +x "$test_home/.local/bin/hermes" own_hermes=$(cat "$test_home/.local/bin/hermes") -run_cli --now || fail "the default agent path accepts the user's own Hermes" "$(cat "$test_tmp/output")" -[[ ! -s $test_tmp/events ]] || fail "a working Hermes of the user's own has the app installed beside it" "$(cat "$test_tmp/events")" -[[ $(cat "$test_home/.local/bin/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is left alone" -run_installer || fail "the app installs over the user's own Hermes" "$(cat "$test_tmp/output")" -grep -qx bootstrap "$test_tmp/events" || fail "the app sets up its own runtime" +run_cli --check && fail "--check calls a Hermes from elsewhere the app's" +run_cli --now || fail "choosing Hermes installs the app over a Hermes from elsewhere" "$(cat "$test_tmp/output")" +[[ $(head -2 "$test_tmp/events") == $'package hermes-desktop\nbootstrap' ]] || fail "the app and its runtime are installed over a Hermes from elsewhere" "$(cat "$test_tmp/events")" backups=("$test_home/.local/bin/".hermes-before-desktop.*) -[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the user's own hermes command is saved aside" -pass "the default agent path stands aside for the user's own Hermes; the app supersedes it" +[[ ${#backups[@]} == 1 && $(cat "${backups[0]}/hermes") == "$own_hermes" ]] || fail "the previous hermes command is saved aside" +grep -qF "$hermes_home/" "$test_home/.local/bin/hermes" || fail "the command is now the app's" +run_cli --check || fail "--check follows the app's Hermes" +pass "Hermes is only installed through the app: a Hermes from elsewhere is superseded, its command saved aside" # A finished runtime whose command is gone, or not its own, gets its command # back from upstream's path stage alone: no bootstrap, the runtime untouched, @@ -567,6 +609,28 @@ rm -f "$test_tmp/bin/hermes" run_cli --check || fail "--check follows the install once nothing shadows it" pass "a hermes ahead of ~/.local/bin on PATH is reported, not set up over" +# A machine that chose Hermes before its migration ran: the wrapper's copy the +# runtime setup saved aside proves the mise environment Omarchy's, and mise's +# shim for it sits ahead of ~/.local/bin. Choosing Hermes again finishes the +# handover: the environment goes, the shim and the saved copy with it, and +# only then is the command the one PATH finds. +new_home handover +run_cli --now || fail "handover fixture sets up" "$(cat "$test_tmp/output")" +saved="$test_home/.local/bin/.hermes-before-desktop.mise01" +mkdir -p "$saved" +printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$saved/hermes" +cp "$test_home/.local/bin/hermes" "$test_tmp/bin/hermes" +: >"$test_tmp/mise-log"; rm -f "$test_tmp/mise-removed" "$test_tmp/mise-unrequested" +OMARCHY_TEST_MISE_BUILT=1 run_cli --check && fail "--check calls a handover with the mise environment still there finished" +: >"$test_tmp/events" +OMARCHY_TEST_MISE_BUILT=1 OMARCHY_TEST_SHIM="$test_tmp/bin/hermes" run_cli --now || fail "--now finishes the handover" "$(cat "$test_tmp/output")" +grep -q 'mise uninstall --all' "$test_tmp/mise-log" || fail "the environment the saved wrapper proves Omarchy's is removed" "$(cat "$test_tmp/mise-log")" +[[ ! -e $test_tmp/bin/hermes ]] || fail "the shim is gone with the environment" +[[ ! -e $saved/hermes && ! -d $saved ]] || fail "the saved wrapper and its directory go once the environment is gone" +[[ ! -s $test_tmp/events ]] || fail "the handover sets nothing up again" "$(cat "$test_tmp/events")" +run_cli --check || fail "--check follows the finished handover" +pass "choosing Hermes again before the migration finishes the handover: the mise Hermes and its shim go" + new_home custom-profile hermes_home="$test_home/custom home" runtime="$hermes_home/hermes-agent" diff --git a/test/shell.d/hermes-mise-migration-test.sh b/test/shell.d/hermes-mise-migration-test.sh index 2cc29791..6a5386c3 100755 --- a/test/shell.d/hermes-mise-migration-test.sh +++ b/test/shell.d/hermes-mise-migration-test.sh @@ -227,18 +227,26 @@ pass "a linked backup directory is neither proof nor touched" write_stub OMARCHY_TEST_DEFAULT_AGENT=hermes OMARCHY_TEST_MISE_BUILT=1 run_migration || fail "the migration succeeds for a Hermes default agent" grep -q 'omarchy default agent hermes' "$test_tmp/output" || fail "a default agent that just went is told how to come back" "$(cat "$test_tmp/output")" -cat >"$hermes" <<'SH' +# A working Hermes is the app's: the package, its runtime finished and seeded, +# and the command upstream's installer wrote for it. +app_runtime="$test_home/.hermes/hermes-agent" +mkdir -p "$app_runtime/apps/desktop/release/linux-unpacked/resources" +touch "$app_runtime/.hermes-bootstrap-complete" "$app_runtime/apps/desktop/release/linux-unpacked/resources/app.asar" "$app_runtime/apps/desktop/release/linux-unpacked/resources/install-stamp.json" +printf '#!/bin/bash\nexit 0\n' >"$app_runtime/apps/desktop/release/linux-unpacked/Hermes" +chmod +x "$app_runtime/apps/desktop/release/linux-unpacked/Hermes" +cat >"$hermes" <