From 42c83058715fa336233e6dc0c8d8e0721efd7b07 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 7 Aug 2026 14:08:07 +0800 Subject: [PATCH] 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. --- .zshenv | 13 ++++++++-- README.md | 11 ++++++--- install_fnm.sh | 8 ++++--- patch.sh | 54 +++++++++++++++++++++++++++--------------- tests/installers.sh | 42 ++++++++++++++++++++++++++++++-- {zshrc => zsh}/fnm.zsh | 6 ++++- 6 files changed, 104 insertions(+), 30 deletions(-) mode change 100644 => 100755 patch.sh rename {zshrc => zsh}/fnm.zsh (57%) diff --git a/.zshenv b/.zshenv index 1554918..dd783dc 100644 --- a/.zshenv +++ b/.zshenv @@ -18,7 +18,16 @@ if [[ -d "$HOME/.local/bin" ]]; then export PATH 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 + . "$f" + done +fi +if [[ -f "$ZSH_ALWAYS_ON_DIR/local.zsh" ]]; then # shellcheck disable=SC1090 - . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh" + . "$ZSH_ALWAYS_ON_DIR/local.zsh" fi diff --git a/README.md b/README.md index 7e28db6..8829898 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ From a git checkout: | 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 | +| `~/.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/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 | easyzsh does **not** manage `~/.zprofile`. Put machine/login hooks in `local.zsh`. @@ -44,5 +45,9 @@ plugins+=(docker) Tool fragments from this repo: ```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 ``` diff --git a/install_fnm.sh b/install_fnm.sh index 8013966..dfea4d6 100755 --- a/install_fnm.sh +++ b/install_fnm.sh @@ -88,11 +88,13 @@ fi # fnm use lts-latest # fnm default lts-latest -# Apply the fnm zsh configuration patch (downloads zshrc/fnm.zsh to ~/.config/zshrc/fnm.zsh) -curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- fnm +# 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 -- 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 "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 " fnm install --lts # Install latest LTS Node.js" echo -e " fnm use lts-latest # Use the latest LTS release" diff --git a/patch.sh b/patch.sh old mode 100644 new mode 100755 index 3d156ce..a4854bb --- a/patch.sh +++ b/patch.sh @@ -2,31 +2,47 @@ 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/.zsh → ~/.config/zshrc/.zsh (interactive) +# Path form → .zsh → ~/.config/.zsh +# e.g. zsh/fnm → ~/.config/zsh/fnm.zsh (always-on) -ZSH_CONFIGS_DIR="$HOME/.config/zshrc" -mkdir -p "$ZSH_CONFIGS_DIR" +BASE_URL="https://git.miomio.moe/mio/easyzsh/raw/branch/master" -if [ "$#" -gt 0 ]; then - for arg in "$@"; do +if [ "$#" -eq 0 ]; then + 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 echo "Invalid config name: $arg" >&2 exit 1 fi + rel="zshrc/${arg}.zsh" + destination="$HOME/.config/zshrc/${arg}.zsh" + fi - destination="${ZSH_CONFIGS_DIR}/${arg}.zsh" - temporary=$(mktemp "$HOME/.config/.easyzsh-${arg}.XXXXXX") - trap 'rm -f "$temporary"' EXIT + mkdir -p "$(dirname "$destination")" + temporary=$(mktemp "$HOME/.config/.easyzsh-XXXXXX") + trap 'rm -f "$temporary"' EXIT - if ! curl -fsSL "${BASE_URL}/${arg}.zsh" -o "$temporary"; then - echo "Failed to download ${arg}.zsh from ${BASE_URL}" - exit 1 - fi + if ! curl -fsSL "${BASE_URL}/${rel}" -o "$temporary"; then + echo "Failed to download ${rel} from ${BASE_URL}" + exit 1 + fi - mv "$temporary" "$destination" - trap - EXIT - done -else - echo "No arguments provided. Please specify the config files to download." - exit 1 -fi + mv "$temporary" "$destination" + trap - EXIT + echo "Installed $destination" +done diff --git a/tests/installers.sh b/tests/installers.sh index 249a0c6..c17fec0 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -74,18 +74,20 @@ printf '%s\n' 'existing-zshenv' | cmp -s - "$main_home/.zshenv" || fnm_home="$TEMP_DIR/fnm-home" mkdir -p "$fnm_home/.local/share/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" for script in "$ROOT"/*.sh; do bash -n "$script" 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 zsh -n "$config" done [[ ! -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 '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 '$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 '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 fail "Homebrew still initializes from interactive-only .zshrc" fi @@ -137,4 +141,38 @@ HOME="$local_bin_home" PATH=/usr/bin:/bin \ zsh -c '. ~/.zshenv; command -v easyzsh-path-probe >/dev/null' || 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" diff --git a/zshrc/fnm.zsh b/zsh/fnm.zsh similarity index 57% rename from zshrc/fnm.zsh rename to zsh/fnm.zsh index 4bc6c25..4604bb0 100644 --- a/zshrc/fnm.zsh +++ b/zsh/fnm.zsh @@ -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" [[ -d "$FNM_PATH" ]] && export PATH="$FNM_PATH:$PATH" if command -v fnm &> /dev/null; then + if [[ -o interactive ]]; then eval "$(fnm env --use-on-cd --shell zsh)" + else + eval "$(fnm env --shell zsh)" + fi fi