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 <noreply@anthropic.com>
631 lines
24 KiB
Bash
Executable File
631 lines
24 KiB
Bash
Executable File
#!/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
|