feat: auto-detect and install missing shell frameworks after restore
After mackup restores dotfiles, ~/.zshrc may reference tools (Oh My Zsh,
nvm, pyenv, rustup, …) that haven't been installed yet, causing errors on
first shell login.
lib/shell.sh:
- Defines 13 built-in frameworks with detect + install commands
- Scans ~/.zshrc / ~/.bashrc etc. for references before installing
(avoids installing things the user doesn't actually use)
- SSR_SHELL_DEPS_EXTRA in ssr.conf for custom user additions
bin/ssr-restore:
- Adds `shell_deps` as a resumable step after mackup_restore
- Sources ~/.config/ssr/post-restore.sh if present (user hook)
config/post-restore.sh.example:
- Template with commented examples (OMZ plugins, npm globals, SSH keys)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,6 +23,8 @@ source "$SSR_ROOT/lib/macos-prefs.sh"
|
|||||||
source "$SSR_ROOT/lib/restore-state.sh"
|
source "$SSR_ROOT/lib/restore-state.sh"
|
||||||
# shellcheck source=../lib/icloud.sh
|
# shellcheck source=../lib/icloud.sh
|
||||||
source "$SSR_ROOT/lib/icloud.sh"
|
source "$SSR_ROOT/lib/icloud.sh"
|
||||||
|
# shellcheck source=../lib/shell.sh
|
||||||
|
source "$SSR_ROOT/lib/shell.sh"
|
||||||
|
|
||||||
ssr::require_macos
|
ssr::require_macos
|
||||||
ssr::load_config
|
ssr::load_config
|
||||||
@@ -100,6 +102,14 @@ ssr::rs::run mackup_restore "Mackup restore" _ssr_mackup_restore_step
|
|||||||
# by Mackup (com.apple.dock.plist etc.) without requiring a logout.
|
# by Mackup (com.apple.dock.plist etc.) without requiring a logout.
|
||||||
ssr::macos::restart_ui
|
ssr::macos::restart_ui
|
||||||
|
|
||||||
|
# After dotfiles are in place, install any shell frameworks / version
|
||||||
|
# managers they reference that aren't already present (e.g. Oh My Zsh).
|
||||||
|
if [[ "${SSR_SHELL_DEPS_ENABLED:-true}" == "true" ]]; then
|
||||||
|
ssr::rs::run shell_deps "Shell framework deps" ssr::shell::detect_and_install
|
||||||
|
else
|
||||||
|
ssr::rs::set "step_shell_deps" "skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
ssr::rs::run macos_defaults "macOS defaults" ssr::macos::apply_defaults
|
ssr::rs::run macos_defaults "macOS defaults" ssr::macos::apply_defaults
|
||||||
|
|
||||||
if [[ "${SSR_SNAPSHOT_MACOS_PREFS:-true}" == "true" ]]; then
|
if [[ "${SSR_SNAPSHOT_MACOS_PREFS:-true}" == "true" ]]; then
|
||||||
@@ -112,6 +122,20 @@ fi
|
|||||||
# resumable step — it's side-effect-only and always safe to re-run).
|
# resumable step — it's side-effect-only and always safe to re-run).
|
||||||
ssr::macos::open_installed_casks
|
ssr::macos::open_installed_casks
|
||||||
|
|
||||||
|
# --- User-defined post-restore hook --------------------------------------
|
||||||
|
# Sources ~/.config/ssr/post-restore.sh when present.
|
||||||
|
# Copy config/post-restore.sh.example from the repo as a starting point.
|
||||||
|
_user_hook="${SSR_CONFIG_DIR:-$HOME/.config/ssr}/post-restore.sh"
|
||||||
|
if [[ -f "$_user_hook" ]]; then
|
||||||
|
ssr::log "Running post-restore hook: $_user_hook"
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$_user_hook" \
|
||||||
|
&& ssr::ok "post-restore hook completed" \
|
||||||
|
|| ssr::warn "post-restore hook exited with errors (non-fatal)"
|
||||||
|
else
|
||||||
|
ssr::log "No post-restore hook found (optional: $_user_hook)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Mark overall completion time.
|
# Mark overall completion time.
|
||||||
ssr::rs::set "finished" "$(ssr::ts)"
|
ssr::rs::set "finished" "$(ssr::ts)"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# config/post-restore.sh — custom post-restore hooks.
|
||||||
|
#
|
||||||
|
# Copy this file to ~/.config/ssr/post-restore.sh and add your own steps.
|
||||||
|
# It is sourced by `ssr restore` after all built-in steps complete.
|
||||||
|
#
|
||||||
|
# Available helpers (from lib/common.sh):
|
||||||
|
# ssr::log "message" — info line
|
||||||
|
# ssr::ok "message" — success line
|
||||||
|
# ssr::warn "message" — warning line (non-fatal)
|
||||||
|
# ssr::confirm "prompt" — interactive y/N prompt
|
||||||
|
#
|
||||||
|
# Available variables:
|
||||||
|
# SSR_ROOT — path to the ssr repository
|
||||||
|
# backup_dir — cloud backup directory for this host
|
||||||
|
# mackup_dir — cloud Mackup directory
|
||||||
|
#
|
||||||
|
# EXAMPLES
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- Custom shell dep (extends SSR_SHELL_DEPS_EXTRA in ssr.conf) ---------
|
||||||
|
# If you prefer the hook-file approach over ssr.conf arrays, add deps here:
|
||||||
|
#
|
||||||
|
# if [[ ! -d "$HOME/.my-custom-tool" ]]; then
|
||||||
|
# ssr::log "Installing my-custom-tool..."
|
||||||
|
# curl -sSL https://example.com/install.sh | bash
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# --- Oh My Zsh plugins not installed via Homebrew ------------------------
|
||||||
|
# Plugins are usually git-cloned into ${ZSH_CUSTOM}/plugins.
|
||||||
|
# Example: zsh-syntax-highlighting
|
||||||
|
#
|
||||||
|
# ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
|
||||||
|
# if [[ -d "$HOME/.oh-my-zsh" && ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]]; then
|
||||||
|
# git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \
|
||||||
|
# "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# --- npm global packages --------------------------------------------------
|
||||||
|
# npm globals are not tracked by Homebrew; restore them here.
|
||||||
|
#
|
||||||
|
# if command -v npm >/dev/null 2>&1; then
|
||||||
|
# npm install -g typescript prettier eslint
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# --- macOS app configuration not covered by Mackup -----------------------
|
||||||
|
# Example: Dock position
|
||||||
|
#
|
||||||
|
# defaults write com.apple.dock orientation -string left
|
||||||
|
# killall Dock
|
||||||
|
|
||||||
|
# --- SSH key import -------------------------------------------------------
|
||||||
|
# Keys are intentionally excluded from Mackup (they are sensitive).
|
||||||
|
# Restore from an encrypted backup or password manager here:
|
||||||
|
#
|
||||||
|
# op item get "SSH Key (personal)" --fields "private key" > "$HOME/.ssh/id_ed25519"
|
||||||
|
# chmod 600 "$HOME/.ssh/id_ed25519"
|
||||||
|
|
||||||
|
# --- GPG key import -------------------------------------------------------
|
||||||
|
# gpg --import /path/to/exported.gpg
|
||||||
@@ -73,6 +73,22 @@ SSR_PREFS_CONFLICT_DEFAULT=accept
|
|||||||
# Set to "true" to skip all conflict prompts and auto-accept everywhere.
|
# Set to "true" to skip all conflict prompts and auto-accept everywhere.
|
||||||
# SSR_ASSUME_YES=false
|
# SSR_ASSUME_YES=false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shell framework auto-install (runs after mackup restore)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Detect shell frameworks referenced in dotfiles that are not yet installed
|
||||||
|
# (e.g. oh-my-zsh, nvm, pyenv, rustup, starship, …) and install them.
|
||||||
|
# Set to "false" to skip this step.
|
||||||
|
# SSR_SHELL_DEPS_ENABLED=true
|
||||||
|
|
||||||
|
# Additional custom deps, space-separated, pipe-delimited format:
|
||||||
|
# "name|check_expression|install_command"
|
||||||
|
# Example:
|
||||||
|
# SSR_SHELL_DEPS_EXTRA=(
|
||||||
|
# "my-tool|command -v my-tool|brew install my-tool"
|
||||||
|
# "my-fw|[[ -d ~/.my-fw ]]|curl -sSL https://example.com/install.sh | bash"
|
||||||
|
# )
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# macOS restore behavior
|
# macOS restore behavior
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user