#!/usr/bin/env bash set -euo pipefail STACK_NAME="Node product stack" NODE_FORMULA="node@22" FORMULAE=(git "$NODE_FORMULA" pnpm bun postgresql@16 redis) CASKS=(visual-studio-code) SERVICES=(postgresql@16 redis) if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then RESET="$(printf '\033[0m')"; BOLD="$(printf '\033[1m')"; BLUE="$(printf '\033[38;5;39;1m')"; CYAN="$(printf '\033[38;5;117;1m')"; GREEN="$(printf '\033[32;1m')"; YELLOW="$(printf '\033[33;1m')"; RED="$(printf '\033[38;5;196;1m')"; DIM="$(printf '\033[2m')" else RESET=""; BOLD=""; BLUE=""; CYAN=""; GREEN=""; YELLOW=""; RED=""; DIM="" fi STEP=0 TOTAL_STEPS=7 say() { printf '%b\n' "$*"; } blank() { printf '\n'; } paint() { printf '%b%s%b' "$1" "$2" "$RESET"; } fail() { blank; say "$(paint "$RED" "Macish failed:") $*"; exit 1; } warn() { say "$(paint "$YELLOW" "warning:") $*"; } note() { say " $(paint "$CYAN" "->") $*"; } step() { STEP=$((STEP + 1)); blank; say "$(paint "$BLUE" "==>") $(paint "$BOLD" "[$STEP/$TOTAL_STEPS] $1")"; } run() { say "$(paint "$DIM" " $*")"; "$@"; } banner() { blank say "$(paint "$BLUE" 'Macish')" blank say "$(paint "$BOLD" "$STACK_NAME")" say "Node.js, pnpm, bun, PostgreSQL, Redis, and VS Code for product work." } confirm_ready() { [[ "${MACISH_YES:-${MACI_YES:-}}" == "1" || "${CI:-}" == "true" ]] && return [[ -r /dev/tty ]] || return blank say "$(paint "$YELLOW" "Ready to install?") Press return to continue, or Ctrl-C to stop." read -r _ >"$file" note "Added shell config: $line" fi } ensure_xcode_clt() { if xcode-select -p >/dev/null 2>&1; then note "Xcode Command Line Tools already installed"; return; fi run xcode-select --install || true warn "Apple opened the Command Line Tools installer. Finish it, then run this script again." exit 1 } brew_bin_after_install() { if [[ -x /opt/homebrew/bin/brew ]]; then printf '/opt/homebrew/bin/brew'; elif [[ -x /usr/local/bin/brew ]]; then printf '/usr/local/bin/brew'; else return 1; fi } load_homebrew() { local brew_bin brew_bin="$(command -v brew || true)" [[ -n "$brew_bin" ]] || brew_bin="$(brew_bin_after_install)" || return 1 eval "$("$brew_bin" shellenv)" } ensure_homebrew() { if load_homebrew; then note "Homebrew already installed" else run env NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" load_homebrew || fail "Homebrew installed, but brew was not found on PATH." fi local brew_bin brew_bin="$(command -v brew)" append_once "$HOME/.zprofile" "eval \"\$(${brew_bin} shellenv)\"" } ensure_formula() { local formula="$1" if brew list --formula "$formula" >/dev/null 2>&1; then note "$formula already installed"; else run brew install "$formula"; fi } ensure_cask() { local cask="$1" if brew list --cask "$cask" >/dev/null 2>&1; then note "$cask already installed"; else run brew install --cask "$cask"; fi } configure_node_path() { local node_prefix node_prefix="$(brew --prefix "$NODE_FORMULA")" export PATH="${node_prefix}/bin:$PATH" append_once "$HOME/.zprofile" "export PATH=\"${node_prefix}/bin:\$PATH\"" append_once "$HOME/.zshrc" "export PATH=\"${node_prefix}/bin:\$PATH\"" hash -r } ensure_service_started() { local service="$1" if brew services list | awk 'NR > 1 { print $1 ":" $2 }' | grep -q "^${service}:started$"; then note "$service service already started"; else run brew services start "$service"; fi } show_version() { local label="$1"; shift if "$@" >/dev/null 2>&1; then note "$label: $("$@" 2>&1 | head -n 1)"; else warn "$label was installed but is not available in this shell yet."; fi } main() { banner confirm_ready step "Checking macOS prerequisites" require_macos ensure_xcode_clt step "Installing Homebrew" ensure_homebrew step "Installing command line packages" for formula in "${FORMULAE[@]}"; do ensure_formula "$formula"; done step "Configuring Node shell path" configure_node_path step "Installing desktop apps" for cask in "${CASKS[@]}"; do ensure_cask "$cask"; done step "Starting local services" for service in "${SERVICES[@]}"; do ensure_service_started "$service"; done step "Finishing up" show_version "Node" node --version show_version "pnpm" pnpm --version show_version "bun" bun --version show_version "PostgreSQL" postgres --version show_version "Redis" redis-server --version blank say "$(paint "$GREEN" "Done.") $STACK_NAME is ready." } main "$@"