#!/usr/bin/env bash # Install easyzsh shell stubs into a home directory. # Overwrites: ~/.zshenv ~/.zshrc # Installs: ~/.zprofile (re-applies ~/.zshenv after macOS path_helper) # Never overwrites: ~/.config/zsh/local.zsh ~/.config/zshrc/** ~/.local/bin/** # Extra ~/.zprofile lines stay as login-only hooks unless --migrate-zprofile # folds them into local.zsh. # # Usage: # ./deploy.sh # ./deploy.sh --home /path/to/home # ./deploy.sh --migrate-zprofile # fold extra ~/.zprofile lines into local.zsh 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" LOCAL_ZSH="${ZSH_CFG}/local.zsh" ZPROFILE="${TARGET_HOME}/.zprofile" 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 } file_has_content() { [[ -f "$1" ]] && grep -q '[^[:space:]]' "$1" } ZPROFILE_MARKER='# easyzsh: reapply always-on PATH after macOS path_helper' PYENV_LOGIN_HOOK='[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh" ]] && . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh"' is_easyzsh_zprofile_stub() { local f="$1" [[ -f "$f" ]] || return 1 # Comment-only placeholder from earlier easyzsh deploys. if grep -Eqv '^[[:space:]]*(#|$)' "$f"; then return 1 fi grep -Eq 'easyzsh|PATH lives in|Leave this file empty|prefer.*local\.zsh' "$f" } is_pyenv_only_zprofile() { local f="$1" local body [[ -f "$f" ]] || return 1 body=$(grep -Ev '^[[:space:]]*(#|$)' "$f" || true) [[ "$body" == "$PYENV_LOGIN_HOOK" ]] } # Lines that are not the managed path_helper block or the old pyenv login hook. zprofile_extras() { local f="$1" [[ -f "$f" ]] || return 0 awk -v marker="$ZPROFILE_MARKER" -v hook="$PYENV_LOGIN_HOOK" ' $0 == marker { in_block=1; next } in_block && $0 == "fi" { in_block=0; next } in_block { next } $0 == hook { next } { print } ' "$f" } echo "Deploying easyzsh shell stubs into $TARGET_HOME" mkdir -p \ "${ZSH_CFG}" \ "${TARGET_HOME}/.config/zshrc" \ "${TARGET_HOME}/.cache/zsh" \ "${TARGET_HOME}/.local/bin" # Migrate legacy fragments into 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 cp "$tmp" "$LOCAL_ZSH" echo "Migrated machine-local fragments -> ~/.config/zsh/local.zsh" fi rm -f "$tmp" fi # ~/.zprofile re-applies ~/.zshenv after macOS path_helper. # Replace comment-only stubs and the old pyenv-only hook. Keep other login # lines, or fold them into local.zsh when --migrate-zprofile is set. zprofile_extras_file=$(mktemp) : >"$zprofile_extras_file" if [[ -f "$ZPROFILE" ]] && ! is_easyzsh_zprofile_stub "$ZPROFILE" && ! is_pyenv_only_zprofile "$ZPROFILE"; then zprofile_extras "$ZPROFILE" | awk ' /Created by `pipx`/ { skip_pipx=1; next } skip_pipx && /local\/bin/ { skip_pipx=0; next } skip_pipx { next } { print } ' >"$zprofile_extras_file" fi if [[ "$MIGRATE_ZPROFILE" -eq 1 ]] && file_has_content "$zprofile_extras_file"; then if [[ -f "$LOCAL_ZSH" ]]; then printf '\n' >>"$LOCAL_ZSH" cat "$zprofile_extras_file" >>"$LOCAL_ZSH" else cp "$zprofile_extras_file" "$LOCAL_ZSH" fi echo "Migrated extra ~/.zprofile lines -> ~/.config/zsh/local.zsh" : >"$zprofile_extras_file" fi backup_file "${TARGET_HOME}/.zshenv" backup_file "${TARGET_HOME}/.zshrc" backup_file "$ZPROFILE" install -m 644 "${ROOT}/.zshenv" "${TARGET_HOME}/.zshenv" install -m 644 "${ROOT}/.zshrc" "${TARGET_HOME}/.zshrc" if file_has_content "$zprofile_extras_file"; then { cat "${ROOT}/.zprofile" printf '\n' cat "$zprofile_extras_file" } >"$ZPROFILE" echo "Installed ~/.zprofile (kept extra login lines)" else install -m 644 "${ROOT}/.zprofile" "$ZPROFILE" echo "Installed ~/.zprofile" fi rm -f "$zprofile_extras_file" # 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" # Grok is `grok`. Cursor owns `agent` when its CLI is present. 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 agent_link="${TARGET_HOME}/.local/bin/agent" cursor_agent="${TARGET_HOME}/.local/bin/cursor-agent" agent_target="" if [[ -L "$agent_link" ]]; then agent_target=$(readlink "$agent_link") fi if [[ -x "$cursor_agent" ]]; then if [[ ! -e "$agent_link" || "$agent_target" == *'/.grok/'* ]]; then ln -sfn "$cursor_agent" "$agent_link" echo "Ensured ~/.local/bin/agent -> cursor-agent" fi elif [[ -L "$agent_link" && "$agent_target" == *'/.grok/'* ]]; then rm -f "$agent_link" echo "Removed ~/.local/bin/agent (was Grok; Cursor is not installed)" fi echo "Deploy complete." echo "Verify: env -i HOME=\"$TARGET_HOME\" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv; command -v grok; command -v agent'"