#!/usr/bin/env bash # lib/shell.sh — Detect and install shell frameworks / version managers that # are referenced in the user's dotfiles but are not yet present on disk. # # Why this exists: # Mackup restores dotfiles (e.g. ~/.zshrc) but not the *tools* those files # source. If ~/.zshrc contains `source ~/.oh-my-zsh/oh-my-zsh.sh` and # Oh My Zsh is not installed, every new shell session prints an error. # # Architecture: # - _SSR_SHELL_DEPS defines built-in known frameworks (name|check|install). # - SSR_SHELL_DEPS_EXTRA in ssr.conf allows the user to add custom entries # using the same pipe-delimited format. # - ssr::shell::detect_and_install iterates all entries, runs the check, # and invokes the installer when the check fails. # --------------------------------------------------------------------------- # Built-in dependency definitions. # Format: "name|check_expression|install_command" # check_expression — any bash expression; success (exit 0) = already OK. # install_command — run as the current user (no sudo). # --------------------------------------------------------------------------- _SSR_SHELL_DEPS=( # Oh My Zsh — most common zsh framework "oh-my-zsh\ |[[ -d \"\$HOME/.oh-my-zsh\" ]]\ |RUNZSH=no CHSH=no sh -c \"\$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\"" # Prezto — lightweight zsh framework "prezto\ |[[ -d \"\$HOME/.zprezto\" ]]\ |git clone --recursive https://github.com/sorin-ionescu/prezto.git \"\${ZDOTDIR:-\$HOME}/.zprezto\"" # Zinit — zsh plugin manager "zinit\ |[[ -d \"\${ZINIT_HOME:-\$HOME/.local/share/zinit/zinit.git}\" ]]\ |bash -c \"\$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)\"" # Antigen — zsh plugin manager "antigen\ |[[ -f \"\$HOME/.antigen.zsh\" ]] || [[ -f \"\$HOME/.config/antigen/antigen.zsh\" ]]\ |mkdir -p \"\$HOME/.config/antigen\" && curl -sSL git.io/antigen -o \"\$HOME/.config/antigen/antigen.zsh\"" # nvm — Node Version Manager "nvm\ |[[ -d \"\${NVM_DIR:-\$HOME/.nvm}\" ]]\ |PROFILE=/dev/null bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/install.sh)\"" # pyenv — Python version manager "pyenv\ |command -v pyenv >/dev/null 2>&1 || [[ -d \"\$HOME/.pyenv\" ]]\ |brew install pyenv" # rbenv — Ruby version manager "rbenv\ |command -v rbenv >/dev/null 2>&1 || [[ -d \"\$HOME/.rbenv\" ]]\ |brew install rbenv" # rustup — Rust toolchain installer "rustup\ |command -v rustup >/dev/null 2>&1 || [[ -d \"\$HOME/.rustup\" ]]\ |curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path" # SDKMAN — JVM version manager (Java, Kotlin, Groovy, …) "sdkman\ |[[ -d \"\$HOME/.sdkman\" ]]\ |curl -s https://get.sdkman.io | bash" # Starship prompt "starship\ |command -v starship >/dev/null 2>&1\ |brew install starship" # Powerlevel10k — when installed standalone (not via OMZ/zinit) "powerlevel10k\ |[[ -d \"\${ZSH_CUSTOM:-\$HOME/.oh-my-zsh/custom}/themes/powerlevel10k\" ]] \ || [[ -d \"\$HOME/.local/share/powerlevel10k\" ]]\ |git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \"\${ZSH_CUSTOM:-\$HOME/.oh-my-zsh/custom}/themes/powerlevel10k\"" # asdf — universal version manager "asdf\ |command -v asdf >/dev/null 2>&1 || [[ -d \"\$HOME/.asdf\" ]]\ |brew install asdf" # mise (formerly rtx) — fast version manager "mise\ |command -v mise >/dev/null 2>&1\ |brew install mise" ) # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- # Return a list of RC files that are likely to reference external frameworks. _ssr_shell_rc_files() { local files=( "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" ) local f out=() for f in "${files[@]}"; do [[ -f "$f" ]] && out+=("$f") done printf '%s\n' "${out[@]}" } # Given a dependency name, return true if that name is referenced in any RC file. # This lets us skip installers for things the user's dotfiles don't actually use. _ssr_shell_dep_referenced() { local name="$1" local pattern case "$name" in oh-my-zsh) pattern="oh-my-zsh" ;; prezto) pattern="zprezto" ;; zinit) pattern="zinit" ;; antigen) pattern="antigen" ;; nvm) pattern="NVM_DIR\|nvm.sh" ;; pyenv) pattern="pyenv" ;; rbenv) pattern="rbenv" ;; rustup) pattern="rustup\|cargo" ;; sdkman) pattern="sdkman\|SDKMAN_DIR" ;; starship) pattern="starship" ;; powerlevel10k) pattern="powerlevel10k\|p10k" ;; asdf) pattern="asdf" ;; mise) pattern="mise" ;; *) return 0 ;; # unknown — always consider referenced esac local rcfile while IFS= read -r rcfile; do grep -qE "$pattern" "$rcfile" 2>/dev/null && return 0 done < <(_ssr_shell_rc_files) return 1 } # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- # Detect missing shell frameworks and install them. # Iterates _SSR_SHELL_DEPS + SSR_SHELL_DEPS_EXTRA (from ssr.conf). # # Each entry: "name|check_expression|install_command" # # Skips any entry whose name is NOT referenced in the user's RC files # (avoids installing things that are irrelevant to this machine). ssr::shell::detect_and_install() { local -a all_deps=("${_SSR_SHELL_DEPS[@]}") # Append user-defined extras (SSR_SHELL_DEPS_EXTRA set in ssr.conf). if [[ -n "${SSR_SHELL_DEPS_EXTRA:-}" ]]; then local entry for entry in "${SSR_SHELL_DEPS_EXTRA[@]}"; do all_deps+=("$entry") done fi local name check_expr install_cmd local installed=0 skipped=0 for dep in "${all_deps[@]}"; do # Parse pipe-delimited fields. name="$( printf '%s' "$dep" | cut -d'|' -f1)" check_expr="$( printf '%s' "$dep" | cut -d'|' -f2)" install_cmd="$( printf '%s' "$dep" | cut -d'|' -f3)" # Skip if not referenced in any RC file (reduces noise on clean systems). if ! _ssr_shell_dep_referenced "$name"; then skipped=$((skipped + 1)) continue fi # Run the check expression; skip if already satisfied. if eval "$check_expr" 2>/dev/null; then ssr::log "shell dep OK: $name" continue fi ssr::warn "Shell dep missing: $name — installing…" if eval "$install_cmd" 2>&1 | { while IFS= read -r line; do ssr::log " [$name] $line"; done; }; then ssr::ok "Installed: $name" installed=$((installed + 1)) else ssr::warn "Install failed: $name (non-fatal — continue manually)" fi done if [[ $installed -gt 0 ]]; then ssr::ok "Shell framework setup: $installed installed, $skipped not referenced (skipped)" else ssr::log "Shell framework setup: all referenced deps already present" fi }