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 -lWhat Changes
A Setup You Can Understand.
The script checks first, skips what is ready, asks before optional steps, and never removes installed tools.
- 01
Packages
Installs Homebrew when needed, then mise, Git, GitHub CLI, OrbStack, and Google Cloud CLI.
- 02
Applications
Offers Slack, Linear, Figma, Bruno, Chrome, VS Code, Raycast, and TestFlight. You can decline the bundle or pass --skip-apps.
- 03
Configuration
Configures Homebrew and mise for new terminals, pins Node.js and pnpm, and installs the official Popina CLI.
- 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}"