From 6c172655d4498f37e6f6f0938559b18e060ac06f Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Sat, 8 Nov 2025 01:11:58 +0100 Subject: [PATCH 01/11] Feat: Add macOS setup script and configuration files for automated installation of Homebrew, applications, and system defaults --- README.md | 70 ++++ macos-setup/.Brewfile | 28 ++ macos-setup/.DS_Store | Bin 0 -> 6148 bytes macos-setup/.Masfile | 12 + macos-setup/.Prepfile | 114 ++++++ macos-setup/prep.command | 822 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1046 insertions(+) create mode 100644 README.md create mode 100644 macos-setup/.Brewfile create mode 100644 macos-setup/.DS_Store create mode 100644 macos-setup/.Masfile create mode 100644 macos-setup/.Prepfile create mode 100755 macos-setup/prep.command diff --git a/README.md b/README.md new file mode 100644 index 0000000..7924e22 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# macOS Setup Script + +Automated macOS setup script that installs Homebrew, apps, fonts, and configures system defaults. + +## 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. + +### `.Prepfile` +macOS system defaults, Dock configuration, and default browser settings. 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 `.Prepfile` +7. Configures Dock from `.Prepfile` +8. Applies macOS defaults from `.Prepfile` +9. Cleans up Homebrew locks +10. Installs App Store apps from `.Masfile` (optional, requires Apple ID) + +## Editing Files + +Each file (`.Brewfile`, `.Masfile`, `.Prepfile`) 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` +- **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/.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 0000000000000000000000000000000000000000..6fffbccb448e4f4c06d7171e7e873c59b1246d15 GIT binary patch literal 6148 zcmeHKJ5Iz<474FdB$}-d<-5Y(AVi@@>;XW#k)YUBRzg%X>9_^AL0pUR2PsjsloT+Q z{3g%#AkMXBnx0YWLVbt~a~ka9mWL4j8wRUIy98 z^PJyaOml~!>$dx0i&*{oaru6C|9L&n_dn*3Pt#|+1(ikxr~nn90#x8%DuAACR$HH? zQ2{DI1*Qtv_o2WIo5V5DKOI 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 "$@" From 056d43df0c6b34bf37bb486aced3b62209f6c84e Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Wed, 14 Jan 2026 22:19:04 +0100 Subject: [PATCH 02/11] Initialize macOS setup with configuration files for Homebrew, applications, and system defaults --- .gitignore | 7 + macos-setup-client/.Brewfile | 16 + macos-setup-client/.Masfile | 9 + macos-setup-client/.Prepfile | 117 +++++ macos-setup-client/prep.command | 822 ++++++++++++++++++++++++++++++++ 5 files changed, 971 insertions(+) create mode 100644 .gitignore create mode 100644 macos-setup-client/.Brewfile create mode 100644 macos-setup-client/.Masfile create mode 100644 macos-setup-client/.Prepfile create mode 100755 macos-setup-client/prep.command 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/macos-setup-client/.Brewfile b/macos-setup-client/.Brewfile new file mode 100644 index 0000000..a5919ba --- /dev/null +++ b/macos-setup-client/.Brewfile @@ -0,0 +1,16 @@ +# 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 Casks (applications) +cask "1password" # 1Password password manager +cask "slack" # Slack messaging app +cask "google-chrome" # Google Chrome browser +cask "google-drive" # Google drive desktop +cask "whatsapp" # Whatsapp Desktop + diff --git a/macos-setup-client/.Masfile b/macos-setup-client/.Masfile new file mode 100644 index 0000000..45c494c --- /dev/null +++ b/macos-setup-client/.Masfile @@ -0,0 +1,9 @@ +# 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 diff --git a/macos-setup-client/.Prepfile b/macos-setup-client/.Prepfile new file mode 100644 index 0000000..7488358 --- /dev/null +++ b/macos-setup-client/.Prepfile @@ -0,0 +1,117 @@ +#!/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/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" +) + +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-client/prep.command b/macos-setup-client/prep.command new file mode 100755 index 0000000..de69605 --- /dev/null +++ b/macos-setup-client/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 "$@" From 1a56639db580cb56101a4629f523babf03c0ed6a Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 16:37:45 +0100 Subject: [PATCH 03/11] Refactor macOS setup: consolidate configuration into a single prep.sh script, removing legacy files (.Brewfile, .Masfile, .Prepfile) and updating installation instructions for Homebrew packages and system defaults. --- README.md | 12 +- macos-setup-client/.Brewfile | 16 - macos-setup-client/.Masfile | 9 - macos-setup-client/.Prepfile | 117 ----- macos-setup-client/prep.command | 822 -------------------------------- macos-setup-client/prep.sh | 434 +++++++++++++++++ 6 files changed, 439 insertions(+), 971 deletions(-) delete mode 100644 macos-setup-client/.Brewfile delete mode 100644 macos-setup-client/.Masfile delete mode 100644 macos-setup-client/.Prepfile delete mode 100755 macos-setup-client/prep.command create mode 100644 macos-setup-client/prep.sh diff --git a/README.md b/README.md index 7924e22..25e642f 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,6 @@ Homebrew packages (formulae, casks, fonts). Contains brief editing instructions ### `.Masfile` Mac App Store apps. One app per line: `app_id|App Name`. Contains brief editing instructions at the top of the file. -### `.Prepfile` -macOS system defaults, Dock configuration, and default browser settings. 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. @@ -33,15 +30,15 @@ Hidden log file with detailed output from script runs. Appends if run multiple t 3. Prepares Homebrew (updates, configures) 4. Installs Rosetta 2 (Apple Silicon only) 5. Installs packages from `.Brewfile` -6. Sets default browser from `.Prepfile` -7. Configures Dock from `.Prepfile` -8. Applies macOS defaults from `.Prepfile` +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`, `.Prepfile`) contains brief editing instructions at the top. Open the file to see how to add/remove items. +Each file (`.Brewfile`, `.Masfile`) contains brief editing instructions at the top. Open the file to see how to add/remove items. ## Requirements @@ -52,6 +49,7 @@ Each file (`.Brewfile`, `.Masfile`, `.Prepfile`) contains brief editing instruct ## 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 diff --git a/macos-setup-client/.Brewfile b/macos-setup-client/.Brewfile deleted file mode 100644 index a5919ba..0000000 --- a/macos-setup-client/.Brewfile +++ /dev/null @@ -1,16 +0,0 @@ -# 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 Casks (applications) -cask "1password" # 1Password password manager -cask "slack" # Slack messaging app -cask "google-chrome" # Google Chrome browser -cask "google-drive" # Google drive desktop -cask "whatsapp" # Whatsapp Desktop - diff --git a/macos-setup-client/.Masfile b/macos-setup-client/.Masfile deleted file mode 100644 index 45c494c..0000000 --- a/macos-setup-client/.Masfile +++ /dev/null @@ -1,9 +0,0 @@ -# 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 diff --git a/macos-setup-client/.Prepfile b/macos-setup-client/.Prepfile deleted file mode 100644 index 7488358..0000000 --- a/macos-setup-client/.Prepfile +++ /dev/null @@ -1,117 +0,0 @@ -#!/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/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" -) - -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-client/prep.command b/macos-setup-client/prep.command deleted file mode 100755 index de69605..0000000 --- a/macos-setup-client/prep.command +++ /dev/null @@ -1,822 +0,0 @@ -#!/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 "$@" diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh new file mode 100644 index 0000000..b34840f --- /dev/null +++ b/macos-setup-client/prep.sh @@ -0,0 +1,434 @@ +#!/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) +# - 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 +# 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)" + +############################################################################### +# 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=( + "mas" + "dockutil" +) + +BREW_CASKS=( + "1password" + "slack" + "google-chrome" + "google-drive" + "whatsapp" +) + +############################################################################### +# Mac App Store (mas) # +############################################################################### + +MAS_APPS=( + "409201541|Pages" + "409203825|Numbers" + "409183694|Keynote" +) + +############################################################################### +# Default Apps # +############################################################################### + +DEFAULT_BROWSER="Google Chrome" + +############################################################################### +# Logging and Helpers # +############################################################################### + +LOG_FILE="${SCRIPT_DIR}/.prep-defaults-$(date '+%Y%m%d').log" +FAILURES=() +STEP_NUM=0 +TOTAL_STEPS=4 +REVERT_DEFAULTS=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[@]}") +} + +filter_dock_items + +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_optional() { + local desc="$1" + shift + if "$@" >> "$LOG_FILE" 2>&1; then + log "$desc" + else + warn "$desc (failed, ignored)" + 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]\n" "$(basename "$0")" + printf " --revert Revert defaults set by this script\n" +} + +############################################################################### +# Homebrew # +############################################################################### + +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 + + 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 + + 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' +} + +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" + run_optional "Remove FinderInfo xattr" xattr -d com.apple.FinderInfo "$HOME/Library" + + # 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) + 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 +} + +main() { + for arg in "$@"; do + case "$arg" in + --revert) + REVERT_DEFAULTS=true + ;; + -h|--help) + show_usage + exit 0 + ;; + esac + done + + 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 "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 "Restarting affected apps" + restart_apps + + # Optional: Install Mac App Store apps (requires mas + sign-in) + # step_start "Installing App Store apps" + # install_mas_apps + + # Optional: Install Homebrew + step_start "Installing Homebrew" + install_homebrew + + # Optional: Install Homebrew packages (inline list) + step_start "Installing Homebrew packages" + install_brew_packages + + # 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 From 07bb3e64e337244290c63a38087cca6a750592fc Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 16:41:19 +0100 Subject: [PATCH 04/11] Add oneliner installation command to README for easier setup --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 25e642f..57a3922 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Automated macOS setup script that installs Homebrew, apps, fonts, and configures system defaults. +## Oneliner +curl -fsSL https://raw.githubusercontent.com/your-org/your-repo/main/yourtool | bash + ## Quick Start 1. Copy the `macos-setup` folder to your Mac From b5f3602d5345da7a03321714f3542e8f65cd7ce1 Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 16:42:10 +0100 Subject: [PATCH 05/11] Update oneliner installation command in README to point to the correct prep.sh script location --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57a3922..8e5f2e2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Automated macOS setup script that installs Homebrew, apps, fonts, and configures system defaults. ## Oneliner -curl -fsSL https://raw.githubusercontent.com/your-org/your-repo/main/yourtool | bash +curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | bash ## Quick Start From d959fb6f242467974675d0f20092681309747b6a Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 17:02:25 +0100 Subject: [PATCH 06/11] Enhance macOS setup script: add user configuration for default apps, implement Xcode Command Line Tools installation, and update log file location. Adjust total steps for installation process. --- macos-setup-client/prep.sh | 70 ++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh index b34840f..6e566b6 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -1,7 +1,7 @@ #!/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 -# - Default browser: Change DEFAULT_BROWSER value # - 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") @@ -13,6 +13,19 @@ 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 # ############################################################################### @@ -58,20 +71,14 @@ MAS_APPS=( "409183694|Keynote" ) -############################################################################### -# Default Apps # -############################################################################### - -DEFAULT_BROWSER="Google Chrome" - ############################################################################### # Logging and Helpers # ############################################################################### -LOG_FILE="${SCRIPT_DIR}/.prep-defaults-$(date '+%Y%m%d').log" +LOG_FILE="${HOME}/Downloads/.prep-defaults-$(date '+%Y%m%d').log" FAILURES=() STEP_NUM=0 -TOTAL_STEPS=4 +TOTAL_STEPS=7 REVERT_DEFAULTS=false log_to_file() { @@ -160,7 +167,7 @@ run_optional() { show_summary() { printf "\nSummary\n" - printf "-------\n" + printf "\n" if [[ ${#FAILURES[@]} -eq 0 ]]; then printf "All tasks completed successfully.\n" else @@ -181,6 +188,22 @@ show_usage() { # Homebrew # ############################################################################### +ensure_xcode_cli_tools() { + if /usr/bin/xcode-select -p >/dev/null 2>&1; then + log "Xcode Command Line Tools already installed" + 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" +} + install_homebrew() { if command -v brew >/dev/null 2>&1; then log "Homebrew already installed" @@ -379,6 +402,21 @@ main() { 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 + + # 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 @@ -392,18 +430,6 @@ main() { step_start "Restarting affected apps" restart_apps - # Optional: Install Mac App Store apps (requires mas + sign-in) - # step_start "Installing App Store apps" - # install_mas_apps - - # Optional: Install Homebrew - step_start "Installing Homebrew" - install_homebrew - - # Optional: Install Homebrew packages (inline list) - step_start "Installing Homebrew packages" - install_brew_packages - # 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 From 9d34c401185bb969d8cc2cf4bc7e81fdaac7a106 Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 17:28:55 +0100 Subject: [PATCH 07/11] Enhance macOS setup script: add support for client profiles, update README with new oneliner commands, and implement sudo session management for admin tasks. --- README.md | 10 +- macos-setup-client/README.md | 69 ++++++++++++++ macos-setup-client/prep.sh | 180 +++++++++++++++++++++++++++++++---- 3 files changed, 238 insertions(+), 21 deletions(-) create mode 100644 macos-setup-client/README.md diff --git a/README.md b/README.md index 8e5f2e2..c1a3df5 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,14 @@ Automated macOS setup script that installs Homebrew, apps, fonts, and configures system defaults. -## Oneliner -curl -fsSL https://raw.githubusercontent.com/samsoeapp/macos/refs/heads/main/macos-setup-client/prep.sh | bash +## 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 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 index 6e566b6..92aec57 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -6,6 +6,7 @@ # - 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 @@ -48,18 +49,8 @@ DOCK_ITEMS=( # Homebrew # ############################################################################### -BREW_FORMULAE=( - "mas" - "dockutil" -) - -BREW_CASKS=( - "1password" - "slack" - "google-chrome" - "google-drive" - "whatsapp" -) +BREW_FORMULAE=() +BREW_CASKS=() ############################################################################### # Mac App Store (mas) # @@ -80,6 +71,8 @@ FAILURES=() STEP_NUM=0 TOTAL_STEPS=7 REVERT_DEFAULTS=false +CLIENT_PROFILE="" +SUDO_KEEPALIVE_PID="" log_to_file() { printf "[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" @@ -126,8 +119,6 @@ filter_dock_items() { DOCK_ITEMS=("${filtered[@]}") } -filter_dock_items - step_start() { STEP_NUM=$((STEP_NUM + 1)) printf "\n[%d/%d] %s\n" "$STEP_NUM" "$TOTAL_STEPS" "$1" @@ -155,6 +146,17 @@ run_cmd() { fi } +run_cmd_admin() { + local desc="$1" + shift + 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 @@ -165,6 +167,46 @@ run_optional() { fi } +run_optional_admin() { + local desc="$1" + shift + 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=$! +} + +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" @@ -180,8 +222,46 @@ show_summary() { } show_usage() { - printf "Usage: %s [--revert]\n" "$(basename "$0")" - printf " --revert Revert defaults set by this script\n" + 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 " 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}" } ############################################################################### @@ -379,18 +459,75 @@ restart_apps() { run_optional "Restart Finder" killall Finder } -main() { - for arg in "$@"; do - case "$arg" in +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 ;; -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 + filter_dock_items printf "macOS Defaults Setup\n" printf "Log file: %s\n\n" "$LOG_FILE" @@ -402,6 +539,8 @@ main() { show_setting "Default Finder view style" "com.apple.Finder" "FXPreferredViewStyle" printf "\n" + init_sudo + step_start "Installing Xcode Command Line Tools" ensure_xcode_cli_tools @@ -427,6 +566,9 @@ main() { apply_defaults fi + step_start "Configuring Dock" + configure_dock + step_start "Restarting affected apps" restart_apps From d5d3769a7019697306f402b3741110ff4c10fd3b Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 17:35:52 +0100 Subject: [PATCH 08/11] Refactor macOS setup script: reintroduce filter_dock_items function to ensure dock items are managed correctly during setup process. --- macos-setup-client/prep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh index 92aec57..6e8edf9 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -527,7 +527,6 @@ main() { trap cleanup_sudo EXIT parse_args "$@" apply_client_profile - filter_dock_items printf "macOS Defaults Setup\n" printf "Log file: %s\n\n" "$LOG_FILE" @@ -551,6 +550,7 @@ main() { # 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" From 052147d8932e15f5dc6d99783c85ec142349a19a Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 17:45:54 +0100 Subject: [PATCH 09/11] Implement sudo session management in macOS setup script: add ensure_sudo function and modify admin command functions to ensure proper sudo initialization. --- macos-setup-client/prep.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh index 6e8edf9..44cad9e 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -73,6 +73,7 @@ TOTAL_STEPS=7 REVERT_DEFAULTS=false CLIENT_PROFILE="" SUDO_KEEPALIVE_PID="" +SUDO_INITIALIZED=false log_to_file() { printf "[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" @@ -149,6 +150,7 @@ run_cmd() { run_cmd_admin() { local desc="$1" shift + ensure_sudo log_to_file "ADMIN: $desc" if sudo -n "$@" >> "$LOG_FILE" 2>&1; then log "$desc" @@ -170,6 +172,7 @@ run_optional() { run_optional_admin() { local desc="$1" shift + ensure_sudo log_to_file "ADMIN: $desc" if sudo -n "$@" >> "$LOG_FILE" 2>&1; then log "$desc" @@ -196,6 +199,14 @@ init_sudo() { sleep 60 done & SUDO_KEEPALIVE_PID=$! + SUDO_INITIALIZED=true +} + +ensure_sudo() { + if [[ "${SUDO_INITIALIZED}" == "true" ]]; then + return 0 + fi + init_sudo } cleanup_sudo() { @@ -295,6 +306,7 @@ install_homebrew() { 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 @@ -310,6 +322,7 @@ install_brew_packages() { return 0 fi + ensure_sudo local formula for formula in "${BREW_FORMULAE[@]}"; do run_optional "Install Homebrew formula ($formula)" brew install "$formula" @@ -538,8 +551,6 @@ main() { show_setting "Default Finder view style" "com.apple.Finder" "FXPreferredViewStyle" printf "\n" - init_sudo - step_start "Installing Xcode Command Line Tools" ensure_xcode_cli_tools From 74c6dbc1726ed9b2950d975ad3317c635fafc9af Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 17:50:24 +0100 Subject: [PATCH 10/11] Add support for accepting Xcode license in macOS setup script: introduce ACCEPT_XCODE_LICENSE flag and update usage instructions. --- macos-setup-client/prep.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh index 44cad9e..707467d 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -74,6 +74,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" @@ -236,6 +237,7 @@ 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" } @@ -282,6 +284,9 @@ apply_client_profile() { 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 @@ -293,6 +298,18 @@ ensure_xcode_cli_tools() { 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() { @@ -518,6 +535,10 @@ parse_args() { CLIENT_PROFILE="${1#*=}" shift ;; + --accept-xcode-license) + ACCEPT_XCODE_LICENSE=true + shift + ;; -h|--help) show_usage exit 0 From e39e8aabfe643ae657c3f97c6ce1e73f253bccba Mon Sep 17 00:00:00 2001 From: samsoeapp Date: Thu, 15 Jan 2026 18:01:32 +0100 Subject: [PATCH 11/11] Add Safari initialization function to macOS setup script: ensure Safari container is created if missing and improve handling of FinderInfo xattr removal. --- macos-setup-client/prep.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/macos-setup-client/prep.sh b/macos-setup-client/prep.sh index 707467d..4a4c070 100644 --- a/macos-setup-client/prep.sh +++ b/macos-setup-client/prep.sh @@ -387,6 +387,18 @@ close_system_settings() { 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 @@ -409,7 +421,11 @@ apply_defaults() { # Unhide ~/Library run_optional "Unhide ~/Library" chflags nohidden "$HOME/Library" - run_optional "Remove FinderInfo xattr" xattr -d com.apple.FinderInfo "$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 @@ -426,6 +442,7 @@ apply_defaults() { 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