diff --git a/setup_env.sh b/setup_env.sh index d227b34..a5899ee 100755 --- a/setup_env.sh +++ b/setup_env.sh @@ -33,10 +33,10 @@ ELIXIR_VERSION="1.19.5-otp-28" # ============================================================================ # Helpers # ============================================================================ -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[1;33m' +NC=$'\033[0m' log_info() { echo -e "${GREEN}=== $1 ===${NC}" @@ -327,16 +327,8 @@ fi # ============================================================================ 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 +LOG_DIR="/tmp/setup_env_steps_$$" +mkdir -p "$LOG_DIR" # Prevent mise from reading host config via OrbStack mount (/Users/john/.mise.toml) export MISE_GLOBAL_CONFIG_FILE="$HOME/.config/mise/config.toml" @@ -368,41 +360,507 @@ for arg in "$@"; do done # ============================================================================ -# Component selection helper +# Dashboard and parallel execution framework # ============================================================================ -prompt_install() { +SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' +SPINNER_IDX=0 +START_TIME=$(date +%s) +DIM=$'\033[2m' +BOLD=$'\033[1m' +CYAN=$'\033[0;36m' +CLEAR_LINE=$'\033[K' + +# Step tracking arrays +declare -a ALL_STEP_IDS=() +declare -A STEP_NAMES=() +declare -A STEP_STATUS=() # pending/running/done/failed/skipped +declare -A STEP_PIDS=() # PID for background jobs +declare -A STEP_LOGS=() # log file path +declare -A STEP_START=() # start timestamp +declare -A STEP_ELAPSED=() # elapsed time when completed +declare -A STEP_ERRORS=() # error messages for failed steps +DASHBOARD_LINES=0 + +format_time() { + local secs=$1 + if [ "$secs" -ge 60 ]; then + printf "%dm %02ds" $((secs / 60)) $((secs % 60)) + else + printf "%ds" "$secs" + fi +} + +get_step_detail() { + local id="$1" + local log="${STEP_LOGS[$id]:-}" + [ -z "$log" ] || [ ! -f "$log" ] && return + # Get last meaningful line, filter noise (empty lines, shell trace, progress bars) + tail -5 "$log" 2>/dev/null | \ + grep -v '^\s*$' | \ + grep -v '^+' | \ + grep -v '^#' | \ + grep -v '^\s*#' | \ + grep -v '%$' | \ + grep -v '^\s*[0-9.]*%' | \ + tail -1 | \ + sed 's/\x1b\[[0-9;]*m//g' | \ + cut -c1-30 | \ + tr -d '\n' +} + +draw_dashboard() { + local now=$(date +%s) + local total_elapsed=$((now - START_TIME)) + + # Count statuses + local running=0 done=0 failed=0 pending=0 skipped=0 + for id in "${ALL_STEP_IDS[@]}"; do + case "${STEP_STATUS[$id]:-pending}" in + running) ((running++)) || true ;; + done) ((done++)) || true ;; + failed) ((failed++)) || true ;; + skipped) ((skipped++)) || true ;; + *) ((pending++)) || true ;; + esac + done + + # Calculate lines to draw + local num_steps=${#ALL_STEP_IDS[@]} + local new_lines=$((num_steps + 4)) + + # Move cursor up to redraw + if [ "$DASHBOARD_LINES" -gt 0 ]; then + printf "\033[%dA" "$DASHBOARD_LINES" + fi + DASHBOARD_LINES=$new_lines + + # Header + printf " ${BOLD}OrbStack Sandbox Setup${NC}%30s${CLEAR_LINE}\n" "[$(format_time $total_elapsed)]" + printf " ───────────────────────────────────────────────────────────${CLEAR_LINE}\n" + + # Steps + for id in "${ALL_STEP_IDS[@]}"; do + local name="${STEP_NAMES[$id]:-$id}" + local status="${STEP_STATUS[$id]:-pending}" + + case "$status" in + done) + local elapsed="${STEP_ELAPSED[$id]:-0}" + printf " ${GREEN}✓${NC} %-40s %10s${CLEAR_LINE}\n" "$name" "$(format_time $elapsed)" + ;; + running) + local start="${STEP_START[$id]:-$now}" + local elapsed=$((now - start)) + local detail=$(get_step_detail "$id") + local spin="${SPINNER_CHARS:SPINNER_IDX%10:1}" + printf " ${YELLOW}%s${NC} %-40s %10s${CLEAR_LINE}\n" "$spin" "$name" "$(format_time $elapsed)" + ;; + failed) + printf " ${RED}✗${NC} %-40s %10s${CLEAR_LINE}\n" "$name" "FAILED" + ;; + skipped) + printf " ${DIM}-${NC} ${DIM}%-40s${NC}${CLEAR_LINE}\n" "$name" + ;; + *) + printf " ${DIM}○${NC} ${DIM}%-40s${NC}${CLEAR_LINE}\n" "$name" + ;; + esac + done + + # Footer + printf "${CLEAR_LINE}\n" + local status_line=" " + [ $running -gt 0 ] && status_line+="${CYAN}⟳${NC} $running running " + [ $done -gt 0 ] && status_line+="${GREEN}✓${NC} $done done " + [ $failed -gt 0 ] && status_line+="${RED}✗${NC} $failed failed " + [ $pending -gt 0 ] && status_line+="${DIM}○ $pending queued${NC}" + printf "%s${CLEAR_LINE}\n" "$status_line" + + ((SPINNER_IDX++)) || true +} + +register_step() { local id="$1" local name="$2" - local description="$3" + ALL_STEP_IDS+=("$id") + STEP_NAMES[$id]="$name" + STEP_STATUS[$id]="pending" + STEP_LOGS[$id]="$LOG_DIR/${id}.log" +} - # Check for SKIP_=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 +skip_step() { + local id="$1" + STEP_STATUS[$id]="skipped" +} + +# Run a function in background +start_step() { + local id="$1" + shift + local log="${STEP_LOGS[$id]}" + + STEP_STATUS[$id]="running" + STEP_START[$id]=$(date +%s) + + # Run in subshell + ( + set +e + "$@" > "$log" 2>&1 + echo $? > "${log}.exit" + ) & + STEP_PIDS[$id]=$! +} + +# Run synchronously (for steps that must complete before others can start) +run_step_sync() { + local id="$1" + shift + local log="${STEP_LOGS[$id]}" + + STEP_STATUS[$id]="running" + STEP_START[$id]=$(date +%s) + + # Show dashboard before running + draw_dashboard + + # Run synchronously in a subshell to capture exit code reliably + ( + set +e + "$@" > "$log" 2>&1 + echo $? > "${log}.exit" + ) + + # Read exit code from file + local exit_code=0 + if [ -f "${log}.exit" ]; then + exit_code=$(cat "${log}.exit") fi - if [ "$AUTO_ACCEPT" = true ]; then - log_info "$name" - echo " $description" + local now=$(date +%s) + STEP_ELAPSED[$id]=$((now - ${STEP_START[$id]:-$now})) + + if [ "$exit_code" -eq 0 ]; then + STEP_STATUS[$id]="done" + else + STEP_STATUS[$id]="failed" + STEP_ERRORS[$id]="Exit code $exit_code" + fi + + # Append to main log + echo "=== $id ===" >> "$LOG_FILE" + cat "$log" >> "$LOG_FILE" 2>/dev/null || true + echo "" >> "$LOG_FILE" + + draw_dashboard + return "$exit_code" +} + +step_running() { + local id="$1" + local pid="${STEP_PIDS[$id]:-}" + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null +} + +finish_step() { + local id="$1" + local log="${STEP_LOGS[$id]}" + local now=$(date +%s) + + STEP_ELAPSED[$id]=$((now - ${STEP_START[$id]:-$now})) + + if [ -f "${log}.exit" ]; then + local exit_code=$(cat "${log}.exit") + if [ "$exit_code" = "0" ]; then + STEP_STATUS[$id]="done" + else + STEP_STATUS[$id]="failed" + STEP_ERRORS[$id]="Exit code $exit_code" + fi + else + STEP_STATUS[$id]="failed" + STEP_ERRORS[$id]="No exit status" + fi + + # Append to main log + echo "=== $id ===" >> "$LOG_FILE" + cat "$log" >> "$LOG_FILE" 2>/dev/null || true + echo "" >> "$LOG_FILE" +} + +# Wait for all currently running steps +wait_for_running_steps() { + while true; do + local any_running=false + for id in "${ALL_STEP_IDS[@]}"; do + if [ "${STEP_STATUS[$id]}" = "running" ]; then + if step_running "$id"; then + any_running=true + else + finish_step "$id" + fi + fi + done + + draw_dashboard + + [ "$any_running" = false ] && break + sleep 0.2 + done +} + +should_skip() { + local id="$1" + local skip_var="SKIP_$(echo "$id" | tr '[:lower:]' '[:upper:]')" + [ "${!skip_var:-}" = "1" ] +} + +cleanup() { + local exit_code=$? + # Kill any remaining background jobs + for id in "${ALL_STEP_IDS[@]}"; do + local pid="${STEP_PIDS[$id]:-}" + [ -n "$pid" ] && kill "$pid" 2>/dev/null || true + done + # Clean up temp logs + rm -rf "$LOG_DIR" 2>/dev/null || true + if [ $exit_code -ne 0 ]; then + echo "" + log_error "Setup failed (exit code: $exit_code). Log saved to: $LOG_FILE" + fi +} + +trap cleanup EXIT + +# ============================================================================ +# Step functions (each is a self-contained install) +# ============================================================================ + +install_postgresql() { + sudo apt-get install -y -q postgresql postgresql-contrib libpq-dev + sudo systemctl enable postgresql + sudo systemctl start postgresql + + # Create user + 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" + fi + + # Create dev database + if ! psql -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw dev; then + createdb dev + echo "Created database: dev" + fi + + # Configure pg_hba.conf + 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) + + [ -z "$PG_HBA" ] || [ -z "$PG_CONF" ] && return 1 + + 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" + + 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 + + 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 +} + +install_mise() { + if command_exists mise; then + echo "mise already installed" + return 0 + fi + curl -fsSL https://mise.run | sh + + # Add to bashrc + if ! grep -q 'mise activate bash' ~/.bashrc; then + echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc + fi + + # Add to profile for SSH + if ! grep -q 'mise/shims' ~/.profile 2>/dev/null; then + echo 'export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"' >> ~/.profile + fi +} + +install_node() { + export PATH="$HOME/.local/bin:$PATH" + eval "$(~/.local/bin/mise activate bash)" + mise use -g node@lts + export PATH="$HOME/.local/share/mise/shims:$PATH" +} + +install_erlang() { + export PATH="$HOME/.local/bin:$PATH" + eval "$(~/.local/bin/mise activate bash)" + # Try precompiled first + MISE_ERLANG_INSTALL_TYPE=precompiled mise use -g "erlang@$ERLANG_VERSION" 2>/dev/null || { + echo "Precompiled unavailable, compiling from source..." + mise use -g "erlang@$ERLANG_VERSION" + } +} + +install_elixir() { + export PATH="$HOME/.local/bin:$PATH" + eval "$(~/.local/bin/mise activate bash)" + mise use -g "elixir@$ELIXIR_VERSION" + export PATH="$HOME/.local/share/mise/shims:$PATH" +} + +install_chromium() { + if command_exists chromium-browser || command_exists chromium; then + echo "Chromium already installed" 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 + # Try apt (may fail due to snap) + if apt-cache showpkg chromium 2>/dev/null | grep -q "^Package:"; then + sudo apt-get install -y -q chromium --no-install-recommends 2>/dev/null || true + fi + + # Symlink if installed + local bin="" + command_exists chromium-browser && bin=$(which chromium-browser) + command_exists chromium && bin=$(which chromium) + if [ -n "$bin" ] && [ ! -L /usr/bin/google-chrome ]; then + sudo ln -sf "$bin" /usr/bin/google-chrome fi - return 0 } -# Track what was installed for dependency checks -INSTALLED_NODE=false -INSTALLED_CHROMIUM=false +install_vnc() { + sudo apt-get install -y -q tigervnc-standalone-server xfce4 xfce4-terminal dbus-x11 + + mkdir -p ~/.vnc + echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd + chmod 600 ~/.vnc/passwd + + cat > ~/.vnc/xstartup <<'XEOF' +#!/bin/sh +unset SESSION_MANAGER +unset DBUS_SESSION_BUS_ADDRESS +exec startxfce4 +XEOF + chmod +x ~/.vnc/xstartup + + mkdir -p ~/bin + + cat > ~/bin/vnc-start <<'VEOF' +#!/bin/bash +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" +VEOF + chmod +x ~/bin/vnc-start + + cat > ~/bin/vnc-stop <<'SEOF' +#!/bin/bash +vncserver -kill :1 2>/dev/null && echo "VNC stopped" || echo "VNC was not running" +SEOF + chmod +x ~/bin/vnc-stop + + if ! grep -q 'export PATH="$HOME/bin:$PATH"' ~/.bashrc; then + echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc + fi +} + +install_ollama() { + if command_exists ollama; then + echo "Ollama already installed" + return 0 + fi + curl -fsSL https://ollama.com/install.sh | sudo sh +} + +install_claude() { + export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH" + npm install -g @anthropic-ai/claude-code +} + +install_playwright() { + export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH" + npx --yes playwright install --with-deps chromium + + # Symlink Playwright's Chromium if no system one + if ! command_exists chromium-browser && ! command_exists chromium; then + local pw_chrome=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1) + if [ -n "$pw_chrome" ]; then + sudo ln -sf "$pw_chrome" /usr/local/bin/chromium + sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome + echo "Using Playwright's bundled Chromium" + fi + fi +} + +install_plugins() { + export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH" + mkdir -p ~/.config/claude + + echo "Adding marketplaces..." + claude plugin marketplace add anthropics/claude-code 2>/dev/null || true + claude plugin marketplace add obra/superpowers 2>/dev/null || true + + echo "Installing plugins..." + for p in code-review code-simplifier feature-dev pr-review-toolkit security-guidance frontend-design; do + claude plugin install "${p}@claude-code-plugins" --scope user 2>/dev/null || true + done + for p in double-shot-latte elements-of-style superpowers superpowers-chrome superpowers-lab; do + claude plugin install "${p}@superpowers-marketplace" --scope user 2>/dev/null || true + done + + echo "Adding MCP servers..." + claude mcp add playwright --scope user -- npx @anthropic-ai/mcp-server-playwright 2>/dev/null || true + claude mcp add superpowers-chrome --scope user -- npx github:obra/superpowers-chrome --headless 2>/dev/null || true +} + +install_base_deps() { + sudo apt-get update -q + sudo apt-get upgrade -y -q + sudo apt-get install -y -q \ + 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 zstd \ + inotify-tools jq ripgrep fd-find direnv tmux \ + python3 python3-pip python3-venv + + # fd symlink (fd-find installs as 'fdfind', symlink to 'fd') + if [ ! -L /usr/local/bin/fd ] && command_exists fdfind; then + sudo ln -sf "$(which fdfind)" /usr/local/bin/fd + fi + + # yq + if ! command_exists yq; then + sudo curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" -o /usr/local/bin/yq + sudo chmod +x /usr/local/bin/yq + fi + + # watchexec + if ! command_exists watchexec; then + curl -fsSL "https://github.com/watchexec/watchexec/releases/download/v2.3.2/watchexec-2.3.2-aarch64-unknown-linux-gnu.tar.xz" | \ + sudo tar -xJ --strip-components=1 -C /usr/local/bin/ --wildcards '*/watchexec' + fi + + # direnv hook + if ! grep -q 'direnv hook bash' ~/.bashrc; then + echo 'eval "$(direnv hook bash)"' >> ~/.bashrc + fi + + return 0 +} # ============================================================================ # Pre-flight checks @@ -417,14 +875,6 @@ if ! command_exists sudo; then 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 # ============================================================================ @@ -454,426 +904,164 @@ else 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) +# Register all steps # ============================================================================ -log_info "Updating system" -sudo apt-get update -qq && sudo apt-get upgrade -y -qq >> "$LOG_FILE" 2>&1 +register_step "base" "System packages (git, build tools, curl)" +register_step "postgresql" "PostgreSQL database server" +register_step "mise" "mise version manager" +register_step "node" "Node.js LTS runtime" +register_step "erlang" "Erlang/OTP VM" +register_step "elixir" "Elixir language" +register_step "chromium" "Chromium browser" +register_step "vnc" "VNC server + XFCE desktop" +register_step "ollama" "Ollama local LLM runner" +register_step "claude" "Claude Code CLI" +register_step "playwright" "Playwright browser automation" +register_step "plugins" "Claude Code plugins" -# ============================================================================ -# 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 +# Mark skipped steps +should_skip "postgresql" && skip_step "postgresql" +should_skip "mise" && skip_step "mise" +should_skip "node" && skip_step "node" +should_skip "erlang" && skip_step "erlang" +should_skip "chromium" && skip_step "chromium" +should_skip "vnc" && skip_step "vnc" +should_skip "ollama" && skip_step "ollama" +should_skip "claude" && skip_step "claude" +should_skip "playwright" && skip_step "playwright" +should_skip "plugins" && skip_step "plugins" -# 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 +# Elixir skipped if Erlang skipped +[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "elixir" + +# Claude/Playwright skipped if Node skipped +if [ "${STEP_STATUS[node]}" = "skipped" ]; then + [ "${STEP_STATUS[mise]}" = "skipped" ] || true # mise can still install + skip_step "claude" + skip_step "playwright" + skip_step "plugins" 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 +# Playwright skipped if Chromium skipped +[ "${STEP_STATUS[chromium]}" = "skipped" ] && skip_step "playwright" -# Enable direnv hook in bashrc -if ! grep -q 'direnv hook bash' ~/.bashrc; then - echo 'eval "$(direnv hook bash)"' >> ~/.bashrc +# Plugins skipped if Claude skipped +[ "${STEP_STATUS[claude]}" = "skipped" ] && skip_step "plugins" + +# Initial dashboard draw +echo "" +draw_dashboard +echo "" + +# ============================================================================ +# Phase 1: Base dependencies (sequential, everything depends on this) +# ============================================================================ +run_step_sync "base" install_base_deps || true + +# ============================================================================ +# Phase 2: Independent installs (parallel) +# Mise, PostgreSQL, VNC, Ollama, Chromium can all run at the same time +# ============================================================================ +[ "${STEP_STATUS[postgresql]}" != "skipped" ] && start_step "postgresql" install_postgresql +[ "${STEP_STATUS[mise]}" != "skipped" ] && start_step "mise" install_mise +[ "${STEP_STATUS[vnc]}" != "skipped" ] && start_step "vnc" install_vnc +[ "${STEP_STATUS[ollama]}" != "skipped" ] && start_step "ollama" install_ollama +[ "${STEP_STATUS[chromium]}" != "skipped" ] && start_step "chromium" install_chromium + +wait_for_running_steps + +# ============================================================================ +# Phase 3: Node + Erlang (parallel, both need mise) +# ============================================================================ +if [ "${STEP_STATUS[mise]}" = "done" ]; then + [ "${STEP_STATUS[node]}" != "skipped" ] && start_step "node" install_node + [ "${STEP_STATUS[erlang]}" != "skipped" ] && start_step "erlang" install_erlang + wait_for_running_steps fi # ============================================================================ -# PostgreSQL +# Phase 4: Elixir + Claude + Playwright (Elixir needs Erlang, others need Node) # ============================================================================ -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 +[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[elixir]}" != "skipped" ] && start_step "elixir" install_elixir +[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[claude]}" != "skipped" ] && start_step "claude" install_claude +[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[playwright]}" != "skipped" ] && start_step "playwright" install_playwright - 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 +wait_for_running_steps # ============================================================================ -# mise (version manager) +# Phase 5: Claude plugins (needs Claude) # ============================================================================ -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 +[ "${STEP_STATUS[claude]}" = "done" ] && [ "${STEP_STATUS[plugins]}" != "skipped" ] && start_step "plugins" install_plugins - # 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 +wait_for_running_steps # ============================================================================ # 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 +# Final status # ============================================================================ echo "" -echo -e "${GREEN}============================================================================${NC}" -echo -e "${GREEN} Setup complete!${NC}" -echo -e "${GREEN}============================================================================${NC}" +echo "" + +# Count results +DONE_COUNT=0 +FAILED_COUNT=0 +SKIPPED_COUNT=0 +for id in "${ALL_STEP_IDS[@]}"; do + case "${STEP_STATUS[$id]}" in + done) ((DONE_COUNT++)) || true ;; + failed) ((FAILED_COUNT++)) || true ;; + skipped) ((SKIPPED_COUNT++)) || true ;; + esac +done + +TOTAL_TIME=$(($(date +%s) - START_TIME)) + +if [ $FAILED_COUNT -gt 0 ]; then + echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW} Setup completed with errors [$(format_time $TOTAL_TIME)]${NC}" + echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e "${RED}Failed steps:${NC}" + for id in "${ALL_STEP_IDS[@]}"; do + if [ "${STEP_STATUS[$id]}" = "failed" ]; then + echo " ✗ ${STEP_NAMES[$id]}" + echo " Log: ${STEP_LOGS[$id]}" + fi + done +else + echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} Setup complete! [$(format_time $TOTAL_TIME)]${NC}" + echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" +fi + 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" + +# Show quick start info based on what was installed +if [ "${STEP_STATUS[vnc]}" = "done" ]; then + echo -e "${CYAN}VNC:${NC} vnc-start / vnc-stop" +fi +if [ "${STEP_STATUS[postgresql]}" = "done" ]; then + echo -e "${CYAN}PostgreSQL:${NC} psql dev" +fi +if [ "${STEP_STATUS[claude]}" = "done" ]; then + echo -e "${CYAN}Claude:${NC} claude" +fi +if [ "${STEP_STATUS[ollama]}" = "done" ]; then + echo -e "${CYAN}Ollama:${NC} ollama run llama3.2" +fi echo ""