#!/usr/bin/env bash set -euo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) TEMP_DIR=$(mktemp -d) trap 'rm -rf "$TEMP_DIR"' EXIT fail() { echo "FAIL: $*" >&2 exit 1 } assert_download_failure_is_reported() { local home="$TEMP_DIR/home-${1%.sh}" mkdir -p "$home" if HOME="$home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/$1" >"$home/output" 2>&1; then fail "$1 returned success after curl failed" fi } mkdir -p "$TEMP_DIR/bin" ln -s /usr/bin/false "$TEMP_DIR/bin/curl" ln -s /usr/bin/true "$TEMP_DIR/bin/unzip" for script in install_fnm.sh install_nvm.sh install_pyenv.sh install_merlin_worker.sh; do assert_download_failure_is_reported "$script" done patch_home="$TEMP_DIR/patch-home" mkdir -p "$patch_home/.config/zshrc" cp "$ROOT/README.md" "$patch_home/.config/zshrc/missing.zsh" if HOME="$patch_home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/patch.sh" missing >/dev/null 2>&1; then fail "patch.sh returned success after curl failed" fi cmp -s "$ROOT/README.md" "$patch_home/.config/zshrc/missing.zsh" || fail "patch.sh replaced an existing config after curl failed" if HOME="$patch_home" PATH="$TEMP_DIR/bin:/usr/bin:/bin" /bin/bash "$ROOT/patch.sh" ../invalid >/dev/null 2>&1; then fail "patch.sh accepted an unsafe config name" 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 ln -s /usr/bin/true "$main_bin/$command_name" done 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" 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" 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 -s /usr/bin/false "$main_bin/wget" cp "$ROOT/README.md" "$main_home/.zshrc" printf '%s\n' 'existing-zshenv' >"$main_home/.zshenv" if HOME="$main_home" PATH="$main_bin:/usr/bin:/bin" /bin/bash "$ROOT/install.sh" --non-interactive >/dev/null 2>&1; then fail "install.sh returned success after the zshrc download failed" fi cmp -s "$ROOT/README.md" "$main_home/.zshrc" || fail "install.sh replaced .zshrc after its download failed" printf '%s\n' 'existing-zshenv' | cmp -s - "$main_home/.zshenv" || fail "install.sh replaced .zshenv after a download failed" fnm_home="$TEMP_DIR/fnm-home" mkdir -p "$fnm_home/.local/share/fnm" ln -s /usr/bin/true "$fnm_home/.local/share/fnm/fnm" HOME="$fnm_home" PATH=/usr/bin:/bin zsh -c 'source "$1"; command -v fnm >/dev/null' zsh "$ROOT/zsh/fnm.zsh" || fail "fnm is not restored to PATH in a new Zsh session" for script in "$ROOT"/*.sh; do bash -n "$script" done for config in "$ROOT/.zshenv" "$ROOT/.zshrc" "$ROOT/.zprofile" "$ROOT"/zshrc/*.zsh "$ROOT"/zsh/*.zsh; do [[ -f "$config" ]] || continue zsh -n "$config" done [[ -f "$ROOT/.zprofile" ]] || fail "easyzsh should ship a managed .zprofile" grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$ROOT/.zprofile" || fail ".zprofile is missing the path_helper reapply marker" [[ -f "$ROOT/zsh/fnm.zsh" ]] || fail "always-on fnm fragment is missing" [[ ! -e "$ROOT/zshrc/fnm.zsh" ]] || fail "legacy interactive fnm fragment still exists" [[ -f "$ROOT/zsh/pyenv.zsh" ]] || fail "always-on pyenv fragment is missing" [[ ! -e "$ROOT/zshrc/pyenv.zsh" ]] || fail "legacy interactive pyenv fragment still exists" grep -q 'NVM_VERSION="v0.40.4"' "$ROOT/install_nvm.sh" || fail "nvm version is stale" grep -q 'apt-get install -y unzip' "$ROOT/install_fnm.sh" || fail "Ubuntu unzip installation is missing" grep -q 'tail -1 || true' "$ROOT/install_merlin_worker.sh" || fail "a missing PYTHONPATH remains fatal" 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 '.zprofile' "$ROOT/install.sh" || fail "install.sh does not download .zprofile" grep -Fq 'mktemp "$(dirname "$destination")/.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 '_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 'OPTIONAL_ADDONS=' "$ROOT/install.sh" || fail "install.sh missing OPTIONAL_ADDONS catalog" grep -Fq -- '--with' "$ROOT/install.sh" || fail "install.sh missing --with option" # --with parses known add-ons and rejects unknown ones without network. if HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --with nosuch >/dev/null 2>&1; then fail "install.sh accepted an unknown --with add-on" fi if ! HOME="$TEMP_DIR" PATH="/usr/bin:/bin" bash "$ROOT/install.sh" --help >/dev/null; then fail "install.sh --help failed" fi 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 '${path:#$HOME/.grok/bin}' "$ROOT/.zshenv" || fail ".zshenv does not drop ~/.grok/bin" grep -Fq '${path:#$HOME/.grok/bin}' "$ROOT/.zshrc" || fail ".zshrc does not drop ~/.grok/bin" grep -Fq 'alias agent=$HOME/.local/bin/cursor-agent' "$ROOT/.zshrc" || fail ".zshrc does not alias agent to Cursor" if grep -Eq '^[[:space:]]*(export[[:space:]]+)?PATH=.*\.grok/bin' "$ROOT/.zshenv" "$ROOT/.zshrc"; then fail "easyzsh stubs must not prepend ~/.grok/bin" fi grep -Fq 'local.zsh' "$ROOT/.zshenv" || fail "local.zsh is not sourced from .zshenv" grep -Fq 'ZSH_ALWAYS_ON_DIR' "$ROOT/.zshenv" || fail "always-on ~/.config/zsh/*.zsh glob is missing from .zshenv" grep -Fq 'zsh/fnm' "$ROOT/install_fnm.sh" || fail "install_fnm.sh does not patch always-on zsh/fnm" grep -Fq 'zsh/pyenv' "$ROOT/install_pyenv.sh" || fail "install_pyenv.sh does not patch always-on zsh/pyenv" 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 stubs and preserves local.zsh; removes obsolete tree deploy_home="$TEMP_DIR/deploy-home" 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/.zshrc" ]] || fail "deploy.sh did not install .zshrc" grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$deploy_home/.zprofile" || fail "deploy.sh did not install .zprofile" 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" # Grok's `agent` must not keep the name when Cursor is absent. printf '%s\n' '#!/bin/sh' 'echo grok-agent' >"$deploy_home/.grok/bin/agent" chmod +x "$deploy_home/.grok/bin/agent" ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent" HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home" >/dev/null [[ ! -e "$deploy_home/.local/bin/agent" ]] || fail "deploy.sh left Grok bound to agent without Cursor" # Cursor takes `agent`; Grok stays on `grok`. printf '%s\n' '#!/bin/sh' 'echo cursor-agent' >"$deploy_home/.local/bin/cursor-agent" chmod +x "$deploy_home/.local/bin/cursor-agent" ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent" HOME="$deploy_home" bash "$ROOT/deploy.sh" --home "$deploy_home" >/dev/null [[ "$(readlink "$deploy_home/.local/bin/agent")" == "$deploy_home/.local/bin/cursor-agent" ]] || fail "deploy.sh did not point agent at cursor-agent" [[ "$(readlink "$deploy_home/.local/bin/grok")" == "$deploy_home/.grok/bin/grok" ]] || fail "deploy.sh did not keep the grok shim" # .zshenv repairs a Grok-stolen agent without deploy. ln -sfn "$deploy_home/.grok/bin/agent" "$deploy_home/.local/bin/agent" HOME="$deploy_home" PATH=/usr/bin:/bin zsh -c '. ~/.zshenv' || fail ".zshenv failed while repairing agent" [[ "$(readlink "$deploy_home/.local/bin/agent")" == "$deploy_home/.local/bin/cursor-agent" ]] || fail ".zshenv did not retarget a Grok-stolen agent" # 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" # Non-interactive zsh must load always-on ~/.config/zsh/*.zsh (fnm) via .zshenv. fnm_env_home="$TEMP_DIR/fnm-env-home" mkdir -p "$fnm_env_home/.local/share/fnm" "$fnm_env_home/.config/zsh" ln -s /usr/bin/true "$fnm_env_home/.local/share/fnm/fnm" cp "$ROOT/zsh/fnm.zsh" "$fnm_env_home/.config/zsh/fnm.zsh" HOME="$fnm_env_home" bash "$ROOT/deploy.sh" --home "$fnm_env_home" >/dev/null HOME="$fnm_env_home" PATH=/usr/bin:/bin \ zsh -c 'command -v fnm >/dev/null' || fail "always-on fnm is not available to non-interactive zsh via .zshenv" # Non-interactive zsh must resolve pyenv's global Python via .zshenv. pyenv_env_home="$TEMP_DIR/pyenv-env-home" pyenv_env_xdg="$pyenv_env_home/xdg" mkdir -p "$pyenv_env_home/.pyenv/shims" "$pyenv_env_xdg/zsh" ln -s /usr/bin/true "$pyenv_env_home/.pyenv/shims/python3" cp "$ROOT/zsh/pyenv.zsh" "$pyenv_env_xdg/zsh/pyenv.zsh" HOME="$pyenv_env_home" bash "$ROOT/deploy.sh" --home "$pyenv_env_home" >/dev/null HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \ PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \ zsh -c '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' || fail "pyenv Python is not available to non-interactive zsh via .zshenv" # Login zsh must reapply always-on PATH after the system profile (path_helper). HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \ PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \ zsh -lc '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' || fail "pyenv Python is not restored in login zsh after the system profile" # A later easyzsh migration still restores pyenv; the old pyenv-only hook is replaced. pyenv_login_hook='[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh" ]] && . "${XDG_CONFIG_HOME:-$HOME/.config}/zsh/pyenv.zsh"' printf '%s\n' "$pyenv_login_hook" > "$pyenv_env_home/.zprofile" HOME="$pyenv_env_home" bash "$ROOT/deploy.sh" --home "$pyenv_env_home" --migrate-zprofile >/dev/null grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$pyenv_env_home/.zprofile" || fail "deploy.sh did not replace the pyenv-only login hook with the path_helper reapply" if grep -Fqx "$pyenv_login_hook" "$pyenv_env_home/.zprofile"; then fail "deploy.sh left the obsolete pyenv login hook in .zprofile" fi HOME="$pyenv_env_home" XDG_CONFIG_HOME="$pyenv_env_xdg" \ PYENV_ROOT="$pyenv_env_home/.pyenv" PATH=/usr/bin:/bin \ zsh -lc '[[ "$(command -v python3)" == "$HOME/.pyenv/shims/python3" ]]' || fail "pyenv Python was lost after a later easyzsh migration" # Extra login lines are folded into local.zsh on --migrate-zprofile; .zprofile stays managed. profile_comment_home="$TEMP_DIR/profile-comment-home" mkdir -p "$profile_comment_home" printf '%s\n' '# pyenv config: ~/.config/zsh/pyenv.zsh' 'ordinary-login-hook' > "$profile_comment_home/.zprofile" HOME="$profile_comment_home" bash "$ROOT/deploy.sh" --home "$profile_comment_home" --migrate-zprofile >/dev/null grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$profile_comment_home/.zprofile" || fail "deploy.sh did not install the managed .zprofile after migrating extra lines" if grep -Fxq 'ordinary-login-hook' "$profile_comment_home/.zprofile"; then fail "deploy.sh left migrated login lines in .zprofile" fi grep -Fxq 'ordinary-login-hook' "$profile_comment_home/.config/zsh/local.zsh" || fail "deploy.sh did not migrate an ordinary profile mentioning pyenv" # Without --migrate-zprofile, extra login lines stay in .zprofile below the reapply block. profile_keep_home="$TEMP_DIR/profile-keep-home" mkdir -p "$profile_keep_home" printf '%s\n' 'ordinary-login-hook' > "$profile_keep_home/.zprofile" HOME="$profile_keep_home" bash "$ROOT/deploy.sh" --home "$profile_keep_home" >/dev/null grep -Fqx '# easyzsh: reapply always-on PATH after macOS path_helper' "$profile_keep_home/.zprofile" || fail "deploy.sh did not install the managed .zprofile in front of extra login lines" grep -Fxq 'ordinary-login-hook' "$profile_keep_home/.zprofile" || fail "deploy.sh dropped extra login lines without --migrate-zprofile" # ~/.local/bin must beat path_helper-promoted system binaries in login zsh. tee_login_home="$TEMP_DIR/tee-login-home" mkdir -p "$tee_login_home/.local/bin" printf '%s\n' '#!/bin/sh' 'echo local-tee' >"$tee_login_home/.local/bin/tee" chmod +x "$tee_login_home/.local/bin/tee" HOME="$tee_login_home" bash "$ROOT/deploy.sh" --home "$tee_login_home" >/dev/null HOME="$tee_login_home" PATH=/usr/bin:/bin \ zsh -c '[[ "$(command -v tee)" == "$HOME/.local/bin/tee" ]]' || fail "~/.local/bin/tee is not preferred in non-login zsh" HOME="$tee_login_home" PATH=/usr/bin:/bin \ zsh -lc '[[ "$(command -v tee)" == "$HOME/.local/bin/tee" ]]' || fail "~/.local/bin/tee is not restored in login zsh after the system profile" # Login zsh must prefer fnm-managed npm over Homebrew/system npm. fnm_login_home="$TEMP_DIR/fnm-login-home" mkdir -p "$fnm_login_home/.local/share/fnm/shim/bin" "$fnm_login_home/.config/zsh" cat >"$fnm_login_home/.local/share/fnm/fnm" <<'EOF' #!/bin/bash if [[ "$1" == "env" ]]; then shim="$HOME/.local/share/fnm/shim" printf '%s\n' "export PATH=\"${shim}/bin:\$PATH\"" printf '%s\n' "export FNM_MULTISHELL_PATH=\"${shim}\"" exit 0 fi exit 0 EOF chmod +x "$fnm_login_home/.local/share/fnm/fnm" printf '%s\n' '#!/bin/sh' 'echo fnm-npm' >"$fnm_login_home/.local/share/fnm/shim/bin/npm" chmod +x "$fnm_login_home/.local/share/fnm/shim/bin/npm" cp "$ROOT/zsh/fnm.zsh" "$fnm_login_home/.config/zsh/fnm.zsh" HOME="$fnm_login_home" bash "$ROOT/deploy.sh" --home "$fnm_login_home" >/dev/null HOME="$fnm_login_home" PATH=/usr/bin:/bin \ zsh -c '[[ "$(command -v npm)" == "$HOME/.local/share/fnm/shim/bin/npm" ]]' || fail "fnm npm is not preferred in non-login zsh" HOME="$fnm_login_home" PATH=/usr/bin:/bin \ zsh -lc '[[ "$(command -v npm)" == "$HOME/.local/share/fnm/shim/bin/npm" ]]' || fail "fnm npm is not restored in login zsh after the system profile" # Re-sourcing fnm.zsh must reuse the existing multishell instead of stacking PATH. HOME="$fnm_login_home" PATH=/usr/bin:/bin zsh -c ' source "$HOME/.config/zsh/fnm.zsh" first=$FNM_MULTISHELL_PATH source "$HOME/.config/zsh/fnm.zsh" [[ $FNM_MULTISHELL_PATH == $first ]] || exit 1 n=0 for p in $path; do [[ $p == "$first/bin" ]] && n=$((n + 1)) done (( n == 1 )) ' || fail "fnm.zsh stacked a second multishell on PATH when re-sourced" # patch.sh path form installs into ~/.config/zsh/ path_patch_home="$TEMP_DIR/path-patch-home" path_bin="$TEMP_DIR/path-bin" mkdir -p "$path_patch_home" "$path_bin" cat >"$path_bin/curl" <<'CURL' #!/bin/bash out="" url="" while [[ $# -gt 0 ]]; do case "$1" in -o) out="$2"; shift 2 ;; -*) shift ;; *) url="$1"; shift ;; esac done [[ "$url" == */zsh/fnm.zsh ]] || exit 1 printf '%s\n' '# patched-fnm' >"$out" CURL chmod +x "$path_bin/curl" # Prefer real coreutils for mktemp/mkdir used by patch.sh HOME="$path_patch_home" XDG_CONFIG_HOME="$path_patch_home/xdg" PATH="$path_bin:/usr/bin:/bin" \ bash "$ROOT/patch.sh" zsh/fnm >/dev/null grep -Fxq '# patched-fnm' "$path_patch_home/xdg/zsh/fnm.zsh" || fail "patch.sh zsh/fnm did not install ~/.config/zsh/fnm.zsh" echo "installer checks passed"