From 2680ba65a7f20061eb1ae6c761913d855fcfc8ae Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:11:32 +0900 Subject: [PATCH 1/4] fix(zsh): keep Nix profiles ahead of /usr/bin and Homebrew in PATH .zshrc prepended /usr/bin, /opt/homebrew/bin, /Library/TeX/texbin and ~/.local/bin, so they outranked the Nix profiles and git resolved to the Xcode shim. Append them after the Nix entries and de-duplicate PATH. Closes #308 Co-Authored-By: Claude Sonnet 5.5 --- tests/zsh/startup.bats | 48 ++++++++++++++++++++++++++++++++++++++++++ zsh/.zprofile | 4 ++-- zsh/.zshenv | 4 +++- zsh/.zshrc | 5 +---- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/zsh/startup.bats b/tests/zsh/startup.bats index f92c8862..620663a2 100644 --- a/tests/zsh/startup.bats +++ b/tests/zsh/startup.bats @@ -91,6 +91,54 @@ EOF ! [[ "$stderr" == *"brew shellenv"* ]] } +@test "startup preserves Nix command precedence without duplicate PATH entries" { + local _user_profile="$TEST_HOME/.nix-profile/bin" + local _per_user_profile="$BATS_TEST_TMPDIR/profiles/per-user/bin" + local _system_profile="$BATS_TEST_TMPDIR/current-system/sw/bin" + mkdir -p "$_user_profile" "$_per_user_profile" "$_system_profile" "$TEST_HOME/.local/bin" + touch "$_user_profile/git" "$TEST_HOME/.local/bin/git" + chmod +x "$_user_profile/git" "$TEST_HOME/.local/bin/git" + + local _mode + for _mode in -dfc -dfic -dflic; do + # Source repository files explicitly to isolate host startup configuration. + run --separate-stderr env -i \ + HOME="$TEST_HOME" \ + XDG_CACHE_HOME="$TEST_HOME/.cache" \ + FPATH="$BATS_TEST_TMPDIR/functions" \ + COMPINIT_LOG="$COMPINIT_LOG" \ + ABBR_LOG="$ABBR_LOG" \ + REPO_ROOT="$REPO_ROOT" \ + PATH="$_user_profile:$_per_user_profile:$_system_profile:$TEST_BIN:/usr/bin:/bin:/opt/homebrew/bin:/usr/bin:$_user_profile" \ + "$(command -v zsh)" "$_mode" ' + source "$REPO_ROOT/zsh/.zshenv" + if [[ -o login ]]; then + source "$REPO_ROOT/zsh/.zprofile" + fi + if [[ -o interactive ]]; then + source "$REPO_ROOT/zsh/.zshrc" + fi + print -r -- "$(command -v git)" + print -rl -- "${path[@]}" + ' + + [ "$status" -eq 0 ] + [ "${lines[0]}" = "$_user_profile/git" ] + [ "${lines[1]}" = "$_user_profile" ] + [ "${lines[2]}" = "$_per_user_profile" ] + [ "${lines[3]}" = "$_system_profile" ] + [ "$(printf '%s\n' "${lines[@]:1}" | sort | uniq -d)" = "" ] || { + printf 'duplicate PATH entries in %s: %s\n' "$_mode" "$output" >&2 + return 1 + } + [[ "$output" == *"/usr/bin"* ]] + [[ "$output" == *"/opt/homebrew/bin"* ]] + if [[ "$_mode" == *i* ]]; then + [[ "$output" == *"/Library/TeX/texbin"* ]] + fi + done +} + @test "Home Manager activation invokes the packaged Zsh cache builder" { run --separate-stderr nix build --no-link --print-out-paths \ "$TEST_FLAKE#homeConfigurations.\"kaito@mbp\".activationPackage" diff --git a/zsh/.zprofile b/zsh/.zprofile index f306f628..0e3912f8 100644 --- a/zsh/.zprofile +++ b/zsh/.zprofile @@ -1,7 +1,7 @@ -export PATH="$HOME/.local/bin:$PATH" +path+=("$HOME/.local/bin") # Added by OrbStack: command-line tools and integration source ~/.orbstack/shell/init.zsh 2>/dev/null || : # Added by Obsidian -export PATH="$PATH:/Applications/Obsidian.app/Contents/MacOS" +path+=(/Applications/Obsidian.app/Contents/MacOS) diff --git a/zsh/.zshenv b/zsh/.zshenv index 18efee13..0d46a665 100644 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -1,4 +1,6 @@ -export PATH=$HOME/.gem/bin:$HOME/.cargo/bin:$PATH +# Keep the Nix profile order supplied by nix-darwin and remove inherited duplicates. +typeset -U path PATH +path+=("$HOME/.gem/bin" "$HOME/.cargo/bin") # Enforced on every zsh start so stale inherited environments # (e.g. a long-running herdr server snapshot) cannot override it. export EDITOR=nvim diff --git a/zsh/.zshrc b/zsh/.zshrc index 370e3bd8..da5dd445 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -75,11 +75,8 @@ function command_not_found_handler() { DOTFILES="$HOME/ghq/github.com/furedea/dotfiles" -export PATH="/usr/bin:$PATH" -export PATH="/opt/homebrew/bin:$PATH" export XDG_CONFIG_HOME="$HOME/.config" -export PATH="/Library/TeX/texbin:$PATH" -export PATH="$HOME/.local/bin:$PATH" +path+=(/opt/homebrew/bin /Library/TeX/texbin "$HOME/.local/bin") # Plugins source "$XDG_CONFIG_HOME/zsh/nix-plugins.zsh" From 77a5c38c6e476f6d11491d8da26e862e83ea6dd3 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:15:55 +0900 Subject: [PATCH 2/4] fix(zsh): repair stale inherited PATH order in zshenv Move Nix profiles and user bin directories ahead of /usr/bin even when the inherited PATH is already wrong, and keep Homebrew and TeX after it. Co-Authored-By: Claude Sonnet 5.5 --- tests/zsh/startup.bats | 27 +++++++++++++++++---------- zsh/.zprofile | 2 -- zsh/.zshenv | 18 ++++++++++++++++-- zsh/.zshrc | 1 - 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/tests/zsh/startup.bats b/tests/zsh/startup.bats index 620663a2..8cc04744 100644 --- a/tests/zsh/startup.bats +++ b/tests/zsh/startup.bats @@ -91,11 +91,12 @@ EOF ! [[ "$stderr" == *"brew shellenv"* ]] } -@test "startup preserves Nix command precedence without duplicate PATH entries" { +@test "startup repairs inherited PATH order without duplicate entries" { local _user_profile="$TEST_HOME/.nix-profile/bin" - local _per_user_profile="$BATS_TEST_TMPDIR/profiles/per-user/bin" - local _system_profile="$BATS_TEST_TMPDIR/current-system/sw/bin" - mkdir -p "$_user_profile" "$_per_user_profile" "$_system_profile" "$TEST_HOME/.local/bin" + local _per_user_profile="/etc/profiles/per-user/startup-test/bin" + local _system_profile="/run/current-system/sw/bin" + local _default_profile="/nix/var/nix/profiles/default/bin" + mkdir -p "$_user_profile" "$TEST_HOME/.local/bin" touch "$_user_profile/git" "$TEST_HOME/.local/bin/git" chmod +x "$_user_profile/git" "$TEST_HOME/.local/bin/git" @@ -104,13 +105,15 @@ EOF # Source repository files explicitly to isolate host startup configuration. run --separate-stderr env -i \ HOME="$TEST_HOME" \ + USER=startup-test \ XDG_CACHE_HOME="$TEST_HOME/.cache" \ FPATH="$BATS_TEST_TMPDIR/functions" \ COMPINIT_LOG="$COMPINIT_LOG" \ ABBR_LOG="$ABBR_LOG" \ REPO_ROOT="$REPO_ROOT" \ - PATH="$_user_profile:$_per_user_profile:$_system_profile:$TEST_BIN:/usr/bin:/bin:/opt/homebrew/bin:/usr/bin:$_user_profile" \ + PATH="$TEST_HOME/.local/bin:/Library/TeX/texbin:/opt/homebrew/bin:/usr/bin:$_user_profile:$_per_user_profile:$_system_profile:$_default_profile:$TEST_BIN:/bin:/usr/bin:$_user_profile:$TEST_HOME/.cargo/bin:$TEST_HOME/.gem/bin:/opt/homebrew/bin:/Library/TeX/texbin" \ "$(command -v zsh)" "$_mode" ' + source "$REPO_ROOT/zsh/.zshenv" source "$REPO_ROOT/zsh/.zshenv" if [[ -o login ]]; then source "$REPO_ROOT/zsh/.zprofile" @@ -127,15 +130,19 @@ EOF [ "${lines[1]}" = "$_user_profile" ] [ "${lines[2]}" = "$_per_user_profile" ] [ "${lines[3]}" = "$_system_profile" ] + [ "${lines[4]}" = "$_default_profile" ] + [ "${lines[5]}" = "$TEST_HOME/.local/bin" ] + [ "${lines[6]}" = "$TEST_HOME/.cargo/bin" ] + [ "${lines[7]}" = "$TEST_HOME/.gem/bin" ] + [ "${lines[8]}" = "/usr/bin" ] + [ "${lines[9]}" = "$TEST_BIN" ] + [ "${lines[10]}" = "/bin" ] + [ "${lines[11]}" = "/opt/homebrew/bin" ] + [ "${lines[12]}" = "/Library/TeX/texbin" ] [ "$(printf '%s\n' "${lines[@]:1}" | sort | uniq -d)" = "" ] || { printf 'duplicate PATH entries in %s: %s\n' "$_mode" "$output" >&2 return 1 } - [[ "$output" == *"/usr/bin"* ]] - [[ "$output" == *"/opt/homebrew/bin"* ]] - if [[ "$_mode" == *i* ]]; then - [[ "$output" == *"/Library/TeX/texbin"* ]] - fi done } diff --git a/zsh/.zprofile b/zsh/.zprofile index 0e3912f8..8ac53fb9 100644 --- a/zsh/.zprofile +++ b/zsh/.zprofile @@ -1,5 +1,3 @@ -path+=("$HOME/.local/bin") - # Added by OrbStack: command-line tools and integration source ~/.orbstack/shell/init.zsh 2>/dev/null || : diff --git a/zsh/.zshenv b/zsh/.zshenv index 0d46a665..98cab09d 100644 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -1,6 +1,20 @@ -# Keep the Nix profile order supplied by nix-darwin and remove inherited duplicates. +# Repair stale inherited PATH order and keep each entry only once. typeset -U path PATH -path+=("$HOME/.gem/bin" "$HOME/.cargo/bin") +# Homebrew and TeX remain fallbacks even when inherited ahead of /usr/bin. +path=("${(@)path:#/opt/homebrew/bin}") +path=("${(@)path:#/Library/TeX/texbin}") +path=( + "$HOME/.nix-profile/bin" + "/etc/profiles/per-user/$USER/bin" + /run/current-system/sw/bin + /nix/var/nix/profiles/default/bin + "$HOME/.local/bin" + "$HOME/.cargo/bin" + "$HOME/.gem/bin" + "${path[@]}" + /opt/homebrew/bin + /Library/TeX/texbin +) # Enforced on every zsh start so stale inherited environments # (e.g. a long-running herdr server snapshot) cannot override it. export EDITOR=nvim diff --git a/zsh/.zshrc b/zsh/.zshrc index da5dd445..5c09a670 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -76,7 +76,6 @@ function command_not_found_handler() { DOTFILES="$HOME/ghq/github.com/furedea/dotfiles" export XDG_CONFIG_HOME="$HOME/.config" -path+=(/opt/homebrew/bin /Library/TeX/texbin "$HOME/.local/bin") # Plugins source "$XDG_CONFIG_HOME/zsh/nix-plugins.zsh" From b2f84f7c69f44f5fe2c286a10bb372a2c5042511 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:06:42 +0900 Subject: [PATCH 3/4] fix(zsh): stop forcing PATH order in zshenv .zshenv runs inside devShell and direnv shells, so rebuilding PATH there would push their tools behind the global profiles. Only append missing user directories and keep inherited order. Co-Authored-By: Claude Sonnet 5.5 --- tests/zsh/startup.bats | 125 ++++++++++++++++++++++++++--------------- zsh/.zshenv | 20 ++----- zsh/.zshrc | 4 ++ 3 files changed, 89 insertions(+), 60 deletions(-) diff --git a/tests/zsh/startup.bats b/tests/zsh/startup.bats index 8cc04744..bd1afbf3 100644 --- a/tests/zsh/startup.bats +++ b/tests/zsh/startup.bats @@ -91,61 +91,98 @@ EOF ! [[ "$stderr" == *"brew shellenv"* ]] } -@test "startup repairs inherited PATH order without duplicate entries" { +@test "clean login startup keeps Nix profiles ahead of system and Homebrew tools without duplicates" { local _user_profile="$TEST_HOME/.nix-profile/bin" local _per_user_profile="/etc/profiles/per-user/startup-test/bin" local _system_profile="/run/current-system/sw/bin" local _default_profile="/nix/var/nix/profiles/default/bin" - mkdir -p "$_user_profile" "$TEST_HOME/.local/bin" - touch "$_user_profile/git" "$TEST_HOME/.local/bin/git" - chmod +x "$_user_profile/git" "$TEST_HOME/.local/bin/git" - local _mode - for _mode in -dfc -dfic -dflic; do - # Source repository files explicitly to isolate host startup configuration. - run --separate-stderr env -i \ - HOME="$TEST_HOME" \ - USER=startup-test \ - XDG_CACHE_HOME="$TEST_HOME/.cache" \ - FPATH="$BATS_TEST_TMPDIR/functions" \ - COMPINIT_LOG="$COMPINIT_LOG" \ - ABBR_LOG="$ABBR_LOG" \ - REPO_ROOT="$REPO_ROOT" \ - PATH="$TEST_HOME/.local/bin:/Library/TeX/texbin:/opt/homebrew/bin:/usr/bin:$_user_profile:$_per_user_profile:$_system_profile:$_default_profile:$TEST_BIN:/bin:/usr/bin:$_user_profile:$TEST_HOME/.cargo/bin:$TEST_HOME/.gem/bin:/opt/homebrew/bin:/Library/TeX/texbin" \ - "$(command -v zsh)" "$_mode" ' - source "$REPO_ROOT/zsh/.zshenv" - source "$REPO_ROOT/zsh/.zshenv" - if [[ -o login ]]; then - source "$REPO_ROOT/zsh/.zprofile" - fi - if [[ -o interactive ]]; then - source "$REPO_ROOT/zsh/.zshrc" - fi - print -r -- "$(command -v git)" - print -rl -- "${path[@]}" - ' + run_startup_path "$_user_profile:$_per_user_profile:$_system_profile:$_default_profile:/usr/bin:$TEST_BIN:/bin" -lic - [ "$status" -eq 0 ] - [ "${lines[0]}" = "$_user_profile/git" ] - [ "${lines[1]}" = "$_user_profile" ] - [ "${lines[2]}" = "$_per_user_profile" ] - [ "${lines[3]}" = "$_system_profile" ] - [ "${lines[4]}" = "$_default_profile" ] - [ "${lines[5]}" = "$TEST_HOME/.local/bin" ] - [ "${lines[6]}" = "$TEST_HOME/.cargo/bin" ] - [ "${lines[7]}" = "$TEST_HOME/.gem/bin" ] - [ "${lines[8]}" = "/usr/bin" ] - [ "${lines[9]}" = "$TEST_BIN" ] - [ "${lines[10]}" = "/bin" ] - [ "${lines[11]}" = "/opt/homebrew/bin" ] - [ "${lines[12]}" = "/Library/TeX/texbin" ] - [ "$(printf '%s\n' "${lines[@]:1}" | sort | uniq -d)" = "" ] || { - printf 'duplicate PATH entries in %s: %s\n' "$_mode" "$output" >&2 + [ "$status" -eq 0 ] + local _expected=( + "$_user_profile" + "$_per_user_profile" + "$_system_profile" + "$_default_profile" + /usr/bin + "$TEST_BIN" + /bin + "$TEST_HOME/.local/bin" + "$TEST_HOME/.cargo/bin" + "$TEST_HOME/.gem/bin" + /Applications/Obsidian.app/Contents/MacOS + /opt/homebrew/bin + /Library/TeX/texbin + ) + [ "${#lines[@]}" -eq "${#_expected[@]}" ] + local _index + for _index in "${!_expected[@]}"; do + [ "${lines[_index]}" = "${_expected[_index]}" ] || { + printf 'unexpected PATH entry at %s: %s\n' "$_index" "$output" >&2 return 1 } done } +@test "startup preserves inherited devShell PATH order in all shell modes" { + local _inherited="/nix/store/devshell-tools/bin:/workspace/project/bin:$TEST_HOME/.cargo/bin:/opt/homebrew/bin:$TEST_HOME/.nix-profile/bin:/usr/bin:$TEST_HOME/.local/bin:/Library/TeX/texbin:$TEST_BIN:/bin" + local _mode + for _mode in -c -ic -lic; do + run_startup_path "$_inherited:$_inherited" "$_mode" + + [ "$status" -eq 0 ] + local _expected=( + /nix/store/devshell-tools/bin + /workspace/project/bin + "$TEST_HOME/.cargo/bin" + /opt/homebrew/bin + "$TEST_HOME/.nix-profile/bin" + /usr/bin + "$TEST_HOME/.local/bin" + /Library/TeX/texbin + "$TEST_BIN" + /bin + "$TEST_HOME/.gem/bin" + ) + if [ "$_mode" = -lic ]; then + _expected+=(/Applications/Obsidian.app/Contents/MacOS) + fi + [ "${#lines[@]}" -eq "${#_expected[@]}" ] + local _index + for _index in "${!_expected[@]}"; do + [ "${lines[_index]}" = "${_expected[_index]}" ] || { + printf 'unexpected PATH entry in %s at %s: %s\n' "$_mode" "$_index" "$output" >&2 + return 1 + } + done + done +} + +run_startup_path() { + # Source repository files explicitly to isolate host startup configuration. + run --separate-stderr env -i \ + HOME="$TEST_HOME" \ + USER=startup-test \ + XDG_CACHE_HOME="$TEST_HOME/.cache" \ + FPATH="$BATS_TEST_TMPDIR/functions" \ + COMPINIT_LOG="$COMPINIT_LOG" \ + ABBR_LOG="$ABBR_LOG" \ + REPO_ROOT="$REPO_ROOT" \ + PATH="$1" \ + "$(command -v zsh)" -df "$2" ' + source "$REPO_ROOT/zsh/.zshenv" + source "$REPO_ROOT/zsh/.zshenv" + if [[ -o login ]]; then + source "$REPO_ROOT/zsh/.zprofile" + fi + if [[ -o interactive ]]; then + source "$REPO_ROOT/zsh/.zshrc" + fi + print -rl -- "${path[@]}" + ' +} + @test "Home Manager activation invokes the packaged Zsh cache builder" { run --separate-stderr nix build --no-link --print-out-paths \ "$TEST_FLAKE#homeConfigurations.\"kaito@mbp\".activationPackage" diff --git a/zsh/.zshenv b/zsh/.zshenv index 98cab09d..7ca8d812 100644 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -1,20 +1,8 @@ -# Repair stale inherited PATH order and keep each entry only once. +# Preserve inherited tool precedence and keep each entry only once. typeset -U path PATH -# Homebrew and TeX remain fallbacks even when inherited ahead of /usr/bin. -path=("${(@)path:#/opt/homebrew/bin}") -path=("${(@)path:#/Library/TeX/texbin}") -path=( - "$HOME/.nix-profile/bin" - "/etc/profiles/per-user/$USER/bin" - /run/current-system/sw/bin - /nix/var/nix/profiles/default/bin - "$HOME/.local/bin" - "$HOME/.cargo/bin" - "$HOME/.gem/bin" - "${path[@]}" - /opt/homebrew/bin - /Library/TeX/texbin -) +(( ${path[(Ie)$HOME/.local/bin]} )) || path+=("$HOME/.local/bin") +(( ${path[(Ie)$HOME/.cargo/bin]} )) || path+=("$HOME/.cargo/bin") +(( ${path[(Ie)$HOME/.gem/bin]} )) || path+=("$HOME/.gem/bin") # Enforced on every zsh start so stale inherited environments # (e.g. a long-running herdr server snapshot) cannot override it. export EDITOR=nvim diff --git a/zsh/.zshrc b/zsh/.zshrc index 5c09a670..35a42e4d 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -10,6 +10,10 @@ if [[ "$OSTYPE" == darwin* && -z "${SSH_AUTH_SOCK:-}" ]]; then } fi +# Global tools remain fallbacks without changing inherited tool precedence. +(( ${path[(Ie)/opt/homebrew/bin]} )) || path+=(/opt/homebrew/bin) +(( ${path[(Ie)/Library/TeX/texbin]} )) || path+=(/Library/TeX/texbin) + # Completion ZSH_COMPDUMP="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/.zcompdump" autoload -Uz compinit From e3616a820b758ac75f06f4051a25ff32ca040c65 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:11:21 +0900 Subject: [PATCH 4/4] fix(zsh): insert fallback tool directories before system entries Insert missing user and Homebrew directories just before the first system PATH entry so they stay behind Nix and devShell entries but ahead of the macOS tools. Co-Authored-By: Claude Sonnet 5.5 --- tests/zsh/startup.bats | 74 +++++++++++++++++++++++++++++++++++++----- zsh/.zshenv | 20 ++++++++++-- zsh/.zshrc | 3 +- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/tests/zsh/startup.bats b/tests/zsh/startup.bats index bd1afbf3..5359132a 100644 --- a/tests/zsh/startup.bats +++ b/tests/zsh/startup.bats @@ -49,7 +49,7 @@ EOF XDG_CACHE_HOME="$TEST_HOME/.cache" \ FPATH="$BATS_TEST_TMPDIR/functions" \ PATH="$TEST_BIN:$PATH" \ - zsh -dfi -c "source '$REPO_ROOT/zsh/.zshrc'" + zsh -dfi -c "source '$REPO_ROOT/zsh/.zshenv'; source '$REPO_ROOT/zsh/.zshrc'" [ "$status" -eq 0 ] grep -Fx -- "--quiet -S hl=herdr --session main" "$ABBR_LOG" @@ -61,7 +61,7 @@ EOF XDG_CACHE_HOME="$TEST_HOME/.cache" \ FPATH="$BATS_TEST_TMPDIR/functions" \ PATH="$TEST_BIN:$PATH" \ - zsh -dfi -c "source '$REPO_ROOT/zsh/.zshrc'" + zsh -dfi -c "source '$REPO_ROOT/zsh/.zshenv'; source '$REPO_ROOT/zsh/.zshrc'" [ "$status" -eq 0 ] grep -Fx -- \ @@ -75,7 +75,7 @@ EOF XDG_CACHE_HOME="$TEST_HOME/.cache" \ FPATH="$BATS_TEST_TMPDIR/functions" \ PATH="$TEST_BIN:$PATH" \ - zsh -dfi -c "source '$REPO_ROOT/zsh/.zshrc'" + zsh -dfi -c "source '$REPO_ROOT/zsh/.zshenv'; source '$REPO_ROOT/zsh/.zshrc'" [ "$status" -eq 0 ] [ "$(cat "$COMPINIT_LOG")" = "-C -d $TEST_HOME/.cache/zsh/.zcompdump" ] @@ -91,7 +91,7 @@ EOF ! [[ "$stderr" == *"brew shellenv"* ]] } -@test "clean login startup keeps Nix profiles ahead of system and Homebrew tools without duplicates" { +@test "clean login startup orders Nix profiles and fallback tools before system entries" { local _user_profile="$TEST_HOME/.nix-profile/bin" local _per_user_profile="/etc/profiles/per-user/startup-test/bin" local _system_profile="/run/current-system/sw/bin" @@ -105,15 +105,15 @@ EOF "$_per_user_profile" "$_system_profile" "$_default_profile" - /usr/bin - "$TEST_BIN" - /bin "$TEST_HOME/.local/bin" "$TEST_HOME/.cargo/bin" "$TEST_HOME/.gem/bin" - /Applications/Obsidian.app/Contents/MacOS /opt/homebrew/bin /Library/TeX/texbin + /usr/bin + "$TEST_BIN" + /bin + /Applications/Obsidian.app/Contents/MacOS ) [ "${#lines[@]}" -eq "${#_expected[@]}" ] local _index @@ -138,12 +138,12 @@ EOF "$TEST_HOME/.cargo/bin" /opt/homebrew/bin "$TEST_HOME/.nix-profile/bin" + "$TEST_HOME/.gem/bin" /usr/bin "$TEST_HOME/.local/bin" /Library/TeX/texbin "$TEST_BIN" /bin - "$TEST_HOME/.gem/bin" ) if [ "$_mode" = -lic ]; then _expected+=(/Applications/Obsidian.app/Contents/MacOS) @@ -159,6 +159,62 @@ EOF done } +@test "startup inserts missing fallback tools after inherited devShell entries in all shell modes" { + local _inherited="/nix/store/devshell-tools/bin:/workspace/project/bin:$TEST_BIN" + local _mode + for _mode in -c -ic -lic; do + run_startup_path "$_inherited:/usr/bin:/bin" "$_mode" + + [ "$status" -eq 0 ] + local _expected=( + /nix/store/devshell-tools/bin + /workspace/project/bin + "$TEST_BIN" + "$TEST_HOME/.local/bin" + "$TEST_HOME/.cargo/bin" + "$TEST_HOME/.gem/bin" + ) + if [ "$_mode" != -c ]; then + _expected+=(/opt/homebrew/bin /Library/TeX/texbin) + fi + _expected+=(/usr/bin /bin) + if [ "$_mode" = -lic ]; then + _expected+=(/Applications/Obsidian.app/Contents/MacOS) + fi + [ "$output" = "$(printf '%s\n' "${_expected[@]}")" ] || { + printf 'unexpected PATH in %s: %s\n' "$_mode" "$output" >&2 + return 1 + } + done +} + +@test "startup inserts before the first system entry or appends when none exists" { + local _system + for _system in /usr/local/bin /usr/bin /bin /usr/sbin /sbin ''; do + local _inherited="$TEST_BIN" + local _expected=( + "$TEST_BIN" + "$TEST_HOME/.local/bin" + "$TEST_HOME/.cargo/bin" + "$TEST_HOME/.gem/bin" + ) + if [ -n "$_system" ]; then + _inherited+=":$_system:/custom/bin:/usr/bin" + _expected+=("$_system" /custom/bin) + if [ "$_system" != /usr/bin ]; then + _expected+=(/usr/bin) + fi + fi + run_startup_path "$_inherited" -c + + [ "$status" -eq 0 ] + [ "$output" = "$(printf '%s\n' "${_expected[@]}")" ] || { + printf 'unexpected PATH for system entry %s: %s\n' "$_system" "$output" >&2 + return 1 + } + done +} + run_startup_path() { # Source repository files explicitly to isolate host startup configuration. run --separate-stderr env -i \ diff --git a/zsh/.zshenv b/zsh/.zshenv index 7ca8d812..3f720c4c 100644 --- a/zsh/.zshenv +++ b/zsh/.zshenv @@ -1,8 +1,22 @@ # Preserve inherited tool precedence and keep each entry only once. typeset -U path PATH -(( ${path[(Ie)$HOME/.local/bin]} )) || path+=("$HOME/.local/bin") -(( ${path[(Ie)$HOME/.cargo/bin]} )) || path+=("$HOME/.cargo/bin") -(( ${path[(Ie)$HOME/.gem/bin]} )) || path+=("$HOME/.gem/bin") +function _insert_fallback_paths() { + local -a _missing + local _entry _index=1 + for _entry in "$@"; do + (( ${path[(Ie)$_entry]} )) || _missing+=("$_entry") + done + (( ${#_missing} )) || return 0 + for _entry in "${path[@]}"; do + case "$_entry" in + /usr/local/bin|/usr/bin|/bin|/usr/sbin|/sbin) break ;; + esac + (( _index++ )) + done + path=("${(@)path[1,_index-1]}" "${_missing[@]}" "${(@)path[_index,-1]}") +} + +_insert_fallback_paths "$HOME/.local/bin" "$HOME/.cargo/bin" "$HOME/.gem/bin" # Enforced on every zsh start so stale inherited environments # (e.g. a long-running herdr server snapshot) cannot override it. export EDITOR=nvim diff --git a/zsh/.zshrc b/zsh/.zshrc index 35a42e4d..4576ae7b 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -11,8 +11,7 @@ if [[ "$OSTYPE" == darwin* && -z "${SSH_AUTH_SOCK:-}" ]]; then fi # Global tools remain fallbacks without changing inherited tool precedence. -(( ${path[(Ie)/opt/homebrew/bin]} )) || path+=(/opt/homebrew/bin) -(( ${path[(Ie)/Library/TeX/texbin]} )) || path+=(/Library/TeX/texbin) +_insert_fallback_paths /opt/homebrew/bin /Library/TeX/texbin # Completion ZSH_COMPDUMP="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/.zcompdump"