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
879 lines
32 KiB
Bash
Executable file
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 ""
|