diff --git a/bin/omarchy-agent b/bin/omarchy-agent index e2c400ff..15188054 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -136,19 +136,27 @@ pi) ;; esac -# Claude, Codex, and Grok start as the active subscription account, the same -# as the shell functions in default/bash/fns/agent-accounts. The home travels -# in the command itself, since a terminal launched through uwsm doesn't -# inherit this process's environment. +# A new session uses the current selection rather than its parent's account +# pin. Explicit provider homes still override the selection. Carry the new pin +# in the command, since terminals launched through uwsm don't inherit our env. case "$agent" in claude) home_env="CLAUDE_CONFIG_DIR" ;; codex) home_env="CODEX_HOME" ;; grok) home_env="GROK_HOME" ;; *) home_env="" ;; esac -if [[ -n $home_env && -z ${!home_env:-} ]]; then - home=$(omarchy-agent-account-home "$agent") - [[ -n $home ]] && command=(env "$home_env=$home" "${command[@]}") +if [[ -n $home_env ]]; then + account_pin="OMARCHY_AGENT_${agent^^}_HOME" + if [[ -v $account_pin && ${!home_env:-} == "${!account_pin}" ]]; then + unset "$home_env" + fi + unset "$account_pin" + if [[ -z ${!home_env:-} ]]; then + home=$(omarchy-agent-account-home "$agent") + account_env=(env -u "$home_env" "$account_pin=$home") + [[ -n $home ]] && account_env+=("$home_env=$home") + command=("${account_env[@]}" "${command[@]}") + fi fi if [[ $inline == "true" ]]; then diff --git a/bin/omarchy-agent-account-add b/bin/omarchy-agent-account-add index d54bebad..4885eaed 100755 --- a/bin/omarchy-agent-account-add +++ b/bin/omarchy-agent-account-add @@ -138,7 +138,7 @@ esac # The CLI's own home, whatever this shell has pointed it at. login() { - interruptible env -u CLAUDE_CONFIG_DIR -u CODEX_HOME -u GROK_HOME "${login_command[@]}" + interruptible env -u CLAUDE_CONFIG_DIR -u CODEX_HOME -u GROK_HOME "OMARCHY_AGENT_${provider^^}_HOME=" "${login_command[@]}" } # Signing an account that's already here in again, where it lives: the diff --git a/bin/omarchy-agent-account-exec b/bin/omarchy-agent-account-exec new file mode 100755 index 00000000..28d123e4 --- /dev/null +++ b/bin/omarchy-agent-account-exec @@ -0,0 +1,43 @@ +#!/bin/bash + +# omarchy:summary=Run Claude, Codex, or Grok as the selected subscription account +# omarchy:args= [args...] +# omarchy:hidden=true + +set -euo pipefail + +case "${1:-}" in + claude) account_env="CLAUDE_CONFIG_DIR" ;; + codex) account_env="CODEX_HOME" ;; + grok) account_env="GROK_HOME" ;; + *) + echo "Usage: omarchy-agent-account-exec [args...]" >&2 + exit 1 + ;; +esac + +provider=$1 +shift + +# mise removes its wrapper dispatch directories before calling us, so exec +# finds the real CLI (or its first-run installer) rather than this wrapper. +# Pin the resolved home for children, including an implicit Main account. +# Explicit provider homes still take precedence. +if [[ -z ${!account_env:-} ]]; then + account_home=$(omarchy-agent-account-home "$provider") + export "OMARCHY_AGENT_${provider^^}_HOME=$account_home" + if [[ -n $account_home ]]; then + export "$account_env=$account_home" + fi +fi + +# Resolve before restoring dispatch on PATH: mise removes the wrapper paths to +# avoid recursion, but the agent's subprocesses need them for other providers. +if ! agent_binary=$(type -P "$provider"); then + # A mise dispatcher can exist before its CLI or first-run installer does. + export MISE_MINIMUM_RELEASE_AGE=0 + mise use -g --quiet "$provider" + agent_binary=$(mise which "$provider") +fi +export PATH="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/command-wrappers/bin:$PATH" +exec "$agent_binary" "$@" diff --git a/bin/omarchy-agent-account-home b/bin/omarchy-agent-account-home index 36f546d0..6e2eb3b8 100755 --- a/bin/omarchy-agent-account-home +++ b/bin/omarchy-agent-account-home @@ -13,6 +13,13 @@ registry="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/agents/accounts/${1:-}.j if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then echo "Usage: omarchy-agent-account-home " >&2 exit 1 +fi + +# A running session pins its home, including an empty value for Main. Claude's +# implicit default also selects ~/.claude.json, so Main must stay implicit. +account_pin="OMARCHY_AGENT_${1^^}_HOME" +if [[ -v $account_pin ]]; then + printf '%s' "${!account_pin}" elif [[ -f $registry ]]; then exec omarchy-agent-account-state home "$1" fi diff --git a/default/bash/env-bootstrap b/default/bash/env-bootstrap index 2bc6d1f3..f88a4a8e 100644 --- a/default/bash/env-bootstrap +++ b/default/bash/env-bootstrap @@ -25,6 +25,27 @@ if [ "$OMARCHY_PATH" != /usr/share/omarchy ]; then esac fi +# mise's command wrappers must precede real tool directories inherited from a +# parent shell, so scripts follow account switches without Bash functions. +# mise activate also keeps this directory ahead of its managed tool binaries. +omarchy_wrapper_dir="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/command-wrappers/bin" +omarchy_wrapper_path=$omarchy_wrapper_dir +if [ -n "${PATH:-}" ]; then + omarchy_remaining_path=$PATH + while :; do + omarchy_path_entry=${omarchy_remaining_path%%:*} + if [ "$omarchy_path_entry" != "$omarchy_wrapper_dir" ]; then + omarchy_wrapper_path="$omarchy_wrapper_path:$omarchy_path_entry" + fi + case "$omarchy_remaining_path" in + *:*) omarchy_remaining_path=${omarchy_remaining_path#*:} ;; + *) break ;; + esac + done +fi +PATH=$omarchy_wrapper_path +unset omarchy_wrapper_dir omarchy_wrapper_path omarchy_remaining_path omarchy_path_entry + # User-level tool paths, appended so system binaries keep precedence. This is # what lets login shells (ssh, bash -lc) and the uwsm session find mise-managed # tools without an interactive rc. Keep these directories in sync with the PAM diff --git a/default/bash/fns/agent-accounts b/default/bash/fns/agent-accounts deleted file mode 100644 index 8d436a0f..00000000 --- a/default/bash/fns/agent-accounts +++ /dev/null @@ -1,35 +0,0 @@ -# Start Claude Code, Codex, and Grok as the active subscription account, so -# `cx`, `cy`, `tdl` and a plain `claude` all follow `omarchy agent account use`. -# An explicitly set CLAUDE_CONFIG_DIR / CODEX_HOME / GROK_HOME still wins. -claude() { - local home="" - [[ -z ${CLAUDE_CONFIG_DIR:-} ]] && home=$(omarchy-agent-account-home claude) - - if [[ -n $home ]]; then - CLAUDE_CONFIG_DIR="$home" command claude "$@" - else - command claude "$@" - fi -} - -codex() { - local home="" - [[ -z ${CODEX_HOME:-} ]] && home=$(omarchy-agent-account-home codex) - - if [[ -n $home ]]; then - CODEX_HOME="$home" command codex "$@" - else - command codex "$@" - fi -} - -grok() { - local home="" - [[ -z ${GROK_HOME:-} ]] && home=$(omarchy-agent-account-home grok) - - if [[ -n $home ]]; then - GROK_HOME="$home" command grok "$@" - else - command grok "$@" - fi -} diff --git a/docs/file-layout.md b/docs/file-layout.md index dfb50cfb..5caaba56 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -172,10 +172,7 @@ Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It: - Prepends `$OMARCHY_PATH/bin` to `PATH` **only when** `OMARCHY_PATH` is not `/usr/share/omarchy`. On a production install the binaries are already on `PATH` as `/usr/bin/omarchy-*` via the `omarchy` package. -- Appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and - the uwsm session find mise-managed tools — kept in sync with the PAM `PATH` - line written by `install/config/ssh-command-path.sh`, which covers SSH - commands that run no shell setup at all. +- Prepends mise's `command-wrappers/bin` so subscription account dispatch runs before inherited tool binaries, then appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and the uwsm session find mise-managed tools. The existing mise tool shims also dispatch these wrappers for SSH commands that run no shell setup; the PAM path needs no additional entry. `etc/mise/conf.d/omarchy-agent-accounts.toml` declares the Claude, Codex, and Grok wrappers; `mise reshim` builds them during user setup and migration. Each invocation resolves the selected account through `omarchy-agent-account-exec`, while an explicit provider home takes precedence. Credentials and running sessions remain in their original account homes. Sourced by every entry point that needs the env set: diff --git a/etc/mise/conf.d/omarchy-agent-accounts.toml b/etc/mise/conf.d/omarchy-agent-accounts.toml new file mode 100644 index 00000000..ffab5bb8 --- /dev/null +++ b/etc/mise/conf.d/omarchy-agent-accounts.toml @@ -0,0 +1,13 @@ +# Resolve the active subscription at execution time, including in subprocesses. +# User and project mise config can override these system defaults. +[wrappers.claude] +command = "omarchy-agent-account-exec" +args = ["claude"] + +[wrappers.codex] +command = "omarchy-agent-account-exec" +args = ["codex"] + +[wrappers.grok] +command = "omarchy-agent-account-exec" +args = ["grok"] diff --git a/install/user/mise.sh b/install/user/mise.sh index 1d85e2b3..6819e48e 100644 --- a/install/user/mise.sh +++ b/install/user/mise.sh @@ -24,3 +24,6 @@ omarchy-mise-install github:OpenRouterLabs/ori-releases ori if omarchy-cmd-missing muse; then omarchy-mise-install "http:muse[url=https://api.meta.ai/muse-launcher.sh,bin=muse,version_list_url=https://api.meta.ai/muse-code/channels/muse-stable,version_json_path=.version]" muse fi + +# Build the account dispatchers declared by /etc/mise/conf.d. +mise reshim diff --git a/manual/17-ai.md b/manual/17-ai.md index 3bcca3e1..f740622a 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -46,11 +46,11 @@ Left-click the bar icon for the panel, right-click to launch your default agent. If you pay for more than one Claude, Codex, or Grok plan — a personal Max and a work one, say — Omarchy can keep them all signed in and move between them. Add a subscription with the + in the agents panel, which walks you through it right there, or with `omarchy agent account add claude Work` (or `codex`, or `grok`). It installs the CLI if it's missing. The first account for a provider signs in through your normal browser, right where the CLI always looks. Every one after that signs in through a private window, so the account your browser is already signed in to doesn't get picked up by mistake; sign in there as the account you're adding, and close any private window you already had open first. Never log out of the CLI to do this: logging out revokes the saved sign-in, and that account would have to be added again. -One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude`, `codex`, or `grok`, and anything launched through `omarchy agent`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account. +One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude`, `codex`, or `grok`, anything launched through `omarchy agent`, and scripts or editors that find the CLI on `PATH`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account. With more than one account, the panel shows each account's limits side by side, with the active one marked. By default Omarchy notifies you when the active account passes 95% of a limit, and clicking the notification switches to the account with the most headroom. Hover an account's line in the panel and click the Autoswitch that appears beside Use (or press `m`), or run `omarchy agent account mode auto`, to have it switch on its own instead; while it's on, Autoswitch takes Use's place, and clicking it again goes back to notifying. Add a number to change the threshold, as in `omarchy agent account mode auto 90`. If every account is over the threshold, you'll hear about it once, along with which account frees up first. While an active account is within 15 points of its threshold (80% at the default), its limits are checked every three minutes rather than every 15. -`omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours shows its last known limits until a session runs in it again. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and editors that start `claude` themselves don't follow the switch. +`omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours shows its last known limits until a session runs in it again. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and tools configured with an absolute path to a CLI binary do not follow the switch. An explicit `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` takes precedence over the selected account. ### Crash diagnosis diff --git a/migrations/1791112479.sh b/migrations/1791112479.sh new file mode 100644 index 00000000..28bcc5bb --- /dev/null +++ b/migrations/1791112479.sh @@ -0,0 +1,5 @@ +echo "Make agent account selection apply to scripts and subprocesses" + +# The settings package ships the mise wrapper declarations. Build the dispatch +# links for this user without installing or changing any agent version. +mise reshim diff --git a/test/shell.d/agent-account-exec-test.sh b/test/shell.d/agent-account-exec-test.sh new file mode 100755 index 00000000..5886050f --- /dev/null +++ b/test/shell.d/agent-account-exec-test.sh @@ -0,0 +1,206 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command mise +require_command python3 + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +account_test_home="$test_tmp/home" +real_bin="$test_tmp/real/bin" +mise_data="$test_tmp/data/mise" +registry_dir="$account_test_home/.local/state/omarchy/agents/accounts" +mkdir -p "$account_test_home" "$real_bin" "$registry_dir" + +# Use real mise dispatch and real account resolution, with fake agent binaries. +# No shell functions, login startup files, credentials, or network are involved. +for provider in claude codex grok; do + cat >"$real_bin/$provider" <<'SH' +#!/bin/bash +case "${0##*/}" in + claude) printf '%s\n' "${CLAUDE_CONFIG_DIR:-default}" ;; + codex) printf '%s\n' "${CODEX_HOME:-default}" ;; + grok) printf '%s\n' "${GROK_HOME:-default}" ;; +esac +if (($#)); then + printf '%s\n' "$@" +fi +if [[ ${OMARCHY_TEST_SWITCH_ACCOUNT:-} == "yes" ]]; then + unset OMARCHY_TEST_SWITCH_ACCOUNT + omarchy-agent-account-state use "${0##*/}" "${OMARCHY_TEST_SWITCH_TO:-side}" >/dev/null + if [[ ${OMARCHY_TEST_NEW_SESSION:-} == "yes" ]]; then + omarchy-agent --inline + else + "${0##*/}" + fi +fi +if [[ -n ${OMARCHY_TEST_AGENT_CHILD:-} ]]; then + child=$OMARCHY_TEST_AGENT_CHILD + unset OMARCHY_TEST_AGENT_CHILD + "$child" +fi +exit "${OMARCHY_TEST_AGENT_EXIT:-0}" +SH + chmod +x "$real_bin/$provider" +done + +run_isolated() { + env -i HOME="$account_test_home" \ + XDG_CONFIG_HOME="$test_tmp/config" XDG_DATA_HOME="$test_tmp/data" XDG_CACHE_HOME="$test_tmp/cache" \ + MISE_DATA_DIR="$mise_data" MISE_SYSTEM_CONFIG_DIR="$ROOT/etc/mise" MISE_OFFLINE=1 \ + OMARCHY_PATH="$ROOT" PATH="$mise_data/command-wrappers/bin:$ROOT/bin:$real_bin:/usr/bin" \ + "$@" +} + +select_account() { + local provider=$1 + local selected=$2 + printf '{"active":"%s","accounts":[{"id":"main","primary":true},{"id":"side","home":"%s"}]}\n' \ + "$selected" "$test_tmp/accounts/$provider side" >"$registry_dir/$provider.json" +} + +for helper in omarchy-agent-usage-update omarchy-notification-send; do + printf '#!/bin/bash\nexit 0\n' >"$real_bin/$helper" + chmod +x "$real_bin/$helper" +done + +cd "$test_tmp" +run_isolated mise reshim + +for provider in claude codex grok; do + select_account "$provider" side + account_dir="$test_tmp/accounts/$provider side" + output=$(run_isolated "$provider" 'a spaced prompt' '' '--flag=$literal') + expected=$(printf '%s\n' "$account_dir" 'a spaced prompt' '' '--flag=$literal') + [[ $output == "$expected" ]] || fail "$provider dispatch selects the active account and preserves arguments" "$output" + pass "$provider dispatch selects the active account and preserves arguments" + + case "$provider" in + claude) account_env=CLAUDE_CONFIG_DIR ;; + codex) account_env=CODEX_HOME ;; + grok) account_env=GROK_HOME ;; + esac + output=$(run_isolated env "$account_env=/explicit/account" "$provider") + [[ $output == /explicit/account ]] || fail "$provider preserves an explicit account home" "$output" + pass "$provider preserves an explicit account home" + + select_account "$provider" main + output=$(run_isolated "$provider") + [[ $output == default ]] || fail "$provider picks up switching back to Main without rebuilding shims" "$output" + pass "$provider picks up switching back to Main without rebuilding shims" + + output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes "$provider") + [[ $output == $'default\ndefault' ]] || fail "$provider Main sessions keep their account after a switch" "$output" + pass "$provider Main sessions keep their account after a switch" + + mkdir -p "$account_test_home/.config/omarchy/defaults" + printf '%s\n' "$provider" >"$account_test_home/.config/omarchy/defaults/agent" + select_account "$provider" main + output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_NEW_SESSION=yes "$provider") + [[ $(sed -n '2p' <<<"$output") == "$account_dir" ]] || fail "$provider new sessions follow a switch from Main" "$output" + pass "$provider new sessions follow a switch from Main" + + output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_SWITCH_TO=main OMARCHY_TEST_NEW_SESSION=yes "$provider") + [[ $(sed -n '2p' <<<"$output") == default ]] || fail "$provider new sessions follow a switch to Main" "$output" + pass "$provider new sessions follow a switch to Main" + + select_account "$provider" side + output=$(run_isolated env OMARCHY_TEST_SWITCH_ACCOUNT=yes OMARCHY_TEST_SWITCH_TO=main "$provider") + [[ $output == "$(printf '%s\n' "$account_dir" "$account_dir")" ]] || fail "$provider side sessions keep their account after a switch" "$output" + pass "$provider side sessions keep their account after a switch" + + output=$(run_isolated env "OMARCHY_AGENT_${provider^^}_HOME=" "$account_env=/explicit/account" omarchy-agent --inline) + [[ ${output%%$'\n'*} == /explicit/account ]] || fail "$provider new sessions preserve explicit account overrides" "$output" + pass "$provider new sessions preserve explicit account overrides" + + select_account "$provider" side + output=$(run_isolated omarchy-agent-account-add --reauth :primary "$provider" /dev/null +mkdir -p "$test_tmp/config/mise" +printf '[tools]\nclaude = "1.0.0"\n' >"$test_tmp/config/mise/config.toml" +run_isolated mise trust "$test_tmp/config/mise/config.toml" >/dev/null 2>&1 + +# Existing tool shims used by SSH already dispatch wrappers, including with a +# relocated mise data directory. No additional stock-only PAM path is needed. +run_isolated mise reshim +output=$(run_isolated env PATH="$mise_data/shims:$ROOT/bin:/usr/bin" claude) +[[ $output == "$test_tmp/accounts/claude side" ]] || fail "SSH-style mise tool shims route through account dispatch in a custom data directory" "$output" +pass "SSH-style mise tool shims route through account dispatch in a custom data directory" + +# Full PATH activation must also prefer the dispatcher over installed tools. +output=$(run_isolated bash -c 'eval "$(mise env -s bash)"; [[ $PATH == *"/installs/claude/"* ]] || exit 99; claude') +[[ $output == "$test_tmp/accounts/claude side" ]] || fail "mise PATH activation retains account dispatch" "$output" +pass "mise PATH activation retains account dispatch" + +status=0 +run_isolated env OMARCHY_TEST_AGENT_EXIT=42 claude >/dev/null || status=$? +(( status == 42 )) || fail "account dispatch preserves the agent exit status" "$status" +pass "account dispatch preserves the agent exit status" + +# A dispatcher alone must not prevent the CLI from being installed on first use. +mv "$real_bin/grok" "$test_tmp/grok-fixture" +cat >"$real_bin/mise" <<'SH' +#!/bin/bash +case "$1" in + use) + [[ ${OMARCHY_TEST_INSTALL_FAIL:-} != "yes" ]] || exit 42 + [[ $* == "use -g --quiet grok" ]] || exit 99 + cp "$HOME/../grok-fixture" "$HOME/../real/bin/grok" + ;; + which) + [[ ${MISE_MINIMUM_RELEASE_AGE:-} == "0" ]] || exit 99 + printf '%s\n' "$HOME/../real/bin/grok" + ;; + *) exit 99 ;; +esac +SH +chmod +x "$real_bin/mise" +select_account grok side +output=$(run_isolated grok) +[[ $output == "$test_tmp/accounts/grok side" ]] || fail "a dispatcher without a CLI installs it and uses the selected account" "$output" +pass "a dispatcher without a CLI installs it and uses the selected account" + +rm "$real_bin/grok" +status=0 +run_isolated env OMARCHY_TEST_INSTALL_FAIL=yes grok >/dev/null || status=$? +(( status == 42 )) || fail "a failed first-run install stops dispatch" "$status" +pass "a failed first-run install stops dispatch" +rm "$real_bin/mise" + +# Migration only rebuilds native mise dispatch; account files stay untouched. +registry_before=$(sha256sum "$registry_dir"/*.json) +for attempt in 1 2; do + run_isolated bash -euo pipefail "$ROOT/migrations/1791112479.sh" >/dev/null + for provider in claude codex grok; do + [[ -L $mise_data/command-wrappers/bin/$provider ]] || fail "migration builds every account dispatcher" + done +done +[[ $(sha256sum "$registry_dir"/*.json) == "$registry_before" ]] || fail "migration preserves account state" +pass "migration rebuilds dispatchers idempotently without changing account state" diff --git a/test/shell.d/agent-account-test.sh b/test/shell.d/agent-account-test.sh index 19cefe59..c0201e3d 100644 --- a/test/shell.d/agent-account-test.sh +++ b/test/shell.d/agent-account-test.sh @@ -201,8 +201,7 @@ OMARCHY_TEST_LOGIN_UUID=u-grok-2 OMARCHY_TEST_LOGIN_EMAIL=side@example.com \ grep -qx -- "--private https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" || fail "a second Grok login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")" omarchy-agent-account-use grok side >/dev/null -source "$ROOT/default/bash/fns/agent-accounts" -[[ $(grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(grok --version)" +[[ $(omarchy-agent-account-exec grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(omarchy-agent-account-exec grok --version)" omarchy-agent-account-use grok main >/dev/null pass "Grok accounts are added and used like the others" @@ -213,11 +212,10 @@ omarchy-agent-account-use claude work >/dev/null grep -q "New Claude sessions now use Work (Max 5x)" "$notifications" || fail "switching says where new sessions go" pass "use makes an account active and says so" -source "$ROOT/default/bash/fns/agent-accounts" -[[ $(claude --version) == "claude home=$work args=--version" ]] || fail "claude at a prompt starts as the active account" -[[ $(CLAUDE_CONFIG_DIR=/elsewhere claude) == "claude home=/elsewhere args=" ]] || fail "an explicit CLAUDE_CONFIG_DIR wins over the active account" -[[ $(codex) == "codex home=default args=" ]] || fail "codex stays on its primary until switched" -pass "shell launches follow the active account" +[[ $(omarchy-agent-account-exec claude --version) == "claude home=$work args=--version" ]] || fail "claude at a prompt starts as the active account" +[[ $(CLAUDE_CONFIG_DIR=/elsewhere omarchy-agent-account-exec claude) == "claude home=/elsewhere args=" ]] || fail "an explicit CLAUDE_CONFIG_DIR wins over the active account" +[[ $(omarchy-agent-account-exec codex) == "codex home=default args=" ]] || fail "codex stays on its primary until switched" +pass "account dispatch follows the active account" [[ $(OMARCHY_TEST_DEFAULT_AGENT=claude omarchy-agent --inline) == "claude home=$work args=--permission-mode auto" ]] || fail "omarchy-agent starts Claude as the active account" diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index 09d4d834..dc523e9f 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -781,11 +781,11 @@ assert_launch pi pi "Review this project" assert_launch omp omp --auto-approve -- "Review this project" assert_launch opencode opencode --auto --prompt "Review this project" assert_launch ori ori code --interactive --prompt "Review this project" -assert_launch claude claude --permission-mode auto -- "Review this project" -assert_launch codex codex --approve-for-me -- "Review this project" +assert_launch claude env -u CLAUDE_CONFIG_DIR OMARCHY_AGENT_CLAUDE_HOME= claude --permission-mode auto -- "Review this project" +assert_launch codex env -u CODEX_HOME OMARCHY_AGENT_CODEX_HOME= codex --approve-for-me -- "Review this project" assert_launch muse muse --approval-mode never -- "Review this project" assert_launch crush crush run "Review this project" -assert_launch grok grok --permission-mode bypassPermissions -- "Review this project" +assert_launch grok env -u GROK_HOME OMARCHY_AGENT_GROK_HOME= grok --permission-mode bypassPermissions -- "Review this project" assert_launch cursor-agent cursor-agent --yolo --trust agent -- "Review this project" assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project" assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project" @@ -809,11 +809,11 @@ assert_bypass pi pi assert_bypass omp omp --auto-approve assert_bypass opencode opencode --auto assert_bypass ori ori code -assert_bypass claude claude --permission-mode auto -assert_bypass codex codex --approve-for-me +assert_bypass claude env -u CLAUDE_CONFIG_DIR OMARCHY_AGENT_CLAUDE_HOME= claude --permission-mode auto +assert_bypass codex env -u CODEX_HOME OMARCHY_AGENT_CODEX_HOME= codex --approve-for-me assert_bypass muse muse --approval-mode never assert_bypass crush crush --yolo -assert_bypass grok grok --permission-mode bypassPermissions +assert_bypass grok env -u GROK_HOME OMARCHY_AGENT_GROK_HOME= grok --permission-mode bypassPermissions assert_bypass cursor-agent cursor-agent --yolo --trust assert_bypass hermes hermes --yolo assert_bypass agy agy --dangerously-skip-permissions diff --git a/test/shell.d/dev-env-path-test.sh b/test/shell.d/dev-env-path-test.sh index d44e1d62..3eeb8fb6 100755 --- a/test/shell.d/dev-env-path-test.sh +++ b/test/shell.d/dev-env-path-test.sh @@ -4,6 +4,8 @@ set -euo pipefail source "$(dirname "$0")/base-test.sh" +unset MISE_DATA_DIR XDG_DATA_HOME + run_bootstrap() { local shell_bin="$1" local bootstrap="$2" @@ -56,7 +58,7 @@ pass "env-bootstrap resolves default OMARCHY_PATH" assert_path_present "$default_path" "$tmpdir/unrelated/bin" "env-bootstrap preserves PATH entries in default mode" assert_path_present "$default_path" "$home/.local/share/mise/shims" "env-bootstrap appends mise shims" assert_path_present "$default_path" "$home/.local/bin" "env-bootstrap appends ~/.local/bin" -assert_path_first "$default_path" "$tmpdir/unrelated/bin" "env-bootstrap appends user-level paths after existing entries" +assert_path_first "$default_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap puts command wrappers ahead of inherited tool binaries" printf 'export OMARCHY_PATH="%s"\n' "$tmpdir/active" >"$tmpdir/omarchy.conf" mapfile -t linked_result < <(run_bootstrap bash "$bootstrap" "$home" "$tmpdir/unrelated/bin:/usr/bin") @@ -64,23 +66,39 @@ linked_path=${linked_result[1]} [[ ${linked_result[0]} == "$tmpdir/active" ]] || fail "env-bootstrap resolves linked OMARCHY_PATH" "actual: ${linked_result[0]}" pass "env-bootstrap resolves linked OMARCHY_PATH" -assert_path_first "$linked_path" "$tmpdir/active/bin" "env-bootstrap prepends active checkout bin in linked mode" +assert_path_first "$linked_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap keeps command wrappers first in linked mode" +assert_path_present "$linked_path" "$tmpdir/active/bin" "env-bootstrap includes the active checkout bin in linked mode" assert_path_present "$linked_path" "$tmpdir/unrelated/bin" "env-bootstrap preserves unrelated PATH entries in linked mode" mapfile -t linked_duplicate_result < <(run_bootstrap bash "$bootstrap" "$home" "$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin") linked_duplicate_path=${linked_duplicate_result[1]} -[[ $linked_duplicate_path == "$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap does not duplicate PATH entries" "actual PATH: $linked_duplicate_path" +[[ $linked_duplicate_path == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap does not duplicate PATH entries" "actual PATH: $linked_duplicate_path" pass "env-bootstrap does not duplicate PATH entries" # An empty PATH must not produce empty entries (a bare ":" means the cwd) mapfile -t empty_path_result < <(run_bootstrap bash "$bootstrap" "$home" "") empty_path=${empty_path_result[1]} -[[ $empty_path == "$tmpdir/active/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap builds a clean PATH from an empty one" "actual PATH: $empty_path" +[[ $empty_path == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || fail "env-bootstrap builds a clean PATH from an empty one" "actual PATH: $empty_path" pass "env-bootstrap builds a clean PATH from an empty one" if command -v zsh >/dev/null 2>&1; then mapfile -t zsh_result < <(run_bootstrap zsh "$bootstrap" "$home" "$tmpdir/unrelated/bin:/usr/bin") zsh_path=${zsh_result[1]} - assert_path_first "$zsh_path" "$tmpdir/active/bin" "env-bootstrap works when sourced by zsh" + assert_path_first "$zsh_path" "$home/.local/share/mise/command-wrappers/bin" "env-bootstrap works when sourced by zsh" assert_path_present "$zsh_path" "$tmpdir/unrelated/bin" "env-bootstrap zsh mode preserves unrelated PATH entries" fi + +mapfile -t repeated_result < <(run_bootstrap bash "$bootstrap" "$home" "$linked_duplicate_path") +[[ ${repeated_result[1]} == "$linked_duplicate_path" ]] || fail "repeated bootstrap leaves the command wrapper path unchanged" +pass "repeated bootstrap leaves the command wrapper path unchanged" + +mapfile -t custom_data_result < <(MISE_DATA_DIR="$tmpdir/custom-mise" run_bootstrap bash "$bootstrap" "$home" "/usr/bin") +assert_path_first "${custom_data_result[1]}" "$tmpdir/custom-mise/command-wrappers/bin" "env-bootstrap honors a custom mise data directory" + +# A parent agent or mise activation may have put real tool bins before a wrapper +# directory that was already present. Move the existing entry rather than skip it. +inherited_path="$tmpdir/managed/bin:$home/.local/share/mise/command-wrappers/bin:/usr/bin" +mapfile -t reordered_result < <(run_bootstrap bash "$bootstrap" "$home" "$inherited_path") +[[ ${reordered_result[1]} == "$home/.local/share/mise/command-wrappers/bin:$tmpdir/active/bin:$tmpdir/managed/bin:/usr/bin:$home/.local/share/mise/shims:$home/.local/bin" ]] || + fail "bootstrap moves existing wrappers ahead of inherited managed tool paths without duplication" "${reordered_result[1]}" +pass "bootstrap moves existing wrappers ahead of inherited managed tool paths without duplication"