diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d5e3a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.AppleDouble +.LSOverride +.Spotlight-V100 +.Trashes +Icon +._* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1a3df5 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# macOS Setup Script + +Automated macOS setup script that installs Homebrew, apps, fonts, and configures system defaults. + +## Oneliner (Needs sudo account permissions) +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash + +## Oneliner with Client Profile +Use `--client NAME` or a positional `NAME`. + +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash -s -- --client RD +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash -s -- RD + +## Quick Start + +1. Copy the `macos-setup` folder to your Mac +2. Open Terminal and navigate to the folder +3. Run: `./prep.command` +4. Enter your admin password when prompted + +## Files + +### `prep.command` +Main executable script. Run this to set up your Mac. + +### `.Brewfile` +Homebrew packages (formulae, casks, fonts). Contains brief editing instructions at the top of the file. + +### `.Masfile` +Mac App Store apps. One app per line: `app_id|App Name`. Contains brief editing instructions at the top of the file. + +### `.prep-YYYYMMDD.log` +Hidden log file with detailed output from script runs. Appends if run multiple times on the same day. + +## What It Does + +1. Obtains admin privileges (password stored securely) +2. Installs Homebrew (if needed) +3. Prepares Homebrew (updates, configures) +4. Installs Rosetta 2 (Apple Silicon only) +5. Installs packages from `.Brewfile` +6. Sets default browser from `prep.command` +7. Configures Dock from `prep.command` +8. Applies macOS defaults from `prep.command` +9. Cleans up Homebrew locks +10. Installs App Store apps from `.Masfile` (optional, requires Apple ID) + +## Editing Files + +Each file (`.Brewfile`, `.Masfile`) contains brief editing instructions at the top. Open the file to see how to add/remove items. + +## Requirements + +- macOS (tested on macOS Sonoma+) +- Admin account +- Internet connection + +## Troubleshooting + +- **Script fails**: Check that `prep.command` is executable: `chmod +x prep.command` +- **Gatekeeper prompt on double-click**: Remove quarantine once: `xattr -dr com.apple.quarantine prep.command` (or run it from Terminal) +- **Homebrew fails**: Check internet connection or install manually from https://brew.sh +- **App Store apps**: Sign in to Mac App Store first: `open -a "App Store"` +- **Dock fails**: Grant Terminal.app Full Disk Access in System Settings +- **Full log**: Check `.prep-YYYYMMDD.log` for detailed output + +## Settings Scope + +**Most settings are user-only** (apply only to the current user). Only 2 settings are system-wide: +- `/Volumes` folder visibility (affects all users) +- Login window hostname display (affects all users) + +All other settings (Finder, Safari, Dock, screenshots, etc.) are user-specific. See `SETTINGS_SCOPE.md` for details. + +## Security + +Password is stored securely in a temporary file, cleared from memory immediately, and deleted on script exit. diff --git a/macos-setup-client/README.md b/macos-setup-client/README.md new file mode 100644 index 0000000..b60577e --- /dev/null +++ b/macos-setup-client/README.md @@ -0,0 +1,69 @@ +# macOS Setup Client + +Lightweight macOS setup script that installs Homebrew packages and applies +user defaults. Configuration lives inline in `prep.sh`. + +## Oneliner (Admin access required for Homebrew install) +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash + +## Oneliner with Client Profile +Use `--client NAME` or a positional `NAME`. + +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash -s -- --client RD +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | sudo bash -s -- RD + +## Quick Start + +1. Copy the `macos-setup-client` folder to your Mac +2. Open Terminal and navigate to the folder +3. Run: `./prep.sh` +4. Optional: `./prep.sh --client RD` + +## Files + +### `prep.sh` +Main executable script. All configuration is inline in this file. + +### `.prep-defaults-YYYYMMDD.log` +Hidden log file written to `~/Downloads`. Appends if run multiple times on the same day. + +## What It Does + +1. Ensures Xcode Command Line Tools are installed +2. Installs Homebrew (if needed) +3. Installs Homebrew formulae and casks (inline lists) +4. Closes System Settings (to avoid conflicts) +5. Applies macOS defaults (or reverts with `--revert`) +6. Restarts affected apps (Safari, Finder) + +## Editing Configuration + +Edit `prep.sh` directly: +- Default apps: `DEFAULT_BROWSER` (and optional defaults) +- Homebrew packages: `BREW_FORMULAE`, `BREW_CASKS` +- Mac App Store apps: `MAS_APPS` (currently disabled in the script) +- macOS defaults: `apply_defaults` / `apply_defaults_revert` +- Admin-only commands: commented at the end of the file + +Client profiles live in `profiles/NAME.sh` and can override or append to any of +the variables above. + +## Requirements + +- macOS (tested on macOS Sonoma+) +- Internet connection +- Admin account (for Homebrew install) + +## Troubleshooting + +- **Script fails**: Ensure executable: `chmod +x prep.sh` +- **Homebrew fails**: Check internet connection or install manually from https://brew.sh +- **App Store apps**: Uncomment the MAS section and sign in first: `open -a "App Store"` +- **Full log**: Check `~/Downloads/.prep-defaults-YYYYMMDD.log` + +## Settings Scope + +All defaults in `prep.sh` are user-level. Two admin-only settings are present +but commented out at the end of the file: +- `/Volumes` folder visibility +- Login window hostname display diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh new file mode 100644 index 0000000..4a4c070 --- /dev/null +++ b/macos-setup-client/prep.sh @@ -0,0 +1,651 @@ +#!/usr/bin/env bash +# HOW TO EDIT THIS FILE: +# - Default apps: Set optional DEFAULT_*_APP values +# - Dock items: Add/remove app paths in DOCK_ITEMS array +# - macOS defaults: Edit defaults commands below (use: defaults read to see current values) +# - Homebrew packages: Add/remove entries in BREW_FORMULAE/BREW_CASKS arrays +# - App Store apps: Add/remove entries in MAS_APPS array (format: "app_id|App Name") +# - Admin-only commands are moved to the end and commented out +# - Client profiles: edit apply_client_profile in this file and run with --client NAME +# After editing, run: ./prep.sh + +set -u -o pipefail + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-${0}}")" && pwd)" + +############################################################################### +# User Configuration # +############################################################################### + +# Default Apps (optional; app must already be installed) +DEFAULT_BROWSER="Google Chrome" +# DEFAULT_EMAIL_APP="Mail" +# DEFAULT_CALENDAR_APP="Calendar" +# DEFAULT_MAPS_APP="Maps" +# DEFAULT_MUSIC_APP="Music" +# DEFAULT_PHOTOS_APP="Photos" +# DEFAULT_TEXT_EDITOR_APP="Visual Studio Code" + +############################################################################### +# Dock Configuration # +############################################################################### + +DOCK_ITEMS=( + "/Applications/Google Chrome.app" + "/Applications/Google Drive.app" + "/Applications/1Password.app" + "/Applications/WhatsApp.app" + "/Applications/Slack.app" + "/Applications/Microsoft Word.app" + "/Applications/Microsoft Excel.app" + "/System/Applications/Notes.app" + "/System/Applications/Calendar.app" + "/Applications/Safari.app" + "/System/Applications/System Settings.app" +) + +############################################################################### +# Homebrew # +############################################################################### + +BREW_FORMULAE=() +BREW_CASKS=() + +############################################################################### +# Mac App Store (mas) # +############################################################################### + +MAS_APPS=( + "409201541|Pages" + "409203825|Numbers" + "409183694|Keynote" +) + +############################################################################### +# Logging and Helpers # +############################################################################### + +LOG_FILE="${HOME}/Downloads/.prep-defaults-$(date '+%Y%m%d').log" +FAILURES=() +STEP_NUM=0 +TOTAL_STEPS=7 +REVERT_DEFAULTS=false +CLIENT_PROFILE="" +SUDO_KEEPALIVE_PID="" +SUDO_INITIALIZED=false +ACCEPT_XCODE_LICENSE=false + +log_to_file() { + printf "[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" +} + +log() { + log_to_file "$*" + printf "[OK] %s\n" "$*" +} + +warn() { + log_to_file "WARN: $*" + printf "[WARN] %s\n" "$*" >&2 +} + +error() { + log_to_file "ERROR: $*" + printf "[ERROR] %s\n" "$*" >&2 + FAILURES+=("$*") +} + +filter_dock_items() { + local item + local filtered=() + local missing=() + + for item in "${DOCK_ITEMS[@]}"; do + if [[ "$item" == "SPACER" ]]; then + filtered+=("$item") + elif [[ -e "$item" ]]; then + filtered+=("$item") + else + missing+=("$item") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + warn "Dock item not found, skipping:" + for item in "${missing[@]}"; do + warn " - $item" + done + fi + + DOCK_ITEMS=("${filtered[@]}") +} + +step_start() { + STEP_NUM=$((STEP_NUM + 1)) + printf "\n[%d/%d] %s\n" "$STEP_NUM" "$TOTAL_STEPS" "$1" + log_to_file "STEP $STEP_NUM/$TOTAL_STEPS: $1" +} + +show_setting() { + local label="$1" + local domain="$2" + local key="$3" + local current="(not set)" + if defaults read "$domain" "$key" >/dev/null 2>&1; then + current="$(defaults read "$domain" "$key")" + fi + printf "%s: %s\n" "$label" "$current" +} + +run_cmd() { + local desc="$1" + shift + if "$@" >> "$LOG_FILE" 2>&1; then + log "$desc" + else + error "$desc (failed)" + fi +} + +run_cmd_admin() { + local desc="$1" + shift + ensure_sudo + log_to_file "ADMIN: $desc" + if sudo -n "$@" >> "$LOG_FILE" 2>&1; then + log "$desc" + else + error "$desc (failed; admin session expired)" + fi +} + +run_optional() { + local desc="$1" + shift + if "$@" >> "$LOG_FILE" 2>&1; then + log "$desc" + else + warn "$desc (failed, ignored)" + fi +} + +run_optional_admin() { + local desc="$1" + shift + ensure_sudo + log_to_file "ADMIN: $desc" + if sudo -n "$@" >> "$LOG_FILE" 2>&1; then + log "$desc" + else + warn "$desc (failed; admin session expired, ignored)" + fi +} + +init_sudo() { + if ! command -v sudo >/dev/null 2>&1; then + warn "sudo not available; skipping admin credential caching" + return 0 + fi + + printf "Admin access is required for some steps. Please authenticate.\n" + if ! sudo -v; then + error "Failed to obtain sudo credentials" + exit 1 + fi + + # Keep sudo timestamp alive while the script runs. + while true; do + sudo -n true >/dev/null 2>&1 || break + sleep 60 + done & + SUDO_KEEPALIVE_PID=$! + SUDO_INITIALIZED=true +} + +ensure_sudo() { + if [[ "${SUDO_INITIALIZED}" == "true" ]]; then + return 0 + fi + init_sudo +} + +cleanup_sudo() { + if [[ -n "${SUDO_KEEPALIVE_PID:-}" ]]; then + kill "$SUDO_KEEPALIVE_PID" >/dev/null 2>&1 || true + fi + if command -v sudo >/dev/null 2>&1; then + sudo -k >/dev/null 2>&1 || true + fi +} + +show_summary() { + printf "\nSummary\n" + printf "\n" + if [[ ${#FAILURES[@]} -eq 0 ]]; then + printf "All tasks completed successfully.\n" + else + printf "Completed with %d issue(s):\n" "${#FAILURES[@]}" + for failure in "${FAILURES[@]}"; do + printf " - %s\n" "$failure" + done + fi + printf "Log file: %s\n" "$LOG_FILE" +} + +show_usage() { + printf "Usage: %s [--revert] [--client NAME] [NAME]\n" "$(basename "$0")" + printf " --revert Revert defaults set by this script\n" + printf " --client NAME Apply inline client profile from prep.sh\n" + printf " --accept-xcode-license Accept Xcode license after install\n" + printf " NAME Positional client name (same as --client)\n" +} + +apply_client_profile() { + if [[ -z "$CLIENT_PROFILE" ]]; then + CLIENT_PROFILE="default" + fi + + case "$CLIENT_PROFILE" in + default) + BREW_FORMULAE=( + "mas" + "dockutil" + ) + + BREW_CASKS=( + "1password" + "slack" + "google-chrome" + "google-drive" + "whatsapp" + ) + ;; + # Example: + # acme) + # DEFAULT_BROWSER="Google Chrome" + # BREW_CASKS+=( + # "slack" + # ) + # ;; + *) + error "Unknown client profile: ${CLIENT_PROFILE}" + exit 1 + ;; + esac + + log "Loaded client profile: ${CLIENT_PROFILE}" +} + +############################################################################### +# Homebrew # +############################################################################### + +ensure_xcode_cli_tools() { + if /usr/bin/xcode-select -p >/dev/null 2>&1; then + log "Xcode Command Line Tools already installed" + if [[ "${ACCEPT_XCODE_LICENSE}" == "true" ]]; then + accept_xcode_license + fi + return 0 + fi + + run_optional "Request Xcode Command Line Tools install" /usr/bin/xcode-select --install + + # Wait until installation completes (user confirmation required). + while ! /usr/bin/xcode-select -p >/dev/null 2>&1; do + printf "Waiting for Xcode Command Line Tools installation to finish...\n" + sleep 20 + done + log "Xcode Command Line Tools installed" + if [[ "${ACCEPT_XCODE_LICENSE}" == "true" ]]; then + accept_xcode_license + fi +} + +accept_xcode_license() { + if ! command -v xcodebuild >/dev/null 2>&1; then + warn "xcodebuild not available; cannot accept Xcode license" + return 0 + fi + + run_cmd_admin "Accept Xcode license" /usr/bin/xcodebuild -license accept +} + +install_homebrew() { + if command -v brew >/dev/null 2>&1; then + log "Homebrew already installed" + return 0 + fi + + if ! command -v /bin/bash >/dev/null 2>&1; then + error "bash not available for Homebrew install" + return 1 + fi + + ensure_sudo + run_cmd "Install Homebrew" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + if [[ -x /opt/homebrew/bin/brew ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [[ -x /usr/local/bin/brew ]]; then + eval "$(/usr/local/bin/brew shellenv)" + fi +} + +install_brew_packages() { + if ! command -v brew >/dev/null 2>&1; then + warn "Homebrew not installed; skipping package install" + return 0 + fi + + ensure_sudo + local formula + for formula in "${BREW_FORMULAE[@]}"; do + run_optional "Install Homebrew formula ($formula)" brew install "$formula" + done + + local cask + for cask in "${BREW_CASKS[@]}"; do + run_optional "Install Homebrew cask ($cask)" brew install --cask "$cask" + done +} + +############################################################################### +# Mac App Store (mas) # +############################################################################### + +install_mas_apps() { + if ! command -v mas >/dev/null 2>&1; then + warn "mas not installed; skipping App Store installs" + return 0 + fi + + if [[ ${#MAS_APPS[@]} -eq 0 ]]; then + warn "No App Store apps listed; skipping" + return 0 + fi + + local entry app_id app_name + for entry in "${MAS_APPS[@]}"; do + app_id="${entry%%|*}" + app_name="${entry#*|}" + if [[ -z "$app_id" || "$app_id" == "$app_name" ]]; then + warn "Skipping invalid MAS entry: $entry" + continue + fi + run_optional "Install App Store app (${app_name})" mas install "$app_id" + done +} + +############################################################################### +# macOS System Defaults # +############################################################################### + +close_system_settings() { + run_optional "Close System Preferences" osascript -e 'tell application "System Preferences" to quit' + run_optional "Close System Settings" osascript -e 'tell application "System Settings" to quit' +} + +ensure_safari_initialized() { + local safari_container="${HOME}/Library/Containers/com.apple.Safari" + if [[ -d "$safari_container" ]]; then + return 0 + fi + + run_optional "Launch Safari to initialize container" open -a "Safari" + sleep 3 + run_optional "Quit Safari after initialization" osascript -e 'tell application "Safari" to quit' + sleep 2 +} + +apply_defaults() { + # General UI/UX + run_cmd "Expand save panel (save)" defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode -bool true + run_cmd "Expand save panel (save2)" defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 -bool true + run_cmd "Expand print panel (print)" defaults write NSGlobalDomain PMPrintingExpandedStateForPrint -bool true + run_cmd "Expand print panel (print2)" defaults write NSGlobalDomain PMPrintingExpandedStateForPrint2 -bool true + run_cmd "Disable Siri status menu" defaults write com.apple.Siri SiriPrefStashedStatusMenuVisible -bool false + run_cmd "Disable Siri voice trigger" defaults write com.apple.Siri VoiceTriggerUserEnabled -bool false + run_cmd "Always prefer tabs" defaults write -g AppleWindowTabbingMode -string always + run_cmd "Show Bluetooth in Control Center" defaults write com.apple.controlcenter "NSStatusItem Visible Bluetooth" -bool true + run_cmd "Show Sound in Control Center" defaults write com.apple.controlcenter "NSStatusItem Visible Sound" -bool true + run_cmd "Hide Spotlight menu item" defaults -currentHost write com.apple.Spotlight MenuItemHidden -int 1 + + # Finder + run_cmd "Show Finder path bar" defaults write com.apple.finder ShowPathbar -bool true + run_cmd "Default Finder view style (list)" defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv" + run_cmd "Show Finder status bar" defaults write com.apple.finder ShowStatusBar -boolean true + run_cmd "Show hidden files" defaults write com.apple.finder AppleShowAllFiles true + run_cmd "Show file extensions" defaults write NSGlobalDomain AppleShowAllExtensions -boolean true + + # Unhide ~/Library + run_optional "Unhide ~/Library" chflags nohidden "$HOME/Library" + if xattr -p com.apple.FinderInfo "$HOME/Library" >/dev/null 2>&1; then + run_optional "Remove FinderInfo xattr" xattr -d com.apple.FinderInfo "$HOME/Library" + else + log "FinderInfo xattr not present; skipping" + fi + + # Text Input + run_cmd "Disable auto-capitalization" defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false + run_cmd "Disable auto-correction" defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false + + # Bluetooth + run_cmd "Set Bluetooth audio quality" defaults write com.apple.BluetoothAudioAgent "Apple Bitpool Min (editable)" -int 40 + + # Network + run_cmd "Browse all network interfaces" defaults write com.apple.NetworkBrowser BrowseAllInterfaces -bool true + + # Screenshots + run_cmd "Save screenshots to Downloads" defaults write com.apple.screencapture location -string "$HOME/Downloads" + run_cmd "Use PNG for screenshots" defaults write com.apple.screencapture type -string "png" + + # Safari (ignore failures if Safari is not present) + ensure_safari_initialized + run_optional "Enable Safari Develop menu" defaults write com.apple.Safari IncludeDevelopMenu -bool true + run_optional "Enable Safari WebKit developer extras (global)" defaults write com.apple.Safari WebKitDeveloperExtrasEnabledPreferenceKey -bool true + run_optional "Enable Safari WebKit developer extras" defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled -bool true + run_optional "Disable Safari AutoFill Address Book" defaults write com.apple.Safari AutoFillFromAddressBook -bool false + run_optional "Disable Safari AutoFill Passwords" defaults write com.apple.Safari AutoFillPasswords -bool false + run_optional "Disable Safari AutoFill Credit Cards" defaults write com.apple.Safari AutoFillCreditCardData -bool false + run_optional "Disable Safari AutoFill Misc" defaults write com.apple.Safari AutoFillMiscellaneousForms -bool false + run_optional "Enable Safari Do Not Track" defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true +} + +apply_defaults_revert() { + # General UI/UX + run_optional "Revert save panel expansion (save)" defaults delete NSGlobalDomain NSNavPanelExpandedStateForSaveMode + run_optional "Revert save panel expansion (save2)" defaults delete NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 + run_optional "Revert print panel expansion (print)" defaults delete NSGlobalDomain PMPrintingExpandedStateForPrint + run_optional "Revert print panel expansion (print2)" defaults delete NSGlobalDomain PMPrintingExpandedStateForPrint2 + run_optional "Revert Siri status menu" defaults delete com.apple.Siri SiriPrefStashedStatusMenuVisible + run_optional "Revert Siri voice trigger" defaults delete com.apple.Siri VoiceTriggerUserEnabled + run_optional "Revert window tabbing preference" defaults delete -g AppleWindowTabbingMode + run_optional "Revert Bluetooth Control Center item" defaults delete com.apple.controlcenter "NSStatusItem Visible Bluetooth" + run_optional "Revert Sound Control Center item" defaults delete com.apple.controlcenter "NSStatusItem Visible Sound" + run_optional "Revert Spotlight menu item" defaults -currentHost delete com.apple.Spotlight MenuItemHidden + + # Finder + run_optional "Revert Finder path bar" defaults delete com.apple.finder ShowPathbar + run_optional "Revert Finder view style" defaults delete com.apple.finder FXPreferredViewStyle + run_optional "Revert Finder status bar" defaults delete com.apple.finder ShowStatusBar + run_optional "Revert hidden files visibility" defaults delete com.apple.finder AppleShowAllFiles + run_optional "Revert file extensions visibility" defaults delete NSGlobalDomain AppleShowAllExtensions + + # ~/Library visibility + run_optional "Hide ~/Library" chflags hidden "$HOME/Library" + + # Text Input + run_optional "Revert auto-capitalization" defaults delete NSGlobalDomain NSAutomaticCapitalizationEnabled + run_optional "Revert auto-correction" defaults delete NSGlobalDomain NSAutomaticSpellingCorrectionEnabled + + # Bluetooth + run_optional "Revert Bluetooth audio quality" defaults delete com.apple.BluetoothAudioAgent "Apple Bitpool Min (editable)" + + # Network + run_optional "Revert network interface browsing" defaults delete com.apple.NetworkBrowser BrowseAllInterfaces + + # Screenshots + run_optional "Revert screenshot location" defaults delete com.apple.screencapture location + run_optional "Revert screenshot format" defaults delete com.apple.screencapture type + + # Safari (ignore failures if Safari is not present) + run_optional "Revert Safari Develop menu" defaults delete com.apple.Safari IncludeDevelopMenu + run_optional "Revert Safari WebKit developer extras (global)" defaults delete com.apple.Safari WebKitDeveloperExtrasEnabledPreferenceKey + run_optional "Revert Safari WebKit developer extras" defaults delete com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled + run_optional "Revert Safari AutoFill Address Book" defaults delete com.apple.Safari AutoFillFromAddressBook + run_optional "Revert Safari AutoFill Passwords" defaults delete com.apple.Safari AutoFillPasswords + run_optional "Revert Safari AutoFill Credit Cards" defaults delete com.apple.Safari AutoFillCreditCardData + run_optional "Revert Safari AutoFill Misc" defaults delete com.apple.Safari AutoFillMiscellaneousForms + run_optional "Revert Safari Do Not Track" defaults delete com.apple.Safari SendDoNotTrackHTTPHeader +} + +restart_apps() { + run_optional "Restart Safari" killall Safari + run_optional "Restart Finder" killall Finder +} + +configure_dock() { + if ! command -v dockutil >/dev/null 2>&1; then + warn "dockutil not installed; skipping Dock configuration" + return 0 + fi + + if [[ ${#DOCK_ITEMS[@]} -eq 0 ]]; then + warn "No Dock items configured; skipping Dock configuration" + return 0 + fi + + run_optional "Clear Dock items" dockutil --remove all --no-restart + + local item + for item in "${DOCK_ITEMS[@]}"; do + if [[ "$item" == "SPACER" ]]; then + run_optional "Add Dock spacer" dockutil --add '' --type spacer --section apps --no-restart + else + run_optional "Add Dock item (${item})" dockutil --add "$item" --no-restart + fi + done + + run_optional "Restart Dock" killall Dock +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --revert) + REVERT_DEFAULTS=true + shift + ;; + --client) + shift + if [[ -z "${1:-}" ]]; then + printf "Missing value for --client\n" >&2 + show_usage + exit 1 + fi + CLIENT_PROFILE="$1" + shift + ;; + --client=*) + CLIENT_PROFILE="${1#*=}" + shift + ;; + --accept-xcode-license) + ACCEPT_XCODE_LICENSE=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + if [[ -z "$CLIENT_PROFILE" ]]; then + CLIENT_PROFILE="$1" + shift + else + printf "Unknown argument: %s\n" "$1" >&2 + show_usage + exit 1 + fi + ;; + esac + done +} + +main() { + trap cleanup_sudo EXIT + parse_args "$@" + apply_client_profile + + printf "macOS Defaults Setup\n" + printf "Log file: %s\n\n" "$LOG_FILE" + + printf "Summary before applying macOS defaults:\n" + show_setting "Show all filename extensions" "NSGlobalDomain" "AppleShowAllExtensions" + show_setting "Show Finder status bar" "com.apple.finder" "ShowStatusBar" + show_setting "Show Finder path bar" "com.apple.finder" "ShowPathbar" + show_setting "Default Finder view style" "com.apple.Finder" "FXPreferredViewStyle" + printf "\n" + + step_start "Installing Xcode Command Line Tools" + ensure_xcode_cli_tools + + # Optional: Install Homebrew + step_start "Installing Homebrew" + install_homebrew + + # Optional: Install Homebrew packages (inline list) + step_start "Installing Homebrew packages" + install_brew_packages + filter_dock_items + + # Optional: Install Mac App Store apps (requires mas + sign-in) + # step_start "Installing App Store apps" + # install_mas_apps + + step_start "Closing System Settings" + close_system_settings + + step_start "Applying macOS defaults" + if [[ "$REVERT_DEFAULTS" == "true" ]]; then + apply_defaults_revert + else + apply_defaults + fi + + step_start "Configuring Dock" + configure_dock + + step_start "Restarting affected apps" + restart_apps + + # Optional: Reset Dock to defaults (uncomment to enable) + # step_start "Resetting Dock to defaults" + # run_cmd "Reset Dock to default icons" defaults delete com.apple.dock + # run_optional "Restart Dock" killall Dock + + printf "\nSummary after applying macOS defaults:\n" + show_setting "Show all filename extensions" "NSGlobalDomain" "AppleShowAllExtensions" + show_setting "Show Finder status bar" "com.apple.finder" "ShowStatusBar" + show_setting "Show Finder path bar" "com.apple.finder" "ShowPathbar" + show_setting "Default Finder view style" "com.apple.Finder" "FXPreferredViewStyle" + + show_summary +} + +main "$@" + +############################################################################### +# Admin-only commands (disabled) # +############################################################################### +# The commands below require admin privileges. They are intentionally commented +# out and moved to the end of the file per request. Uncomment and run manually +# if/when you want to apply them. +# +# Show /Volumes +# sudo chflags nohidden /Volumes +# +# Login Window - show host info +# sudo defaults write /Library/Preferences/com.apple.loginwindow AdminHostInfo HostName diff --git a/macos-setup/.Brewfile b/macos-setup/.Brewfile new file mode 100644 index 0000000..af24541 --- /dev/null +++ b/macos-setup/.Brewfile @@ -0,0 +1,28 @@ +# HOW TO EDIT THIS FILE: +# Add/remove packages using: brew "package-name" or cask "app-name" +# Run: brew bundle --file=.Brewfile to install everything listed here +# Find packages: brew search or visit https://formulae.brew.sh + +# Homebrew Formulae (command-line tools) +brew "mas" # Mac App Store CLI tool +brew "dockutil" # Dock management tool + +# Homebrew Fonts (no tap needed - fonts are now in main cask repository) +cask "font-sf-pro" # SF Pro font +cask "font-roboto" # Roboto font + +# Homebrew Casks (applications) +cask "alfred" # Alfred productivity app spotlight alternative +cask "1password" # 1Password password manager +cask "slack" # Slack messaging app +cask "google-chrome" # Google Chrome browser +cask "google-chrome@canary" # Google Chrome Canary browser +cask "vlc" # VLC media player +cask "macpar-deluxe" # MacPar Deluxe media player +cask "downie" # Downie media downloader +cask "qfinder-pro" # Qfinder Pro file manager +cask "transmit" # Transmit file transfer app +cask "permute" # Permute media converter +cask "zerotier-one" # Zerotier VPN +cask "wifiman" # Wifiman for teleport + diff --git a/macos-setup/.DS_Store b/macos-setup/.DS_Store new file mode 100644 index 0000000..6fffbcc Binary files /dev/null and b/macos-setup/.DS_Store differ diff --git a/macos-setup/.Masfile b/macos-setup/.Masfile new file mode 100644 index 0000000..8d65c91 --- /dev/null +++ b/macos-setup/.Masfile @@ -0,0 +1,12 @@ +# HOW TO EDIT THIS FILE: +# Format: app_id|App Name (one per line) +# Find app IDs: mas search "App Name" or visit Mac App Store +# Remove a line to uninstall (or comment with #) +# Requires: Sign in to Mac App Store before running prep.command + +409201541|Pages +409203825|Numbers +409183694|Keynote +472226235|LanScan +1092006274|AJA System Test Lite + diff --git a/macos-setup/.Prepfile b/macos-setup/.Prepfile new file mode 100644 index 0000000..7dc8afc --- /dev/null +++ b/macos-setup/.Prepfile @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# HOW TO EDIT THIS FILE: +# - Dock items: Add/remove app paths in DOCK_ITEMS array +# - Default browser: Change DEFAULT_BROWSER value +# - macOS defaults: Edit defaults commands below (use: defaults read to see current values) +# After editing, run: ./prep.command + +############################################################################### +# Dock Configuration # +############################################################################### + +DOCK_ITEMS=( + "/Applications/Transmit.app" + "/Applications/Slack.app" + "/System/Applications/Notes.app" + "/System/Applications/Calendar.app" + "/Applications/Google Chrome.app" + "/Applications/Google Chrome Canary.app" + "/Applications/Safari.app" + "/System/Applications/System Settings.app" +) + +DEFAULT_BROWSER="Google Chrome" + +############################################################################### +# macOS System Defaults # +############################################################################### + +# Close System Preferences/Settings to prevent conflicts +osascript -e 'tell application "System Preferences" to quit' 2>/dev/null || true +osascript -e 'tell application "System Settings" to quit' 2>/dev/null || true + +# General UI/UX +defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode -bool true +defaults write NSGlobalDomain NSNavPanelExpandedStateForSaveMode2 -bool true +defaults write NSGlobalDomain PMPrintingExpandedStateForPrint -bool true +defaults write NSGlobalDomain PMPrintingExpandedStateForPrint2 -bool true +defaults write com.apple.Siri SiriPrefStashedStatusMenuVisible -bool false +defaults write com.apple.Siri VoiceTriggerUserEnabled -bool false +defaults write -g AppleWindowTabbingMode -string always +defaults write com.apple.controlcenter "NSStatusItem Visible Bluetooth" -bool true +defaults write com.apple.controlcenter "NSStatusItem Visible Sound" -bool true +defaults -currentHost write com.apple.Spotlight MenuItemHidden -int 1 + +# Finder +defaults write com.apple.finder ShowPathbar -bool true +defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv" +defaults write com.apple.finder ShowStatusBar -boolean true +defaults write com.apple.finder AppleShowAllFiles true +defaults write NSGlobalDomain AppleShowAllExtensions -boolean true + +# Show /Volumes (requires sudo) +if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true + sudo -A chflags nohidden /Volumes 2>/dev/null || true +else + sudo -v 2>/dev/null || { + echo "Sudo credentials expired. Please enter your password:" >&2 + sudo -v || exit 0 + } + sudo chflags nohidden /Volumes 2>/dev/null || true +fi + +# Unhide ~/Library +chflags nohidden "$HOME/Library" 2>/dev/null || true +xattr -d com.apple.FinderInfo "$HOME/Library" 2>/dev/null || true + +# Text Input +defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false +defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false + +# Bluetooth +defaults write com.apple.BluetoothAudioAgent "Apple Bitpool Min (editable)" -int 40 + +# Network +defaults write com.apple.NetworkBrowser BrowseAllInterfaces -bool true + +# Screenshots +defaults write com.apple.screencapture location -string "$HOME/Downloads" +defaults write com.apple.screencapture type -string "png" + +# Safari +defaults write com.apple.Safari IncludeDevelopMenu -bool true 2>/dev/null || true +defaults write com.apple.Safari WebKitDeveloperExtrasEnabledPreferenceKey -bool true 2>/dev/null || true +defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled -bool true 2>/dev/null || true +defaults write com.apple.Safari AutoFillFromAddressBook -bool false 2>/dev/null || true +defaults write com.apple.Safari AutoFillPasswords -bool false 2>/dev/null || true +defaults write com.apple.Safari AutoFillCreditCardData -bool false 2>/dev/null || true +defaults write com.apple.Safari AutoFillMiscellaneousForms -bool false 2>/dev/null || true +defaults write com.apple.Safari SendDoNotTrackHTTPHeader -bool true 2>/dev/null || true + +# Login Window (requires sudo) +if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true + sudo -A defaults write /Library/Preferences/com.apple.loginwindow AdminHostInfo HostName 2>/dev/null || true +else + sudo -v 2>/dev/null || { + echo "Sudo credentials expired. Please enter your password:" >&2 + sudo -v || exit 0 + } + sudo defaults write /Library/Preferences/com.apple.loginwindow AdminHostInfo HostName 2>/dev/null || true +fi + +# Restart applications to apply changes +if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true +else + sudo -v 2>/dev/null || { sudo -v || true; } +fi +killall Safari >/dev/null 2>&1 || true +sleep 0.5 +killall Finder >/dev/null 2>&1 || true +sleep 0.5 + diff --git a/macos-setup/prep.command b/macos-setup/prep.command new file mode 100755 index 0000000..de69605 --- /dev/null +++ b/macos-setup/prep.command @@ -0,0 +1,822 @@ +#!/usr/bin/env bash + +############################################################################### +# macOS bootstrap script for Crafture # +# # +# This script installs Homebrew, Rosetta (on Apple Silicon), a curated list # +# of apps, fonts, and App Store software, and it applies a set of macOS and # +# Finder defaults. Whenever you hit an issue the inline comments describe # +# what to adjust to get things working again. # +# # +# Configuration is loaded from prep.config (or prep.conf) - edit that file to customize. # +############################################################################### + +# Remove -e to allow script to continue on errors +set -uo pipefail + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-${0}}")" && pwd)" + +# Check if script is being run with sudo (should not be) +if [[ $EUID -eq 0 ]]; then + echo "Error: Do not run this script with sudo. Run it as a regular user:" >&2 + echo " ./prep.command" >&2 + echo "The script will prompt for your password when needed." >&2 + exit 1 +fi + +# Load Dock and Default Browser config from .Prepfile (only config vars, not commands) +PREPFILE="${SCRIPT_DIR}/.Prepfile" +if [[ -f "$PREPFILE" ]]; then + # Parse .Prepfile to extract only configuration variables + # Stop parsing when we hit the "macOS System Defaults" section (commands start there) + config_buffer="" + in_dock_section=false + + while IFS= read -r line || [[ -n "$line" ]]; do + # Stop at the macOS System Defaults section (that's where commands start) + if [[ "$line" =~ "macOS System Defaults" ]]; then + break + fi + + # Track if we're in Dock Configuration section + if [[ "$line" =~ "Dock Configuration" ]]; then + in_dock_section=true + continue + fi + + # Collect variable assignments and array contents + if [[ "$line" =~ ^[[:space:]]*DOCK_ITEMS= ]] || \ + [[ "$line" =~ ^[[:space:]]*DEFAULT_BROWSER= ]] || \ + [[ "$in_dock_section" == "true" && "$line" =~ ^[[:space:]]*\" ]] || \ + [[ "$in_dock_section" == "true" && "$line" =~ ^[[:space:]]*\) ]]; then + config_buffer+="$line"$'\n' + if [[ "$line" =~ ^[[:space:]]*\) ]]; then + in_dock_section=false + fi + fi + done < "$PREPFILE" + + # Evaluate the config buffer to set variables (safely) + if [[ -n "$config_buffer" ]]; then + eval "$config_buffer" 2>/dev/null || true + fi + + # Set defaults if not found + if [[ -z "${DOCK_ITEMS:-}" ]]; then + DOCK_ITEMS=() + fi + DEFAULT_BROWSER="${DEFAULT_BROWSER:-Google Chrome}" +else + echo "Error: .Prepfile not found at $PREPFILE" >&2 + exit 1 +fi + +# Uncomment for very noisy output useful while debugging the script. +# set -x + +# Track failures for summary at the end +FAILURES=() +SUCCESSES=() +CURRENT_STEP="" +STEP_NUM=0 +TOTAL_STEPS=11 + +# Setup log file (create it early so all functions can use it) +# Log file appends if run multiple times on the same day +LOG_FILE="${SCRIPT_DIR}/.prep-$(date '+%Y%m%d').log" +# Append separator if file exists (new run), otherwise create it +if [[ -f "$LOG_FILE" ]]; then + echo "" >> "$LOG_FILE" + echo "==========================================" >> "$LOG_FILE" + echo "New run started at $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE" + echo "==========================================" >> "$LOG_FILE" +else + touch "$LOG_FILE" # Create log file if it doesn't exist +fi +exec 3>&1 4>&2 # Save stdout/stderr + +# Log to both terminal (friendly) and file (detailed) +log_to_file() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" +} + +# Friendly terminal output +log() { + local msg="$*" + log_to_file "$msg" + printf "\r\033[K✓ %s\n" "$msg" >&3 +} + +# Detailed log (only to file, not terminal) +log_detail() { + log_to_file "$*" +} + +# Warning (friendly terminal + detailed file) +warn() { + local msg="$*" + log_to_file "WARN: $msg" + printf "\r\033[K⚠ %s\n" "$msg" >&3 +} + +# Error (friendly terminal + detailed file) +error() { + local error_msg="$1" + log_to_file "ERROR: $error_msg" + printf "\r\033[K✗ %s\n" "$error_msg" >&3 + FAILURES+=("$error_msg") +} + +# Step header (friendly) +step_start() { + STEP_NUM=$((STEP_NUM + 1)) + CURRENT_STEP="$1" + local step_msg="Step $STEP_NUM/$TOTAL_STEPS: $CURRENT_STEP" + log_to_file "==========================================" + log_to_file "STEP $STEP_NUM: $CURRENT_STEP" + log_to_file "==========================================" + printf "\n\033[1m[%d/%d]\033[0m %s...\n" "$STEP_NUM" "$TOTAL_STEPS" "$CURRENT_STEP" >&3 +} + +# Step success +step_success() { + local msg="${1:-$CURRENT_STEP completed}" + SUCCESSES+=("$msg") + log_to_file "SUCCESS: $msg" + printf "\r\033[K ✓ %s\n" "$msg" >&3 +} + +# Step in progress +step_progress() { + local msg="$*" + log_to_file "PROGRESS: $msg" + printf "\r\033[K → %s" "$msg" >&3 +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + error "Missing required command: $1 - Install it manually and re-run the script." + return 1 + fi +} + +check_apple_id_login() { + # Check if mas is installed first + if ! command -v mas >/dev/null 2>&1; then + return 0 # mas not installed yet, check will happen later + fi + + if mas account >/dev/null 2>&1; then + local apple_id + apple_id=$(mas account 2>/dev/null) + log_detail "Apple ID signed in: $apple_id (App Store apps will be installed)" + return 0 + else + # Not signed in - this is fine, App Store apps are optional + log_detail "Apple ID not signed in - App Store apps will be skipped (optional)" + return 1 + fi +} + +keep_sudo_alive() { + step_progress "Requesting admin password..." + log_detail "Requesting sudo privileges (you will be prompted for your password once)..." + + # Prompt for password once and store it securely + # We'll use SUDO_ASKPASS to automate password entry + local sudo_password + echo -n "Enter your admin password: " + read -rs sudo_password + echo "" # New line after password input + + if [[ -z "$sudo_password" ]]; then + error "Password cannot be empty." + return 1 + fi + + # Create a temporary askpass helper script + # This script will be used by sudo to get the password automatically + local askpass_script + askpass_script=$(mktemp -t sudo_askpass.XXXXXX) + chmod 700 "$askpass_script" # Restrict permissions to owner only + + # Write the password to the helper script + cat > "$askpass_script" </dev/null; then + rm -f "$askpass_script" + unset SUDO_ASKPASS + error "Failed to obtain sudo privileges. Password may be incorrect." + return 1 + fi + + # Keep sudo alive until the script ends by refreshing credentials every 2 seconds + # Use sudo -A to use our askpass helper + ( + set +e + while true; do + sleep 2 + # Refresh sudo timestamp using askpass helper + sudo -A -v >/dev/null 2>&1 || true + done + ) & + SUDO_KEEPALIVE_PID=$! + export SUDO_KEEPALIVE_PID + + # Store the askpass script path for cleanup validation + export SUDO_ASKPASS_SCRIPT="$askpass_script" + + # Cleanup function to remove askpass script and clear environment + cleanup_sudo_password() { + local cleaned=0 + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + # Overwrite the file with random data before deleting (security best practice) + if command -v shred >/dev/null 2>&1; then + shred -u "$SUDO_ASKPASS" 2>/dev/null || rm -f "$SUDO_ASKPASS" + else + # Fallback: overwrite with zeros then delete + dd if=/dev/zero of="$SUDO_ASKPASS" bs=1 count=1024 2>/dev/null || true + rm -f "$SUDO_ASKPASS" + fi + cleaned=1 + fi + # Also check the stored path + if [[ -n "${SUDO_ASKPASS_SCRIPT:-}" ]] && [[ -f "$SUDO_ASKPASS_SCRIPT" ]]; then + if command -v shred >/dev/null 2>&1; then + shred -u "$SUDO_ASKPASS_SCRIPT" 2>/dev/null || rm -f "$SUDO_ASKPASS_SCRIPT" + else + dd if=/dev/zero of="$SUDO_ASKPASS_SCRIPT" bs=1 count=1024 2>/dev/null || true + rm -f "$SUDO_ASKPASS_SCRIPT" + fi + cleaned=1 + fi + unset SUDO_ASKPASS + unset SUDO_ASKPASS_SCRIPT + kill ${SUDO_KEEPALIVE_PID} >/dev/null 2>&1 || true + + # Log cleanup status + if [[ $cleaned -eq 1 ]]; then + log_detail "Password storage cleaned up securely." + fi + } + + # Register cleanup on script exit + trap cleanup_sudo_password EXIT + + # Verify the background process started successfully + sleep 0.5 + if ! kill -0 ${SUDO_KEEPALIVE_PID} 2>/dev/null; then + warn "Sudo keep-alive background process failed to start." + cleanup_sudo_password + return 1 + fi + + # Do an immediate refresh to ensure credentials are fresh + sudo -A -v >/dev/null 2>&1 || true + + log_detail "Sudo credentials cached. Password stored securely and will be deleted when script completes." + log_detail "Keep-alive process running (PID: ${SUDO_KEEPALIVE_PID})." +} + +refresh_sudo_if_needed() { + # If SUDO_ASKPASS is set, use it for automatic password entry + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + # Use askpass helper for automatic password entry + if sudo -A -v 2>/dev/null; then + return 0 + fi + fi + + # Fallback: Try non-interactive refresh first (silent, no prompt if credentials valid) + if sudo -n -v 2>/dev/null; then + # Credentials are still valid + return 0 + fi + + # Credentials expired or invalid - refresh interactively + # This will prompt for password if needed + # macOS may invalidate credentials when system processes restart (Finder/Dock) + if sudo -v; then + # Successfully refreshed (may have prompted for password) + return 0 + fi + + # If refresh failed completely, show error + error "Failed to refresh sudo credentials. You may need to re-run the script." + return 1 +} + +ensure_homebrew() { + # Check if Homebrew is already available + if command -v brew >/dev/null 2>&1; then + log_detail "Homebrew is already installed." + configure_brew_shellenv || return 1 + return 0 + fi + + step_progress "Downloading and installing Homebrew (this may take a few minutes)..." + log_detail "Installing Homebrew (this takes a minute and might prompt for your password)." + # Redirect Homebrew installer output to log file only (suppress terminal output) + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" >> "$LOG_FILE" 2>&1 || { + error "Homebrew installation failed. Check the log file for details." + return 1 + } + configure_brew_shellenv || return 1 +} + +configure_brew_shellenv() { + if [[ -x /opt/homebrew/bin/brew ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [[ -x /usr/local/bin/brew ]]; then + eval "$(/usr/local/bin/brew shellenv)" + else + error "Homebrew binary not found after install. Check the installer output." + return 1 + fi +} + +prepare_brew() { + # Ensure shellenv is configured first + if ! command -v brew >/dev/null 2>&1; then + configure_brew_shellenv || { + error "Failed to configure Homebrew shellenv." + return 1 + } + fi + + # Verify brew is now available + if ! command -v brew >/dev/null 2>&1; then + error "Homebrew is not available even after configuring shellenv." + return 1 + fi + + step_progress "Updating Homebrew..." + log_detail "Updating Homebrew." + # Redirect brew update output to log file only + brew update >> "$LOG_FILE" 2>&1 || { + warn "Failed to update Homebrew. Check your network connection." + return 1 + } +} + +install_rosetta_if_needed() { + if [[ "$(uname -m)" != "arm64" ]]; then + log_detail "Skipping Rosetta 2 (Intel/AMD Mac detected)." + return + fi + + if /usr/bin/pgrep -q oahd; then + log_detail "Rosetta 2 already installed." + return + fi + + step_progress "Installing Rosetta 2..." + log_detail "Installing Rosetta 2 (required for Intel-only apps)." + # Refresh sudo credentials before running sudo command + refresh_sudo_if_needed || return 1 + # NOTE: --agree-to-license flag auto-accepts the license agreement + # No GUI interaction needed for Rosetta installation + # Redirect output to log file only + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A softwareupdate --install-rosetta --agree-to-license >> "$LOG_FILE" 2>&1 || { + error "Rosetta installation failed. Check the log file for details." + return 1 + } + else + sudo softwareupdate --install-rosetta --agree-to-license >> "$LOG_FILE" 2>&1 || { + error "Rosetta installation failed. Check the log file for details." + return 1 + } + fi +} + + +install_mas_first() { + # Install mas first since we need it for Apple ID login check + if brew list --formula mas >/dev/null 2>&1; then + log_detail "Formula 'mas' already installed." + return 0 + fi + step_progress "Installing mas (Mac App Store CLI)..." + log_detail "Installing brew formula: mas (needed for Apple ID check)" + # Redirect brew install output to log file only + brew install mas >> "$LOG_FILE" 2>&1 || { + error "Failed to install mas. Check the log file for details." + return 1 + } +} + +install_from_brewfile() { + local brewfile_path="${SCRIPT_DIR}/.Brewfile" + if [[ ! -f "$brewfile_path" ]]; then + error ".Brewfile not found at $brewfile_path" + return 1 + fi + + step_progress "Installing packages (this may take several minutes)..." + log_detail "Installing packages from .Brewfile: $brewfile_path" + # Redirect brew bundle output to log file only (not terminal) + if ! brew bundle --file="$brewfile_path" >> "$LOG_FILE" 2>&1; then + error "Some packages from .Brewfile failed to install. Check the log file for details." + return 1 + fi + log_detail "All packages from .Brewfile installed successfully." +} + +install_mas_apps() { + local masfile_path="${SCRIPT_DIR}/.Masfile" + if [[ ! -f "$masfile_path" ]]; then + warn ".Masfile not found at $masfile_path - skipping App Store apps" + return 0 + fi + + if ! mas account >/dev/null 2>&1; then + log "Skipping Mac App Store installs (not signed in - this is optional)" + return 0 + fi + + # Read .Masfile and install apps + local installed_ids + if ! installed_ids=$(mas list 2>/dev/null | awk '{print $1}'); then + error "Failed to list installed Mac App Store apps." + return 1 + fi + + while IFS='|' read -r app_id app_name || [[ -n "$app_id" ]]; do + # Skip empty lines and comments + [[ -z "$app_id" ]] && continue + [[ "$app_id" =~ ^[[:space:]]*# ]] && continue + # Trim whitespace + app_id=$(echo "$app_id" | xargs) + app_name=$(echo "${app_name:-}" | xargs) + + if printf '%s\n' "$installed_ids" | grep -qx "$app_id"; then + log_detail "App Store app '$app_name' already installed." + continue + fi + step_progress "Installing ${app_name:-$app_id}..." + log_detail "Installing App Store app: ${app_name:-$app_id} ($app_id)" + # Redirect mas install output to log file only + if ! mas install "$app_id" >> "$LOG_FILE" 2>&1; then + error "mas failed for ${app_name:-$app_id} ($app_id). Check the log file for details." + fi + done < "$masfile_path" +} + +make_default_browser() { + local browser="${DEFAULT_BROWSER:-Google Chrome}" + + # Skip if no default browser is configured + if [[ -z "$browser" ]]; then + log_detail "No default browser configured. Skipping." + return 0 + fi + + # NOTE: macOS may show a system dialog asking to confirm default browser change + # This is a GUI notification that requires user interaction + # The dialog will appear even if this command succeeds + step_progress "Setting default browser..." + log_detail "Setting $browser as default browser..." + if ! open -a "$browser" --new --args --make-default-browser 2>/dev/null; then + error "Failed to set $browser as default browser. macOS Sonoma and later sometimes block this flag; set it manually in the browser's settings." + else + log_detail "Attempted to set $browser as default browser." + log_detail "If a system dialog appears, click 'Use $browser' to confirm." + fi +} + +configure_dock() { + step_progress "Configuring Dock..." + log_detail "Configuring Dock layout via dockutil." + if ! command -v dockutil >/dev/null 2>&1; then + error "dockutil missing even after brew install. Run 'brew install dockutil' manually." + return 1 + fi + + # Check if DOCK_ITEMS is defined in config + if [[ -z "${DOCK_ITEMS:-}" ]] || [[ ${#DOCK_ITEMS[@]} -eq 0 ]]; then + warn "No dock items configured in config file. Skipping dock configuration." + return 0 + fi + + # NOTE: dockutil may require Full Disk Access permission + # If it fails, grant Terminal.app Full Disk Access in System Settings > Privacy & Security + # Redirect dockutil output to log file only + dockutil --remove all --no-restart >> "$LOG_FILE" 2>&1 || { + error "Could not clear Dock; you may need to grant Full Disk Access to Terminal." + return 1 + } + + for item in "${DOCK_ITEMS[@]}"; do + if [[ "$item" == "SPACER" ]]; then + # Add spacer tile + dockutil --add '' --type spacer --no-restart >> "$LOG_FILE" 2>&1 || { + error "Failed to add spacer to the Dock." + } + elif [[ -e "$item" ]]; then + dockutil --add "$item" --no-restart >> "$LOG_FILE" 2>&1 || { + error "Failed to add $item to the Dock." + } + else + warn "Dock item not found: $item (install the app first, then run 'dockutil --add \"$item\"')" + fi + done + + # NOTE: Dock restart is visual - you'll see the Dock refresh + killall Dock >/dev/null 2>&1 || true +} + +close_system_settings() { + log_detail "Closing System Settings to avoid configuration conflicts." + osascript -e 'tell application "System Preferences" to quit' 2>&1 | tee -a "$LOG_FILE" >/dev/null || true + osascript -e 'tell application "System Settings" to quit' 2>&1 | tee -a "$LOG_FILE" >/dev/null || true +} + +apply_system_defaults() { + step_progress "Applying system defaults..." + log_detail "Applying Finder, Safari, and system defaults." + + local prepfile="${SCRIPT_DIR}/.Prepfile" + if [[ ! -f "$prepfile" ]]; then + error ".Prepfile not found at $prepfile" + return 1 + fi + + # Refresh sudo before running .Prepfile (it contains sudo commands) + refresh_sudo_if_needed || return 1 + + # Export SUDO_ASKPASS so .Prepfile can use it + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + export SUDO_ASKPASS + fi + + # CRITICAL: Verify keep-alive process is still running before executing .Prepfile + if ! kill -0 ${SUDO_KEEPALIVE_PID:-0} 2>/dev/null; then + warn "Sudo keep-alive process stopped before .Prepfile execution. Restarting..." + ( + set +e + while true; do + sleep 2 + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true + else + sudo -v >/dev/null 2>&1 || true + fi + done + ) & + SUDO_KEEPALIVE_PID=$! + sleep 0.5 + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true + else + sudo -v >/dev/null 2>&1 || true + fi + fi + + # Source .Prepfile to apply macOS defaults + # We temporarily disable -e so it continues on errors (some defaults may fail) + set +e + source "$prepfile" 2>&1 | while IFS= read -r line; do + if [[ -n "$line" ]]; then + echo "$line" >&2 + fi + done + local prepfile_exit=${PIPESTATUS[0]} + set -e + + # Verify keep-alive process is still running after .Prepfile execution + if ! kill -0 ${SUDO_KEEPALIVE_PID:-0} 2>/dev/null; then + warn "Sudo keep-alive process stopped during .Prepfile execution. Restarting..." + ( + set +e + while true; do + sleep 2 + if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -f "$SUDO_ASKPASS" ]]; then + sudo -A -v >/dev/null 2>&1 || true + else + sudo -v >/dev/null 2>&1 || true + fi + done + ) & + SUDO_KEEPALIVE_PID=$! + fi + + # CRITICAL: Refresh sudo credentials immediately after .Prepfile completes + log_detail "Refreshing sudo credentials after system changes..." + if ! refresh_sudo_if_needed; then + warn "Sudo credentials expired after system restarts (this is normal)." + fi + + if [[ $prepfile_exit -ne 0 ]]; then + error "Some macOS defaults failed to apply. Check .Prepfile output above." + return 1 + fi +} + +cleanup_homebrew_locks() { + log_detail "Making sure no stale Homebrew locks remain." + + # Ensure brew is available + if ! command -v brew >/dev/null 2>&1; then + configure_brew_shellenv 2>/dev/null || true + fi + + # Try to get brew prefix + local brew_prefix + if command -v brew >/dev/null 2>&1; then + brew_prefix="$(brew --prefix 2>/dev/null || echo "")" + else + # Fallback: check standard locations + if [[ -x /opt/homebrew/bin/brew ]]; then + brew_prefix="/opt/homebrew" + elif [[ -x /usr/local/bin/brew ]]; then + brew_prefix="/usr/local" + else + warn "Homebrew not found. Skipping lock cleanup." + return 0 # Not an error - just skip if Homebrew isn't installed + fi + fi + + if [[ -n "$brew_prefix" ]] && [[ -d "${brew_prefix}/var/homebrew/locks" ]]; then + rm -rf "${brew_prefix}/var/homebrew/locks" 2>/dev/null || true + log_detail "Homebrew locks cleaned up." + else + log_detail "No Homebrew locks found (or Homebrew not installed)." + fi +} + +main() { + # Redirect stdout/stderr to log file while keeping friendly output on terminal + exec 1>&3 2>&4 + + printf "\n\033[1m═══════════════════════════════════════════════════════════════\033[0m\n" + printf "\033[1m macOS Setup Script\033[0m\n" + printf "\033[1m═══════════════════════════════════════════════════════════════\033[0m\n\n" + log_to_file "==========================================" + log_to_file "macOS Setup Script Started" + log_to_file "Log file: $LOG_FILE" + log_to_file "==========================================" + + printf "📝 Full log saved to: \033[2m%s\033[0m\n\n" "$LOG_FILE" + + require_command curl || { + error "curl is required but not found. Please install curl and re-run the script." + show_failures_summary + exit 1 + } + + # Step 1: Get admin privileges + step_start "Obtaining admin privileges" + if keep_sudo_alive; then + step_success "Admin privileges obtained" + else + warn "Could not cache sudo credentials. Some operations may prompt for password multiple times." + fi + + # Step 2: Install Homebrew + step_start "Installing/Checking Homebrew" + if ensure_homebrew; then + step_success "Homebrew ready" + else + error "Homebrew installation/configuration failed." + fi + + # Step 3: Prepare Homebrew + step_start "Preparing Homebrew" + if prepare_brew; then + step_success "Homebrew updated" + else + error "Homebrew preparation failed." + fi + + # Step 4: Install Rosetta + step_start "Installing Rosetta 2 (if needed)" + if install_rosetta_if_needed; then + step_success "Rosetta 2 ready" + else + error "Rosetta installation failed." + fi + + # Step 5: Install packages + step_start "Installing packages from .Brewfile" + if install_from_brewfile; then + step_success "Packages installed" + else + error "Package installation had errors." + fi + + # Step 6: Configure default browser + step_start "Setting default browser" + if make_default_browser; then + step_success "Default browser configured" + else + error "Default browser setup failed." + fi + + # Step 7: Configure Dock + step_start "Configuring Dock" + if configure_dock; then + step_success "Dock configured" + else + error "Dock configuration had errors." + fi + + # Step 8: Close System Settings + step_start "Closing System Settings" + if close_system_settings; then + step_success "System Settings closed" + else + warn "Failed to close system settings (may already be closed)" + fi + + # Step 9: Apply system defaults + step_start "Applying system defaults" + refresh_sudo_if_needed || { + error "Sudo credentials expired. Please re-run the script." + return 1 + } + if apply_system_defaults; then + step_success "System defaults applied" + else + error "System defaults application had errors." + fi + + # Step 10: Cleanup + step_start "Cleaning up" + if cleanup_homebrew_locks; then + step_success "Cleanup completed" + else + warn "Homebrew lock cleanup had issues (non-critical)" + fi + + # Step 11: Install App Store apps + step_start "Installing Mac App Store apps (optional)" + install_mas_first || warn "mas installation failed - skipping App Store apps" + if mas account >/dev/null 2>&1; then + if install_mas_apps; then + step_success "App Store apps installed" + else + warn "Some App Store apps failed to install" + fi + else + warn "Apple ID not signed in - App Store apps skipped" + log_detail "To install App Store apps: sign in via App Store app, then re-run this script" + fi + + printf "\n" + show_failures_summary +} + +show_failures_summary() { + printf "\n\033[1m═══════════════════════════════════════════════════════════════\033[0m\n" + printf "\033[1m Summary\033[0m\n" + printf "\033[1m═══════════════════════════════════════════════════════════════\033[0m\n\n" + + log_to_file "==========================================" + log_to_file "SUMMARY" + log_to_file "==========================================" + + if [[ ${#FAILURES[@]} -eq 0 ]]; then + printf "\033[32m✓ All tasks completed successfully!\033[0m\n\n" + printf "Next steps:\n" + printf " • Restart your Mac to apply all changes\n" + printf " • Check for any GUI notifications that need your attention\n" + printf " • Review the full log: \033[2m%s\033[0m\n" "$LOG_FILE" + log_to_file "SUCCESS: All tasks completed" + else + printf "\033[33m⚠ Completed with %d issue(s)\033[0m\n\n" "${#FAILURES[@]}" + printf "Issues encountered:\n" + for failure in "${FAILURES[@]}"; do + printf " \033[31m✗\033[0m %s\n" "$failure" + log_to_file "FAILURE: $failure" + done + printf "\nWhat to do:\n" + printf " • Review the errors above\n" + printf " • Check the full log for details: \033[2m%s\033[0m\n" "$LOG_FILE" + printf " • Fix any issues and re-run the script if needed\n" + log_to_file "FAILURES: ${#FAILURES[@]} issue(s) encountered" + fi + + printf "\n\033[2mNote: Some operations may require manual attention:\033[0m\n" + printf " • App installation confirmations\n" + printf " • Default browser change confirmation\n" + printf " • Accessibility permissions\n" + printf " • Full Disk Access requests\n" + + printf "\n\033[1m═══════════════════════════════════════════════════════════════\033[0m\n" + log_to_file "Script completed at $(date '+%Y-%m-%d %H:%M:%S')" +} + +main "$@"