fix(fnm): load managed Node from always-on zsh layer

fnm lived in interactive-only ~/.config/zshrc, so ssh/agents/zsh -c
missed fnm-managed node. Source ~/.config/zsh/*.zsh from .zshenv and
install the fragment as zsh/fnm instead.
This commit is contained in:
Jeffrey
2026-08-07 14:08:07 +08:00
parent 7a43940317
commit 42c8305871
6 changed files with 104 additions and 30 deletions
+11 -2
View File
@@ -18,7 +18,16 @@ if [[ -d "$HOME/.local/bin" ]]; then
export PATH export PATH
fi fi
if [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh" ]]; then # Always-on tool fragments (~/.config/zsh/*.zsh). local.zsh is sourced last so it wins.
ZSH_ALWAYS_ON_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/zsh"
if [[ -d "$ZSH_ALWAYS_ON_DIR" ]]; then
for f in "$ZSH_ALWAYS_ON_DIR"/*.zsh(N); do
[[ ${f:t} == local.zsh ]] && continue
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh" . "$f"
done
fi
if [[ -f "$ZSH_ALWAYS_ON_DIR/local.zsh" ]]; then
# shellcheck disable=SC1090
. "$ZSH_ALWAYS_ON_DIR/local.zsh"
fi fi
+8 -3
View File
@@ -23,10 +23,11 @@ From a git checkout:
| Path | Role | Reinstall | | Path | Role | Reinstall |
| --- | --- | --- | | --- | --- | --- |
| `~/.zshenv` | Always-on: Homebrew, `~/.local/bin`, source `local.zsh` | Overwritten | | `~/.zshenv` | Always-on: Homebrew, `~/.local/bin`, `~/.config/zsh/*.zsh`, then `local.zsh` | Overwritten |
| `~/.zshrc` | Interactive: omz, plugins, aliases, fzf, zoxide | Overwritten | | `~/.zshrc` | Interactive: omz, plugins, aliases, fzf, zoxide | Overwritten |
| `~/.config/zsh/*.zsh` | Always-on tool fragments (e.g. `fnm.zsh`) | Left alone (via `patch.sh`) |
| `~/.config/zsh/local.zsh` | Machine-local overrides (every zsh, including agents) | **Never overwritten** | | `~/.config/zsh/local.zsh` | Machine-local overrides (every zsh, including agents) | **Never overwritten** |
| `~/.config/zshrc/*.zsh` | Personal/tool overlays before oh-my-zsh | Left alone | | `~/.config/zshrc/*.zsh` | Personal/tool overlays before oh-my-zsh (interactive) | Left alone |
| `~/.local/bin/` | User tools + shims (e.g. `grok``~/.grok/bin/grok`) | Left alone | | `~/.local/bin/` | User tools + shims (e.g. `grok``~/.grok/bin/grok`) | Left alone |
easyzsh does **not** manage `~/.zprofile`. Put machine/login hooks in `local.zsh`. easyzsh does **not** manage `~/.zprofile`. Put machine/login hooks in `local.zsh`.
@@ -44,5 +45,9 @@ plugins+=(docker)
Tool fragments from this repo: Tool fragments from this repo:
```bash ```bash
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- fnm pyenv # always-on (sourced from ~/.zshenv)
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- zsh/fnm
# interactive-only (sourced from ~/.zshrc)
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- pyenv
``` ```
+5 -3
View File
@@ -88,11 +88,13 @@ fi
# fnm use lts-latest # fnm use lts-latest
# fnm default lts-latest # fnm default lts-latest
# Apply the fnm zsh configuration patch (downloads zshrc/fnm.zsh to ~/.config/zshrc/fnm.zsh) # Always-on fragment: ~/.config/zsh/fnm.zsh (sourced from ~/.zshenv)
curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- fnm curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- zsh/fnm
# Drop legacy interactive-only copy if present
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/zshrc/fnm.zsh"
echo -e "\nfnm installation completed!" echo -e "\nfnm installation completed!"
echo -e "To start using fnm, restart your terminal or run: source ~/.zshrc" echo -e "To start using fnm, restart your terminal or run: source ~/.zshenv"
echo -e "\nUseful fnm commands:" echo -e "\nUseful fnm commands:"
echo -e " fnm install --lts # Install latest LTS Node.js" echo -e " fnm install --lts # Install latest LTS Node.js"
echo -e " fnm use lts-latest # Use the latest LTS release" echo -e " fnm use lts-latest # Use the latest LTS release"
Regular → Executable
+30 -14
View File
@@ -2,31 +2,47 @@
set -euo pipefail set -euo pipefail
BASE_URL="https://git.miomio.moe/mio/easyzsh/raw/branch/master/zshrc" # Install tool fragments from this repo into ~/.config.
#
# Bare name → zshrc/<name>.zsh → ~/.config/zshrc/<name>.zsh (interactive)
# Path form → <path>.zsh → ~/.config/<path>.zsh
# e.g. zsh/fnm → ~/.config/zsh/fnm.zsh (always-on)
ZSH_CONFIGS_DIR="$HOME/.config/zshrc" BASE_URL="https://git.miomio.moe/mio/easyzsh/raw/branch/master"
mkdir -p "$ZSH_CONFIGS_DIR"
if [ "$#" -gt 0 ]; then if [ "$#" -eq 0 ]; then
for arg in "$@"; do echo "No arguments provided. Please specify the config files to download."
exit 1
fi
for arg in "$@"; do
if [[ "$arg" == */* ]]; then
# Path form: one or more simple segments (zsh/fnm).
if [[ "$arg" == *..* || "$arg" == /* || ! "$arg" =~ ^[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)+$ ]]; then
echo "Invalid config path: $arg" >&2
exit 1
fi
rel="${arg}.zsh"
destination="$HOME/.config/${arg}.zsh"
else
if [[ -z "$arg" || "$arg" == *[![:alnum:]_-]* ]]; then if [[ -z "$arg" || "$arg" == *[![:alnum:]_-]* ]]; then
echo "Invalid config name: $arg" >&2 echo "Invalid config name: $arg" >&2
exit 1 exit 1
fi fi
rel="zshrc/${arg}.zsh"
destination="$HOME/.config/zshrc/${arg}.zsh"
fi
destination="${ZSH_CONFIGS_DIR}/${arg}.zsh" mkdir -p "$(dirname "$destination")"
temporary=$(mktemp "$HOME/.config/.easyzsh-${arg}.XXXXXX") temporary=$(mktemp "$HOME/.config/.easyzsh-XXXXXX")
trap 'rm -f "$temporary"' EXIT trap 'rm -f "$temporary"' EXIT
if ! curl -fsSL "${BASE_URL}/${arg}.zsh" -o "$temporary"; then if ! curl -fsSL "${BASE_URL}/${rel}" -o "$temporary"; then
echo "Failed to download ${arg}.zsh from ${BASE_URL}" echo "Failed to download ${rel} from ${BASE_URL}"
exit 1 exit 1
fi fi
mv "$temporary" "$destination" mv "$temporary" "$destination"
trap - EXIT trap - EXIT
done echo "Installed $destination"
else done
echo "No arguments provided. Please specify the config files to download."
exit 1
fi
+40 -2
View File
@@ -74,18 +74,20 @@ printf '%s\n' 'existing-zshenv' | cmp -s - "$main_home/.zshenv" ||
fnm_home="$TEMP_DIR/fnm-home" fnm_home="$TEMP_DIR/fnm-home"
mkdir -p "$fnm_home/.local/share/fnm" mkdir -p "$fnm_home/.local/share/fnm"
ln -s /usr/bin/true "$fnm_home/.local/share/fnm/fnm" ln -s /usr/bin/true "$fnm_home/.local/share/fnm/fnm"
HOME="$fnm_home" PATH=/usr/bin:/bin zsh -c 'source "$1"; command -v fnm >/dev/null' zsh "$ROOT/zshrc/fnm.zsh" || HOME="$fnm_home" PATH=/usr/bin:/bin zsh -c 'source "$1"; command -v fnm >/dev/null' zsh "$ROOT/zsh/fnm.zsh" ||
fail "fnm is not restored to PATH in a new Zsh session" fail "fnm is not restored to PATH in a new Zsh session"
for script in "$ROOT"/*.sh; do for script in "$ROOT"/*.sh; do
bash -n "$script" bash -n "$script"
done done
for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT"/zshrc/*.zsh; do for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT"/zshrc/*.zsh "$ROOT"/zsh/*.zsh; do
[[ -f "$config" ]] || continue [[ -f "$config" ]] || continue
zsh -n "$config" zsh -n "$config"
done done
[[ ! -e "$ROOT/.zprofile" ]] || fail "easyzsh should not ship a managed .zprofile" [[ ! -e "$ROOT/.zprofile" ]] || fail "easyzsh should not ship a managed .zprofile"
[[ -f "$ROOT/zsh/fnm.zsh" ]] || fail "always-on fnm fragment is missing"
[[ ! -e "$ROOT/zshrc/fnm.zsh" ]] || fail "legacy interactive fnm fragment still exists"
grep -q 'NVM_VERSION="v0.40.4"' "$ROOT/install_nvm.sh" || fail "nvm version is stale" grep -q 'NVM_VERSION="v0.40.4"' "$ROOT/install_nvm.sh" || fail "nvm version is stale"
grep -q 'apt-get install -y unzip' "$ROOT/install_fnm.sh" || fail "Ubuntu unzip installation is missing" grep -q 'apt-get install -y unzip' "$ROOT/install_fnm.sh" || fail "Ubuntu unzip installation is missing"
@@ -101,6 +103,8 @@ grep -Fq 'brew shellenv' "$ROOT/.zshenv" || fail "Homebrew is not initialized in
grep -Fq '/usr/local/bin/brew' "$ROOT/.zshenv" || fail "Intel Homebrew path is missing from .zshenv" grep -Fq '/usr/local/bin/brew' "$ROOT/.zshenv" || fail "Intel Homebrew path is missing from .zshenv"
grep -Fq '$HOME/.local/bin' "$ROOT/.zshenv" || fail "~/.local/bin is not added in .zshenv" grep -Fq '$HOME/.local/bin' "$ROOT/.zshenv" || fail "~/.local/bin is not added in .zshenv"
grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "local.zsh is not sourced from .zshenv" grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "local.zsh is not sourced from .zshenv"
grep -Fq 'ZSH_ALWAYS_ON_DIR' "$ROOT/.zshenv" || fail "always-on ~/.config/zsh/*.zsh glob is missing from .zshenv"
grep -Fq 'zsh/fnm' "$ROOT/install_fnm.sh" || fail "install_fnm.sh does not patch always-on zsh/fnm"
if grep -Fq 'brew shellenv' "$ROOT/.zshrc"; then if grep -Fq 'brew shellenv' "$ROOT/.zshrc"; then
fail "Homebrew still initializes from interactive-only .zshrc" fail "Homebrew still initializes from interactive-only .zshrc"
fi fi
@@ -137,4 +141,38 @@ HOME="$local_bin_home" PATH=/usr/bin:/bin \
zsh -c '. ~/.zshenv; command -v easyzsh-path-probe >/dev/null' || zsh -c '. ~/.zshenv; command -v easyzsh-path-probe >/dev/null' ||
fail "~/.local/bin is not available to non-interactive zsh via .zshenv" fail "~/.local/bin is not available to non-interactive zsh via .zshenv"
# Non-interactive zsh must load always-on ~/.config/zsh/*.zsh (fnm) via .zshenv.
fnm_env_home="$TEMP_DIR/fnm-env-home"
mkdir -p "$fnm_env_home/.local/share/fnm" "$fnm_env_home/.config/zsh"
ln -s /usr/bin/true "$fnm_env_home/.local/share/fnm/fnm"
cp "$ROOT/zsh/fnm.zsh" "$fnm_env_home/.config/zsh/fnm.zsh"
HOME="$fnm_env_home" bash "$ROOT/deploy.sh" --home "$fnm_env_home" >/dev/null
HOME="$fnm_env_home" PATH=/usr/bin:/bin \
zsh -c 'command -v fnm >/dev/null' ||
fail "always-on fnm is not available to non-interactive zsh via .zshenv"
# patch.sh path form installs into ~/.config/zsh/
path_patch_home="$TEMP_DIR/path-patch-home"
path_bin="$TEMP_DIR/path-bin"
mkdir -p "$path_patch_home" "$path_bin"
cat >"$path_bin/curl" <<'CURL'
#!/bin/bash
out=""
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
[[ "$url" == */zsh/fnm.zsh ]] || exit 1
printf '%s\n' '# patched-fnm' >"$out"
CURL
chmod +x "$path_bin/curl"
# Prefer real coreutils for mktemp/mkdir used by patch.sh
HOME="$path_patch_home" PATH="$path_bin:/usr/bin:/bin" bash "$ROOT/patch.sh" zsh/fnm >/dev/null
grep -Fxq '# patched-fnm' "$path_patch_home/.config/zsh/fnm.zsh" ||
fail "patch.sh zsh/fnm did not install ~/.config/zsh/fnm.zsh"
echo "installer checks passed" echo "installer checks passed"
+5 -1
View File
@@ -1,6 +1,10 @@
# fnm (Fast Node Manager) # fnm (Fast Node Manager) — always-on (sourced from ~/.zshenv)
FNM_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/fnm" FNM_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/fnm"
[[ -d "$FNM_PATH" ]] && export PATH="$FNM_PATH:$PATH" [[ -d "$FNM_PATH" ]] && export PATH="$FNM_PATH:$PATH"
if command -v fnm &> /dev/null; then if command -v fnm &> /dev/null; then
if [[ -o interactive ]]; then
eval "$(fnm env --use-on-cd --shell zsh)" eval "$(fnm env --use-on-cd --shell zsh)"
else
eval "$(fnm env --shell zsh)"
fi
fi fi