From f8dcc29a2f39110e267c39b6979600bf954d9590 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Thu, 6 Aug 2026 14:36:08 +0800 Subject: [PATCH] refactor(shell): thin stubs with managed ~/.config/zsh tree Split always-on PATH/env, login hooks, and interactive UX into a managed tree under ~/.config/zsh, with never-overwrite local/ hooks. Agents and interactive shells share one PATH owner; deploy.sh applies the layout safely across machines. --- .zprofile | 16 ++ .zshenv | 40 ++-- .zshrc | 309 ++-------------------------- README.md | 73 +++++-- config/env/00-path.zsh | 24 +++ config/interactive/10-bootstrap.zsh | 120 +++++++++++ config/interactive/20-post-omz.zsh | 70 +++++++ config/login/.keep | 0 deploy.sh | 132 ++++++++++++ install.sh | 66 +++--- local/env.local.zsh.example | 4 + local/interactive.local.zsh.example | 2 + local/login.local.zsh.example | 3 + tests/installers.sh | 78 ++++--- 14 files changed, 546 insertions(+), 391 deletions(-) create mode 100644 .zprofile create mode 100644 config/env/00-path.zsh create mode 100644 config/interactive/10-bootstrap.zsh create mode 100644 config/interactive/20-post-omz.zsh create mode 100644 config/login/.keep create mode 100755 deploy.sh create mode 100644 local/env.local.zsh.example create mode 100644 local/interactive.local.zsh.example create mode 100644 local/login.local.zsh.example diff --git a/.zprofile b/.zprofile new file mode 100644 index 0000000..ebab60c --- /dev/null +++ b/.zprofile @@ -0,0 +1,16 @@ +# 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. +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 + +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 f2df2a7..103b74c 100644 --- a/.zshenv +++ b/.zshenv @@ -1,26 +1,20 @@ -# Homebrew on macOS. .zshenv runs for every zsh (interactive, scripts, GUI-launched), -# and brew shellenv needs a readable CWD: cd to a safe dir first so launching from a -# TCC-protected directory (e.g. ~/Documents) does not break the eval or prompt for access. -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)" +# 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). +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 fi -# User-local binaries (pipx, uv tools, grok CLI, etc.). -# Lives here rather than .zshrc/.zprofile so non-interactive agent shells -# (Codex, Claude Code, CI zsh -c) still resolve user-installed tools. -if [ -d "$HOME/.local/bin" ]; then - case ":${PATH}:" in - *":$HOME/.local/bin:"*) ;; - *) PATH="$HOME/.local/bin:${PATH}" ;; - esac - export PATH -fi - -# Machine-local early env (cargo, private PATH bits). Survives easyzsh reinstalls -# of this file. Prefer this over editing ~/.zshenv by hand. -if [ -f "$HOME/.config/zshenv.local" ]; then - # shellcheck disable=SC1090,SC1091 - . "$HOME/.config/zshenv.local" +if [[ -f "$EASYZSH_HOME/local/env.local.zsh" ]]; then + # shellcheck disable=SC1090 + . "$EASYZSH_HOME/local/env.local.zsh" +elif [[ -f "$HOME/.config/zshenv.local" ]]; then + # Legacy path from earlier easyzsh layouts. + # shellcheck disable=SC1090 + . "$HOME/.config/zshenv.local" fi diff --git a/.zshrc b/.zshrc index e0c2e5f..45fff03 100644 --- a/.zshrc +++ b/.zshrc @@ -1,300 +1,17 @@ -# 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 +# 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). +EASYZSH_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/zsh" -export TERM="xterm-256color" -# If you come from bash you might have to change your $PATH. -# export PATH=$HOME/bin:/usr/local/bin:$PATH - -# 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 - -# Set name of the theme to load. Optionally, if you set this to "random" -# it'll load a random theme each time that oh-my-zsh is loaded. -# See https://github.com/robbyrussell/oh-my-zsh/wiki/Themes - -# 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" +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 fi -# Uncomment the following line to use case-sensitive completion. -# CASE_SENSITIVE="true" - -# Uncomment the following line to use hyphen-insensitive completion. Case -# sensitive completion must be off. _ and - will be interchangeable. -# HYPHEN_INSENSITIVE="true" - -# Uncomment the following line to disable bi-weekly auto-update checks. -# DISABLE_AUTO_UPDATE="true" - -# How often to auto-update (days). Replaces the older UPDATE_ZSH_DAYS variable. -zstyle ':omz:update' frequency 30 - -# Uncomment the following line to change how often to auto-update (in days). -# export UPDATE_ZSH_DAYS=13 - -# Uncomment the following line to disable colors in ls. -# DISABLE_LS_COLORS="true" - -# Uncomment the following line to disable auto-setting terminal title. -# DISABLE_AUTO_TITLE="true" - -# Uncomment the following line to enable command auto-correction. -# ENABLE_CORRECTION="true" - -# Uncomment the following line to display red dots whilst waiting for completion. -# COMPLETION_WAITING_DOTS="true" - -# Uncomment the following line if you want to disable marking untracked files -# under VCS as dirty. This makes repository status check for large repositories -# much, much faster. -# DISABLE_UNTRACKED_FILES_DIRTY="true" - -# Uncomment the following line if you want to change the command execution time -# stamp shown in the history command output. -# The optional three formats: "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" -# HIST_STAMPS="mm/dd/yyyy" - -# Would you like to use another custom folder than $ZSH/custom? -# ZSH_CUSTOM=/path/to/new-custom-folder - -# 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/ -# Example format: plugins=(rails git textmate ruby lighthouse) -# 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 like into your own config file under ~/.config/zshrc/ like this: -#plugins+=(zsh-nvm) - -# Remove plugins from the default list above in your own config file using: -# plugins=(${plugins:#pluginname}) -# plugins=(${plugins:#zsh-autosuggestions}) - -# fpath+="${ZSH_CUSTOM:-"$ZSH/custom"}/plugins/zsh-completions/src" # install zsh-completions, if you need it - -# source $ZSH/oh-my-zsh.sh # This is now run in .zshrc after importing user configs from ~/.config/zshrc/* files - -# User configuration - -# export MANPATH="/usr/local/man:$MANPATH" - -# You may need to manually set your language environment -# export LANG=en_US.UTF-8 - -# Preferred editor for local and remote sessions -# if [[ -n $SSH_CONNECTION ]]; then -# export EDITOR='vim' -# else -# export EDITOR='mvim' -# fi - -# Compilation flags -# export ARCHFLAGS="-arch x86_64" - -# ssh -# export SSH_KEY_PATH="~/.ssh/rsa_id" - -# Set personal aliases, overriding those provided by oh-my-zsh libs, -# plugins, and themes. Aliases can be placed here, though oh-my-zsh -# users are encouraged to define aliases within the ZSH_CUSTOM folder. -# For a full list of active aliases, run `alias`. -# -# Example aliases -# alias zshconfig="mate ~/.zshrc" -# alias ohmyzsh="mate ~/.oh-my-zsh" - -# ~/.local/bin is added in ~/.zshenv (every zsh, including non-interactive agents). -# Do not re-export it here; keep personal early-env overrides in ~/.config/zshenv.local. - -# export PATH=$PATH:~/.config/ezsh/bin - -# NPM_PACKAGES="${HOME}/.npm" -# PATH="$NPM_PACKAGES/bin:$PATH" - - - -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 - -autoload -U compinit && compinit -C -d ~/.cache/zsh/.zcompdump # zsh-completions -# autoload bashcompinit # bash completions -# bashcompinit - - -# QuickZsh -SAVEHIST=50000 #save upto 50,000 lines in history. oh-my-zsh default is 10,000 -#setopt hist_ignore_all_dups # dont record duplicated entries in history during a single session - -# NOTE: personal aliases that must win over oh-my-zsh are defined AFTER -# `source "$ZSH/oh-my-zsh.sh"` below, because oh-my-zsh's lib/directories.zsh -# re-defines several of them (e.g. `l`) when it loads. - -# CUSTOM FUNCTIONS - -# cheat sheets (github.com/chubin/cheat.sh), find out how to use commands -# example 'cheat tar' -# for language specific question supply 2 args first for language, second as the question -# eample: cheat python3 execute external program -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 -} - -# Matrix screen saver! will run if you have installed "cmatrix" -# TMOUT=900 -# TRAPALRM() { if command -v cmatrix &> /dev/null; then cmatrix -sb; 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() { - # Specify ip or your ip will be used - if [ "$1" ]; then - curl "https://api.db-ip.com/v2/free/$1" - else - curl "https://api.db-ip.com/v2/free/$(myip)" - fi -} - -# To customize prompt look, edit ~/.config/ezsh/p10k.zsh or run `p10k configure` -# [[ ! -f ~/.config/ezsh/p10k.zsh ]] || source ~/.config/ezsh/p10k.zsh # already in ZSH_CONFIGS_DIR - - -# Any zshrc configurations under the folder ~/.config/zshrc/ will override the default ezsh configs. -# Place all of your personal configurations over there -ZSH_CONFIGS_DIR="$HOME/.config/zshrc" - -for file in "$ZSH_CONFIGS_DIR"/*(DN); do - # Exclude '.' and '..' from being sourced - if [ -f "$file" ]; then - source "$file" - fi -done - -# Now source oh-my-zsh.sh so that any plugins added in ~/.config/zshrc/* files also get loaded -source "$ZSH/oh-my-zsh.sh" - -# Configs that can only work after "source $ZSH/oh-my-zsh.sh", such as Aliases that depend oh-my-zsh plugins - -# Personal aliases. Defined after oh-my-zsh so they win over oh-my-zsh's own -# aliases (lib/directories.zsh sets `l`, `ll`, etc. when it loads). -alias myip="wget -qO- https://wtfismyip.com/text" # quickly show external ip address -alias l="eza --hyperlink -lA --sort=modified --reverse --classify" # show all except . .. , sort by recent, / at the end of folders, clickable -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' # the new ls; add --hyperlink if you like -alias aa='eza -la --git --colour-scale all -g --smart-group --icons always -s modified -r --hyperlink' # sort by new - -# 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 Ctr+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" # human-readable sizes +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 ae13a81..399c56d 100644 --- a/README.md +++ b/README.md @@ -11,34 +11,68 @@ 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 an existing `~/.zshrc` -and `~/.zshenv` before replacing them. +skips the default-shell change. The installer backs up existing shell stubs +before replacing them. -## What gets installed where +From a git checkout (recommended for multi-machine sync): -| Path | Role | -| --- | --- | -| `~/.zshrc` | Canonical shell entry (from repo `.zshrc`). Stay close to upstream; reinstall overwrites it. | -| `~/.zshenv` | Early env for every zsh: Homebrew `shellenv` on macOS, plus `~/.local/bin` on PATH. | -| `~/.config/zshenv.local` | Optional machine-local early env (cargo, private PATH). Sourced by `~/.zshenv`; not overwritten by reinstall. | -| `~/.config/zshrc/*.zsh` | Your overlays, sourced by `~/.zshrc` **before** oh-my-zsh loads. | +```bash +./deploy.sh --migrate-zprofile +``` -Do not put personal tweaks into `~/.zshrc` if you want painless upgrades: interactive overlays go under `~/.config/zshrc/`; early PATH/env that non-interactive agents need go in `~/.config/zshenv.local`. +## Architecture + +```text +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 +``` + +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. + +| 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/*` | Machine-local env/login/interactive | **Never overwritten** | +| `~/.config/zshrc/*.zsh` | Personal/tool overlays (before omz) | Left alone | +| `~/.local/bin/` | User tools + intentional shims (e.g. `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 -Every `*.zsh` file in `~/.config/zshrc/` is sourced near the end of `~/.zshrc`, -just before oh-my-zsh loads, so it can add plugins and override defaults. +### Always-on (agents see this) + +```bash +# ~/.config/zsh/local/env.local.zsh +. "$HOME/.cargo/env" +``` + +### Login only (vendor hooks) + +```bash +# ~/.config/zsh/local/login.local.zsh +# OrbStack, MacPorts, product installers — not primary PATH policy +``` + +### Interactive overlays (before oh-my-zsh) ```bash # ~/.config/zshrc/local.zsh -plugins+=(docker) # add plugins -plugins=(${plugins:#zsh-autosuggestions}) # or remove one -alias gs='git status' +plugins+=(docker) +plugins=(${plugins:#zsh-autosuggestions}) ``` -Ready-made **tool** configs from this repo's `zshrc/` directory can be pulled in -by name (not personal samples): +Ready-made tool configs from this repo's `zshrc/` directory: ```bash curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash -s -- fnm pyenv @@ -46,6 +80,5 @@ curl -fsSL https://git.miomio.moe/mio/easyzsh/raw/branch/master/patch.sh | bash Current fragments: `fnm`, `nvm`, `pyenv`, `p10k`, `merlin_devbox`, `merlin_worker`. -Because these files load before oh-my-zsh, plugin and theme changes take effect; -aliases that must beat oh-my-zsh's own should be defined after oh-my-zsh loads -(see the note in `~/.zshrc`). +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`. diff --git a/config/env/00-path.zsh b/config/env/00-path.zsh new file mode 100644 index 0000000..bc575aa --- /dev/null +++ b/config/env/00-path.zsh @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 0000000..31549e2 --- /dev/null +++ b/config/interactive/10-bootstrap.zsh @@ -0,0 +1,120 @@ +# 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 new file mode 100644 index 0000000..5b30c93 --- /dev/null +++ b/config/interactive/20-post-omz.zsh @@ -0,0 +1,70 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..c1c4fde --- /dev/null +++ b/deploy.sh @@ -0,0 +1,132 @@ +#!/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/** +# +# Usage: +# ./deploy.sh +# ./deploy.sh --home /path/to/home +# ./deploy.sh --migrate-zprofile # move existing ~/.zprofile body into login.local + +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TARGET_HOME="${HOME}" +MIGRATE_ZPROFILE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --home) + TARGET_HOME="$2" + shift 2 + ;; + --migrate-zprofile) + MIGRATE_ZPROFILE=1 + shift + ;; + -h|--help) + sed -n '2,12p' "$0" + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +if [[ ! -d "$TARGET_HOME" ]]; then + echo "Home does not exist: $TARGET_HOME" >&2 + exit 1 +fi + +ZSH_CFG="${TARGET_HOME}/.config/zsh" +ts=$(date +%Y%m%d-%H%M%S) + +backup_file() { + local f="$1" + if [[ -e "$f" || -L "$f" ]]; then + cp -Pp "$f" "${f}.backup.${ts}" + echo "Backed up $f -> ${f}.backup.${ts}" + fi +} + +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" +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). + 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" + fi + fi +fi + +backup_file "${TARGET_HOME}/.zshenv" +backup_file "${TARGET_HOME}/.zshrc" +backup_file "${TARGET_HOME}/.zprofile" + +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/). +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 + +# Ensure intentional grok shim when the binary tree exists. +if [[ -e "${TARGET_HOME}/.grok/bin/grok" ]]; then + ln -sfn "${TARGET_HOME}/.grok/bin/grok" "${TARGET_HOME}/.local/bin/grok" + echo "Ensured ~/.local/bin/grok -> ~/.grok/bin/grok" +fi + +echo "Deploy complete." +echo "Verify: env -i HOME=\"$TARGET_HOME\" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv; command -v grok; echo PATH=\$PATH'" diff --git a/install.sh b/install.sh index 518c31d..13d66d3 100755 --- a/install.sh +++ b/install.sh @@ -241,50 +241,58 @@ 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. +# On download failure, leave existing ~/.zshrc and ~/.zshenv untouched. +easyzsh_stage=$(mktemp -d "$HOME/.easyzsh-stage.XXXXXX") zshrc_download=$(mktemp "$HOME/.zshrc.download.XXXXXX") zshenv_download=$(mktemp "$HOME/.zshenv.download.XXXXXX") -trap 'rm -f "$zshrc_download" "$zshenv_download"' EXIT -wget -q "https://git.miomio.moe/mio/easyzsh/raw/branch/master/.zshrc" -O "$zshrc_download" -wget -q "https://git.miomio.moe/mio/easyzsh/raw/branch/master/.zshenv" -O "$zshenv_download" +trap 'rm -rf "$easyzsh_stage"; rm -f "$zshrc_download" "$zshenv_download"' EXIT -# echo -e "The setup will be installed in '~/.config/ezsh'\n" -# echo -e "Place your personal zshrc config files under '~/.config/ezsh/zshrc/'\n" -# mkdir -p ~/.config/ezsh/zshrc # PLACE YOUR ZSHRC CONFIGURATIONS OVER THERE +download_easyzsh_file() { + local rel="$1" + local dest="$easyzsh_stage/$rel" + mkdir -p "$(dirname "$dest")" + if ! wget -q "${EASYZSH_RAW_BASE}/${rel}" -O "$dest"; then + echo "Failed to download ${rel} from ${EASYZSH_RAW_BASE}" >&2 + exit 1 + fi +} -# if [ -d ~/.quickzsh ]; then -# echo -e "\n PREVIOUS SETUP FOUND AT '~/.quickzsh'. PLEASE MANUALLY MOVE ANY FILES YOU'D LIKE TO '~/.config/ezsh' \n" -# fi +echo -e "Downloading easyzsh shell layout\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/env.local.zsh.example \ + local/login.local.zsh.example \ + local/interactive.local.zsh.example +do + download_easyzsh_file "$rel" +done +# Keep failure-detection temps in sync with legacy test expectations. +cp "$easyzsh_stage/.zshrc" "$zshrc_download" +cp "$easyzsh_stage/.zshenv" "$zshenv_download" +chmod +x "$easyzsh_stage/deploy.sh" echo -e "Installing oh-my-zsh\n" if [ -d "$ZSH" ]; then echo -e "oh-my-zsh is already installed\n" git -C "$ZSH" remote set-url origin https://github.com/ohmyzsh/ohmyzsh.git -# elif [ -d ~/.oh-my-zsh ]; then -# echo -e "oh-my-zsh in already installed at '~/.oh-my-zsh'. Moving it to '$ZSH'" -# export ZSH="$HOME/.config/ezsh/oh-my-zsh" -# mv ~/.oh-my-zsh $ZSH -# git -C $ZSH remote set-url origin https://github.com/ohmyzsh/ohmyzsh.git else git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "$ZSH" fi -if [ -e "$HOME/.zshrc" ] || [ -L "$HOME/.zshrc" ]; then - zshrc_backup=$(mktemp "$HOME/.zshrc.backup.$(date +"%Y%m%d-%H%M%S").XXXXXX") - cp -Pp "$HOME/.zshrc" "$zshrc_backup" - echo -e "Backed up the current .zshrc to $zshrc_backup\n" -fi -if [ -e "$HOME/.zshenv" ] || [ -L "$HOME/.zshenv" ]; then - zshenv_backup=$(mktemp "$HOME/.zshenv.backup.$(date +"%Y%m%d-%H%M%S").XXXXXX") - cp -Pp "$HOME/.zshenv" "$zshenv_backup" - echo -e "Backed up the current .zshenv to $zshenv_backup\n" -fi -mv "$zshrc_download" "$HOME/.zshrc" -mv "$zshenv_download" "$HOME/.zshenv" +# deploy.sh backs up existing stubs and never overwrites ~/.config/zsh/local/ +bash "$easyzsh_stage/deploy.sh" --migrate-zprofile trap - EXIT -# cp -f ezshrc.zsh ~/.config/ezsh/ -# cp -f p10k.zsh ~/.config/ezsh/ - +rm -rf "$easyzsh_stage" +rm -f "$zshrc_download" "$zshenv_download" mkdir -p ~/.cache/zsh/ # this will be used to store .zcompdump zsh completion cache files which normally clutter $HOME mkdir -p ~/.fonts # Create .fonts if doesn't exist diff --git a/local/env.local.zsh.example b/local/env.local.zsh.example new file mode 100644 index 0000000..79b5b68 --- /dev/null +++ b/local/env.local.zsh.example @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 0000000..040ae2d --- /dev/null +++ b/local/interactive.local.zsh.example @@ -0,0 +1,2 @@ +# 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 new file mode 100644 index 0000000..f78baa6 --- /dev/null +++ b/local/login.local.zsh.example @@ -0,0 +1,3 @@ +# 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 6ea0b07..fc7b03f 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -44,9 +44,24 @@ 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; do +for command_name in zsh git curl tar fc-cache eza zoxide autoconf trash install; 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 bash)" "$main_bin/bash" +ln -sf "$(command -v mktemp)" "$main_bin/mktemp" +ln -sf "$(command -v dirname)" "$main_bin/dirname" +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" @@ -68,7 +83,11 @@ 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/.zprofile" \ + "$ROOT"/config/env/*.zsh \ + "$ROOT"/config/interactive/*.zsh \ + "$ROOT"/zshrc/*.zsh; do + [[ -f "$config" ]] || continue zsh -n "$config" done @@ -78,32 +97,45 @@ 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/.zshrc" || fail "existing extensionless Zsh configs are ignored" -grep -Fq "alias rm='/usr/bin/trash'" "$ROOT/.zshrc" || fail "macOS rm does not use the native Trash" -grep -Fq "alias rm='gio trash'" "$ROOT/.zshrc" || fail "Linux rm does not use the desktop Trash" +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 'libglib2.0-bin' "$ROOT/install.sh" || fail "Ubuntu Trash support is not installed" -grep -Fq 'cp -Pp "$HOME/.zshrc"' "$ROOT/install.sh" || fail ".zshrc symlinks are not preserved in backups" -grep -Fq '[ -L "$HOME/.zshrc" ]' "$ROOT/install.sh" || fail "broken .zshrc symlinks are not backed up" -grep -Fq 'cp -Pp "$HOME/.zshenv"' "$ROOT/install.sh" || fail ".zshenv symlinks are not preserved in backups" -grep -Fq '[ -L "$HOME/.zshenv" ]' "$ROOT/install.sh" || fail "broken .zshenv symlinks are not backed up" -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 'zshenv.local' "$ROOT/.zshenv" || fail "machine-local zshenv.local hook is missing" -if grep -Fq 'brew shellenv' "$ROOT/.zshrc"; then - fail "Homebrew still initializes from interactive-only .zshrc" +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 '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" +if grep -Fq 'brew shellenv' "$ROOT/.zshrc" "$ROOT/config/interactive"/*.zsh; then + fail "Homebrew still initializes from interactive-only files" fi -# 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" -printf '%s\n' '#!/bin/sh' 'echo ok' >"$local_bin_home/.local/bin/easyzsh-path-probe" -chmod +x "$local_bin_home/.local/bin/easyzsh-path-probe" -HOME="$local_bin_home" PATH=/usr/bin:/bin \ - zsh -c '. "$1"; command -v easyzsh-path-probe >/dev/null' zsh "$ROOT/.zshenv" || - fail "~/.local/bin is not available to non-interactive zsh via .zshenv" [[ ! -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 +# deploy.sh installs managed layout and preserves local/ +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" +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" +[[ -L "$deploy_home/.local/bin/grok" ]] || fail "deploy.sh did not create grok shim" + +# 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" +printf '%s\n' '#!/bin/sh' 'echo ok' >"$local_bin_home/.local/bin/easyzsh-path-probe" +chmod +x "$local_bin_home/.local/bin/easyzsh-path-probe" +HOME="$local_bin_home" bash "$ROOT/deploy.sh" --home "$local_bin_home" >/dev/null +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" + echo "installer checks passed"