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 <noreply@anthropic.com>
This commit is contained in:
parent
6de818ed53
commit
dbd58ff11e
60
README.md
60
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.
|
||||
|
||||
630
macstarter.sh
Executable file
630
macstarter.sh
Executable file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user