From dbd58ff11e390c5f90b16fbaf3bab2d7ffce773d Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 14:58:30 +1000 Subject: [PATCH] MACSTARTER: interactive Mac setup script (brew/cask/mas, tweaks, keys, Tailscale) Ultrabunt-style dialog checklist installer for fresh Intel/Apple Silicon Macs. Bootstraps CLT + Homebrew, then categorized menus: core CLI, dev/AI CLIs, network, media, Android, full Xcode, GUI apps, macOS tweaks, SSH/Tailscale key setup, and the legacy macpress WordPress stack. Co-Authored-By: Claude Fable 5 --- README.md | 60 ++++- macstarter.sh | 630 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 688 insertions(+), 2 deletions(-) create mode 100755 macstarter.sh diff --git a/README.md b/README.md index 1041ac4..1eb14d5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ -# macpress -fast lamp/lemp for intel/arm macs +# 🍎 MACSTARTER + +*Fresh Mac β†’ fully loaded, without pissfarting around in terminal for hours.* + +Little sibling of [UltraBunt](https://github.com/type-two/ultrabunt). Was once a fast +LAMP/WordPress installer for Intel & Apple Silicon Macs β€” now it's a full first-time +Mac setup script with UltraBunt-style checklist menus. (The WordPress bit is still +in there as a category, for old times' sake.) + +## πŸš€ Quick start + +On a brand new Mac (Apple Silicon or Intel): + +```bash +git clone https://gitea.partly.party/monster/MACSTARTER.git +cd MACSTARTER +./macstarter.sh +``` + +That's it. The script bootstraps everything it needs itself: +Xcode Command Line Tools β†’ Homebrew β†’ menus. + +Pick **FULL SEND** from the main menu to install every recommended item in one go, +or dive into categories and tick exactly what you want. Safe to re-run β€” +everything checks before it installs. + +```bash +./macstarter.sh --catalog # just print everything it can install +``` + +## πŸŽͺ Categories + +| Category | Highlights | +|---|---| +| **Core CLI tools** | git, jq, ripgrep, fzf, htop, btop, real rsync, bat, eza, tmux… | +| **Dev tools & AI CLIs** | node, GitHub CLI, Claude Code, Antigravity CLI/IDE | +| **Network & sync** | Tailscale, rclone, nmap, mtr | +| **Media** | ffmpeg, yt-dlp, imagemagick, VLC | +| **Android** | adb + fastboot (platform-tools), scrcpy | +| **Apple dev** | Full Xcode via App Store (license accept + first-run handled) | +| **GUI apps** | Brave, Warp, Raycast, Rectangle… | +| **macOS tweaks** | natural scrolling OFF, show extensions/hidden files, fast key repeat, screenshots to ~/Screenshots, no .DS_Store litter | +| **Keys & Tailscale network** | ed25519 SSH key + keychain agent, ~/.ssh/config defaults, Tailscale login/join, CLI alias | +| **WordPress (macpress classic)** | php, mariadb, wp-cli, one-shot local WP site | + +## πŸ“ Notes + +- Full Xcode needs you signed in to the App Store first (it's ~12GB, go make a coffee). +- Tailscale join opens the menu bar app for login β€” the script picks up once you're in. +- The SSH key step copies your new public key to the clipboard, ready to paste into + your tailnet boxes or GitHub. +- Scrolling/keyboard tweaks fully apply after you log out and back in. +- Everything logs to `~/macstarter-install.log`. + +## βœ… Install policy + +Apps install via `brew` (formulae + casks) or `mas` (App Store) only. +No curl-pipe-bash randomness beyond Homebrew's own installer. diff --git a/macstarter.sh b/macstarter.sh new file mode 100755 index 0000000..2e08a6e --- /dev/null +++ b/macstarter.sh @@ -0,0 +1,630 @@ +#!/bin/bash +# ============================================================================== +# +# β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— +# β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•— +# β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• +# β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•— +# β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ +# β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β• β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β• +# +# MACSTARTER β€” fast first-time setup for Intel & Apple Silicon Macs +# Little sibling of UltraBunt, grew out of macpress. Menus, categories, +# zero ball-ache. +# +# Usage: ./macstarter.sh interactive menus +# ./macstarter.sh --catalog print the app catalog and exit +# +# Safe to re-run: everything checks before it installs. +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Colors & logging +# ------------------------------------------------------------------------------ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +NC='\033[0m' + +LOGFILE="$HOME/macstarter-install.log" + +log() { + echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOGFILE" + echo -e "$*" +} + +ok() { log "${GREEN}βœ“${NC} $*"; } +warn() { log "${YELLOW}⚠${NC} $*"; } +err() { log "${RED}βœ—${NC} $*"; } + +RESULTS_OK=() +RESULTS_FAIL=() +RESULTS_SKIP=() + +# ------------------------------------------------------------------------------ +# THE CATALOG +# Format: tag|TYPE|payload|default(on/off)|Description +# TYPE: BREW (formula) | CASK | MAS (App Store id) | CUSTOM (function name) +# ------------------------------------------------------------------------------ + +CAT_CORE=( + "git|BREW|git|on|Version control" + "wget|BREW|wget|on|Network downloader" + "jq|BREW|jq|on|JSON processor" + "ripgrep|BREW|ripgrep|on|Fast text search (rg)" + "fd|BREW|fd|on|Modern find" + "fzf|BREW|fzf|on|Fuzzy finder" + "tree|BREW|tree|on|Directory visualizer" + "tmux|BREW|tmux|on|Terminal multiplexer" + "ncdu|BREW|ncdu|on|Disk usage analyzer" + "bat|BREW|bat|on|Cat with syntax highlighting" + "eza|BREW|eza|on|Modern ls" + "tldr|BREW|tldr|on|Simplified man pages" + "htop|BREW|htop|on|Process viewer" + "btop|BREW|btop|on|Prettier process viewer" + "rsync|BREW|rsync|on|Real rsync (macOS ships a crusty old one)" + "watch|BREW|watch|off|Re-run a command on interval" + "coreutils|BREW|coreutils|off|GNU coreutils (gls, gdate, etc)" +) + +CAT_DEV=( + "node|BREW|node|on|Node.js + npm" + "gh|BREW|gh|on|GitHub CLI" + "python|BREW|python|off|Python 3 via brew" + "go|BREW|go|off|Go language" + "claude-code|CASK|claude-code|on|Claude Code β€” terminal AI coding" + "antigravity-cli|CASK|antigravity-cli|on|Google Antigravity CLI" + "antigravity|CASK|antigravity|off|Google Antigravity IDE" + "vscode|CASK|visual-studio-code|off|Visual Studio Code" + "lazygit|BREW|lazygit|off|Terminal UI for git" + "shellcheck|BREW|shellcheck|off|Shell script linter" +) + +CAT_NET=( + "tailscale|CASK|tailscale-app|on|Tailscale mesh VPN (GUI app + CLI)" + "rclone|BREW|rclone|on|Sync to/from cloud storage" + "nmap|BREW|nmap|off|Network scanner" + "mtr|BREW|mtr|off|Traceroute + ping combo" + "speedtest|BREW|speedtest-cli|off|Internet speed test" + "wireguard|BREW|wireguard-tools|off|WireGuard CLI tools" +) + +CAT_MEDIA=( + "ffmpeg|BREW|ffmpeg|on|Swiss-army media converter" + "yt-dlp|BREW|yt-dlp|off|Video downloader" + "imagemagick|BREW|imagemagick|off|Image conversion toolkit" + "exiftool|BREW|exiftool|off|Read/write media metadata" + "vlc|CASK|vlc|off|Plays literally anything" +) + +CAT_ANDROID=( + "platform-tools|CASK|android-platform-tools|on|adb + fastboot" + "scrcpy|BREW|scrcpy|on|Mirror & control Android over adb" +) + +CAT_APPLE=( + "xcode|CUSTOM|install_full_xcode|on|Full Xcode IDE via App Store (~12GB, needs sign-in)" + "xcodes|BREW|xcodesorg/made/xcodes|off|Manage multiple Xcode versions" + "swiftlint|BREW|swiftlint|off|Swift linter" +) + +CAT_APPS=( + "brave|CASK|brave-browser|on|Brave browser" + "warp|CASK|warp|on|Warp terminal" + "raycast|CASK|raycast|off|Launcher on steroids" + "rectangle|CASK|rectangle|off|Window snapping" + "the-unarchiver|CASK|the-unarchiver|off|Opens every archive format" + "stats|CASK|stats|off|Menubar system monitor" + "keka|CASK|keka|off|Archiver / compressor" +) + +CAT_TWEAKS=( + "scroll-off|CUSTOM|tweak_scroll_off|on|Turn OFF natural scrolling" + "show-ext|CUSTOM|tweak_show_extensions|on|Finder: always show file extensions" + "show-hidden|CUSTOM|tweak_show_hidden|on|Finder: show hidden files" + "finder-bars|CUSTOM|tweak_finder_bars|on|Finder: path bar + status bar" + "fast-keys|CUSTOM|tweak_fast_keys|on|Fast key repeat, no press-and-hold" + "screenshots|CUSTOM|tweak_screenshots_dir|on|Screenshots to ~/Screenshots" + "dock-autohide|CUSTOM|tweak_dock_autohide|off|Auto-hide the Dock" + "no-ds-store|CUSTOM|tweak_no_ds_store|on|No .DS_Store on network/USB drives" +) + +CAT_KEYS=( + "ssh-key|CUSTOM|setup_ssh_key|on|Generate ed25519 SSH key + keychain agent" + "ssh-config|CUSTOM|setup_ssh_config|on|~/.ssh/config with keychain defaults" + "tailscale-join|CUSTOM|tailscale_join|on|Log in to Tailscale + join your tailnet" + "tailscale-alias|CUSTOM|tailscale_alias|on|Add 'tailscale' CLI alias to ~/.zshrc" +) + +CAT_WORDPRESS=( + "php|BREW|php|on|PHP" + "mariadb|BREW|mariadb|on|MariaDB (MySQL)" + "wp-cli|BREW|wp-cli|on|WordPress CLI" + "wp-site|CUSTOM|wp_quick_site|off|Spin up a local WP site right now" +) + +CATEGORIES=( + "core|Core CLI tools|CAT_CORE" + "dev|Dev tools & AI CLIs|CAT_DEV" + "net|Network & sync|CAT_NET" + "media|Media|CAT_MEDIA" + "android|Android|CAT_ANDROID" + "apple|Apple dev|CAT_APPLE" + "apps|GUI apps|CAT_APPS" + "tweaks|macOS tweaks|CAT_TWEAKS" + "keys|Keys & Tailscale network|CAT_KEYS" + "wordpress|WordPress (macpress classic)|CAT_WORDPRESS" +) + +# ------------------------------------------------------------------------------ +# Catalog dump (non-interactive, for a quick look / sanity check) +# ------------------------------------------------------------------------------ +print_catalog() { + local cat_entry cat_title cat_var items entry tag type payload def desc + for cat_entry in "${CATEGORIES[@]}"; do + IFS='|' read -r _ cat_title cat_var <<< "$cat_entry" + echo "" + echo "== $cat_title ==" + eval "items=(\"\${${cat_var}[@]}\")" + for entry in "${items[@]}"; do + IFS='|' read -r tag type payload def desc <<< "$entry" + printf " %-16s %-7s %-28s [%s] %s\n" "$tag" "$type" "$payload" "$def" "$desc" + done + done +} + +if [[ "${1:-}" == "--catalog" ]]; then + print_catalog + exit 0 +fi + +# ------------------------------------------------------------------------------ +# Bootstrap: CLT -> Homebrew -> dialog + mas +# Runs every launch; every step is a no-op if already done. +# ------------------------------------------------------------------------------ +BREW_PREFIX="/opt/homebrew" +[[ "$(uname -m)" == "x86_64" ]] && BREW_PREFIX="/usr/local" + +bootstrap() { + echo -e "${CYAN}── macstarter bootstrap ──${NC}" + + # Xcode Command Line Tools (Homebrew needs these) + if ! xcode-select -p &>/dev/null; then + warn "Xcode Command Line Tools missing β€” triggering install dialog..." + xcode-select --install 2>/dev/null + echo -e "${WHITE}Click 'Install' in the popup, then wait. I'll poll until it's done.${NC}" + until xcode-select -p &>/dev/null; do sleep 10; done + ok "Command Line Tools installed" + else + ok "Command Line Tools present" + fi + + # Homebrew + if [[ -x "$BREW_PREFIX/bin/brew" ]]; then + ok "Homebrew present" + else + warn "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || { + err "Homebrew install failed β€” cannot continue"; exit 1; } + ok "Homebrew installed" + fi + eval "$("$BREW_PREFIX/bin/brew" shellenv)" + + # Make sure future shells get brew too + if ! grep -q 'brew shellenv' "$HOME/.zprofile" 2>/dev/null; then + echo "eval \"\$($BREW_PREFIX/bin/brew shellenv)\"" >> "$HOME/.zprofile" + ok "Added brew shellenv to ~/.zprofile" + fi + + # dialog drives the menus, mas drives the App Store + for tool in dialog mas; do + if ! command -v "$tool" &>/dev/null; then + warn "Installing $tool..." + brew install "$tool" >>"$LOGFILE" 2>&1 && ok "$tool installed" || err "$tool failed" + fi + done +} + +# ------------------------------------------------------------------------------ +# dialog helpers +# ------------------------------------------------------------------------------ +ui_msg() { + dialog --title "$1" --msgbox "$2" 14 72 +} + +ui_yesno() { + dialog --title "$1" --yesno "$2" 12 72 +} + +# ------------------------------------------------------------------------------ +# Installers +# ------------------------------------------------------------------------------ +install_formula() { + local name="$1" + if brew list --formula "$name" &>/dev/null; then + ok "$name already installed (formula)" + RESULTS_SKIP+=("$name") + elif brew install "$name" >>"$LOGFILE" 2>&1; then + ok "$name installed" + RESULTS_OK+=("$name") + else + err "$name FAILED (see $LOGFILE)" + RESULTS_FAIL+=("$name") + fi +} + +install_cask() { + local name="$1" + if brew list --cask "$name" &>/dev/null; then + ok "$name already installed (cask)" + RESULTS_SKIP+=("$name") + elif brew install --cask "$name" >>"$LOGFILE" 2>&1; then + ok "$name installed" + RESULTS_OK+=("$name") + else + err "$name FAILED (see $LOGFILE)" + RESULTS_FAIL+=("$name") + fi +} + +install_mas() { + local app_id="$1" name="$2" + if mas list 2>/dev/null | grep -q "^$app_id "; then + ok "$name already installed (App Store)" + RESULTS_SKIP+=("$name") + elif mas install "$app_id" >>"$LOGFILE" 2>&1; then + ok "$name installed from App Store" + RESULTS_OK+=("$name") + else + err "$name FAILED β€” are you signed in to the App Store? (open it, sign in, retry)" + RESULTS_FAIL+=("$name") + fi +} + +run_custom() { + local fn="$1" label="$2" + if "$fn"; then + RESULTS_OK+=("$label") + else + RESULTS_FAIL+=("$label") + fi +} + +dispatch() { + local entry="$1" tag type payload def desc + IFS='|' read -r tag type payload def desc <<< "$entry" + echo "" + log "${CYAN}── $tag: $desc ──${NC}" + case "$type" in + BREW) install_formula "$payload" ;; + CASK) install_cask "$payload" ;; + MAS) install_mas "$payload" "$tag" ;; + CUSTOM) run_custom "$payload" "$tag" ;; + esac +} + +# ------------------------------------------------------------------------------ +# CUSTOM: Apple dev +# ------------------------------------------------------------------------------ +install_full_xcode() { + if [[ -d "/Applications/Xcode.app" ]]; then + ok "Xcode already installed" + return 0 + fi + warn "Installing full Xcode from the App Store β€” this is a ~12GB download." + warn "You must be signed in to the App Store (open it and sign in if this fails)." + if mas install 497799835 >>"$LOGFILE" 2>&1; then + ok "Xcode downloaded" + warn "Accepting license + first-run setup (needs sudo)..." + sudo xcodebuild -license accept >>"$LOGFILE" 2>&1 || true + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer >>"$LOGFILE" 2>&1 || true + xcodebuild -runFirstLaunch >>"$LOGFILE" 2>&1 || true + ok "Xcode ready" + return 0 + fi + err "Xcode install failed β€” sign in to the App Store app, then re-run this item" + return 1 +} + +# ------------------------------------------------------------------------------ +# CUSTOM: macOS tweaks +# ------------------------------------------------------------------------------ +tweak_scroll_off() { + defaults write NSGlobalDomain com.apple.swipescrolldirection -bool false + ok "Natural scrolling OFF (log out/in for it to stick everywhere)" +} + +tweak_show_extensions() { + defaults write NSGlobalDomain AppleShowAllExtensions -bool true + NEED_FINDER_RESTART=1 + ok "File extensions always shown" +} + +tweak_show_hidden() { + defaults write com.apple.finder AppleShowAllFiles -bool true + NEED_FINDER_RESTART=1 + ok "Hidden files shown in Finder" +} + +tweak_finder_bars() { + defaults write com.apple.finder ShowPathbar -bool true + defaults write com.apple.finder ShowStatusBar -bool true + NEED_FINDER_RESTART=1 + ok "Finder path bar + status bar on" +} + +tweak_fast_keys() { + defaults write NSGlobalDomain KeyRepeat -int 2 + defaults write NSGlobalDomain InitialKeyRepeat -int 15 + defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false + ok "Fast key repeat, press-and-hold disabled (log out/in to apply)" +} + +tweak_screenshots_dir() { + mkdir -p "$HOME/Screenshots" + defaults write com.apple.screencapture location -string "$HOME/Screenshots" + killall SystemUIServer 2>/dev/null || true + ok "Screenshots now land in ~/Screenshots" +} + +tweak_dock_autohide() { + defaults write com.apple.dock autohide -bool true + killall Dock 2>/dev/null || true + ok "Dock auto-hides" +} + +tweak_no_ds_store() { + defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true + defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true + ok "No more .DS_Store litter on network/USB drives" +} + +# ------------------------------------------------------------------------------ +# CUSTOM: Keys & Tailscale network +# ------------------------------------------------------------------------------ +SSH_KEY="$HOME/.ssh/id_ed25519" + +setup_ssh_key() { + mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" + if [[ -f "$SSH_KEY" ]]; then + ok "SSH key already exists: $SSH_KEY" + else + ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -C "$USER@$(scutil --get LocalHostName 2>/dev/null || hostname -s)" >>"$LOGFILE" 2>&1 + ok "Generated ed25519 key: $SSH_KEY" + fi + ssh-add --apple-use-keychain "$SSH_KEY" >>"$LOGFILE" 2>&1 && ok "Key added to keychain agent" + echo "" + echo -e "${WHITE}Your public key β€” paste this into ~/.ssh/authorized_keys on your tailnet boxes" + echo -e "(or GitHub β†’ Settings β†’ SSH keys):${NC}" + echo "" + cat "$SSH_KEY.pub" + echo "" + pbcopy < "$SSH_KEY.pub" && ok "Public key copied to clipboard" + return 0 +} + +setup_ssh_config() { + local cfg="$HOME/.ssh/config" + mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" + if [[ -f "$cfg" ]] && grep -q "UseKeychain" "$cfg"; then + ok "~/.ssh/config already has keychain defaults" + return 0 + fi + cat >> "$cfg" << 'EOF' +# macstarter defaults +Host * + AddKeysToAgent yes + UseKeychain yes + IdentityFile ~/.ssh/id_ed25519 + ServerAliveInterval 60 +EOF + chmod 600 "$cfg" + ok "Wrote keychain + keepalive defaults to ~/.ssh/config" +} + +TAILSCALE_CLI="/Applications/Tailscale.app/Contents/MacOS/Tailscale" + +tailscale_join() { + if [[ ! -d "/Applications/Tailscale.app" ]]; then + err "Tailscale app not installed β€” install it from the Network & sync menu first" + return 1 + fi + open -a Tailscale + if "$TAILSCALE_CLI" status &>/dev/null; then + ok "Already connected to your tailnet:" + "$TAILSCALE_CLI" status | head -10 + return 0 + fi + echo "" + echo -e "${WHITE}Tailscale is open in your menu bar. Log in with your usual account" + echo -e "to join your tailnet, then press Enter here.${NC}" + read -r + if "$TAILSCALE_CLI" status &>/dev/null; then + ok "Connected! Your tailnet:" + "$TAILSCALE_CLI" status | head -10 + return 0 + fi + warn "Not connected yet β€” finish login in the menu bar app, it'll join automatically" + return 0 +} + +tailscale_alias() { + if [[ ! -d "/Applications/Tailscale.app" ]]; then + err "Tailscale app not installed yet" + return 1 + fi + if grep -q "alias tailscale=" "$HOME/.zshrc" 2>/dev/null; then + ok "tailscale alias already in ~/.zshrc" + return 0 + fi + echo "alias tailscale=\"$TAILSCALE_CLI\"" >> "$HOME/.zshrc" + ok "Added tailscale CLI alias to ~/.zshrc (new shells can run: tailscale status)" +} + +# ------------------------------------------------------------------------------ +# CUSTOM: macpress classic β€” a local WordPress in about a minute +# ------------------------------------------------------------------------------ +wp_quick_site() { + for need in php mariadb wp-cli; do + if ! brew list --formula "$need" &>/dev/null; then + err "$need not installed β€” tick it in the WordPress menu first" + return 1 + fi + done + local site_dir="$HOME/Sites/wp" + local db_name="wp_local" + warn "Setting up a local WordPress at $site_dir ..." + brew services start mariadb >>"$LOGFILE" 2>&1 + sleep 3 + local mysql_cmd="mariadb" + command -v mariadb &>/dev/null || mysql_cmd="mysql" + "$mysql_cmd" -u root -e "CREATE DATABASE IF NOT EXISTS $db_name" 2>>"$LOGFILE" || { + err "Couldn't create database β€” check 'brew services info mariadb'" + return 1 + } + mkdir -p "$site_dir" && cd "$site_dir" || return 1 + if [[ ! -f wp-settings.php ]]; then + wp core download >>"$LOGFILE" 2>&1 || { err "WP download failed"; return 1; } + fi + if [[ ! -f wp-config.php ]]; then + wp config create --dbname="$db_name" --dbuser=root --dbpass="" >>"$LOGFILE" 2>&1 + fi + if ! wp core is-installed &>/dev/null; then + wp core install --url="http://localhost:8080" --title="macstarter" \ + --admin_user=admin --admin_password=macstarter \ + --admin_email="admin@localhost.local" --skip-email >>"$LOGFILE" 2>&1 + fi + ok "WordPress ready at $site_dir" + echo "" + echo -e "${WHITE}Start it any time with: cd ~/Sites/wp && wp server --port=8080" + echo -e "Admin: http://localhost:8080/wp-admin (admin / macstarter β€” change it!)${NC}" + cd - >/dev/null || true +} + +# ------------------------------------------------------------------------------ +# Menus +# ------------------------------------------------------------------------------ +NEED_FINDER_RESTART=0 + +run_entries_by_tags() { + # $1 = category array name, $2 = space-separated chosen tags + local cat_var="$1" chosen="$2" items entry tag + eval "items=(\"\${${cat_var}[@]}\")" + for entry in "${items[@]}"; do + tag="${entry%%|*}" + for c in $chosen; do + c="${c%\"}"; c="${c#\"}" + [[ "$c" == "$tag" ]] && dispatch "$entry" + done + done +} + +category_checklist() { + # $1 = category array name, $2 = title + local cat_var="$1" title="$2" items entry tag type payload def desc args=() chosen + eval "items=(\"\${${cat_var}[@]}\")" + for entry in "${items[@]}"; do + IFS='|' read -r tag type payload def desc <<< "$entry" + args+=("$tag" "$desc" "$def") + done + chosen=$(dialog --title "$title" --checklist \ + "Space toggles, Enter installs. Pre-ticked = recommended." \ + 22 76 14 "${args[@]}" 3>&1 1>&2 2>&3) || return 0 + clear + [[ -z "$chosen" ]] && return 0 + run_entries_by_tags "$cat_var" "$chosen" + echo "" + read -r -p "Done. Press Enter to go back to the menu..." +} + +install_all_defaults() { + ui_yesno "FULL SEND" "Install every pre-ticked (recommended) item across ALL categories?\n\nThis is the whole M4 setup in one go. Xcode alone is ~12GB.\nTailscale login and SSH key steps will prompt when they need you." || { clear; return 0; } + clear + local cat_entry cat_var items entry def + for cat_entry in "${CATEGORIES[@]}"; do + IFS='|' read -r _ _ cat_var <<< "$cat_entry" + eval "items=(\"\${${cat_var}[@]}\")" + for entry in "${items[@]}"; do + IFS='|' read -r _ _ _ def _ <<< "$entry" + [[ "$def" == "on" ]] && dispatch "$entry" + done + done + echo "" + read -r -p "Full send complete. Press Enter for the summary..." + show_summary +} + +show_summary() { + local msg="" + msg+="Installed: ${#RESULTS_OK[@]}\n" + msg+="Skipped (already there): ${#RESULTS_SKIP[@]}\n" + msg+="Failed: ${#RESULTS_FAIL[@]}\n" + if [[ ${#RESULTS_FAIL[@]} -gt 0 ]]; then + msg+="\nFailures:\n" + local f; for f in "${RESULTS_FAIL[@]}"; do msg+=" βœ— $f\n"; done + msg+="\nDetails in $LOGFILE" + fi + ui_msg "SUMMARY" "$msg" + clear +} + +finish_up() { + if [[ "$NEED_FINDER_RESTART" == "1" ]]; then + killall Finder 2>/dev/null || true + fi + clear + echo -e "${GREEN}════════════════════════════════════════════════════${NC}" + echo -e "${WHITE} macstarter done.${NC}" + echo -e " Installed: ${GREEN}${#RESULTS_OK[@]}${NC} Already there: ${YELLOW}${#RESULTS_SKIP[@]}${NC} Failed: ${RED}${#RESULTS_FAIL[@]}${NC}" + if [[ ${#RESULTS_FAIL[@]} -gt 0 ]]; then + echo -e " ${RED}Failed:${NC} ${RESULTS_FAIL[*]}" + fi + echo -e " Log: $LOGFILE" + echo "" + echo -e " ${YELLOW}Heads-up:${NC} scrolling & keyboard tweaks fully apply after you log out/in." + echo -e " Open a new terminal (or Warp, once you're in it) to pick up brew + aliases." + echo -e "${GREEN}════════════════════════════════════════════════════${NC}" + exit 0 +} + +main_menu() { + while true; do + local args=("all" ">>> FULL SEND β€” install all recommended <<<") + local cat_entry cat_key cat_title + for cat_entry in "${CATEGORIES[@]}"; do + IFS='|' read -r cat_key cat_title _ <<< "$cat_entry" + args+=("$cat_key" "$cat_title") + done + args+=("summary" "Show install summary") + args+=("quit" "Finish & exit") + + local choice + choice=$(dialog --title "MACSTARTER" --menu \ + "Fresh Mac to fully-loaded. Pick a category:" \ + 22 70 14 "${args[@]}" 3>&1 1>&2 2>&3) || finish_up + clear + + case "$choice" in + all) install_all_defaults ;; + summary) show_summary ;; + quit) finish_up ;; + *) + for cat_entry in "${CATEGORIES[@]}"; do + IFS='|' read -r cat_key cat_title cat_var <<< "$cat_entry" + if [[ "$choice" == "$cat_key" ]]; then + category_checklist "$cat_var" "$cat_title" + fi + done + ;; + esac + done +} + +# ------------------------------------------------------------------------------ +# GO +# ------------------------------------------------------------------------------ +echo "macstarter run started $(date)" >> "$LOGFILE" +bootstrap +main_menu