Install script from previously captured info

#!/bin/zsh
# Always run in zsh, even if started with "sh" or "bash"
if [ -z "$ZSH_VERSION" ]; then exec /bin/zsh "$0" "$@"; fi
# =====================================================================
# install.sh - STEP 2. Run this on your NEW Mac.
#
# Installs everything still in install-list.txt, restores the app
# settings saved in Settings/, then writes install-report.txt:
# everything that didn't install and what's left to do by hand.
#
# Safe to run again: anything already installed is skipped.
# Usage: zsh install.sh
# =====================================================================
cd "${0:A:h}" || exit 1
HERE=$PWD
LIST="$HERE/install-list.txt"
REPORT="$HERE/install-report.txt"
WORK="$HERE/.install"; mkdir -p "$WORK"
LOG="$WORK/install-log.txt"; : > "$LOG"
[[ -f $LIST ]] || { echo "Can't find install-list.txt in this folder."; exit 1; }
export HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_CLEANUP=1 MISE_YES=1
SEP= \x1f'
trim() { local s=$1; s=${s#"${s%%[![:space:]]*}"}; s=${s%"${s##*[![:space:]]}"}; print -r -- "$s" }
# ---- Read the list --------------------------------------------------
typeset -a TAPS BREWS CASKS CASKL APPS APPL MANUAL MANUALL NPM UV PIPX CARGO MISE VSCODE CURSOR SETS
while IFS= read -r line || [[ -n $line ]]; do
t=$(trim "$line")
[[ -z $t || $t == \#* ]] && continue
f=( "${(@s:|:)t}" )
type=$(trim "$f[1]"); key=$(trim "$f[2]"); label=$(trim "$f[3]")
[[ -z $key ]] && continue
case $type in
tap) TAPS+=("$key") ;;
brew) BREWS+=("$key") ;;
cask) CASKS+=("$key"); CASKL+=("$label") ;;
appstore) APPS+=("$key"); APPL+=("$label") ;;
manual) MANUAL+=("$key"); MANUALL+=("$label") ;;
npm) NPM+=("$key") ;;
uv) UV+=("$key") ;;
pipx) PIPX+=("$key") ;;
cargo) CARGO+=("$key") ;;
mise) MISE+=("$key") ;;
vscode) VSCODE+=("$key") ;;
cursor) CURSOR+=("$key") ;;
settings) SETS+=("$key") ;;
*) echo "Skipping a line I don't recognise: $t" ;;
esac
done < "$LIST"
# ---- Helpers ----------------------------------------------------------
typeset -a FAILED HANDS TODO
OK=0
step() { print -P "\n%B$1%b" }
try() { # try
local cat=$1 item=$2; shift 2
local out="$WORK/last.txt"
print -n " $item ... "
if "$@" >"$out" 2>&1 </dev/null; then
print -P "%F{green}done%f"; (( OK++ ))
else
print -P "%F{red}failed%f"
local why=$(grep -v '^[[:space:]]* "$out" | tail -1 | cut -c1-200)
FAILED+=("$cat$SEP$item$SEP${(j: :)${(q-)@}}$SEP$why")
fi
{ echo "===== [$cat] $item"; cat "$out"; echo } >> "$LOG"
}
skip() { # skip
FAILED+=("$1$SEP$2$SEP$3$SEP$4")
print -P " $2 ... %F{yellow}skipped%f ($4)"
}
restore_settings() { # restore_settings
local dir=$1 p
while IFS= read -r p; do
if [[ $p == defaults:* ]]; then
defaults import "${p#defaults:}" "$dir/${p#defaults:}.plist" || return 1
else
mkdir -p "$(dirname "$HOME/${p#\~/}")"
ditto "$dir/files/${p#\~/}" "$HOME/${p#\~/}" || return 1
fi
done < "$dir/.manifest"
}
print -P "%BSetting up this Mac from install-list.txt%b"
print "(Full output from every install goes to .install/install-log.txt)"
# ---- 1. Homebrew -------------------------------------------------------
[[ -x /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)"
if (( ${#TAPS} + ${#BREWS} + ${#CASKS} + ${#APPS} + ${#MISE} )) && ! command -v brew >/dev/null; then
step "Installing Homebrew (it will ask for your password)"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
[[ -x /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)"
fi
if command -v brew >/dev/null; then
grep -qs 'brew shellenv' ~/.zprofile || echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
brew update >>"$LOG" 2>&1
export HOMEBREW_NO_AUTO_UPDATE=1
fi
# ---- 2. Rosetta, only if something in the list needs it ----------------
if [[ "${(j: :)APPL} ${(j: :)CASKL} ${(j: :)MANUALL}" == *Intel-only* ]] && ! /usr/bin/pgrep -q oahd; then
step "Installing Rosetta (some of your apps are Intel-only)"
softwareupdate --install-rosetta --agree-to-license >>"$LOG" 2>&1
fi
# ---- 3. Homebrew taps, tools and apps -----------------------------------
if (( ${#TAPS} )); then
step "Homebrew sources (taps)"
for x in $TAPS; do try "Homebrew tap" "$x" brew tap "$x"; done
fi
if (( ${#BREWS} )); then
step "Command-line tools and libraries"
for x in $BREWS; do try "Homebrew" "$x" brew install "$x"; done
fi
if (( ${#CASKS} )); then
step "Apps (Homebrew)"
for x in $CASKS; do try "Homebrew app" "$x" brew install --cask "$x"; done
fi
# ---- 4. App Store apps -------------------------------------------------
if (( ${#APPS} )); then
step "App Store apps"
command -v mas >/dev/null || brew install mas >>"$LOG" 2>&1
open -a "App Store" 2>/dev/null
read -r "? Sign into the App Store app (if you haven't), then press Return to continue... "
for (( i = 1; i <= ${#APPS}; i++ )); do
name=${APPL[i]%% \[*}
if [[ ${APPS[i]} == "?" ]]; then
HANDS+=("$name: App Store app, ID wasn't found. Search for it in the App Store app.")
elif command -v mas >/dev/null; then
try "App Store" "$name" mas install "${APPS[i]}"
else
skip "App Store" "$name" "mas install ${APPS[i]}" "mas isn't installed: run brew install mas"
fi
done
fi
# ---- 5. Language runtimes (mise) ----------------------------------------
if (( ${#MISE} )); then
step "Language runtimes (mise)"
command -v mise >/dev/null || brew install mise >>"$LOG" 2>&1
for x in $MISE; do try "mise" "$x" mise use -g "$x"; done
path=("$HOME/.local/share/mise/shims" $path)
TODO+=("Turn on mise in your shell so your runtimes load: echo 'eval \"\$(mise activate zsh)\"' >> ~/.zshrc")
fi
# ---- 6. Global packages -------------------------------------------------
path=("$HOME/.cargo/bin" "$HOME/.local/bin" $path)
pkgs() { # pkgs --
local title=$1 tool=$2; shift 2
local -a cmd; while [[ $1 != -- ]]; do cmd+=("$1"); shift; done; shift
(( $# )) || return
step "$title"
local p
for p in "$@"; do
if command -v "$tool" >/dev/null; then try "$title" "$p" "${cmd[@]}" "$p"
else skip "$title" "$p" "${(j: :)cmd} $p" "$tool isn't installed. Keep it in the list, or install it first"; fi
done
}
pkgs "Global npm packages" npm npm install -g -- $NPM
pkgs "Python tools (uv)" uv uv tool install -- $UV
pkgs "Python tools (pipx)" pipx pipx install -- $PIPX
pkgs "Rust tools (cargo)" cargo cargo install -- $CARGO
# ---- 7. Editor extensions -----------------------------------------------
CODE=$(command -v code 2>/dev/null); [[ -z $CODE ]] && CODE="/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
CURS=$(command -v cursor 2>/dev/null); [[ -z $CURS ]] && CURS="/Applications/Cursor.app/Contents/Resources/app/bin/cursor"
if (( ${#VSCODE} )); then
step "VS Code extensions"
for x in $VSCODE; do
if [[ -x $CODE ]]; then try "VS Code extension" "$x" "$CODE" --install-extension "$x"
else skip "VS Code extension" "$x" "code --install-extension $x" "VS Code isn't installed"; fi
done
fi
if (( ${#CURSOR} )); then
step "Cursor extensions"
for x in $CURSOR; do
if [[ -x $CURS ]]; then try "Cursor extension" "$x" "$CURS" --install-extension "$x"
else skip "Cursor extension" "$x" "cursor --install-extension $x" "Cursor isn't installed"; fi
done
fi
# ---- 8. App settings ----------------------------------------------------
if (( ${#SETS} )); then
step "App settings"
for x in $SETS; do
if [[ -f $HERE/Settings/$x/.manifest ]]; then try "Settings" "$x" restore_settings "$HERE/Settings/$x"
else skip "Settings" "$x" "" "nothing saved in Settings/$x"; fi
done
fi
# ---- 9. Apps to install by hand -----------------------------------------
for (( i = 1; i <= ${#MANUAL}; i++ )); do
HANDS+=("${MANUAL[i]}: ${MANUALL[i]}")
done
# =====================================================================
# Write install-report.txt
# =====================================================================
(( ${CASKS[(I)raycast]} )) && TODO+=("Raycast: import your .rayconfig export (Raycast Settings > Advanced > Import)")
(( ${CASKS[(I)bettertouchtool]} )) && TODO+=("BetterTouchTool: import your preset (Presets > Import)")
TODO+=(
"Sign into iCloud and turn on the iCloud features you use (System Settings > Apple Account)"
"Sign into your browser's sync to bring back bookmarks and extensions"
"Enter license keys or sign into accounts for paid apps"
"Approve permissions as apps ask (System Settings > Privacy & Security: Accessibility, Screen Recording, Full Disk Access, Input Monitoring)"
)
{
print "MAC SETUP REPORT - $(date '+%B %-d, %Y at %-I:%M %p')"
print "Installed successfully (or already installed): $OK"
print "Didn't install: ${#FAILED} To install by hand: ${#HANDS}"
print "\n======================================================================"
print "DIDN'T INSTALL (${#FAILED})"
print "======================================================================"
if (( ${#FAILED} )); then
print "Fix the cause and run the retry command, or just run install.sh again.\n"
for f in $FAILED; do
parts=( "${(@ps:$SEP:)f}" )
print "[ ] ${parts[2]} (${parts[1]})"
[[ -n ${parts[4]} ]] && print " Why: ${parts[4]}"
if [[ ${parts[1]} == Settings ]]; then
[[ ${parts[4]} != nothing\ saved* ]] && print " Fix: copy the files from Settings/${parts[2]} by hand"
elif [[ -n ${parts[3]} ]]; then print " Retry: ${parts[3]}"; fi
done
else
print "Nothing. Everything in the list installed."
fi
print "\n======================================================================"
print "INSTALL BY HAND (${#HANDS})"
print "======================================================================"
if (( ${#HANDS} )); then for h in $HANDS; do print "[ ] $h"; done
else print "Nothing."; fi
print "\n======================================================================"
print "STILL TO DO"
print "======================================================================"
for t in $TODO; do print "[ ] $t"; done
print "\nFull install output: .install/install-log.txt"
} > "$REPORT"
print -P "\n%BFinished.%b Installed: $OK Didn't install: ${#FAILED} Install by hand: ${#HANDS}"
print "See install-report.txt for what's left."
open -e "$REPORT" 2>/dev/null
参与讨论