From caac9235f69bb878cbcc652a7a321287f0d37d4a Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Thu, 6 Aug 2026 14:41:31 +0800 Subject: [PATCH] fix(shell): use single ~/.config/zsh/local.zsh for machine local Replace the local/ directory of split env/login/interactive files with one always-on local.zsh sourced from .zshenv. Deploy migrates old fragments and never overwrites local.zsh. --- .zprofile | 7 +-- .zshenv | 6 +- .zshrc | 7 +-- README.md | 19 ++---- deploy.sh | 97 +++++++++++++++++++---------- install.sh | 4 +- local.zsh.example | 6 ++ local/env.local.zsh.example | 4 -- local/interactive.local.zsh.example | 2 - local/login.local.zsh.example | 3 - tests/installers.sh | 25 +++++--- 11 files changed, 99 insertions(+), 81 deletions(-) create mode 100644 local.zsh.example delete mode 100644 local/env.local.zsh.example delete mode 100644 local/interactive.local.zsh.example delete mode 100644 local/login.local.zsh.example diff --git a/.zprofile b/.zprofile index ebab60c..f0c8e69 100644 --- a/.zprofile +++ b/.zprofile @@ -1,5 +1,5 @@ # easyzsh stub: login shells only. Not the PATH source of truth (see env/00-path.zsh). -# Put OS/vendor login hooks in ~/.config/zsh/local/login.local.zsh. +# Machine-local overrides live in ~/.config/zsh/local.zsh (loaded from .zshenv). EASYZSH_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" if [[ -d "$EASYZSH_HOME/login" ]]; then @@ -9,8 +9,3 @@ if [[ -d "$EASYZSH_HOME/login" ]]; then done unset _easyzsh_f fi - -if [[ -f "$EASYZSH_HOME/local/login.local.zsh" ]]; then - # shellcheck disable=SC1090 - . "$EASYZSH_HOME/local/login.local.zsh" -fi diff --git a/.zshenv b/.zshenv index 103b74c..c9cef4f 100644 --- a/.zshenv +++ b/.zshenv @@ -1,5 +1,5 @@ # easyzsh stub: every zsh loads this. Keep it thin; logic lives under ~/.config/zsh/. -# Machine-local early env: ~/.config/zsh/local/env.local.zsh (never overwritten). +# Machine-local overrides: ~/.config/zsh/local.zsh (never overwritten by deploy). EASYZSH_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" if [[ -d "$EASYZSH_HOME/env" ]]; then @@ -10,9 +10,9 @@ if [[ -d "$EASYZSH_HOME/env" ]]; then unset _easyzsh_f fi -if [[ -f "$EASYZSH_HOME/local/env.local.zsh" ]]; then +if [[ -f "$EASYZSH_HOME/local.zsh" ]]; then # shellcheck disable=SC1090 - . "$EASYZSH_HOME/local/env.local.zsh" + . "$EASYZSH_HOME/local.zsh" elif [[ -f "$HOME/.config/zshenv.local" ]]; then # Legacy path from earlier easyzsh layouts. # shellcheck disable=SC1090 diff --git a/.zshrc b/.zshrc index 45fff03..1717e51 100644 --- a/.zshrc +++ b/.zshrc @@ -1,6 +1,6 @@ # easyzsh stub: interactive shells only. Keep it thin; logic lives under ~/.config/zsh/. # Personal interactive overlays: ~/.config/zshrc/*.zsh (before oh-my-zsh). -# Machine-local interactive: ~/.config/zsh/local/interactive.local.zsh (after omz). +# Machine-local overrides: ~/.config/zsh/local.zsh (loaded from .zshenv for every zsh). EASYZSH_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" if [[ -d "$EASYZSH_HOME/interactive" ]]; then @@ -10,8 +10,3 @@ if [[ -d "$EASYZSH_HOME/interactive" ]]; then done unset _easyzsh_f fi - -if [[ -f "$EASYZSH_HOME/local/interactive.local.zsh" ]]; then - # shellcheck disable=SC1090 - source "$EASYZSH_HOME/local/interactive.local.zsh" -fi diff --git a/README.md b/README.md index 399c56d..d500c69 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,7 @@ every zsh login only interactive only ────────── ────────── ──────────────── ~/.zshenv (stub) ~/.zprofile (stub) ~/.zshrc (stub) → config/zsh/env/* → config/zsh/login/* → config/zsh/interactive/* - → local/env.local.zsh → local/login.local.zsh → ~/.config/zshrc/* (before omz) - → local/interactive.local.zsh + → config/zsh/local.zsh → ~/.config/zshrc/* (before omz) ``` PATH for user tools is built **once** in `~/.config/zsh/env/00-path.zsh` @@ -41,7 +40,7 @@ PATH for user tools is built **once** in `~/.config/zsh/env/00-path.zsh` | `~/.config/zsh/env/` | Managed always-on env (PATH, Homebrew) | Overwritten | | `~/.config/zsh/interactive/` | Managed interactive UX (omz, aliases) | Overwritten | | `~/.config/zsh/login/` | Managed login hooks (usually empty) | Overwritten | -| `~/.config/zsh/local/*` | Machine-local env/login/interactive | **Never overwritten** | +| `~/.config/zsh/local.zsh` | Machine-local overrides (every zsh) | **Never overwritten** | | `~/.config/zshrc/*.zsh` | Personal/tool overlays (before omz) | Left alone | | `~/.local/bin/` | User tools + intentional shims (e.g. `grok`) | Left alone | @@ -50,18 +49,12 @@ other tools). Link wanted entrypoints into `~/.local/bin` instead. ## Personal configuration -### Always-on (agents see this) +### Machine-local (agents see this) ```bash -# ~/.config/zsh/local/env.local.zsh +# ~/.config/zsh/local.zsh . "$HOME/.cargo/env" -``` - -### Login only (vendor hooks) - -```bash -# ~/.config/zsh/local/login.local.zsh -# OrbStack, MacPorts, product installers — not primary PATH policy +source ~/.orbstack/shell/init.zsh 2>/dev/null || : ``` ### Interactive overlays (before oh-my-zsh) @@ -81,4 +74,4 @@ curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash Current fragments: `fnm`, `nvm`, `pyenv`, `p10k`, `merlin_devbox`, `merlin_worker`. Aliases that must beat oh-my-zsh belong after omz loads (see managed -`interactive/20-post-omz.zsh`) or in `~/.config/zsh/local/interactive.local.zsh`. +`interactive/20-post-omz.zsh`). diff --git a/deploy.sh b/deploy.sh index c1c4fde..59fe6d3 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # Install managed easyzsh shell layout into a home directory. # Overwrites: ~/.zshenv ~/.zshrc ~/.zprofile ~/.config/zsh/{env,interactive,login} -# Never overwrites: ~/.config/zsh/local/** ~/.config/zshrc/** ~/.local/bin/** +# Never overwrites: ~/.config/zsh/local.zsh ~/.config/zshrc/** ~/.local/bin/** # # Usage: # ./deploy.sh # ./deploy.sh --home /path/to/home -# ./deploy.sh --migrate-zprofile # move existing ~/.zprofile body into login.local +# ./deploy.sh --migrate-zprofile # fold pre-easyzsh ~/.zprofile into local.zsh set -euo pipefail @@ -41,6 +41,7 @@ if [[ ! -d "$TARGET_HOME" ]]; then fi ZSH_CFG="${TARGET_HOME}/.config/zsh" +LOCAL_ZSH="${ZSH_CFG}/local.zsh" ts=$(date +%Y%m%d-%H%M%S) backup_file() { @@ -51,49 +52,83 @@ backup_file() { fi } +file_has_content() { + [[ -f "$1" ]] && grep -q '[^[:space:]]' "$1" +} + echo "Deploying easyzsh managed shell layout into $TARGET_HOME" mkdir -p \ "${ZSH_CFG}/env" \ "${ZSH_CFG}/interactive" \ "${ZSH_CFG}/login" \ - "${ZSH_CFG}/local" \ "${TARGET_HOME}/.config/zshrc" \ "${TARGET_HOME}/.cache/zsh" \ "${TARGET_HOME}/.local/bin" -# Migrate legacy early env if present and new path missing. -if [[ -f "${TARGET_HOME}/.config/zshenv.local" && ! -f "${ZSH_CFG}/local/env.local.zsh" ]]; then - cp -Pp "${TARGET_HOME}/.config/zshenv.local" "${ZSH_CFG}/local/env.local.zsh" - echo "Migrated ~/.config/zshenv.local -> ~/.config/zsh/local/env.local.zsh" +# Migrate split local/* and legacy paths into ~/.config/zsh/local.zsh once. +if [[ ! -f "$LOCAL_ZSH" ]]; then + tmp=$(mktemp) + : >"$tmp" + if file_has_content "${TARGET_HOME}/.config/zshenv.local"; then + cat "${TARGET_HOME}/.config/zshenv.local" >>"$tmp" + printf '\n' >>"$tmp" + fi + if [[ -d "${ZSH_CFG}/local" ]]; then + for part in env.local.zsh login.local.zsh interactive.local.zsh; do + if file_has_content "${ZSH_CFG}/local/${part}"; then + cat "${ZSH_CFG}/local/${part}" >>"$tmp" + printf '\n' >>"$tmp" + fi + done + fi + if file_has_content "$tmp"; then + # De-dup consecutive identical non-empty lines lightly by collapsing exact file repeats later if needed. + cp "$tmp" "$LOCAL_ZSH" + echo "Migrated machine-local fragments -> ~/.config/zsh/local.zsh" + fi + rm -f "$tmp" fi -# Optionally migrate existing zprofile body into login.local (strip redundant PATH). -if [[ "$MIGRATE_ZPROFILE" -eq 1 ]]; then - if [[ -f "${TARGET_HOME}/.zprofile" && ! -f "${ZSH_CFG}/local/login.local.zsh" ]]; then - # Drop easyzsh stub lines and pipx ~/.local/bin PATH (now owned by env/00-path.zsh). +# After migration (or if local.zsh already exists), drop obsolete split files. +if [[ -d "${ZSH_CFG}/local" ]]; then + rm -f \ + "${ZSH_CFG}/local/env.local.zsh" \ + "${ZSH_CFG}/local/login.local.zsh" \ + "${ZSH_CFG}/local/interactive.local.zsh" \ + "${ZSH_CFG}/local/env.local.zsh.example" \ + "${ZSH_CFG}/local/login.local.zsh.example" \ + "${ZSH_CFG}/local/interactive.local.zsh.example" + rmdir "${ZSH_CFG}/local" 2>/dev/null || true +fi + +# Fold a pre-easyzsh ~/.zprofile into local.zsh. Skip if already an easyzsh stub. +if [[ "$MIGRATE_ZPROFILE" -eq 1 && -f "${TARGET_HOME}/.zprofile" ]]; then + if grep -Fq 'easyzsh stub' "${TARGET_HOME}/.zprofile"; then + echo "Skipping zprofile migration: already an easyzsh stub" + elif [[ ! -f "$LOCAL_ZSH" ]]; then + zprofile_extract=$(mktemp) awk ' - /^# easyzsh stub/ { next } - /^EASYZSH_HOME=/ { next } - /for _easyzsh_f in/ { skip=1 } - skip && /unset _easyzsh_f/ { skip=0; next } - skip { next } - /login\.local\.zsh/ { next } /Created by `pipx`/ { skip_pipx=1; next } skip_pipx && /local\/bin/ { skip_pipx=0; next } skip_pipx { next } { print } - ' "${TARGET_HOME}/.zprofile" > "${ZSH_CFG}/local/login.local.zsh" - # Trim trailing blank lines noise if file became empty-ish - if ! grep -q '[^[:space:]]' "${ZSH_CFG}/local/login.local.zsh"; then - rm -f "${ZSH_CFG}/local/login.local.zsh" - echo "Existing .zprofile had no login-local content after migration" - else - echo "Migrated ~/.zprofile body -> ~/.config/zsh/local/login.local.zsh" + ' "${TARGET_HOME}/.zprofile" >"$zprofile_extract" + if file_has_content "$zprofile_extract"; then + cp "$zprofile_extract" "$LOCAL_ZSH" + echo "Migrated ~/.zprofile body -> ~/.config/zsh/local.zsh" fi + rm -f "$zprofile_extract" fi fi +# Remove a broken local.zsh produced by an earlier stub-body migration. +if [[ -f "$LOCAL_ZSH" ]] && grep -Fq 'local/login.local.zsh' "$LOCAL_ZSH" && ! grep -Eq 'cargo|OrbStack|MacPorts|Kiro|fnm' "$LOCAL_ZSH"; then + backup_file "$LOCAL_ZSH" + rm -f "$LOCAL_ZSH" + echo "Removed broken stub residue from local.zsh" +fi + backup_file "${TARGET_HOME}/.zshenv" backup_file "${TARGET_HOME}/.zshrc" backup_file "${TARGET_HOME}/.zprofile" @@ -102,25 +137,19 @@ install -m 644 "${ROOT}/.zshenv" "${TARGET_HOME}/.zshenv" install -m 644 "${ROOT}/.zshrc" "${TARGET_HOME}/.zshrc" install -m 644 "${ROOT}/.zprofile" "${TARGET_HOME}/.zprofile" -# Replace managed modules only (never touch local/). +# Replace managed modules only (never touch local.zsh). rm -f "${ZSH_CFG}/env"/*.zsh rm -f "${ZSH_CFG}/interactive"/*.zsh rm -f "${ZSH_CFG}/login"/*.zsh install -m 644 "${ROOT}/config/env/"*.zsh "${ZSH_CFG}/env/" install -m 644 "${ROOT}/config/interactive/"*.zsh "${ZSH_CFG}/interactive/" -# login may be empty of modules if compgen -G "${ROOT}/config/login/"*.zsh > /dev/null; then install -m 644 "${ROOT}/config/login/"*.zsh "${ZSH_CFG}/login/" fi -# Seed examples only when missing (never overwrite user locals). -for example in env.local.zsh login.local.zsh interactive.local.zsh; do - src="${ROOT}/local/${example}.example" - dst="${ZSH_CFG}/local/${example}.example" - if [[ -f "$src" ]]; then - install -m 644 "$src" "$dst" - fi -done +if [[ -f "${ROOT}/local.zsh.example" ]]; then + install -m 644 "${ROOT}/local.zsh.example" "${ZSH_CFG}/local.zsh.example" +fi # Ensure intentional grok shim when the binary tree exists. if [[ -e "${TARGET_HOME}/.grok/bin/grok" ]]; then diff --git a/install.sh b/install.sh index 13d66d3..aefe880 100755 --- a/install.sh +++ b/install.sh @@ -269,9 +269,7 @@ for rel in \ config/env/00-path.zsh \ config/interactive/10-bootstrap.zsh \ config/interactive/20-post-omz.zsh \ - local/env.local.zsh.example \ - local/login.local.zsh.example \ - local/interactive.local.zsh.example + local.zsh.example do download_easyzsh_file "$rel" done diff --git a/local.zsh.example b/local.zsh.example new file mode 100644 index 0000000..e98ca98 --- /dev/null +++ b/local.zsh.example @@ -0,0 +1,6 @@ +# Copy to ~/.config/zsh/local.zsh (deploy never overwrites local.zsh). +# Sourced from ~/.zshenv after managed env modules — every zsh sees this, +# including non-interactive agent shells. +# +# . "$HOME/.cargo/env" +# source ~/.orbstack/shell/init.zsh 2>/dev/null || : diff --git a/local/env.local.zsh.example b/local/env.local.zsh.example deleted file mode 100644 index 79b5b68..0000000 --- a/local/env.local.zsh.example +++ /dev/null @@ -1,4 +0,0 @@ -# Copy to ~/.config/zsh/local/env.local.zsh (installer never overwrites local/). -# Runs for every zsh after managed env modules. -# -# . "$HOME/.cargo/env" diff --git a/local/interactive.local.zsh.example b/local/interactive.local.zsh.example deleted file mode 100644 index 040ae2d..0000000 --- a/local/interactive.local.zsh.example +++ /dev/null @@ -1,2 +0,0 @@ -# Copy to ~/.config/zsh/local/interactive.local.zsh (installer never overwrites local/). -# Interactive only, after oh-my-zsh and managed post-omz aliases. diff --git a/local/login.local.zsh.example b/local/login.local.zsh.example deleted file mode 100644 index f78baa6..0000000 --- a/local/login.local.zsh.example +++ /dev/null @@ -1,3 +0,0 @@ -# Copy to ~/.config/zsh/local/login.local.zsh (installer never overwrites local/). -# Login shells only: OS/vendor hooks (OrbStack, MacPorts, product installers). -# Do not put primary user-tool PATH here; that belongs in env/00-path.zsh. diff --git a/tests/installers.sh b/tests/installers.sh index fc7b03f..56b76af 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -104,8 +104,8 @@ grep -Fq 'deploy.sh' "$ROOT/install.sh" || fail "install.sh does not invoke depl grep -Fq 'brew shellenv' "$ROOT/config/env/00-path.zsh" || fail "Homebrew is not initialized in env path module" grep -Fq '/usr/local/bin/brew' "$ROOT/config/env/00-path.zsh" || fail "Intel Homebrew path is missing from env path module" grep -Fq '${HOME}/.local/bin' "$ROOT/config/env/00-path.zsh" || fail "~/.local/bin is not added in env path module" -grep -Fq 'env.local.zsh' "$ROOT/.zshenv" || fail "machine-local env.local.zsh hook is missing" -grep -Fq 'login.local.zsh' "$ROOT/.zprofile" || fail "machine-local login.local.zsh hook is missing" +grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "machine-local local.zsh hook is missing" +grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "local.zsh is not sourced from .zshenv" if grep -Fq 'brew shellenv' "$ROOT/.zshrc" "$ROOT/config/interactive"/*.zsh; then fail "Homebrew still initializes from interactive-only files" fi @@ -114,20 +114,31 @@ if grep -Eq '^[[:space:]]*export MLX_USER_TOKEN=' "$ROOT"/zshrc/*.zsh; then fail "a static MLX user token remains" fi -# deploy.sh installs managed layout and preserves local/ +# deploy.sh installs managed layout and preserves local.zsh deploy_home="$TEMP_DIR/deploy-home" -mkdir -p "$deploy_home/.config/zsh/local" "$deploy_home/.local/bin" "$deploy_home/.grok/bin" -printf '%s\n' 'machine-only' >"$deploy_home/.config/zsh/local/env.local.zsh" +mkdir -p "$deploy_home/.config/zsh" "$deploy_home/.local/bin" "$deploy_home/.grok/bin" +printf '%s\n' 'machine-only' >"$deploy_home/.config/zsh/local.zsh" printf '%s\n' '#!/bin/sh' 'echo grok-ok' >"$deploy_home/.grok/bin/grok" chmod +x "$deploy_home/.grok/bin/grok" HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home" [[ -f "$deploy_home/.zshenv" ]] || fail "deploy.sh did not install .zshenv" [[ -f "$deploy_home/.config/zsh/env/00-path.zsh" ]] || fail "deploy.sh did not install env modules" [[ -f "$deploy_home/.config/zsh/interactive/10-bootstrap.zsh" ]] || fail "deploy.sh did not install interactive modules" -grep -Fxq 'machine-only' "$deploy_home/.config/zsh/local/env.local.zsh" || - fail "deploy.sh overwrote local/env.local.zsh" +grep -Fxq 'machine-only' "$deploy_home/.config/zsh/local.zsh" || + fail "deploy.sh overwrote local.zsh" [[ -L "$deploy_home/.local/bin/grok" ]] || fail "deploy.sh did not create grok shim" +# deploy migrates split local/* into local.zsh +split_home="$TEMP_DIR/split-home" +mkdir -p "$split_home/.config/zsh/local" +printf '%s\n' 'from-env-local' >"$split_home/.config/zsh/local/env.local.zsh" +printf '%s\n' 'from-login-local' >"$split_home/.config/zsh/local/login.local.zsh" +HOME="$split_home" bash "$ROOT/deploy.sh" --home "$split_home" >/dev/null +grep -Fq 'from-env-local' "$split_home/.config/zsh/local.zsh" || + fail "deploy.sh did not migrate env.local.zsh into local.zsh" +grep -Fq 'from-login-local' "$split_home/.config/zsh/local.zsh" || + fail "deploy.sh did not migrate login.local.zsh into local.zsh" + # Non-interactive zsh must resolve ~/.local/bin without sourcing .zshrc. local_bin_home="$TEMP_DIR/local-bin-home" mkdir -p "$local_bin_home/.local/bin"