Replaces sequential installation with parallel step execution Introduces real-time progress dashboard with spinner and status Removes color variables to improve terminal compatibility Restructures logging with per-step files for better debugging Significantly reduces total setup time by running independent steps concurrently
1067 lines
35 KiB
Bash
Executable file
1067 lines
35 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"
|
|
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"
|
|
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
|
|
|
|
# ============================================================================
|
|
# Dashboard and parallel execution framework
|
|
# ============================================================================
|
|
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"
|
|
ALL_STEP_IDS+=("$id")
|
|
STEP_NAMES[$id]="$name"
|
|
STEP_STATUS[$id]="pending"
|
|
STEP_LOGS[$id]="$LOG_DIR/${id}.log"
|
|
}
|
|
|
|
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
|
|
|
|
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
|
|
|
|
# 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
|
|
}
|
|
|
|
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
|
|
# ============================================================================
|
|
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
|
|
|
|
# ============================================================================
|
|
# 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
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Register all steps
|
|
# ============================================================================
|
|
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"
|
|
|
|
# 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"
|
|
|
|
# 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
|
|
|
|
# Playwright skipped if Chromium skipped
|
|
[ "${STEP_STATUS[chromium]}" = "skipped" ] && skip_step "playwright"
|
|
|
|
# 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
|
|
|
|
# ============================================================================
|
|
# Phase 4: Elixir + Claude + Playwright (Elixir needs Erlang, others need Node)
|
|
# ============================================================================
|
|
[ "${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
|
|
|
|
wait_for_running_steps
|
|
|
|
# ============================================================================
|
|
# Phase 5: Claude plugins (needs Claude)
|
|
# ============================================================================
|
|
[ "${STEP_STATUS[claude]}" = "done" ] && [ "${STEP_STATUS[plugins]}" != "skipped" ] && start_step "plugins" install_plugins
|
|
|
|
wait_for_running_steps
|
|
|
|
# ============================================================================
|
|
# Git configuration (always runs)
|
|
# ============================================================================
|
|
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
|
|
|
|
# Projects directory
|
|
mkdir -p ~/projects
|
|
|
|
# ============================================================================
|
|
# Final status
|
|
# ============================================================================
|
|
echo ""
|
|
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 ""
|
|
|
|
# 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 ""
|