diff --git a/README.md b/README.md index fed5a72..0a9f8e1 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ The setup process consists of two main phases: │ ├── formulae.txt # Homebrew formulae list │ ├── casks.txt # Homebrew casks list │ ├── logrotate.conf # Log rotation configuration -│ ├── iterm2.plist # iTerm2 profile settings +│ ├── com.googlecode.iterm2.plist # iTerm2 profile settings │ └── Orangebrew.terminal # Terminal.app profile └── docs/ # Documentation ├── setup/ # Setup documentation diff --git a/app-setup/plex-setup.sh b/app-setup/plex-setup.sh index 1de0b7a..81451d2 100755 --- a/app-setup/plex-setup.sh +++ b/app-setup/plex-setup.sh @@ -645,8 +645,8 @@ _try_ssh_host() { log "SSH connection details:" log " Target host: '${host}'" - log " SSH options: ConnectTimeout=5, BatchMode=yes" - log " Command: ssh -o ConnectTimeout=5 -o BatchMode=yes '${host}' 'echo \"SSH_OK\"'" + log " SSH options: StrictHostKeyChecking=no ConnectTimeout=5, BatchMode=yes" + log " Command: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes '${host}' 'echo \"SSH_OK\"'" # Test basic connectivity first log "Checking basic network connectivity to ${host}..." @@ -659,7 +659,7 @@ _try_ssh_host() { # Test SSH with verbose output captured log "Attempting SSH connection with detailed diagnostics..." local ssh_output - ssh_output=$(ssh -o ConnectTimeout=5 -o BatchMode=yes -v "${host}" 'echo "SSH_OK"' 2>&1) + ssh_output=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes -v "${host}" 'echo "SSH_OK"' 2>&1) local ssh_result=$? if [[ ${ssh_result} -eq 0 ]]; then @@ -676,7 +676,7 @@ _try_ssh_host() { log "Additional SSH diagnostics:" ssh_version=$(ssh -V 2>&1) log " SSH client version: ${ssh_version}" - log " SSH config test: ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=5 '${host}' 'echo test' (would use system defaults)" + log " SSH config test: ssh -F /dev/null -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 '${host}' 'echo test' (would use system defaults)" return 1 fi @@ -1435,7 +1435,8 @@ main() { if test_ssh_connection "${MIGRATE_FROM}" true >/dev/null 2>&1; then log "Manual hostname resolved successfully" else - log "⚠️ Manual hostname resolution failed - will attempt migration anyway" + log "❌️ ${MIGRATE_FROM} hostname resolution failed" + return 1 fi fi else @@ -1444,7 +1445,8 @@ main() { if test_ssh_connection "${MIGRATE_FROM}" true >/dev/null 2>&1; then log "Manual hostname resolved successfully" else - log "⚠️ Manual hostname resolution failed - will attempt migration anyway" + log "❌️ ${MIGRATE_FROM} hostname resolution failed" + return 1 fi fi else diff --git a/app-setup/templates/mount-nas-media.sh b/app-setup/templates/mount-nas-media.sh index dfec785..082b942 100755 --- a/app-setup/templates/mount-nas-media.sh +++ b/app-setup/templates/mount-nas-media.sh @@ -70,7 +70,7 @@ wait_for_network() { test_mount() { # Test basic mount verification using user-based pattern if ! mount | grep "${WHOAMI}" | grep -q "${PLEX_MEDIA_MOUNT}"; then - log "❌ Mount not visible in system mount table for user ${WHOAMI}" + log "⚠️ Mount not visible in system mount table for user ${WHOAMI}" return 1 fi log "✅ Mount verification successful (active mount found for ${WHOAMI})" diff --git a/docs/apps/plex-setup-README.md b/docs/apps/plex-setup-README.md index d2ca850..4853f95 100644 --- a/docs/apps/plex-setup-README.md +++ b/docs/apps/plex-setup-README.md @@ -112,6 +112,22 @@ rsync -av --exclude='Cache' "~/Library/Application Support/Plex Media Server/" ~ The script supports both local migration (files in `~/plex-migration/`) and remote migration via SSH. +### SSH Security for Migration + +**SSH Host Key Verification**: For automated migration workflows, the script uses `StrictHostKeyChecking=no` to prevent blocking on unknown host keys during server-to-server transfers. This is intentional for migration scenarios where: + +- Target server may not have established SSH relationships with source servers +- Migration typically occurs between trusted servers on the same network +- Automation workflows need to proceed without manual intervention for host key acceptance + +**Security Context**: This setting is used specifically for: + +- Migration connection testing: `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes` +- Automated file transfers during configuration migration +- One-time setup operations between known server pairs + +**Note**: This does not affect ongoing SSH security for regular server operations, which continue to use standard SSH host key verification. + ### Post-Migration: Home Screen Setup **⚠️ Important**: After migrating from an existing Plex server, you may need to re-pin your media sources to the home screen. diff --git a/docs/operator.md b/docs/operator.md index 36a99a6..a48476c 100644 --- a/docs/operator.md +++ b/docs/operator.md @@ -12,6 +12,8 @@ The system is configured to automatically log in as the operator user after rebo - **Desktop with clean dock** (iTerm, Plex, essential apps) - **Fast User Switching menu** in the menu bar (showing current user) +> **📋 First Login Dialogs**: On first operator login, you may encounter Apple Setup Assistant dialogs. For guidance on handling these, see [Apple First-Boot Dialog Guide](setup/apple-first-boot-dialogs.md#operator-account-first-login) + ## Automatic Setup ### 1. First-Login Customization diff --git a/docs/setup/apple-first-boot-dialogs.md b/docs/setup/apple-first-boot-dialogs.md new file mode 100644 index 0000000..ba5c91c --- /dev/null +++ b/docs/setup/apple-first-boot-dialogs.md @@ -0,0 +1,197 @@ +# Apple First-Boot Dialog Guide + +This guide provides step-by-step instructions for navigating Apple's Setup Assistant dialogs during initial Mac Mini configuration and operator account first login. + +## Admin Account Setup (Initial macOS Installation) + +When setting up the Mac Mini for the first time, you'll encounter these Apple dialogs in sequence: + +### 1. Language Selection + +- **Action**: Select your preferred language +- **Recommendation**: Choose your primary language + +### 2. Region Selection + +- **Action**: Select your country/region +- **Recommendation**: Choose your current location for proper timezone and regional settings + +### 3. Data Transfer & Migration + +- **Action**: Choose transfer method +- **Options**: + - **iPhone/iPad Transfer** ✅ Recommended if available (requires iOS device with backup) + - **Time Machine Backup** (if you have an existing backup) + - **Don't transfer any information now** (manual setup) +- **Note**: iPhone/iPad transfer can pre-configure WiFi, Apple ID, and other settings + +### 4. Accessibility + +- **Action**: Configure accessibility options +- **Recommendation**: Configure as needed for your requirements, or skip if not needed + +### 5. Data & Privacy + +- **Action**: Review Apple's privacy policy +- **Recommendation**: Continue after reading + +### 6. Create Administrator Account + +- **Full Name**: Will be pre-populated if you used iPhone/iPad transfer +- **Account Name**: Will be pre-populated if you used iPhone/iPad transfer +- **Password**: Create a strong password (you'll use this for first-boot setup) +- **Hint**: Optional password hint + +### 7. Apple Account Configuration + +#### 7.1 Terms & Conditions + +- **Action**: Agree to Apple's Terms and Conditions +- **Recommendation**: Review and agree + +#### 7.2 Customize Settings + +##### 7.2.1 Location Services + +- **Action**: Enable or disable location services +- **Recommendation**: Enable for timezone and system functionality + +##### 7.2.2 Analytics & Improvement + +- **Action**: Choose whether to share analytics with Apple +- **Recommendation**: Configure based on your privacy preferences + +##### 7.2.3 Screen Time + +- **Action**: Set up Screen Time monitoring +- **Recommendation**: Skip for server setup + +##### 7.2.4 Apple Intelligence + +- **Action**: Configure Apple's AI features +- **Recommendation**: Configure based on your preferences + +##### 7.2.5 FileVault Disk Encryption + +- **Action**: Choose whether to enable FileVault +- **⚠️ CRITICAL**: **Turn OFF FileVault!** +- **Reason**: FileVault prevents automatic login for the operator account +- **Note**: This is essential for proper server operation + +##### 7.2.6 Touch ID + +- **Action**: Set up Touch ID fingerprint authentication +- **Recommendation**: Set up for convenient sudo access during administration + +##### 7.2.7 Apple Pay + +- **Action**: Set up Apple Pay +- **Recommendation**: Configure based on your preferences + +##### 7.2.8 Choose Your Look + +- **Action**: Select Light, Dark, or Auto appearance +- **Recommendation**: Auto (adapts to time of day) + +##### 7.2.9 Software Updates + +- **Action**: Configure automatic update preferences +- **Recommendation**: Enable automatic security updates, manual for system updates + +### 8. Continue Setup + +- **Action**: Complete the setup process +- **Result**: Proceed to desktop + +### 9. Desktop + +- **Result**: macOS setup complete, ready for first-boot script execution + +--- + +## Operator Account First Login + +When the operator account logs in for the first time, they'll encounter a simplified Setup Assistant: + +### 1. Accessibility + +- **Action**: Configure accessibility options +- **Recommendation**: Configure as needed, or skip if not required + +### 2. Apple Account + +- **Action**: Sign in with Apple ID +- **⚠️ RECOMMENDATION**: **Skip this step** +- **Reason**: Server operations don't require operator Apple ID integration +- **Note**: You can always add this later if needed + +### 3. Find My + +- **Action**: Enable Find My for the device +- **Recommendation**: Configure based on your security preferences + +### 4. Analytics & Improvement + +- **Action**: Choose analytics sharing preferences +- **Recommendation**: Configure based on privacy preferences + +### 5. Screen Time + +- **Action**: Set up Screen Time +- **⚠️ RECOMMENDATION**: **Skip this step** +- **Reason**: Not relevant for server operation + +### 6. Apple Intelligence + +- **Action**: Configure Apple AI features +- **⚠️ RECOMMENDATION**: **Skip this step** +- **Reason**: Not needed for server operation, may impact performance + +### 7. Touch ID Setup Assistant + +- **Action**: Set up Touch ID +- **⚠️ RECOMMENDATION**: **Cancel/Skip this step** +- **Reason**: Operator account uses automatic login, Touch ID not typically needed + +### 8. Choose Your Look + +- **Action**: Select appearance theme +- **Recommendation**: Auto or user preference + +### 9. Continue + +- **Action**: Complete operator setup +- **Result**: Proceed to desktop + +### 10. Desktop + +- **Result**: Operator account setup complete, automatic application launch will begin + +--- + +## Important Notes + +### FileVault Warning + +**CRITICAL**: Ensure FileVault is disabled during admin account setup. FileVault encryption prevents the automatic login functionality required for proper server operation. + +### Account Purpose + +- **Admin Account**: Used for system administration, setup, and maintenance +- **Operator Account**: Used for day-to-day server operation and automatic application launch + +### Setup Timing + +- Complete Apple dialogs **before** running `first-boot.sh` +- Operator dialogs appear automatically on first operator login after reboot + +### Migration Assistant Benefits + +Using iPhone/iPad transfer during initial setup can significantly reduce manual configuration by pre-populating: + +- WiFi network settings +- Apple ID information +- Basic user account details +- System preferences + +This reduces the overall setup time and ensures consistent configuration across your devices. diff --git a/docs/setup/first-boot.md b/docs/setup/first-boot.md index 7de63d9..2621066 100644 --- a/docs/setup/first-boot.md +++ b/docs/setup/first-boot.md @@ -16,6 +16,8 @@ The `first-boot.sh` script performs complete automated setup of your Mac Mini se 5. **Reach the desktop** before proceeding 6. **Enable AirDrop:** Press Cmd-Shift-R to open AirDrop, and select "Allow me to be discovered by: Everyone" +> **📋 Detailed Setup Guide**: For step-by-step instructions on navigating Apple's Setup Assistant dialogs, see [Apple First-Boot Dialog Guide](apple-first-boot-dialogs.md) + ### Transfer Setup Files 1. **AirDrop the complete macmini-setup folder** from your development Mac @@ -297,3 +299,43 @@ After successful first boot setup: 4. **Run application setup scripts** in `~/app-setup/` The Mac Mini is now ready for native application deployment and service configuration. + +## Post-Setup Configuration + +### Safari Extension Syncing + +To sync Safari extensions across your devices (iPhone, iPad, Mac), enable extension syncing: + +1. **Open System Settings** → **Apple ID** → **iCloud** +2. **Enable Safari syncing** if not already enabled +3. **In Safari**: Go to **Safari** → **Settings** → **Extensions** +4. **Enable "Sync Safari Extensions"** to share extensions across all your devices + +This ensures your Safari extensions are available on your Mac Mini server when accessing web interfaces for applications like Plex, Transmission, or other web-based management tools. + +**Reference**: [Safari Extension Syncing Guide](https://ios.gadgethacks.com/how-to/safari-now-lets-you-sync-and-manage-all-your-web-extensions-across-your-iphone-ipad-and-mac-0385127/) + +## Known Issues + +The following issues are known limitations of the current setup system: + +### Terminal Profiles + +**iTerm2 Profile Syncing**: iTerm2 profiles do not sync properly during automated setup. The profile files are copied correctly, but iTerm2 may not recognize or apply the imported settings immediately. + +**Workaround**: + +* Manually import profiles via iTerm2 → **Preferences** → **Profiles** → **Other Actions** → **Import JSON Profiles** +* Or restart iTerm2 multiple times until profiles are recognized + +### System Notifications + +**Background Item Notifications**: macOS generates numerous "background item added" notifications during Homebrew package installation and application setup. These notifications cannot be automatically suppressed by the setup scripts. + +**Impact**: + +* Users will see multiple system notifications during setup +* Notifications are informational and do not affect setup functionality +* No user action required - notifications will clear automatically + +**Future Improvement**: Apple does not currently provide an API to suppress these notifications during automated setup processes. diff --git a/docs/setup/prep-airdrop.md b/docs/setup/prep-airdrop.md index 05dc2fa..3c31163 100644 --- a/docs/setup/prep-airdrop.md +++ b/docs/setup/prep-airdrop.md @@ -106,7 +106,7 @@ macmini-setup/ │ ├── config.conf # Server settings │ ├── dev_fingerprint.conf # Safety check data │ ├── formulae.txt # Homebrew packages -│ ├── iterm2.plist # iTerm2 profile/settings (optional) +│ ├── com.googlecode.iterm2.plist # iTerm2 profile/settings (optional) │ ├── keychain_manifest.conf # Keychain service identifiers │ ├── logrotate.conf │ ├── mac-server-setup-db # External keychain file diff --git a/prep-airdrop.sh b/prep-airdrop.sh index bc69832..9f38ada 100755 --- a/prep-airdrop.sh +++ b/prep-airdrop.sh @@ -831,12 +831,7 @@ if [[ -d "${SCRIPT_SOURCE_DIR}" ]]; then if [[ "${USE_ITERM2:-false}" == "true" ]]; then if command -v it2check >/dev/null 2>&1; then echo "Exporting iTerm2 preferences..." - if defaults export com.googlecode.iterm2 "${OUTPUT_PATH}/config/iterm2.plist"; then - add_to_manifest "config/iterm2.plist" "OPTIONAL" - echo "iTerm2 preferences exported to deployment package" - else - echo "Warning: Failed to export iTerm2 preferences" - fi + copy_with_manifest "${HOME}/Library/Preferences/com.googlecode.iterm2.plist" "config/com.googlecode.iterm2.plist" "OPTIONAL" else echo "Warning: USE_ITERM2 is enabled but iTerm2 is not installed (it2check not found)" fi @@ -890,6 +885,10 @@ chmod -R 755 "${OUTPUT_PATH}/app-setup" chmod 600 "${OUTPUT_PATH}/config/"* 2>/dev/null || true chmod 600 "${OUTPUT_PATH}/app-setup/config/"* 2>/dev/null || true +# Ensure all shell scripts are executable +echo "Making all shell scripts executable..." +find "${OUTPUT_PATH}" -name '*.sh' -exec chmod -v a+rx {} \; + # Create Keychain manifest for server-side credential access create_keychain_manifest diff --git a/scripts/server/first-boot.sh b/scripts/server/first-boot.sh index 1a1fe3e..341e0ab 100755 --- a/scripts/server/first-boot.sh +++ b/scripts/server/first-boot.sh @@ -751,34 +751,6 @@ mkdir -p "${LOG_DIR}" touch "${LOG_FILE}" chmod 600 "${LOG_FILE}" -# Function to check if a Terminal window with specific title exists -check_terminal_window_exists() { - local window_title="$1" - osascript -e " - tell application \"Terminal\" - set window_exists to false - try - repeat with w in windows - if (name of w) contains \"${window_title}\" then - set window_exists to true - exit repeat - end if - end repeat - end try - return window_exists - end tell - " 2>/dev/null -} - -# Tail log in separate window (only if one doesn't already exist) -window_exists=$(check_terminal_window_exists "Setup Log") -if [[ "${window_exists}" == "true" ]]; then - log "Found existing Setup Log window - reusing it" -else - log "Opening new Setup Log window" - osascript -e 'tell application "Terminal" to do script "printf \"\\e]0;Setup Log\\a\"; tail -F '"${LOG_FILE}"'"' || echo "oops, no tail" -fi - # Print header set_section "Starting Mac Mini '${SERVER_NAME}' Server Setup" log "Running as user: ${ADMIN_USERNAME}" diff --git a/scripts/server/operator-first-login.sh b/scripts/server/operator-first-login.sh index c30fa6c..c7c1eda 100755 --- a/scripts/server/operator-first-login.sh +++ b/scripts/server/operator-first-login.sh @@ -260,46 +260,10 @@ setup_terminal_profile() { # Task: Configure iTerm2 preferences setup_iterm2_preferences() { progress "Setting up iTerm2 preferences..." - - local iterm2_config_dir="${HOME}/.config/iterm2" - local preferences_file="${iterm2_config_dir}/iterm2.plist" - - # Check if preferences file exists - if [[ ! -f "${preferences_file}" ]]; then - progress "No iTerm2 preferences found at ${preferences_file} - skipping iTerm2 setup" - return 0 - fi - - # Check if iTerm2 is installed (more reliable detection method) - if [[ ! -d /Applications/iTerm.app ]]; then - progress "iTerm2 not installed - skipping preferences import" - return 0 - fi - - progress "Importing iTerm2 preferences..." - - # Ensure iTerm2 is not running during import for better reliability - if pgrep -f "iTerm.app" >/dev/null 2>&1; then - progress "iTerm2 is currently running - preferences import may not take effect until restart" - fi - - # Import preferences using defaults import - if defaults import com.googlecode.iterm2 "${preferences_file}"; then - progress "iTerm2 preferences import command succeeded" - - # Verify that import actually worked by checking for a key preference - if defaults read com.googlecode.iterm2 "Default Bookmark Guid" >/dev/null 2>&1; then - progress "✅ Successfully imported and verified iTerm2 preferences" - progress "Preferences will be active when iTerm2 is next launched" - else - progress "⚠️ Import command succeeded but preferences verification failed" - progress "iTerm2 preferences may not have been properly imported" - fi - else - progress "❌ Failed to import iTerm2 preferences" - progress "Check that preferences file is valid: ${preferences_file}" - progress "You can manually import by opening iTerm2 > Preferences > Profiles > Other Actions > Import JSON Profiles" - fi + killall iTerm2 &>/dev/null || true + open -a iTerm2 &>/dev/null || true + killall iTerm2 &>/dev/null || true + progress "Imported iTerm2 preferences" } # Task: Start logrotate service diff --git a/scripts/server/setup-application-preparation.sh b/scripts/server/setup-application-preparation.sh old mode 100644 new mode 100755 diff --git a/scripts/server/setup-command-line-tools.sh b/scripts/server/setup-command-line-tools.sh old mode 100644 new mode 100755 index 314d31a..e1ecab0 --- a/scripts/server/setup-command-line-tools.sh +++ b/scripts/server/setup-command-line-tools.sh @@ -229,8 +229,8 @@ ENHANCED_MONITOR_EOF done # Stop monitoring and cleanup (suppress job termination messages) - (kill "${monitor_pid}" && wait "${monitor_pid}") 2>/dev/null || true - rm -f "${monitor_script}" 2>/dev/null || true + (kill "${monitor_pid}" && wait "${monitor_pid}") &>/dev/null || true + rm -f "${monitor_script}" &>/dev/null || true # Wait for the install process to fully complete if kill -0 "${install_pid}" 2>/dev/null; then diff --git a/scripts/server/setup-dock-configuration.sh b/scripts/server/setup-dock-configuration.sh index 31cb5ee..825ca97 100755 --- a/scripts/server/setup-dock-configuration.sh +++ b/scripts/server/setup-dock-configuration.sh @@ -42,6 +42,12 @@ else exit 1 fi +# HOMEBREW_PREFIX is set and exported by first-boot.sh based on architecture +if [[ -z "${HOMEBREW_PREFIX:-}" ]]; then + echo "Error: HOMEBREW_PREFIX not set - this script must be run from first-boot.sh" + exit 1 +fi + # Set derived variables HOSTNAME="${HOSTNAME_OVERRIDE:-${SERVER_NAME}}" HOSTNAME_LOWER="$(tr '[:upper:]' '[:lower:]' <<<"${HOSTNAME}")" @@ -134,42 +140,56 @@ check_success() { fi } +# Make sure dockutil is installed +check_dockutil() { + if command -v dockutil; then + # installed and in path, all good! + return 0 + elif [[ -f "${HOMEBREW_PREFIX}/bin/dockutil" ]]; then + # installed but not in path + export PATH="${PATH}:${HOMEBREW_PREFIX}/bin" + command -v dockutil || return 1 + else + # not installed + "${HOMEBREW_PREFIX}/bin/brew" install dockutil + export PATH="${PATH}:${HOMEBREW_PREFIX}/bin" + command -v dockutil || return 1 + fi +} + # Main dock configuration function configure_dock() { set_section "Cleaning up Administrator Dock" log "Cleaning up Administrator Dock" - if command -v dockutil &>/dev/null; then - dockutil \ - --remove Messages \ - --remove Mail \ - --remove Maps \ - --remove Photos \ - --remove FaceTime \ - --remove Calendar \ - --remove Contacts \ - --remove Reminders \ - --remove Freeform \ - --remove TV \ - --remove Music \ - --remove News \ - --remove 'iPhone Mirroring' \ - --remove /System/Applications/Utilities/Terminal.app \ - --add /Applications/iTerm.app \ - --add /System/Applications/Passwords.app \ - --allhomes \ - &>/dev/null || true - check_success "Administrator Dock cleaned up" - else - log "Could not locate dockutil" - fi + dockutil \ + --remove Messages \ + --remove Mail \ + --remove Maps \ + --remove Photos \ + --remove FaceTime \ + --remove Calendar \ + --remove Contacts \ + --remove Reminders \ + --remove Freeform \ + --remove TV \ + --remove Music \ + --remove News \ + --remove 'iPhone Mirroring' \ + --remove /System/Applications/Utilities/Terminal.app \ + --add /Applications/iTerm.app \ + --add /System/Applications/Passwords.app \ + --allhomes \ + &>/dev/null || true + check_success "Administrator Dock cleaned up" } # Main execution main() { log "Starting dock configuration module" + check_dockutil configure_dock # Simple completion message diff --git a/scripts/server/setup-package-installation.sh b/scripts/server/setup-package-installation.sh index 34b1b4e..3ba1995 100755 --- a/scripts/server/setup-package-installation.sh +++ b/scripts/server/setup-package-installation.sh @@ -241,30 +241,52 @@ if [[ "${SKIP_PACKAGES}" = false ]]; then # Function to install formulae if not already installed install_formula() { - if ! "${HOMEBREW_PREFIX}/bin/brew" list "$1" &>/dev/null; then - log "Installing formula: $1" - if "${HOMEBREW_PREFIX}/bin/brew" install "$1"; then - log "✅ Formula installation: $1" + local formula="$1" + local current_count="$2" + local total_count="$3" + + if ! "${HOMEBREW_PREFIX}/bin/brew" list "${formula}" &>/dev/null; then + show_log -n "Installing formulae... (${current_count}/${total_count}) ${formula}... " + if "${HOMEBREW_PREFIX}/bin/brew" install "${formula}" &>/dev/null; then + show_log "✅" + log "✅ Formula installation: ${formula}" else - collect_error "Formula installation failed: $1" + show_log "❌" + collect_error "Formula installation failed: ${formula}" # Continue instead of exiting fi else - log "Formula already installed: $1" + show_log "Installing formulae... (${current_count}/${total_count}) ${formula} (already installed) ✅" + log "Formula already installed: ${formula}" fi } # Function to install casks if not already installed install_cask() { - if ! "${HOMEBREW_PREFIX}/bin/brew" list --cask "$1" &>/dev/null; then - log "Installing cask: $1" + local cask="$1" + local current_count="$2" + local total_count="$3" + + # Get estimated size for large packages + local size_hint="" + case "${cask}" in + plex-media-server) size_hint=" (~250 MB)" ;; + filebot) size_hint=" (~50 MB)" ;; + vlc) size_hint=" (~75 MB)" ;; + bbedit) size_hint=" (~25 MB)" ;; + *) size_hint="" ;; + esac + + if ! "${HOMEBREW_PREFIX}/bin/brew" list --cask "${cask}" &>/dev/null; then + show_log -n "Installing casks... (${current_count}/${total_count}) ${cask}${size_hint}... " # Capture /Applications before installation local before_apps before_apps=$(find /Applications -maxdepth 1 -type d -name "*.app" 2>/dev/null | sort) - if "${HOMEBREW_PREFIX}/bin/brew" install --cask "$1"; then - log "✅ Cask installation: $1" + if "${HOMEBREW_PREFIX}/bin/brew" install --cask "${cask}" &>/dev/null; then + show_log "✅" + log "✅ Cask installation: ${cask}" # Capture /Applications after installation local after_apps @@ -283,17 +305,18 @@ if [[ "${SKIP_PACKAGES}" = false ]]; then done <<<"${new_apps}" fi else - collect_error "Cask installation failed: $1" + show_log "❌" + collect_error "Cask installation failed: ${cask}" # Continue instead of exiting fi else - log "Cask already installed: $1" + show_log "Installing casks... (${current_count}/${total_count}) ${cask} (already installed) ✅" + log "Cask already installed: ${cask}" fi } # Install formulae from list if [[ -f "${FORMULAE_FILE}" ]]; then - show_log "Installing formulae from ${FORMULAE_FILE}" formulae=() if [[ -f "${FORMULAE_FILE}" ]]; then while IFS= read -r line; do @@ -302,8 +325,14 @@ if [[ "${SKIP_PACKAGES}" = false ]]; then fi done <"${FORMULAE_FILE}" fi + + formulae_count=${#formulae[@]} + show_log "Installing ${formulae_count} formulae from ${FORMULAE_FILE}" + + current=1 for formula in "${formulae[@]}"; do - install_formula "${formula}" + install_formula "${formula}" "${current}" "${formulae_count}" + ((current += 1)) done else log "Formulae list not found, skipping formula installations" @@ -311,7 +340,6 @@ if [[ "${SKIP_PACKAGES}" = false ]]; then # Install casks from list if [[ -f "${CASKS_FILE}" ]]; then - show_log "Installing casks from ${CASKS_FILE}" casks=() if [[ -f "${CASKS_FILE}" ]]; then while IFS= read -r line; do @@ -320,24 +348,39 @@ if [[ "${SKIP_PACKAGES}" = false ]]; then fi done <"${CASKS_FILE}" fi + + casks_count=${#casks[@]} + show_log "Installing ${casks_count} casks from ${CASKS_FILE}" + + current=1 for cask in "${casks[@]}"; do - install_cask "${cask}" + install_cask "${cask}" "${current}" "${casks_count}" + ((current += 1)) done else log "Casks list not found, skipping cask installations" fi # Cleanup after installation - log "Cleaning up Homebrew files" - "${HOMEBREW_PREFIX}/bin/brew" cleanup - check_success "Homebrew cleanup" + show_log -n "Cleaning up Homebrew files... " + if "${HOMEBREW_PREFIX}/bin/brew" cleanup &>/dev/null; then + show_log "✅" + log "✅ Homebrew cleanup completed" + else + show_log "⚠️" + log "⚠️ Homebrew cleanup had warnings" + fi # Run brew doctor and save output - log "Running brew doctor diagnostic" + show_log -n "Running Homebrew diagnostic... " BREW_DOCTOR_OUTPUT="${LOG_DIR}/brew-doctor-$(date +%Y%m%d-%H%M%S).log" - "${HOMEBREW_PREFIX}/bin/brew" doctor >"${BREW_DOCTOR_OUTPUT}" 2>&1 || true - log "Brew doctor output saved to: ${BREW_DOCTOR_OUTPUT}" - check_success "Brew doctor diagnostic" + if "${HOMEBREW_PREFIX}/bin/brew" doctor >"${BREW_DOCTOR_OUTPUT}" 2>&1; then + show_log "✅" + log "✅ Brew doctor diagnostic completed successfully" + else + show_log "⚠️" + log "⚠️ Brew doctor found issues - check ${BREW_DOCTOR_OUTPUT}" + fi fi diff --git a/scripts/server/setup-terminal-profiles.sh b/scripts/server/setup-terminal-profiles.sh index 6f44397..6c34213 100755 --- a/scripts/server/setup-terminal-profiles.sh +++ b/scripts/server/setup-terminal-profiles.sh @@ -147,7 +147,7 @@ else fi # iTerm2 preferences path (exported plist file) -ITERM2_PREFERENCES_PATH="${SETUP_DIR}/config/iterm2.plist" +ITERM2_PREFERENCES_PATH="${SETUP_DIR}/config/com.googlecode.iterm2.plist" # Backup preferences for a user backup_user_preferences() { @@ -185,6 +185,8 @@ import_terminal_profile_for_user() { return 1 fi + xattr -d com.apple.quarantine "${profile_file}" 2>/dev/null || true + # Extract profile name from the plist local profile_name if ! profile_name=$(plutil -extract name raw "${profile_file}" 2>/dev/null); then @@ -196,15 +198,66 @@ import_terminal_profile_for_user() { if [[ "${username}" == "${ADMIN_USERNAME}" ]]; then # Import for current admin user - direct registration - if open "${profile_file}"; then + log "Opening Terminal profile to import settings..." + + # Use AppleScript to track window IDs and close only the newly created window + local applescript_result + applescript_result=$(osascript -e " +tell application \"Terminal\" + -- Get list of existing window IDs before opening profile + set existing_window_ids to {} + repeat with w in windows + set existing_window_ids to existing_window_ids & {id of w} + end repeat + + -- Open the profile file (this will create a new window) + open POSIX file \"${profile_file}\" as alias + + -- Wait for new window to appear (max 5 seconds) + set wait_count to 0 + repeat while wait_count < 10 + delay 0.5 + set wait_count to wait_count + 1 + + -- Check if we have a new window + repeat with w in windows + if (id of w) is not in existing_window_ids then + -- Found the new window - close it and exit + close w + return \"success\" + end if + end repeat + end repeat + + return \"timeout\" +end tell +") + + if [[ "${applescript_result}" == "success" ]]; then + log "Successfully imported Terminal profile and closed temporary window" + # Set as default and startup profile defaults write com.apple.Terminal "Default Window Settings" -string "${profile_name}" defaults write com.apple.Terminal "Startup Window Settings" -string "${profile_name}" + + local new_default + new_default=$(defaults read com.apple.Terminal "Default Window Settings") + if [[ ${new_default} != "${profile_name}" ]]; then + collect_error "Failed to set ${profile_name} as Default profile for ${username}" + fi + + local new_startup + new_startup=$(defaults read com.apple.Terminal "Startup Window Settings") + if [[ ${new_startup} != "${profile_name}" ]]; then + collect_error "Failed to set ${profile_name} as Startup profile for ${username}" + fi + log "Successfully imported Terminal profile for ${username}" log "New profile will be active in next Terminal session" return 0 else - collect_error "Failed to open Terminal profile file for ${username}" + log "AppleScript window management failed: ${applescript_result}" + collect_error "Failed to import Terminal profile for ${username} - window management issue" return 1 fi else @@ -253,8 +306,13 @@ import_iterm2_preferences_for_user() { fi if [[ "${username}" == "${ADMIN_USERNAME}" ]]; then - # Import for current admin user - direct import - if defaults import com.googlecode.iterm2 "${preferences_file}"; then + # Import for current admin user - file copy + if cp -f "${preferences_file}" "${HOME}/Library/Preferences"; then + killall iTerm2 &>/dev/null || true + sleep 1 + open -a iTerm2 &>/dev/null || true + sleep 1 + killall iTerm2 &>/dev/null || true log "Successfully imported iTerm2 preferences for ${username}" log "Restart iTerm2 to see changes" return 0 @@ -266,20 +324,23 @@ import_iterm2_preferences_for_user() { # For operator user - copy preferences file to their config directory # Import will happen during operator-first-login.sh local operator_home="/Users/${username}" - local operator_config_dir="${operator_home}/.config/iterm2" - local operator_preferences_file - operator_preferences_file="${operator_config_dir}/$(basename "${preferences_file}")" + local operator_library_prefs_dir="${operator_home}/Library/Preferences" # Create config directory with proper ownership - if ! sudo -iu "${username}" mkdir -p "${operator_config_dir}"; then + if ! sudo -iu "${username}" mkdir -p "${operator_library_prefs_dir}"; then collect_error "Failed to create iTerm2 config directory for ${username}" return 1 fi # Copy preferences file to operator's config directory - if sudo cp "${preferences_file}" "${operator_preferences_file}" \ - && sudo chown "${username}:staff" "${operator_preferences_file}"; then - log "Successfully copied iTerm2 preferences to ${operator_preferences_file}" + if sudo cp "${preferences_file}" "${operator_library_prefs_dir}" \ + && sudo chown "${username}:staff" "${operator_library_prefs_dir}/${preferences_file}"; then + sudo -iu "{username}" killall iTerm2 &>/dev/null || true + sleep 1 + sudo -iu "{username}" open -a iTerm2 &>/dev/null || true + sleep 1 + sudo -iu "{username}" killall iTerm2 &>/dev/null || true + log "Successfully copied iTerm2 preferences to ${operator_library_prefs_dir}" log "Preferences will be imported during operator first login" return 0 else @@ -308,6 +369,20 @@ check_running_terminal_apps() { fi } +# Function to handle optional operations gracefully +check_optional_success() { + local exit_code="$1" + local operation_name="$2" + if [[ ${exit_code} -eq 0 ]]; then + show_log "✅ ${operation_name}" + log "✅ ${operation_name}" + else + show_log "⚠️ ${operation_name} failed (optional feature - continuing)" + log "⚠️ ${operation_name} failed (optional feature - continuing)" + # Don't use collect_error since this is optional and shouldn't block setup + fi +} + # Main terminal profile configuration function configure_terminal_profiles() { set_section "Configuring Terminal Profiles" @@ -329,14 +404,14 @@ configure_terminal_profiles() { if [[ -n "${TERMINAL_PROFILE_PATH}" ]] && [[ -f "${TERMINAL_PROFILE_PATH}" ]]; then log "Importing Terminal profiles..." import_terminal_profile_for_user "${ADMIN_USERNAME}" "${TERMINAL_PROFILE_PATH}" - check_success "Terminal profile import for admin user" + check_optional_success $? "Terminal profile import for admin user" if dscl . -list /Users 2>/dev/null | grep -q "^${OPERATOR_USERNAME}$"; then import_terminal_profile_for_user "${OPERATOR_USERNAME}" "${TERMINAL_PROFILE_PATH}" - check_success "Terminal profile import for operator user" + check_optional_success $? "Terminal profile import for operator user" fi elif [[ -n "${TERMINAL_PROFILE_PATH}" ]]; then - log "Terminal profile file not found: ${TERMINAL_PROFILE_PATH}" + log "⚠️ Terminal profile file not found: ${TERMINAL_PROFILE_PATH} (optional feature - continuing)" else log "No Terminal profile configured - skipping Terminal profile setup" fi @@ -345,14 +420,14 @@ configure_terminal_profiles() { if [[ "${USE_ITERM2:-false}" == "true" ]] && [[ -f "${ITERM2_PREFERENCES_PATH}" ]]; then log "Importing iTerm2 preferences..." import_iterm2_preferences_for_user "${ADMIN_USERNAME}" "${ITERM2_PREFERENCES_PATH}" - check_success "iTerm2 preferences import for admin user" + check_optional_success $? "iTerm2 preferences import for admin user" if dscl . -list /Users 2>/dev/null | grep -q "^${OPERATOR_USERNAME}$"; then import_iterm2_preferences_for_user "${OPERATOR_USERNAME}" "${ITERM2_PREFERENCES_PATH}" - check_success "iTerm2 preferences import for operator user" + check_optional_success $? "iTerm2 preferences import for operator user" fi elif [[ "${USE_ITERM2:-false}" == "true" ]] && [[ ! -f "${ITERM2_PREFERENCES_PATH}" ]]; then - log "iTerm2 preferences file not found: ${ITERM2_PREFERENCES_PATH}" + log "⚠️ iTerm2 preferences file not found: ${ITERM2_PREFERENCES_PATH} (optional feature - continuing)" else log "iTerm2 not configured - skipping iTerm2 preferences setup" fi @@ -366,17 +441,20 @@ main() { configure_terminal_profiles - # Simple completion message + # Since terminal profiles are optional, always report success + # Any failures in optional operations are handled as warnings, not errors local error_count=${#COLLECTED_ERRORS[@]} if [[ ${error_count} -eq 0 ]]; then show_log "✅ Terminal profile configuration completed successfully" show_log "ℹ️ Restart Terminal and iTerm2 to see new profiles" - return 0 else - show_log "❌ Terminal profile configuration completed with ${error_count} errors" - return 1 + show_log "✅ Terminal profile configuration completed (${error_count} non-critical issues)" + show_log "ℹ️ Check logs for details on optional profile import issues" + show_log "ℹ️ Restart Terminal and iTerm2 to see any successfully imported profiles" fi + + return 0 # Always return success since this is an optional module } # Execute main function