Exact production installer

Read Every Line Before You Run It.

This is the exact Bash script returned by popina.sh/install. The readable page is highlighted at build time; your terminal still receives plain text.

curl -fsSL https://popina.sh/install -o /tmp/popina-install && bash /tmp/popina-install && exec zsh -l

What Changes

A Setup You Can Understand.

The script checks first, skips what is ready, asks before optional steps, and never removes installed tools.

  1. 01

    Packages

    Installs Homebrew when needed, then mise, Git, GitHub CLI, OrbStack, and Google Cloud CLI.

  2. 02

    Applications

    Offers Slack, Linear, Figma, Bruno, Chrome, VS Code, Raycast, and TestFlight. You can decline the bundle or pass --skip-apps.

  3. 03

    Configuration

    Configures Homebrew and mise for new terminals, pins Node.js and pnpm, and installs the official Popina CLI.

  4. 04

    Workspace

    Explains the workspace, recommends ~/Workspace, accepts another location, and launches popina init.

Source

The Exact Installer.

Bash · 721 lines · macOS web profile

#!/usr/bin/env bash
# cspell:ignore loglevel macappstore NONINTERACTIVE reshim testflight TESTFLIGHT

set -Eeuo pipefail
IFS=$'\n\t'

if [[ -z "${BASH_SOURCE[0]:-}" ]]; then
  printf '%s\n' \
    'Popina setup cannot run directly from a pipe because interactive installers may consume its input.' \
    'Download the complete script first:' \
    '  curl -fsSL https://popina.sh/install -o /tmp/popina-install && bash /tmp/popina-install && exec zsh -l' >&2
  exit 1
fi

readonly INSTALLER_VERSION="0.2.2"
readonly NODE_VERSION="24.14.1"
readonly PNPM_VERSION="11.17.0"
readonly TESTFLIGHT_APP_ID="899247664"
readonly -a FORMULAE=(mise git gh)

PROFILE="web"
WORKSPACE_DIR="${POPINA_WORKSPACE:-$HOME/Workspace}"
POPINA_CLI_VERSION="${POPINA_CLI_VERSION:-latest}"
ASSUME_YES=false
DRY_RUN=false
SHOULD_INSTALL_TEAM_APPS="ask"
SHOULD_INITIALIZE_PROJECT="ask"
HAS_MANUAL_APP_STEP=false
CURRENT_PHASE="Preflight"

if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
  readonly BOLD=$'\033[1m'
  readonly MUTED=$'\033[2m'
  readonly CYAN=$'\033[36m'
  readonly GREEN=$'\033[32m'
  readonly YELLOW=$'\033[33m'
  readonly RED=$'\033[31m'
  readonly RESET=$'\033[0m'
else
  readonly BOLD=""
  readonly MUTED=""
  readonly CYAN=""
  readonly GREEN=""
  readonly YELLOW=""
  readonly RED=""
  readonly RESET=""
fi

print_line() {
  printf '%b\n' "$*"
}

step() {
  print_line "\n${CYAN}${BOLD}→${RESET} ${BOLD}$*${RESET}"
}

phase() {
  local number=$1
  local title=$2
  CURRENT_PHASE="Phase ${number} of 3 — ${title}"
  print_line "\n${CYAN}${BOLD}${CURRENT_PHASE}${RESET}"
}

success() {
  print_line "${GREEN}✓${RESET} $*"
}

note() {
  print_line "${MUTED}$*${RESET}"
}

warn() {
  print_line "${YELLOW}!${RESET} $*" >&2
}

fail() {
  print_line "${RED}Error:${RESET} $*" >&2
  exit 1
}

handle_error() {
  local status=$?
  local line=${1:-unknown}
  trap - ERR
  print_line "\n${RED}Setup stopped during ${CURRENT_PHASE} (near line ${line}).${RESET}" >&2
  print_line "Nothing already installed has been removed. Fix the issue, then run the same command again to resume." >&2
  exit "$status"
}

trap 'handle_error "$LINENO"' ERR

print_help() {
  cat <<'HELP'
Popina macOS developer setup

Usage:
  curl -fsSL https://popina.sh/install -o /tmp/popina-install
  bash /tmp/popina-install [options] && exec zsh -l

Options:
  --profile web       Select the supported web developer profile
  --workspace PATH    Choose the workspace directory (default: ~/Workspace)
  --skip-apps         Skip Slack, Linear, Figma, Bruno, Chrome, VS Code,
                      Raycast, and TestFlight
  --skip-project      Install the machine toolchain without running popina init
  --dry-run           Show the plan without changing the Mac
  --yes               Accept installer defaults
  --help              Show this help

Environment:
  POPINA_WORKSPACE     Default projects directory
  POPINA_CLI_VERSION   Override the Popina CLI npm version (default: latest)
  NO_COLOR             Disable colored output
HELP
}

while (($# > 0)); do
  case "$1" in
    --profile)
      (($# >= 2)) || fail '--profile requires a value.'
      PROFILE="$2"
      shift 2
      ;;
    --profile=*)
      PROFILE="${1#*=}"
      shift
      ;;
    --workspace)
      (($# >= 2)) || fail '--workspace requires a path.'
      WORKSPACE_DIR="$2"
      shift 2
      ;;
    --workspace=*)
      WORKSPACE_DIR="${1#*=}"
      shift
      ;;
    --skip-apps)
      SHOULD_INSTALL_TEAM_APPS=false
      shift
      ;;
    --skip-project)
      SHOULD_INITIALIZE_PROJECT=false
      shift
      ;;
    --dry-run)
      DRY_RUN=true
      shift
      ;;
    --yes)
      ASSUME_YES=true
      shift
      ;;
    --help|-h)
      print_help
      exit 0
      ;;
    *)
      fail "Unknown option: $1. Run with --help for usage."
      ;;
  esac
done

case "$PROFILE" in
  web) ;;
  mobile)
    fail 'The mobile developer profile is coming later. Use --profile web for now.'
    ;;
  *)
    fail "Unknown profile: ${PROFILE}. The supported profile is web."
    ;;
esac

expand_workspace_path() {
  [[ "$WORKSPACE_DIR" =~ [^[:space:]] ]] || fail 'The workspace path cannot be empty.'

  if [[ "$WORKSPACE_DIR" == \~ ]]; then
    WORKSPACE_DIR="$HOME"
  elif [[ "$WORKSPACE_DIR" == \~/* ]]; then
    WORKSPACE_DIR="$HOME/${WORKSPACE_DIR:2}"
  elif [[ "$WORKSPACE_DIR" != /* ]]; then
    WORKSPACE_DIR="$HOME/$WORKSPACE_DIR"
  fi
}

confirm() {
  local prompt=$1
  local default_answer=${2:-yes}
  local hint='[Y/n]'
  local answer=''

  if [[ "$ASSUME_YES" == true ]]; then
    return 0
  fi

  has_tty || fail 'Interactive input is unavailable. Re-run in a terminal or pass --yes with --skip-project.'

  if [[ "$default_answer" == no ]]; then
    hint='[y/N]'
  fi

  while true; do
    printf '%b' "${BOLD}?${RESET} ${prompt} ${MUTED}${hint}${RESET} " > /dev/tty
    IFS= read -r answer < /dev/tty || fail 'Could not read your answer.'

    if [[ -z "$answer" ]]; then
      [[ "$default_answer" == yes ]]
      return
    fi

    case "$answer" in
      y|Y|yes|YES|Yes) return 0 ;;
      n|N|no|NO|No) return 1 ;;
      *) print_line 'Please answer yes or no.' > /dev/tty ;;
    esac
  done
}

has_tty() {
  (: < /dev/tty) 2>/dev/null
}

prompt_workspace() {
  local answer=''

  if [[ "$ASSUME_YES" == true ]]; then
    expand_workspace_path
    return
  fi

  printf '%b' "${BOLD}?${RESET} Workspace directory ${MUTED}[${WORKSPACE_DIR}]${RESET} " > /dev/tty
  IFS= read -r answer < /dev/tty || fail 'Could not read the workspace path.'
  if [[ -n "$answer" ]]; then
    WORKSPACE_DIR="$answer"
  fi
  expand_workspace_path
}

ensure_macos() {
  [[ "$(uname -s)" == Darwin ]] || fail 'This installer currently supports macOS only. Linux support is coming later.'

  local architecture
  architecture="$(uname -m)"
  case "$architecture" in
    arm64|x86_64) success "macOS detected (${architecture})" ;;
    *) fail "Unsupported Mac architecture: ${architecture}." ;;
  esac
}

print_plan() {
  expand_workspace_path
  print_line "\n${BOLD}What this installer will do${RESET}"
  print_line "  1. Install Homebrew, mise, Git, GitHub CLI, OrbStack, and Google Cloud CLI."

  if [[ "$SHOULD_INSTALL_TEAM_APPS" == false ]]; then
    print_line "  2. Skip optional team applications."
  elif [[ "$ASSUME_YES" == true ]]; then
    print_line "  2. Install the eight Popina team applications."
  else
    print_line "  2. Offer the eight Popina team applications."
  fi

  print_line "  3. Install the official @popina/cli with pinned Node.js and pnpm versions."

  if [[ "$SHOULD_INITIALIZE_PROJECT" == false ]]; then
    print_line "  4. Leave workspace and project setup for later."
  else
    print_line "  4. Create ${WORKSPACE_DIR} and use popina init to set up pragma-web."
  fi
  print_line "\n${MUTED}Team apps: Slack, Linear, Figma, Bruno, Chrome, VS Code, Raycast, and TestFlight.${RESET}"
  print_line "${MUTED}You may be asked for your macOS password. GitHub and Google authentication open in your browser.${RESET}"
  print_line "${MUTED}TestFlight comes from the Mac App Store and may require Apple Account authentication.${RESET}"
  print_line "${MUTED}The installer is safe to rerun: existing tools and applications are kept.${RESET}"
}

print_dry_run_status() {
  load_homebrew_environment
  print_line "\n${BOLD}Current Mac${RESET}"

  if command -v brew >/dev/null 2>&1; then
    success 'Homebrew is already installed'
  else
    note '→ Would install Homebrew'
  fi

  local package
  for package in "${FORMULAE[@]}"; do
    if command -v brew >/dev/null 2>&1 && brew list --formula "$package" >/dev/null 2>&1; then
      success "${package} is already installed"
    else
      note "→ Would install ${package}"
    fi
  done

  if is_cask_ready orbstack '/Applications/OrbStack.app' docker; then
    success 'OrbStack is already installed'
  else
    note '→ Would install OrbStack'
  fi

  if is_cask_ready gcloud-cli '/opt/homebrew/share/google-cloud-sdk' gcloud; then
    success 'Google Cloud CLI is already installed'
  else
    note '→ Would install Google Cloud CLI'
  fi

  if [[ "$SHOULD_INSTALL_TEAM_APPS" == false ]]; then
    note '→ Team applications skipped by option'
  else
    local app
    local label
    local marker
    for app in slack linear figma bruno google-chrome visual-studio-code raycast; do
      case "$app" in
        slack)
          label='Slack'
          marker='/Applications/Slack.app'
          ;;
        linear)
          label='Linear'
          marker='/Applications/Linear.app'
          ;;
        figma)
          label='Figma'
          marker='/Applications/Figma.app'
          ;;
        bruno)
          label='Bruno'
          marker='/Applications/Bruno.app'
          ;;
        google-chrome)
          label='Google Chrome'
          marker='/Applications/Google Chrome.app'
          ;;
        visual-studio-code)
          label='Visual Studio Code'
          marker='/Applications/Visual Studio Code.app'
          ;;
        raycast)
          label='Raycast'
          marker='/Applications/Raycast.app'
          ;;
      esac

      if is_cask_ready "$app" "$marker"; then
        success "${label} is already installed"
      else
        note "→ Would install ${label}"
      fi
    done

    if [[ -d '/Applications/TestFlight.app' ]]; then
      success 'TestFlight is already installed'
    else
      note '→ Would install TestFlight (Mac App Store authentication may be required)'
    fi
  fi

  local installed_cli_version=''
  if command -v popina >/dev/null 2>&1; then
    installed_cli_version="$(popina --version 2>/dev/null || true)"
  fi

  if [[ "$installed_cli_version" == "$POPINA_CLI_VERSION" ]]; then
    success "Popina CLI ${installed_cli_version} is already installed"
  elif [[ -n "$installed_cli_version" ]]; then
    note "→ Would install Popina CLI ${POPINA_CLI_VERSION} (currently ${installed_cli_version})"
  else
    note "→ Would install Popina CLI ${POPINA_CLI_VERSION}"
  fi

  if [[ -d "$WORKSPACE_DIR" ]]; then
    success "Workspace already exists at ${WORKSPACE_DIR}"
  elif [[ "$SHOULD_INITIALIZE_PROJECT" == false ]]; then
    note '→ Workspace creation skipped by option'
  else
    note "→ Would offer to create ${WORKSPACE_DIR}"
  fi
}

load_homebrew_environment() {
  if [[ -x /opt/homebrew/bin/brew ]]; then
    eval "$(/opt/homebrew/bin/brew shellenv)"
  elif [[ -x /usr/local/bin/brew ]]; then
    eval "$(/usr/local/bin/brew shellenv)"
  fi
}

ensure_homebrew() {
  load_homebrew_environment
  if command -v brew >/dev/null 2>&1; then
    success "Homebrew is already installed"
    return
  fi

  has_tty || fail 'Homebrew installation requires an interactive terminal.'
  step 'Installing Homebrew'
  note 'macOS may ask for your administrator password.'
  if ! /usr/bin/sudo -v < /dev/tty; then
    fail 'Homebrew requires administrator access on this Mac.'
  fi
  NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  load_homebrew_environment
  command -v brew >/dev/null 2>&1 || fail 'Homebrew installed, but brew is not available on PATH.'
  success 'Homebrew installed'
}

brew_install() {
  HOMEBREW_NO_ASK=1 HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
    brew install --quiet "$@"
}

configure_homebrew_shell() {
  local brew_executable
  local profile="$HOME/.zprofile"
  local shellenv_line

  brew_executable="$(command -v brew)"
  shellenv_line="eval \"\$(${brew_executable} shellenv)\""

  touch "$profile"
  if grep -Fq "$brew_executable shellenv" "$profile"; then
    success 'Homebrew is already configured for new terminals'
  else
    printf '\n# Homebrew\n%s\n' "$shellenv_line" >> "$profile"
    success 'Homebrew added to ~/.zprofile'
  fi
}

install_formulae() {
  local missing=()
  local package

  for package in "${FORMULAE[@]}"; do
    if brew list --formula "$package" >/dev/null 2>&1; then
      success "${package} is already installed"
    else
      missing+=("$package")
    fi
  done

  if ((${#missing[@]} > 0)); then
    step 'Installing command-line tools'
    brew_install "${missing[@]}"
  fi
}

is_cask_ready() {
  local cask=$1
  local marker=$2
  local command_name=${3:-}

  brew list --cask "$cask" >/dev/null 2>&1 && return 0
  [[ -e "$marker" ]] && return 0
  [[ -n "$command_name" ]] && command -v "$command_name" >/dev/null 2>&1 && return 0
  return 1
}

install_required_apps() {
  local missing=()

  if is_cask_ready orbstack '/Applications/OrbStack.app' docker; then
    success 'OrbStack is already installed'
  else
    missing+=(orbstack)
  fi

  if is_cask_ready gcloud-cli '/opt/homebrew/share/google-cloud-sdk' gcloud; then
    success 'Google Cloud CLI is already installed'
  else
    missing+=(gcloud-cli)
  fi

  if ((${#missing[@]} > 0)); then
    step 'Installing local development services'
    brew_install --cask "${missing[@]}"
  fi
}

install_team_apps() {
  local missing=()

  if is_cask_ready slack '/Applications/Slack.app'; then
    success 'Slack is already installed'
  else
    missing+=(slack)
  fi

  if is_cask_ready linear '/Applications/Linear.app'; then
    success 'Linear is already installed'
  else
    missing+=(linear)
  fi

  if is_cask_ready figma '/Applications/Figma.app'; then
    success 'Figma is already installed'
  else
    missing+=(figma)
  fi

  if is_cask_ready bruno '/Applications/Bruno.app'; then
    success 'Bruno is already installed'
  else
    missing+=(bruno)
  fi

  if is_cask_ready google-chrome '/Applications/Google Chrome.app'; then
    success 'Google Chrome is already installed'
  else
    missing+=(google-chrome)
  fi

  if is_cask_ready visual-studio-code '/Applications/Visual Studio Code.app' code; then
    success 'Visual Studio Code is already installed'
  else
    missing+=(visual-studio-code)
  fi

  if is_cask_ready raycast '/Applications/Raycast.app'; then
    success 'Raycast is already installed'
  else
    missing+=(raycast)
  fi

  if ((${#missing[@]} > 0)); then
    step 'Installing team applications'
    brew_install --cask "${missing[@]}"
  fi

  install_testflight
}

open_testflight_store_page() {
  if command -v mas >/dev/null 2>&1 && mas open "$TESTFLIGHT_APP_ID" >/dev/null 2>&1; then
    return
  fi

  open "macappstore://apps.apple.com/app/id${TESTFLIGHT_APP_ID}" >/dev/null 2>&1 || true
}

install_testflight() {
  if [[ -d '/Applications/TestFlight.app' ]]; then
    success 'TestFlight is already installed'
    return
  fi

  step 'Installing TestFlight from the Mac App Store'
  note 'macOS may ask you to sign in to the App Store and approve the download.'

  if ! command -v mas >/dev/null 2>&1 && ! brew_install mas; then
    HAS_MANUAL_APP_STEP=true
    warn 'Could not install the Mac App Store helper. Opening TestFlight in the App Store instead.'
    open_testflight_store_page
    return
  fi

  if mas get "$TESTFLIGHT_APP_ID"; then
    success 'TestFlight installed'
    return
  fi

  HAS_MANUAL_APP_STEP=true
  warn 'TestFlight needs attention in the Mac App Store.'
  note 'Finish its installation there, then rerun this installer. Completed steps will be skipped.'
  open_testflight_store_page
}

configure_shell() {
  local mise_executable
  local profile="$HOME/.zprofile"
  local zshrc="$HOME/.zshrc"
  local activation_line
  local shims_line

  mise_executable="$(command -v mise)"
  activation_line="eval \"\$(${mise_executable} activate zsh)\""
  shims_line="eval \"\$(${mise_executable} activate zsh --shims)\""

  touch "$profile"
  if grep -Fq 'mise activate zsh --shims' "$profile"; then
    success 'mise shims are already available to login shells'
  else
    printf '\n# Popina developer toolchain\n%s\n' "$shims_line" >> "$profile"
    success 'mise shims added to ~/.zprofile'
  fi

  touch "$zshrc"
  if grep -Fq 'mise activate zsh' "$zshrc"; then
    success 'mise is already activated in zsh'
  else
    printf '\n# Popina developer toolchain\n%s\n' "$activation_line" >> "$zshrc"
    success 'mise activation added to ~/.zshrc'
  fi
}

install_popina_cli() {
  step 'Installing the official Popina CLI'
  note 'The @popina/cli package will clone and configure the Popina project in Phase 3.'
  note "Package: @popina/cli@${POPINA_CLI_VERSION}"
  mise use --quiet --global --yes "node@${NODE_VERSION}" "npm:pnpm@${PNPM_VERSION}"
  mise exec --quiet "node@${NODE_VERSION}" -- npm install --global --no-audit --no-fund --loglevel=error "@popina/cli@${POPINA_CLI_VERSION}"
  mise reshim --quiet

  local installed_version
  installed_version="$(mise exec --quiet "node@${NODE_VERSION}" -- popina --version)"
  success "Popina CLI ${installed_version} is ready"
}

start_orbstack() {
  if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
    success 'Docker-compatible runtime is running'
    return
  fi

  if open -Ra OrbStack >/dev/null 2>&1; then
    step 'Starting OrbStack'
    open -a OrbStack
    note 'Complete the first-launch prompts if OrbStack asks, then return here.'

    local attempt
    for ((attempt = 1; attempt <= 30; attempt += 1)); do
      if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
        success 'OrbStack is running'
        return
      fi
      sleep 2
    done
  fi

  warn 'OrbStack is installed but not ready yet. Open it before choosing database setup.'
}

prepare_workspace() {
  prompt_workspace
  [[ ! -e "$WORKSPACE_DIR" || -d "$WORKSPACE_DIR" ]] || fail "${WORKSPACE_DIR} exists but is not a directory."
  mkdir -p -- "$WORKSPACE_DIR"
  [[ -w "$WORKSPACE_DIR" ]] || fail "The workspace is not writable: ${WORKSPACE_DIR}."
  success "Workspace ready at ${WORKSPACE_DIR}"
}

run_project_setup() {
  has_tty || fail 'popina init needs an interactive terminal. Re-run without --skip-project from Terminal.'
  step 'Starting the official Popina project setup'
  note "@popina/cli will clone pragma-web into ${WORKSPACE_DIR}, install its mise tools, fetch secrets, and prepare the database."
  (
    cd "$WORKSPACE_DIR"
    mise exec "node@${NODE_VERSION}" -- popina init < /dev/tty
  )
}

print_line "${CYAN}${BOLD}  Popina${RESET} ${MUTED}developer setup v${INSTALLER_VERSION}${RESET}"
print_line "${MUTED}  Web profile · macOS${RESET}"

ensure_macos
print_plan

if [[ "$DRY_RUN" == true ]]; then
  print_dry_run_status
  print_line "\n${GREEN}${BOLD}Dry run complete.${RESET} No changes were made."
  exit 0
fi

confirm 'Continue with this setup?' yes || {
  note 'Setup canceled. No changes were made.'
  exit 0
}

phase 1 'Prepare Your Mac'
ensure_homebrew
configure_homebrew_shell
install_formulae
install_required_apps

if [[ "$SHOULD_INSTALL_TEAM_APPS" == ask ]]; then
  if confirm 'Install Slack, Linear, Figma, Bruno, Chrome, VS Code, Raycast, and TestFlight?' yes; then
    SHOULD_INSTALL_TEAM_APPS=true
  else
    SHOULD_INSTALL_TEAM_APPS=false
  fi
fi

if [[ "$SHOULD_INSTALL_TEAM_APPS" == true ]]; then
  install_team_apps
else
  note 'Skipping optional team applications.'
fi

configure_shell
install_popina_cli

phase 2 'Create Your Workspace'
print_line 'A workspace is the parent folder where you keep your code repositories.'
print_line 'We recommend ~/Workspace so Popina and future repositories stay together.'
print_line 'You can choose ~/Code, Code, or any other writable directory.'
if [[ "$SHOULD_INITIALIZE_PROJECT" == ask ]]; then
  if confirm 'Create a workspace and continue with project setup?' yes; then
    SHOULD_INITIALIZE_PROJECT=true
  else
    SHOULD_INITIALIZE_PROJECT=false
  fi
fi

if [[ "$SHOULD_INITIALIZE_PROJECT" == true ]]; then
  prepare_workspace
  phase 3 'Set Up Popina'
  start_orbstack
  run_project_setup
else
  expand_workspace_path
  print_line "\n${GREEN}${BOLD}Your Mac is ready for Popina.${RESET}"
  print_line "Run this when you want to create the project:"
  print_line "  ${CYAN}mkdir -p \"${WORKSPACE_DIR}\" && cd \"${WORKSPACE_DIR}\" && popina init${RESET}"
fi

if [[ "$HAS_MANUAL_APP_STEP" == true ]]; then
  print_line "\n${YELLOW}${BOLD}Setup completed with one follow-up.${RESET} Finish installing TestFlight from the Mac App Store."
else
  print_line "\n${GREEN}${BOLD}Setup complete.${RESET}"
fi

print_line "\n${BOLD}Reload this terminal before using Popina:${RESET}"
print_line "  ${CYAN}exec zsh -l${RESET}"