secure_agent_envs/setup_env.sh
guessthepw d7788a5212 Creates dual-mode development sandbox setup
Implements orchestration on macOS and provisioning on Linux for isolated Claude Code environments

Adds interactive component selection with visual menu interface
Enables secure VM creation with disabled host filesystem access
Provides comprehensive toolchain including PostgreSQL, Erlang/Elixir, and browser automation
Configures VNC desktop access for OAuth workflows and browser-based tasks
2026-01-25 09:25:57 -05:00

879 lines
32 KiB
Bash
Executable file

#!/bin/bash
# setup_env.sh
# ============================================================================
# OrbStack Development Sandbox Setup Script
# ============================================================================
#
# Dual-mode script:
# - On macOS: Orchestrates VM creation via OrbStack
# - On Linux: Provisions the VM with all development tools
#
# USAGE (from macOS):
# ./setup_env.sh [vm-name] # Creates and provisions a new VM
# ./setup_env.sh my-project # Custom VM name
#
# On first run, prompts for git credentials and VNC password, saves to config.env.
# Subsequent runs reuse config.env (shared across all VMs).
#
# USAGE (from inside a VM):
# ./setup_env.sh # Interactive mode (prompts per component)
# ./setup_env.sh -y # Accept all components without prompting
# ./setup_env.sh --non-interactive # Requires env vars, implies -y
#
# ============================================================================
set -euo pipefail
# ============================================================================
# Configuration - edit versions here
# ============================================================================
ERLANG_VERSION="28.3.1"
ELIXIR_VERSION="1.19.5-otp-28"
# ============================================================================
# Helpers
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}=== $1 ===${NC}"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date +%H:%M:%S)] INFO: $1" >> "$LOG_FILE"
fi
}
log_warn() {
echo -e "${YELLOW}WARNING: $1${NC}"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date +%H:%M:%S)] WARN: $1" >> "$LOG_FILE"
fi
}
log_error() {
echo -e "${RED}ERROR: $1${NC}" >&2
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date +%H:%M:%S)] ERROR: $1" >> "$LOG_FILE"
fi
}
command_exists() {
command -v "$1" &>/dev/null
}
# ============================================================================
# macOS Host Mode
# ============================================================================
if [[ "$(uname -s)" == "Darwin" ]]; then
echo -e "${GREEN}"
echo "============================================================================"
echo " OrbStack Development Sandbox - Host Setup"
echo "============================================================================"
echo -e "${NC}"
# Check for OrbStack
if ! command_exists orb; then
log_error "OrbStack is not installed."
echo ""
echo "Install OrbStack from: https://orbstack.dev"
echo " brew install orbstack"
echo ""
echo "After installing, run this script again."
exit 1
fi
# VM name from argument or default
VM_NAME="${1:-dev-sandbox}"
# Locate config.env relative to this script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.env"
# Create or load config
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
echo -e "${YELLOW}Using saved config from: $CONFIG_FILE${NC}"
echo " Name: $GIT_NAME"
echo " Email: $GIT_EMAIL"
echo " VNC Pass: ******"
echo ""
else
echo "First run — creating config file for shared credentials."
echo ""
read -rp "Enter your name for git config: " GIT_NAME
read -rp "Enter your email for git config: " GIT_EMAIL
while true; do
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
echo
if [ ${#VNC_PASSWORD} -ge 6 ]; then
break
fi
echo "Password must be at least 6 characters."
done
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
log_error "Name and email are required."
exit 1
fi
cat > "$CONFIG_FILE" <<EOF
GIT_NAME="$GIT_NAME"
GIT_EMAIL="$GIT_EMAIL"
VNC_PASSWORD="$VNC_PASSWORD"
EOF
chmod 600 "$CONFIG_FILE"
echo ""
echo "Config saved to: $CONFIG_FILE"
echo ""
fi
# ---- Interactive component selection ----
# Component IDs and descriptions
COMP_IDS=(postgresql mise node erlang chromium vnc ollama claude playwright plugins)
COMP_NAMES=(
"PostgreSQL"
"mise"
"Node.js (LTS)"
"Erlang/Elixir"
"Chromium"
"VNC + XFCE"
"Ollama"
"Claude Code"
"Playwright"
"Claude Plugins"
)
COMP_DESCS=(
"Database server for local development"
"Version manager for Node.js, Erlang, Elixir"
"JavaScript runtime (required for Claude Code)"
"BEAM VM + Elixir functional language"
"Browser for automation and testing"
"Remote desktop for browser-based login flows"
"Local LLM runner for offline inference"
"AI coding assistant CLI from Anthropic"
"Browser testing and automation framework"
"Code review, feature dev, browser automation plugins"
)
# All selected by default
COMP_COUNT=${#COMP_IDS[@]}
declare -a COMP_SELECTED
for ((i=0; i<COMP_COUNT; i++)); do
COMP_SELECTED[$i]=1
done
draw_menu() {
# Move cursor up to redraw (except first draw)
if [ "${MENU_DRAWN:-}" = "1" ]; then
printf "\033[%dA" $((COMP_COUNT + 3))
fi
MENU_DRAWN=1
echo -e "${YELLOW}Select components to install:${NC} "
for ((i=0; i<COMP_COUNT; i++)); do
local check=" "
if [ "${COMP_SELECTED[$i]}" = "1" ]; then
check="x"
fi
local prefix=" "
if [ "$i" = "$CURSOR" ]; then
prefix="> "
fi
printf "%s[%s] %-20s %s\033[K\n" "$prefix" "$check" "${COMP_NAMES[$i]}" "${COMP_DESCS[$i]}"
done
echo ""
echo -e " ↑/↓: move space: toggle a: all n: none enter: confirm\033[K"
}
CURSOR=0
draw_menu
while true; do
# Read a single character
IFS= read -rsn1 char
case "$char" in
$'\x1b')
# Escape sequence (arrow keys) — read remaining 2 bytes
IFS= read -rsn2 seq
case "$seq" in
"[A") # Up
((CURSOR > 0)) && ((CURSOR--)) || true
;;
"[B") # Down
((CURSOR < COMP_COUNT - 1)) && ((CURSOR++)) || true
;;
esac
;;
" ")
# Toggle
if [ "${COMP_SELECTED[$CURSOR]}" = "1" ]; then
COMP_SELECTED[$CURSOR]=0
else
COMP_SELECTED[$CURSOR]=1
fi
;;
"a"|"A")
# Select all
for ((i=0; i<COMP_COUNT; i++)); do
COMP_SELECTED[$i]=1
done
;;
"n"|"N")
# Deselect all
for ((i=0; i<COMP_COUNT; i++)); do
COMP_SELECTED[$i]=0
done
;;
"")
# Enter - confirm
break
;;
esac
draw_menu
done
echo ""
# Build SKIP env var exports for unselected components
SKIP_EXPORTS=""
for ((i=0; i<COMP_COUNT; i++)); do
if [ "${COMP_SELECTED[$i]}" = "0" ]; then
UPPER_ID=$(echo "${COMP_IDS[$i]}" | tr '[:lower:]' '[:upper:]')
SKIP_EXPORTS="${SKIP_EXPORTS}export SKIP_${UPPER_ID}=1; "
fi
done
# Show summary
echo -e "${YELLOW}Selected components:${NC}"
ANY_SELECTED=false
for ((i=0; i<COMP_COUNT; i++)); do
if [ "${COMP_SELECTED[$i]}" = "1" ]; then
echo " + ${COMP_NAMES[$i]}"
ANY_SELECTED=true
fi
done
if [ "$ANY_SELECTED" = false ]; then
echo " (none selected, only base dependencies will be installed)"
fi
echo ""
# Check if VM already exists
if orb list 2>/dev/null | grep -qw "$VM_NAME"; then
log_error "VM '$VM_NAME' already exists."
echo " To delete it: orb delete $VM_NAME"
echo " To shell in: ssh $VM_NAME@orb"
exit 1
fi
echo -e "${YELLOW}Creating VM: $VM_NAME${NC}"
orb create ubuntu "$VM_NAME"
echo ""
echo -e "${YELLOW}Disabling host filesystem access inside VM...${NC}"
# Remove macOS home directory access for security isolation
# OrbStack mounts the host home at /mnt/mac and symlinks /Users -> /mnt/mac/Users
orb run -m "$VM_NAME" sudo bash -c "
umount /mnt/mac 2>/dev/null || true
rm -rf /mnt/mac
rm -f /Users
mkdir -p /Users
# Prevent remount on reboot by masking the mount
echo '# Disabled by setup_env.sh for security isolation' > /etc/fstab.d/orbstack-mac 2>/dev/null || true
"
echo ""
echo -e "${YELLOW}Copying setup script into VM...${NC}"
orb push -m "$VM_NAME" "$SCRIPT_DIR/setup_env.sh" /tmp/setup_env.sh
echo ""
if [ -n "$SKIP_EXPORTS" ]; then
echo -e "${YELLOW}Skipping:${NC} $SKIP_EXPORTS"
fi
echo -e "${YELLOW}Running provisioning inside VM (this will take a while)...${NC}"
echo ""
orb run -m "$VM_NAME" bash -c "${SKIP_EXPORTS}export GIT_NAME='$GIT_NAME'; export GIT_EMAIL='$GIT_EMAIL'; export VNC_PASSWORD='$VNC_PASSWORD'; bash /tmp/setup_env.sh --non-interactive"
echo ""
echo -e "${GREEN}============================================================================${NC}"
echo -e "${GREEN} VM '$VM_NAME' is ready!${NC}"
echo -e "${GREEN}============================================================================${NC}"
echo ""
echo -e "${YELLOW}CONNECT:${NC}"
echo " SSH: ssh $VM_NAME@orb"
echo " Zed: Open Remote Folder -> $VM_NAME@orb:~/projects"
echo " VS Code: Remote-SSH -> $VM_NAME@orb"
echo " Files: /Volumes/OrbStack/$VM_NAME/home/\$(whoami)/"
echo ""
echo -e "${YELLOW}VNC (for browser login):${NC}"
echo " Start: ssh $VM_NAME@orb -- vnc-start"
echo " Connect: open vnc://$VM_NAME.orb.local:5901"
echo " Stop: ssh $VM_NAME@orb -- vnc-stop"
echo ""
echo -e "${YELLOW}POSTGRESQL:${NC}"
echo " From VM: psql dev"
echo " From macOS: psql -h $VM_NAME.orb.local -d dev"
echo ""
echo -e "${YELLOW}CLAUDE CODE:${NC}"
echo " ssh $VM_NAME@orb -- claude"
echo ""
exit 0
fi
# ============================================================================
# Linux VM Mode (everything below runs inside the VM)
# ============================================================================
LOG_FILE="/tmp/setup_env_$(date +%Y%m%d_%H%M%S).log"
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Setup failed (exit code: $exit_code). Log saved to: $LOG_FILE"
log_error "Review the log for details on what went wrong."
fi
}
trap cleanup EXIT
# Prevent mise from reading host config via OrbStack mount (/Users/john/.mise.toml)
export MISE_GLOBAL_CONFIG_FILE="$HOME/.config/mise/config.toml"
export MISE_CONFIG_DIR="$HOME/.config/mise"
export MISE_DATA_DIR="$HOME/.local/share/mise"
export MISE_IGNORED_CONFIG_PATHS="/Users"
export MISE_YES=1
# ============================================================================
# Argument parsing
# ============================================================================
NON_INTERACTIVE=false
AUTO_ACCEPT=false
for arg in "$@"; do
case $arg in
--non-interactive)
NON_INTERACTIVE=true
AUTO_ACCEPT=true
;;
--yes|-y)
AUTO_ACCEPT=true
;;
*)
log_error "Unknown argument: $arg"
echo "Usage: $0 [--non-interactive] [--yes|-y]"
exit 1
;;
esac
done
# ============================================================================
# Component selection helper
# ============================================================================
prompt_install() {
local id="$1"
local name="$2"
local description="$3"
# Check for SKIP_<ID>=1 env var (set by macOS host mode component selector)
local skip_var="SKIP_${id^^}"
if [ "${!skip_var:-}" = "1" ]; then
log_warn "Skipping $name (deselected)"
return 1
fi
if [ "$AUTO_ACCEPT" = true ]; then
log_info "$name"
echo " $description"
return 0
fi
echo ""
echo -e "${GREEN}=== $name ===${NC}"
echo " $description"
read -rp " Install? (Y/n) " -n 1 REPLY
echo
if [[ "$REPLY" =~ ^[Nn]$ ]]; then
log_warn "Skipping $name"
return 1
fi
return 0
}
# Track what was installed for dependency checks
INSTALLED_NODE=false
INSTALLED_CHROMIUM=false
# ============================================================================
# Pre-flight checks
# ============================================================================
if [ "$(id -u)" -eq 0 ]; then
log_error "Do not run this script as root. It will use sudo where needed."
exit 1
fi
if ! command_exists sudo; then
log_error "sudo is required but not installed."
exit 1
fi
echo -e "${GREEN}"
echo "============================================================================"
echo " OrbStack Development Sandbox Setup (VM Provisioning)"
echo "============================================================================"
echo -e "${NC}"
echo "Log file: $LOG_FILE"
echo ""
# ============================================================================
# Git configuration prompts
# ============================================================================
if [ "$NON_INTERACTIVE" = true ]; then
if [ -z "${GIT_NAME:-}" ] || [ -z "${GIT_EMAIL:-}" ]; then
log_error "In non-interactive mode, GIT_NAME and GIT_EMAIL env vars are required."
exit 1
fi
if [ -z "${VNC_PASSWORD:-}" ]; then
log_error "In non-interactive mode, VNC_PASSWORD env var is required (min 6 chars)."
exit 1
fi
else
read -rp "Enter your name for git config: " GIT_NAME
read -rp "Enter your email for git config: " GIT_EMAIL
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
log_error "Name and email are required."
exit 1
fi
while true; do
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
echo
if [ ${#VNC_PASSWORD} -ge 6 ]; then
break
fi
echo "Password must be at least 6 characters."
done
echo ""
echo -e "${YELLOW}Setting up with:${NC}"
echo " Name: $GIT_NAME"
echo " Email: $GIT_EMAIL"
echo " Erlang: $ERLANG_VERSION"
echo " Elixir: $ELIXIR_VERSION"
echo ""
fi
# ============================================================================
# System update (always runs)
# ============================================================================
log_info "Updating system"
sudo apt-get update -qq && sudo apt-get upgrade -y -qq >> "$LOG_FILE" 2>&1
# ============================================================================
# Base dependencies (always runs)
# ============================================================================
log_info "Installing base dependencies"
sudo apt-get install -y -qq \
curl \
wget \
git \
build-essential \
autoconf \
libncurses5-dev \
libssl-dev \
libwxgtk3.2-dev \
libgl1-mesa-dev \
libglu1-mesa-dev \
libpng-dev \
libssh-dev \
unixodbc-dev \
xsltproc \
fop \
libxml2-utils \
unzip \
inotify-tools \
jq \
ripgrep \
fd-find \
direnv \
tmux \
python3 \
python3-pip \
python3-venv >> "$LOG_FILE" 2>&1
# Create symlink for fd (Ubuntu packages it as fdfind)
if [ ! -L /usr/local/bin/fd ] && command_exists fdfind; then
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
fi
# Install yq and watchexec (not in apt)
log_info "Installing yq and watchexec"
if ! command_exists yq; then
YQ_URL="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64"
sudo curl -fsSL "$YQ_URL" -o /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
fi
if ! command_exists watchexec; then
WATCHEXEC_VERSION="2.3.2"
WATCHEXEC_URL="https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/watchexec-${WATCHEXEC_VERSION}-aarch64-unknown-linux-gnu.tar.xz"
curl -fsSL "$WATCHEXEC_URL" | sudo tar -xJ --strip-components=1 -C /usr/local/bin/ --wildcards '*/watchexec'
fi
# Enable direnv hook in bashrc
if ! grep -q 'direnv hook bash' ~/.bashrc; then
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
fi
# ============================================================================
# PostgreSQL
# ============================================================================
if prompt_install "postgresql" "PostgreSQL" "Database server for local development"; then
sudo apt-get install -y -qq postgresql postgresql-contrib libpq-dev >> "$LOG_FILE" 2>&1
sudo systemctl enable postgresql
sudo systemctl start postgresql
# Create user (idempotent)
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$USER'" | grep -q 1; then
sudo -u postgres createuser -s "$USER"
echo " Created PostgreSQL superuser: $USER"
else
log_warn "PostgreSQL user '$USER' already exists, skipping"
fi
# Create dev database (idempotent)
if ! psql -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw dev; then
createdb dev
echo " Created database: dev"
else
log_warn "Database 'dev' already exists, skipping"
fi
# Configure pg_hba.conf idempotently
PG_HBA=$(find /etc/postgresql -name pg_hba.conf 2>/dev/null | head -1)
PG_CONF=$(find /etc/postgresql -name postgresql.conf 2>/dev/null | head -1)
if [ -z "$PG_HBA" ] || [ -z "$PG_CONF" ]; then
log_error "Could not find PostgreSQL configuration files"
exit 1
fi
# Local socket auth: keep peer (authenticates via OS username, passwordless for matching user)
# Local TCP (127.0.0.1): trust (passwordless for localhost convenience)
sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+scram-sha-256$/host all all 127.0.0.1\/32 trust/' "$PG_HBA"
# Verify sed actually changed the localhost line (catches format mismatches)
if sudo grep -qE '^host\s+all\s+all\s+127\.0\.0\.1/32\s+scram-sha-256' "$PG_HBA"; then
log_warn "pg_hba.conf localhost sed replacement did not match. The line format may differ from expected. Check manually: $PG_HBA"
fi
# Allow connections from OrbStack host network (idempotent)
if ! sudo grep -q "# OrbStack host access" "$PG_HBA"; then
echo "# OrbStack host access" | sudo tee -a "$PG_HBA" > /dev/null
echo "host all all 192.168.0.0/16 scram-sha-256" | sudo tee -a "$PG_HBA" > /dev/null
fi
# Listen on all interfaces (idempotent - replace commented default, then ensure uncommented line exists)
sudo sed -i 's/^#listen_addresses = .*/listen_addresses = '"'"'*'"'"'/' "$PG_CONF"
if ! sudo grep -q "^listen_addresses" "$PG_CONF"; then
echo "listen_addresses = '*'" | sudo tee -a "$PG_CONF" > /dev/null
fi
sudo systemctl restart postgresql
fi
# ============================================================================
# mise (version manager)
# ============================================================================
if prompt_install "mise" "mise" "Version manager for Node.js, Erlang, and Elixir runtimes"; then
if command_exists mise; then
log_warn "mise already installed, skipping"
else
curl -fsSL https://mise.run | sh
fi
# Ensure mise activation is in bashrc (idempotent, independent of install check)
if ! grep -q 'mise activate bash' ~/.bashrc; then
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
fi
export PATH="$HOME/.local/bin:$PATH"
eval "$(~/.local/bin/mise activate bash)"
fi
# ============================================================================
# Node.js
# ============================================================================
if prompt_install "node" "Node.js (LTS)" "JavaScript runtime, required for Claude Code and Playwright"; then
if ! command_exists mise; then
log_warn "mise not installed, cannot install Node.js via mise. Skipping."
else
mise use -g node@lts
INSTALLED_NODE=true
fi
fi
# ============================================================================
# Erlang & Elixir
# ============================================================================
if prompt_install "erlang" "Erlang $ERLANG_VERSION & Elixir $ELIXIR_VERSION" "BEAM VM and Elixir language for functional programming"; then
if ! command_exists mise; then
log_warn "mise not installed, cannot install Erlang/Elixir via mise. Skipping."
else
mise use -g "erlang@$ERLANG_VERSION"
mise use -g "elixir@$ELIXIR_VERSION"
fi
fi
# ============================================================================
# Chromium
# ============================================================================
if prompt_install "chromium" "Chromium" "Browser for automation, testing, and VNC-based login flows"; then
if command_exists chromium-browser || command_exists chromium; then
log_warn "Chromium already installed, skipping"
else
# Ubuntu defaults chromium-browser to a snap package which hangs in
# non-interactive/container environments. Use the .deb from the
# Ubuntu universe repo via apt preference pinning, or install
# chromium directly (non-snap) on newer Ubuntu.
# First try the non-snap 'chromium' package, fall back to downloading
# via Playwright if that also pulls snap.
if apt-cache showpkg chromium 2>/dev/null | grep -q "^Package:"; then
# Prevent snap-based install by blocking snapd trigger
sudo apt-get install -y -qq chromium --no-install-recommends >> "$LOG_FILE" 2>&1 || true
fi
# If that didn't work (snap redirect or not available), install via Playwright
if ! command_exists chromium-browser && ! command_exists chromium; then
log_warn "apt chromium unavailable or snap-based, will use Playwright's bundled Chromium"
fi
fi
# Create symlink so tools expecting 'google-chrome' work
CHROMIUM_BIN=""
if command_exists chromium-browser; then
CHROMIUM_BIN="$(which chromium-browser)"
elif command_exists chromium; then
CHROMIUM_BIN="$(which chromium)"
fi
if [ -n "$CHROMIUM_BIN" ] && [ ! -L /usr/bin/google-chrome ]; then
sudo ln -sf "$CHROMIUM_BIN" /usr/bin/google-chrome
fi
INSTALLED_CHROMIUM=true
fi
# ============================================================================
# VNC Server + Desktop Environment
# ============================================================================
if prompt_install "vnc" "VNC + XFCE Desktop" "Remote desktop for browser-based login flows (e.g., Claude Code OAuth)"; then
sudo apt-get install -y -qq \
tigervnc-standalone-server \
xfce4 \
xfce4-terminal \
dbus-x11 >> "$LOG_FILE" 2>&1
# Configure VNC password
mkdir -p ~/.vnc
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
chmod 600 ~/.vnc/passwd
# Create xstartup
cat > ~/.vnc/xstartup <<'EOF'
#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
exec startxfce4
EOF
chmod +x ~/.vnc/xstartup
# Create helper scripts in ~/bin
mkdir -p ~/bin
cat > ~/bin/vnc-start <<'EOF'
#!/bin/bash
# Use short hostname (strip any domain suffix) + .orb.local for OrbStack DNS
VNC_HOST="$(hostname -s).orb.local"
if pgrep -f "Xtigervnc :1" > /dev/null 2>&1; then
echo "VNC is already running on display :1"
echo "Connect: open vnc://${VNC_HOST}:5901"
exit 0
fi
vncserver :1 -geometry 1280x800 -depth 24 -localhost no
echo "VNC started on display :1"
echo "Connect: open vnc://${VNC_HOST}:5901"
EOF
chmod +x ~/bin/vnc-start
cat > ~/bin/vnc-stop <<'EOF'
#!/bin/bash
vncserver -kill :1 2>/dev/null && echo "VNC stopped" || echo "VNC was not running"
EOF
chmod +x ~/bin/vnc-stop
# Add ~/bin to PATH if not already there
if ! grep -q 'export PATH="$HOME/bin:$PATH"' ~/.bashrc; then
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
fi
fi
# ============================================================================
# Ollama
# ============================================================================
if prompt_install "ollama" "Ollama" "Local LLM runner for offline inference"; then
if command_exists ollama; then
log_warn "Ollama already installed, skipping"
else
curl -fsSL https://ollama.com/install.sh | sudo sh >> "$LOG_FILE" 2>&1
fi
fi
# ============================================================================
# Claude Code
# ============================================================================
if prompt_install "claude" "Claude Code" "AI coding assistant CLI from Anthropic"; then
if [ "$INSTALLED_NODE" = false ] && ! command_exists node; then
log_warn "Node.js not available. Claude Code requires Node.js. Skipping."
else
if command_exists claude; then
log_warn "Claude Code already installed, upgrading"
fi
npm install -g @anthropic-ai/claude-code >> "$LOG_FILE" 2>&1
fi
fi
# ============================================================================
# Playwright
# ============================================================================
if prompt_install "playwright" "Playwright" "Browser testing and automation framework"; then
if [ "$INSTALLED_NODE" = false ] && ! command_exists node; then
log_warn "Node.js not available. Playwright requires Node.js. Skipping."
elif [ "$INSTALLED_CHROMIUM" = false ]; then
log_warn "Chromium not selected. Playwright requires a browser. Skipping."
else
# Install Playwright with its bundled Chromium and system deps.
# This also serves as the Chromium install if apt-based install failed.
npx --yes playwright install --with-deps chromium >> "$LOG_FILE" 2>&1
# If no system chromium was installed, symlink Playwright's bundled one
if ! command_exists chromium-browser && ! command_exists chromium; then
PW_CHROMIUM=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1)
if [ -n "$PW_CHROMIUM" ]; then
sudo ln -sf "$PW_CHROMIUM" /usr/local/bin/chromium
sudo ln -sf "$PW_CHROMIUM" /usr/local/bin/google-chrome
echo " Using Playwright's bundled Chromium as system browser"
fi
fi
fi
fi
# ============================================================================
# Git configuration (always runs)
# ============================================================================
log_info "Configuring Git"
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
git config --global init.defaultBranch main
git config --global pull.rebase false
# ============================================================================
# Claude Code plugins
# ============================================================================
if prompt_install "plugins" "Claude Code Plugins" "Code review, feature dev, and browser automation plugins + MCP servers"; then
if ! command_exists claude; then
log_warn "Claude Code not installed. Skipping plugins."
else
mkdir -p ~/.config/claude
# Add marketplaces (idempotent - claude handles duplicates)
echo " Adding marketplaces..."
claude plugin marketplace add anthropics/claude-code 2>> "$LOG_FILE" || true
claude plugin marketplace add obra/superpowers 2>> "$LOG_FILE" || true
# Install plugins from Anthropic official marketplace
echo " Installing Anthropic plugins..."
ANTHROPIC_PLUGINS=(
"code-review@claude-code-plugins"
"code-simplifier@claude-code-plugins"
"feature-dev@claude-code-plugins"
"pr-review-toolkit@claude-code-plugins"
"security-guidance@claude-code-plugins"
"frontend-design@claude-code-plugins"
)
for plugin in "${ANTHROPIC_PLUGINS[@]}"; do
claude plugin install "$plugin" --scope user 2>> "$LOG_FILE" || log_warn "Failed to install $plugin"
done
# Install plugins from superpowers marketplace
echo " Installing superpowers plugins..."
SUPERPOWERS_PLUGINS=(
"double-shot-latte@superpowers-marketplace"
"elements-of-style@superpowers-marketplace"
"superpowers@superpowers-marketplace"
"superpowers-chrome@superpowers-marketplace"
"superpowers-lab@superpowers-marketplace"
)
for plugin in "${SUPERPOWERS_PLUGINS[@]}"; do
claude plugin install "$plugin" --scope user 2>> "$LOG_FILE" || log_warn "Failed to install $plugin"
done
# MCP servers
echo " Adding MCP servers..."
claude mcp add playwright --scope user -- npx @anthropic-ai/mcp-server-playwright 2>> "$LOG_FILE" || true
claude mcp add superpowers-chrome --scope user -- npx github:obra/superpowers-chrome --headless 2>> "$LOG_FILE" || true
fi
fi
# ============================================================================
# Projects directory
# ============================================================================
mkdir -p ~/projects
# ============================================================================
# Verification
# ============================================================================
log_info "Verifying installations"
echo "---"
echo "Node: $(node --version 2>/dev/null || echo 'not installed')"
echo "npm: $(npm --version 2>/dev/null || echo 'not installed')"
echo "Erlang: $(erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell 2>/dev/null || echo 'not installed')"
echo "Elixir: $(elixir --version 2>/dev/null | head -1 || echo 'not installed')"
echo "Chromium: $(chromium-browser --version 2>/dev/null || chromium --version 2>/dev/null || echo 'not installed')"
echo "Claude: $(claude --version 2>/dev/null || echo 'not installed')"
echo "PostgreSQL: $(psql --version 2>/dev/null || echo 'not installed')"
echo "mise: $(mise --version 2>/dev/null || echo 'not installed')"
echo "VNC: $(vncserver -version 2>&1 | head -1 || echo 'not installed')"
echo "Ollama: $(ollama --version 2>/dev/null || echo 'not installed')"
echo "ripgrep: $(rg --version 2>/dev/null | head -1 || echo 'not installed')"
echo "fd: $(fd --version 2>/dev/null || echo 'not installed')"
echo "yq: $(yq --version 2>/dev/null || echo 'not installed')"
echo "direnv: $(direnv --version 2>/dev/null || echo 'not installed')"
echo "watchexec: $(watchexec --version 2>/dev/null || echo 'not installed')"
echo "tmux: $(tmux -V 2>/dev/null || echo 'not installed')"
echo "Python: $(python3 --version 2>/dev/null || echo 'not installed')"
echo "---"
# ============================================================================
# Done
# ============================================================================
echo ""
echo -e "${GREEN}============================================================================${NC}"
echo -e "${GREEN} Setup complete!${NC}"
echo -e "${GREEN}============================================================================${NC}"
echo ""
echo "Log file: $LOG_FILE"
echo ""
echo "Restart your shell: source ~/.bashrc"
echo ""
echo -e "${YELLOW}VNC:${NC}"
echo " Start: vnc-start"
echo " Stop: vnc-stop"
echo ""
echo -e "${YELLOW}POSTGRESQL:${NC}"
echo " psql dev"
echo ""
echo -e "${YELLOW}CLAUDE CODE:${NC}"
echo " claude"
echo ""