diff --git a/.zprofile b/.zprofile index f0c8e69..ecdcf07 100644 --- a/.zprofile +++ b/.zprofile @@ -1,11 +1,3 @@ -# easyzsh stub: login shells only. Not the PATH source of truth (see env/00-path.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 - for _easyzsh_f in "$EASYZSH_HOME/login"/*.zsh(N); do - # shellcheck disable=SC1090 - . "$_easyzsh_f" - done - unset _easyzsh_f -fi +# Login shell hook. easyzsh keeps PATH and machine-local env in ~/.zshenv +# (via ~/.config/zsh/local.zsh). Leave this file empty unless a tool insists +# on writing login-only hooks here — prefer moving those into local.zsh. diff --git a/.zshenv b/.zshenv index c9cef4f..1554918 100644 --- a/.zshenv +++ b/.zshenv @@ -1,20 +1,24 @@ -# easyzsh stub: every zsh loads this. Keep it thin; logic lives under ~/.config/zsh/. +# Always-on env for every zsh (interactive, login, agents, zsh -c). # 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 - for _easyzsh_f in "$EASYZSH_HOME/env"/*.zsh(N); do - # shellcheck disable=SC1090 - . "$_easyzsh_f" - done - unset _easyzsh_f +# Homebrew (macOS). cd /tmp so TCC-protected CWDs do not break brew shellenv. +if [[ -x /opt/homebrew/bin/brew ]]; then + eval "$(cd /tmp && /opt/homebrew/bin/brew shellenv)" +elif [[ -x /usr/local/bin/brew ]]; then + eval "$(cd /tmp && /usr/local/bin/brew shellenv)" fi -if [[ -f "$EASYZSH_HOME/local.zsh" ]]; then +# User-local binaries (pipx, uv tools, grok shim, …). +# Do not put vendor trees like ~/.grok/bin on PATH wholesale (names like `agent` shadow tools). +if [[ -d "$HOME/.local/bin" ]]; then + case ":${PATH}:" in + *":$HOME/.local/bin:"*) ;; + *) PATH="$HOME/.local/bin:${PATH}" ;; + esac + export PATH +fi + +if [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh" ]]; then # shellcheck disable=SC1090 - . "$EASYZSH_HOME/local.zsh" -elif [[ -f "$HOME/.config/zshenv.local" ]]; then - # Legacy path from earlier easyzsh layouts. - # shellcheck disable=SC1090 - . "$HOME/.config/zshenv.local" + . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/local.zsh" fi diff --git a/.zshrc b/.zshrc index 1717e51..6e94455 100644 --- a/.zshrc +++ b/.zshrc @@ -1,12 +1,150 @@ -# 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 overrides: ~/.config/zsh/local.zsh (loaded from .zshenv for every zsh). -EASYZSH_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" +# Interactive shell only. PATH lives in ~/.zshenv (agents do not load this file). +# Personal overlays: ~/.config/zshrc/*.zsh (before oh-my-zsh). +# Machine-local env: ~/.config/zsh/local.zsh (from ~/.zshenv). -if [[ -d "$EASYZSH_HOME/interactive" ]]; then - for _easyzsh_f in "$EASYZSH_HOME/interactive"/*.zsh(N); do - # shellcheck disable=SC1090 - source "$_easyzsh_f" - done - unset _easyzsh_f +# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. +# if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then +# source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +# fi + +export TERM="xterm-256color" + +# Skip oh-my-zsh's completion security audit (compaudit). +export ZSH_DISABLE_COMPFIX=true + +export ZSH=$HOME/.oh-my-zsh + +# Agent/AI terminals (Cursor sets CURSOR_AGENT) get a light theme; humans use p10k. +if [[ -n "$CURSOR_AGENT" ]]; then + ZSH_THEME="robbyrussell" +else + ZSH_THEME="powerlevel10k/powerlevel10k" +fi + +zstyle ':omz:update' frequency 30 + +plugins=( + zsh-autosuggestions + zsh-syntax-highlighting + history-substring-search + systemd + k + extract + sudo + fzf-tab + poetry + ) +# In ~/.config/zshrc/*.zsh: +# plugins+=(docker) +# plugins=(${plugins:#zsh-autosuggestions}) + +fpath+=$ZSH/custom/plugins/zsh-completions/src + +if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &> /dev/null; then + FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" +fi + +mkdir -p ~/.cache/zsh +autoload -U compinit && compinit -C -d ~/.cache/zsh/.zcompdump + +SAVEHIST=50000 + +cheat() { + if [ "$2" ]; then + curl "https://cheat.sh/$1/$2+$3+$4+$5+$6+$7+$8+$9+$10" + else + curl "https://cheat.sh/$1" + fi +} + +speedtest() { + setopt localoptions pipefail + curl -fsSL https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py | python3 - +} + +dadjoke() { + curl https://icanhazdadjoke.com +} + +cleanzip() { + zip -d "$@" __MACOSX/\* \*/.DS_Store +} + +ipgeo() { + if [ "$1" ]; then + curl "https://api.db-ip.com/v2/free/$1" + else + curl "https://api.db-ip.com/v2/free/$(myip)" + fi +} + +# Personal / tool overlays before oh-my-zsh so plugins+=(…) apply. +ZSH_CONFIGS_DIR="$HOME/.config/zshrc" +for file in "$ZSH_CONFIGS_DIR"/*(DN); do + if [ -f "$file" ]; then + source "$file" + fi +done + +source "$ZSH/oh-my-zsh.sh" + +# Aliases after omz so they win over omz defaults. +alias myip="wget -qO- https://wtfismyip.com/text" +alias l="eza --hyperlink -lA --sort=modified --reverse --classify" +alias e="exit" +alias ip="ip --color=auto" +alias a='eza -la --git --colour-scale all -g --smart-group --icons always --hyperlink' +alias aa='eza -la --git --colour-scale all -g --smart-group --icons always -s modified -r --hyperlink' + +# rm → Trash. Shell snapshots restore functions but not global arrays, so re-resolve when empty. +_resolve_trash_cmd() { + if [[ "$OSTYPE" == darwin* && -x /usr/bin/trash ]]; then + typeset -ga _TRASH_CMD=(/usr/bin/trash) + elif [[ "$OSTYPE" == darwin* ]] && command -v rmtrash &> /dev/null; then + typeset -ga _TRASH_CMD=(rmtrash) + elif [[ "$OSTYPE" != darwin* ]] && command -v gio &> /dev/null; then + typeset -ga _TRASH_CMD=(gio trash) + elif [[ "$OSTYPE" != darwin* ]] && command -v trash-put &> /dev/null; then + typeset -ga _TRASH_CMD=(trash-put) + fi +} +_resolve_trash_cmd +if (( ${#_TRASH_CMD[@]} )); then + rm() { + (( ${#_TRASH_CMD[@]} )) || _resolve_trash_cmd + (( ${#_TRASH_CMD[@]} )) || { command rm "$@"; return } + local force=0 arg + local -a paths existing + for arg in "$@"; do + case "$arg" in + --) ;; + -*f*) force=1 ;; + -*) ;; + *) paths+=("$arg") ;; + esac + done + (( ${#paths[@]} )) || return 0 + if (( force )); then + for arg in "${paths[@]}"; do + [[ -e "$arg" || -L "$arg" ]] && existing+=("$arg") + done + (( ${#existing[@]} )) || return 0 + "${_TRASH_CMD[@]}" "${existing[@]}" + else + "${_TRASH_CMD[@]}" "${paths[@]}" + fi + } +fi + +if [[ -x "$HOME/.fzf/bin/fzf" ]] && ! fzf --zsh >/dev/null 2>&1; then + path=("$HOME/.fzf/bin" ${path:#$HOME/.fzf/bin}) +fi +[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh +export FZF_DEFAULT_OPS="--extended" + +command -v zoxide &> /dev/null && eval "$(zoxide init zsh)" +alias j="z" + +if (( $+functions[k] )); then + alias k="k -h" fi diff --git a/README.md b/README.md index d500c69..a8c1f0f 100644 --- a/README.md +++ b/README.md @@ -11,67 +11,37 @@ curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/install.sh | bas ``` Options: `--cp-hist` / `-c` imports bash history; `--non-interactive` / `-n` -skips the default-shell change. The installer backs up existing shell stubs -before replacing them. +skips the default-shell change. -From a git checkout (recommended for multi-machine sync): +From a git checkout: ```bash -./deploy.sh --migrate-zprofile +./deploy.sh ``` -## Architecture - -```text -every zsh login only interactive only -────────── ────────── ──────────────── -~/.zshenv (stub) ~/.zprofile (stub) ~/.zshrc (stub) - → config/zsh/env/* → config/zsh/login/* → config/zsh/interactive/* - → config/zsh/local.zsh → ~/.config/zshrc/* (before omz) -``` - -PATH for user tools is built **once** in `~/.config/zsh/env/00-path.zsh` -(always-on), so non-interactive agent shells (`zsh -c`, Codex, Claude) resolve -`~/.local/bin` the same way interactive terminals do. +## Layout | Path | Role | Reinstall | | --- | --- | --- | -| `~/.zshenv` `~/.zprofile` `~/.zshrc` | Thin stubs | Overwritten | -| `~/.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.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 | +| `~/.zshenv` | Always-on: Homebrew, `~/.local/bin`, source `local.zsh` | Overwritten | +| `~/.zshrc` | Interactive: omz, plugins, aliases, fzf, zoxide | Overwritten | +| `~/.zprofile` | Login placeholder (prefer `local.zsh` for machine hooks) | 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 | +| `~/.local/bin/` | User tools + shims (e.g. `grok` → `~/.grok/bin/grok`) | Left alone | -Do **not** put `~/.grok/bin` on PATH wholesale (generic names like `agent` shadow -other tools). Link wanted entrypoints into `~/.local/bin` instead. - -## Personal configuration - -### Machine-local (agents see this) +Do **not** put `~/.grok/bin` on PATH wholesale. ```bash # ~/.config/zsh/local.zsh . "$HOME/.cargo/env" -source ~/.orbstack/shell/init.zsh 2>/dev/null || : -``` -### Interactive overlays (before oh-my-zsh) - -```bash -# ~/.config/zshrc/local.zsh +# ~/.config/zshrc/prefs.zsh plugins+=(docker) -plugins=(${plugins:#zsh-autosuggestions}) ``` -Ready-made tool configs from this repo's `zshrc/` directory: +Tool fragments from this repo: ```bash curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- fnm pyenv ``` - -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`). diff --git a/config/env/00-path.zsh b/config/env/00-path.zsh deleted file mode 100644 index bc575aa..0000000 --- a/config/env/00-path.zsh +++ /dev/null @@ -1,24 +0,0 @@ -# Sole managed PATH builder for every zsh (including non-interactive agents). -# Do not re-append user tool paths in .zprofile or .zshrc. - -easyzsh_path_prepend() { - local dir="$1" - [[ -n "$dir" && -d "$dir" ]] || return 0 - case ":${PATH}:" in - *":${dir}:"*) ;; - *) PATH="${dir}:${PATH}" ;; - esac -} - -# Homebrew first so its shellenv can set prefix vars; we re-assert ~/.local/bin after. -if [[ -x /opt/homebrew/bin/brew ]]; then - eval "$(cd /tmp && /opt/homebrew/bin/brew shellenv)" -elif [[ -x /usr/local/bin/brew ]]; then - eval "$(cd /tmp && /usr/local/bin/brew shellenv)" -fi - -# User-local binaries (pipx, uv tools, intentional shims such as grok). -# Never put vendor trees like ~/.grok/bin on PATH wholesale (generic names shadow tools). -easyzsh_path_prepend "${HOME}/.local/bin" - -export PATH diff --git a/config/interactive/10-bootstrap.zsh b/config/interactive/10-bootstrap.zsh deleted file mode 100644 index 31549e2..0000000 --- a/config/interactive/10-bootstrap.zsh +++ /dev/null @@ -1,120 +0,0 @@ -# Interactive bootstrap: omz, plugins, personal overlays, completions. -# PATH is owned by env/00-path.zsh; do not rebuild it here. - -# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. -# Initialization code that may require console input (password prompts, [y/n] -# confirmations, etc.) must go above this block; everything else may go below. -# if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then -# source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" -# fi - -export TERM="xterm-256color" - -# Skip oh-my-zsh's completion security audit (compaudit). The completion dirs are -# machine-owned and not group/world-writable, so the per-startup audit only costs time. -export ZSH_DISABLE_COMPFIX=true - -# Path to your oh-my-zsh installation. -export ZSH=$HOME/.oh-my-zsh - -# Agent/AI terminals (Cursor's agent sets CURSOR_AGENT) get a light, fast theme; -# interactive shells use powerlevel10k. -if [[ -n "$CURSOR_AGENT" ]]; then - ZSH_THEME="robbyrussell" -else - ZSH_THEME="powerlevel10k/powerlevel10k" -fi - -# How often to auto-update (days). Replaces the older UPDATE_ZSH_DAYS variable. -zstyle ':omz:update' frequency 30 - -# Which plugins would you like to load? (plugins can be found in ~/.oh-my-zsh/plugins/*) -# Custom plugins may be added to ~/.oh-my-zsh/custom/plugins/ -# Add wisely, as too many plugins slow down shell startup. -plugins=( - # zsh-completions INSTALL METHOD CHANGED https://github.com/zsh-users/zsh-completions/issues/603 - zsh-autosuggestions - zsh-syntax-highlighting - history-substring-search - systemd - k - extract - sudo - fzf-tab - poetry - # web-search - # httpie - # git - # python - # docker - # lol - # pip - # pyenv - # redis-cli - # screen - # zsh-wakatime # enable if you use wakatime with 'https://github.com/wbingli/zsh-wakatime' - ) -# Plugins can be added in ~/.config/zshrc/*.zsh like: -# plugins+=(zsh-nvm) -# Remove plugins from the default list with: -# plugins=(${plugins:#pluginname}) - -fpath+=$ZSH/custom/plugins/zsh-completions/src - -# Setup Homebrew completions for macOS -if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &> /dev/null; then - FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" -fi - -mkdir -p ~/.cache/zsh -autoload -U compinit && compinit -C -d ~/.cache/zsh/.zcompdump - -SAVEHIST=50000 - -# CUSTOM FUNCTIONS - -# cheat sheets (github.com/chubin/cheat.sh) -cheat() { - if [ "$2" ]; then - curl "https://cheat.sh/$1/$2+$3+$4+$5+$6+$7+$8+$9+$10" - else - curl "https://cheat.sh/$1" - fi -} - -speedtest() { - setopt localoptions pipefail - curl -fsSL https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py | python3 - -} - -dadjoke() { - curl https://icanhazdadjoke.com -} - -# Strip macOS junk from a zip (requires `zip`). Usage: cleanzip archive.zip -cleanzip() { - zip -d "$@" __MACOSX/\* \*/.DS_Store -} - -# Find geo info from IP -ipgeo() { - if [ "$1" ]; then - curl "https://api.db-ip.com/v2/free/$1" - else - curl "https://api.db-ip.com/v2/free/$(myip)" - fi -} - -# Personal / tool overlays under ~/.config/zshrc/ (before oh-my-zsh so plugins apply). -# Managed interactive code lives in ~/.config/zsh/interactive/; do not put machine -# secrets here if they need to survive reinstall of managed files. -ZSH_CONFIGS_DIR="$HOME/.config/zshrc" - -for file in "$ZSH_CONFIGS_DIR"/*(DN); do - if [ -f "$file" ]; then - source "$file" - fi -done - -# Source oh-my-zsh after overlays so plugins+=(…) from ~/.config/zshrc take effect. -source "$ZSH/oh-my-zsh.sh" diff --git a/config/interactive/20-post-omz.zsh b/config/interactive/20-post-omz.zsh deleted file mode 100644 index 5b30c93..0000000 --- a/config/interactive/20-post-omz.zsh +++ /dev/null @@ -1,70 +0,0 @@ -# Runs after oh-my-zsh so these aliases win over omz defaults. - -alias myip="wget -qO- https://wtfismyip.com/text" -alias l="eza --hyperlink -lA --sort=modified --reverse --classify" -alias e="exit" -alias ip="ip --color=auto" -## EZA - the better ls command -alias a='eza -la --git --colour-scale all -g --smart-group --icons always --hyperlink' -alias aa='eza -la --git --colour-scale all -g --smart-group --icons always -s modified -r --hyperlink' - -# rm moves files to Trash while tolerating standard rm flags: -f suppresses -# missing-file errors; other flags are no-ops (trash backends are already -# recursive). Files whose names start with "-" still need /bin/rm. -# Shell snapshots (Claude Code etc.) restore functions but not global arrays, -# so rm() re-resolves the backend when _TRASH_CMD is empty at call time. -_resolve_trash_cmd() { - if [[ "$OSTYPE" == darwin* && -x /usr/bin/trash ]]; then - typeset -ga _TRASH_CMD=(/usr/bin/trash) - elif [[ "$OSTYPE" == darwin* ]] && command -v rmtrash &> /dev/null; then - typeset -ga _TRASH_CMD=(rmtrash) - elif [[ "$OSTYPE" != darwin* ]] && command -v gio &> /dev/null; then - typeset -ga _TRASH_CMD=(gio trash) - elif [[ "$OSTYPE" != darwin* ]] && command -v trash-put &> /dev/null; then - typeset -ga _TRASH_CMD=(trash-put) - fi -} -_resolve_trash_cmd -if (( ${#_TRASH_CMD[@]} )); then - rm() { - (( ${#_TRASH_CMD[@]} )) || _resolve_trash_cmd - (( ${#_TRASH_CMD[@]} )) || { command rm "$@"; return } - local force=0 arg - local -a paths existing - for arg in "$@"; do - case "$arg" in - --) ;; - -*f*) force=1 ;; - -*) ;; - *) paths+=("$arg") ;; - esac - done - (( ${#paths[@]} )) || return 0 - if (( force )); then - for arg in "${paths[@]}"; do - [[ -e "$arg" || -L "$arg" ]] && existing+=("$arg") - done - (( ${#existing[@]} )) || return 0 - "${_TRASH_CMD[@]}" "${existing[@]}" - else - "${_TRASH_CMD[@]}" "${paths[@]}" - fi - } -fi - -# Now source fzf.zsh , otherwise Ctrl+r is overwritten by ohmyzsh -if [[ -x "$HOME/.fzf/bin/fzf" ]] && ! fzf --zsh >/dev/null 2>&1; then - path=("$HOME/.fzf/bin" ${path:#$HOME/.fzf/bin}) -fi -[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh -export FZF_DEFAULT_OPS="--extended" - -# Initialize zoxide (smarter cd command) -command -v zoxide &> /dev/null && eval "$(zoxide init zsh)" -# Short jump alias; expands at use time after zoxide defines `z`. -alias j="z" - -# k plugin ships a `k` function; only alias when it actually loaded. -if (( $+functions[k] )); then - alias k="k -h" -fi diff --git a/config/login/.keep b/config/login/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/deploy.sh b/deploy.sh index 59fe6d3..af7a774 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Install managed easyzsh shell layout into a home directory. -# Overwrites: ~/.zshenv ~/.zshrc ~/.zprofile ~/.config/zsh/{env,interactive,login} +# Install easyzsh shell stubs into a home directory. +# Overwrites: ~/.zshenv ~/.zshrc ~/.zprofile # Never overwrites: ~/.config/zsh/local.zsh ~/.config/zshrc/** ~/.local/bin/** # # Usage: @@ -56,17 +56,15 @@ file_has_content() { [[ -f "$1" ]] && grep -q '[^[:space:]]' "$1" } -echo "Deploying easyzsh managed shell layout into $TARGET_HOME" +echo "Deploying easyzsh shell stubs into $TARGET_HOME" mkdir -p \ - "${ZSH_CFG}/env" \ - "${ZSH_CFG}/interactive" \ - "${ZSH_CFG}/login" \ + "${ZSH_CFG}" \ "${TARGET_HOME}/.config/zshrc" \ "${TARGET_HOME}/.cache/zsh" \ "${TARGET_HOME}/.local/bin" -# Migrate split local/* and legacy paths into ~/.config/zsh/local.zsh once. +# Migrate legacy fragments into local.zsh once. if [[ ! -f "$LOCAL_ZSH" ]]; then tmp=$(mktemp) : >"$tmp" @@ -83,29 +81,17 @@ if [[ ! -f "$LOCAL_ZSH" ]]; then 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 -# 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. +# Fold a pre-easyzsh ~/.zprofile into local.zsh. Skip easyzsh stub / empty files. 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" + if grep -Eq 'easyzsh|PATH lives in|Machine-local env' "${TARGET_HOME}/.zprofile" 2>/dev/null \ + || grep -Fq 'Leave this file empty' "${TARGET_HOME}/.zprofile" 2>/dev/null; then + echo "Skipping zprofile migration: already easyzsh-managed" elif [[ ! -f "$LOCAL_ZSH" ]]; then zprofile_extract=$(mktemp) awk ' @@ -122,13 +108,6 @@ if [[ "$MIGRATE_ZPROFILE" -eq 1 && -f "${TARGET_HOME}/.zprofile" ]]; then 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" @@ -137,19 +116,15 @@ 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.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/" -if compgen -G "${ROOT}/config/login/"*.zsh > /dev/null; then - install -m 644 "${ROOT}/config/login/"*.zsh "${ZSH_CFG}/login/" -fi - -if [[ -f "${ROOT}/local.zsh.example" ]]; then - install -m 644 "${ROOT}/local.zsh.example" "${ZSH_CFG}/local.zsh.example" -fi +# Remove obsolete managed-tree layout and examples from earlier deploys. +/bin/rm -rf \ + "${ZSH_CFG}/env" \ + "${ZSH_CFG}/interactive" \ + "${ZSH_CFG}/login" \ + "${ZSH_CFG}/local" +/bin/rm -f \ + "${ZSH_CFG}/local.zsh.example" \ + "${TARGET_HOME}/.config/zshenv.local" # Ensure intentional grok shim when the binary tree exists. if [[ -e "${TARGET_HOME}/.grok/bin/grok" ]]; then @@ -158,4 +133,4 @@ if [[ -e "${TARGET_HOME}/.grok/bin/grok" ]]; then fi echo "Deploy complete." -echo "Verify: env -i HOME=\"$TARGET_HOME\" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv; command -v grok; echo PATH=\$PATH'" +echo "Verify: env -i HOME=\"$TARGET_HOME\" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv; command -v grok'" diff --git a/install.sh b/install.sh index aefe880..75a0ab9 100755 --- a/install.sh +++ b/install.sh @@ -243,7 +243,7 @@ fi ZSH="$HOME/.oh-my-zsh" EASYZSH_RAW_BASE="${EASYZSH_RAW_BASE:-https://git.miomio.moe/mio/easyzsh/raw/branch/master}" -# Stage managed shell layout (stubs + ~/.config/zsh modules), then deploy.sh installs it. +# Stage shell stubs, then deploy.sh installs them. # On download failure, leave existing ~/.zshrc and ~/.zshenv untouched. easyzsh_stage=$(mktemp -d "$HOME/.easyzsh-stage.XXXXXX") zshrc_download=$(mktemp "$HOME/.zshrc.download.XXXXXX") @@ -260,16 +260,12 @@ download_easyzsh_file() { fi } -echo -e "Downloading easyzsh shell layout\n" +echo -e "Downloading easyzsh shell stubs\n" for rel in \ deploy.sh \ .zshenv \ .zshrc \ - .zprofile \ - config/env/00-path.zsh \ - config/interactive/10-bootstrap.zsh \ - config/interactive/20-post-omz.zsh \ - local.zsh.example + .zprofile do download_easyzsh_file "$rel" done @@ -286,7 +282,7 @@ else git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "$ZSH" fi -# deploy.sh backs up existing stubs and never overwrites ~/.config/zsh/local/ +# deploy.sh backs up existing stubs and never overwrites ~/.config/zsh/local.zsh bash "$easyzsh_stage/deploy.sh" --migrate-zprofile trap - EXIT rm -rf "$easyzsh_stage" diff --git a/local.zsh.example b/local.zsh.example index e98ca98..6786c97 100644 --- a/local.zsh.example +++ b/local.zsh.example @@ -1,6 +1,4 @@ -# 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. +# Copy to ~/.config/zsh/local.zsh (deploy never overwrites it). +# Sourced from ~/.zshenv — every zsh sees this, including agents. # # . "$HOME/.cargo/env" -# source ~/.orbstack/shell/init.zsh 2>/dev/null || : diff --git a/tests/installers.sh b/tests/installers.sh index 56b76af..d9d8f17 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -44,16 +44,15 @@ fi main_bin="$TEMP_DIR/main-bin" main_home="$TEMP_DIR/main-home" mkdir -p "$main_bin" "$main_home" -for command_name in zsh git curl tar fc-cache eza zoxide autoconf trash install; do +for command_name in zsh git curl tar fc-cache eza zoxide autoconf trash; do ln -s /usr/bin/true "$main_bin/$command_name" done -# real install binary needed by deploy.sh ln -sf "$(command -v install)" "$main_bin/install" ln -sf "$(command -v cp)" "$main_bin/cp" ln -sf "$(command -v mkdir)" "$main_bin/mkdir" ln -sf "$(command -v chmod)" "$main_bin/chmod" ln -sf "$(command -v ln)" "$main_bin/ln" -ln -sf "$(command -v rm)" "$main_bin/rm" +ln -sf "$(command -v rm)" "$main_bin/rm" 2>/dev/null || ln -sf /bin/rm "$main_bin/rm" ln -sf "$(command -v bash)" "$main_bin/bash" ln -sf "$(command -v mktemp)" "$main_bin/mktemp" ln -sf "$(command -v dirname)" "$main_bin/dirname" @@ -61,7 +60,6 @@ ln -sf "$(command -v date)" "$main_bin/date" ln -sf "$(command -v sed)" "$main_bin/sed" ln -sf "$(command -v grep)" "$main_bin/grep" ln -sf "$(command -v awk)" "$main_bin/awk" -ln -sf "$(command -v compgen)" "$main_bin/compgen" 2>/dev/null || true ln -s /usr/bin/false "$main_bin/wget" cp "$ROOT/README.md" "$main_home/.zshrc" printf '%s\n' 'existing-zshenv' >"$main_home/.zshenv" @@ -83,10 +81,7 @@ for script in "$ROOT"/*.sh; do bash -n "$script" done -for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT/.zprofile" \ - "$ROOT"/config/env/*.zsh \ - "$ROOT"/config/interactive/*.zsh \ - "$ROOT"/zshrc/*.zsh; do +for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT/.zprofile" "$ROOT"/zshrc/*.zsh; do [[ -f "$config" ]] || continue zsh -n "$config" done @@ -97,48 +92,40 @@ grep -q 'tail -1 || true' "$ROOT/install_merlin_worker.sh" || fail "a missing PY grep -Fq 'mktemp "$HOME/.zshrc.download.' "$ROOT/install.sh" || fail ".zshrc is not downloaded on its target filesystem" grep -Fq 'mktemp "$HOME/.zshenv.download.' "$ROOT/install.sh" || fail ".zshenv is not downloaded on its target filesystem" grep -Fq 'mktemp "$HOME/.config/.easyzsh-' "$ROOT/patch.sh" || fail "patch downloads can be sourced while incomplete" -grep -Fq '"$ZSH_CONFIGS_DIR"/*(DN)' "$ROOT/config/interactive/10-bootstrap.zsh" || fail "existing extensionless Zsh configs are ignored" -grep -Fq '_resolve_trash_cmd' "$ROOT/config/interactive/20-post-omz.zsh" || fail "trash-backed rm helper is missing" +grep -Fq '"$ZSH_CONFIGS_DIR"/*(DN)' "$ROOT/.zshrc" || fail "existing extensionless Zsh configs are ignored" +grep -Fq '_resolve_trash_cmd' "$ROOT/.zshrc" || fail "trash-backed rm helper is missing" grep -Fq 'libglib2.0-bin' "$ROOT/install.sh" || fail "Ubuntu Trash support is not installed" grep -Fq 'deploy.sh' "$ROOT/install.sh" || fail "install.sh does not invoke deploy.sh" -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 'local.zsh' "$ROOT/.zshenv" || fail "machine-local local.zsh hook is missing" +grep -Fq 'brew shellenv' "$ROOT/.zshenv" || fail "Homebrew is not initialized in .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 '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" +if grep -Fq 'brew shellenv' "$ROOT/.zshrc"; then + fail "Homebrew still initializes from interactive-only .zshrc" fi [[ ! -e "$ROOT/install/fnm.sh" ]] || fail "vendored fnm installer still exists" if grep -Eq '^[[:space:]]*export MLX_USER_TOKEN=' "$ROOT"/zshrc/*.zsh; then fail "a static MLX user token remains" fi +# No multi-file managed tree under config/ +[[ ! -d "$ROOT/config" ]] || fail "obsolete config/ managed tree still exists" -# deploy.sh installs managed layout and preserves local.zsh +# deploy.sh installs stubs and preserves local.zsh; removes obsolete tree deploy_home="$TEMP_DIR/deploy-home" -mkdir -p "$deploy_home/.config/zsh" "$deploy_home/.local/bin" "$deploy_home/.grok/bin" +mkdir -p "$deploy_home/.config/zsh/env" "$deploy_home/.local/bin" "$deploy_home/.grok/bin" printf '%s\n' 'machine-only' >"$deploy_home/.config/zsh/local.zsh" +printf '%s\n' 'stale' >"$deploy_home/.config/zsh/env/00-path.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" +[[ -f "$deploy_home/.zshrc" ]] || fail "deploy.sh did not install .zshrc" grep -Fxq 'machine-only' "$deploy_home/.config/zsh/local.zsh" || fail "deploy.sh overwrote local.zsh" +[[ ! -e "$deploy_home/.config/zsh/env" ]] || fail "deploy.sh left obsolete env/ tree" +[[ ! -e "$deploy_home/.config/zsh/local.zsh.example" ]] || fail "deploy.sh left local.zsh.example in home" [[ -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"