diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2549b3d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.env diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8e2c0c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +## Project Overview + +This repository contains a dual-mode setup script for creating OrbStack-based development sandboxes tailored for Claude Code with Elixir/Erlang, browser automation, and PostgreSQL. + +## Repository Structure + +``` +setup_env.sh - Main script (macOS host mode + Linux VM provisioning mode) +config.env.example - Example credentials file +config.env - User credentials (gitignored, created on first run) +.gitignore - Ignores config.env +README.md - User-facing documentation +CLAUDE.md - This file (context for Claude Code sessions) +``` + +## How the Script Works + +The script has two modes, detected via `uname -s`: + +- **Darwin (macOS)**: Orchestrator mode. Checks for OrbStack, reads/creates `config.env`, creates a VM, copies itself in, runs itself inside the VM with `--non-interactive`. +- **Linux (VM)**: Provisioning mode. Installs all tools, configures PostgreSQL, sets up VNC, installs Claude Code plugins. + +This means `./setup_env.sh my-vm` on macOS does everything end-to-end. + +## Key Technical Details + +- **Target environment**: OrbStack Ubuntu VM on macOS Apple Silicon (ARM64) +- **Version manager**: mise (manages Node.js, Erlang, Elixir) +- **Language versions**: Configured at the top of `setup_env.sh` as variables (`ERLANG_VERSION`, `ELIXIR_VERSION`) +- **PostgreSQL auth**: Peer for local socket, trust for localhost TCP (127.0.0.1), scram-sha-256 for host network (192.168.0.0/16) +- **Browser**: Chromium (no Chrome ARM64 Linux builds exist), symlinked to `google-chrome` +- **VNC**: TigerVNC + XFCE on display :1 (port 5901), controlled via `vnc-start`/`vnc-stop` helpers in `~/bin` +- **Shared credentials**: `config.env` is created once and reused for all VMs + +## Working on This Project + +### Editing setup_env.sh + +- The macOS host-mode block is at the top (inside the `if [[ "$(uname -s)" == "Darwin" ]]` block) +- The Linux VM-mode block is everything after that conditional +- Version numbers are defined as variables at the top (`ERLANG_VERSION`, `ELIXIR_VERSION`) +- All installation steps must be idempotent (safe to run multiple times) +- Use `log_info`, `log_warn`, `log_error` helpers for output +- New apt packages go in the base dependencies section +- New Claude plugins go in the appropriate array (`ANTHROPIC_PLUGINS` or `SUPERPOWERS_PLUGINS`) +- Redirect verbose output to `$LOG_FILE`, show only meaningful progress to the user +- The `LOG_FILE` variable is only set in VM mode (not available in macOS host mode) +- Each optional component is wrapped in `prompt_install "Name" "Description"` — this handles both interactive prompts and `--yes`/`--non-interactive` auto-accept +- Track dependency flags (`INSTALLED_NODE`, `INSTALLED_CHROMIUM`) to skip dependent components gracefully + +### Script Conventions + +- `set -euo pipefail` is enforced - handle potential failures with `|| true` or explicit checks +- Use `command_exists` to check for already-installed tools +- Use `apt-get` (not `apt`) for scripting reliability +- Quote all variable expansions +- Use `find` for PostgreSQL config paths (version-agnostic) +- The `--non-interactive` flag is for VM mode (implies `--yes`); macOS mode always uses config.env +- `--yes`/`-y` accepts all components without prompting but still allows interactive credential entry +- `MISE_GLOBAL_CONFIG_FILE` and `MISE_CONFIG_DIR` are set to prevent OrbStack host-mount config pollution + +### Testing + +There are no automated tests. To test changes: +1. Remove an existing test VM: `orb delete test-sandbox` +2. Run: `./setup_env.sh test-sandbox` +3. Verify provisioning completes +4. Run again to verify idempotency: `ssh test-sandbox@orb -- bash /tmp/setup_env.sh --non-interactive` (will need env vars) +5. Test VNC: `ssh test-sandbox@orb -- vnc-start`, then `open vnc://test-sandbox.orb.local:5901` +6. Clean up: `orb delete test-sandbox` + +### Security Considerations + +- `config.env` is chmod 600 and gitignored +- PostgreSQL remote access uses scram-sha-256 (not trust) +- The script refuses to run as root inside the VM +- VNC password is required (min 6 chars) +- VNC binds to all interfaces (`-localhost no`) to allow connections from the macOS host — this is intentional for the OrbStack use case diff --git a/README.md b/README.md index e69de29..f5fa24a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,297 @@ +# OrbStack Development Sandbox + +Disposable, isolated Linux VMs for running Claude Code with `--dangerously-skip-permissions`. One command creates a fully provisioned environment. Blow it away and recreate it in minutes. + +The VM is a real Linux machine with its own filesystem, network, and process space — but you edit files from your Mac, access services on `*.orb.local`, and SSH in without any key setup. All the isolation of a container with none of the friction. + +## Why This Exists + +Running `claude --dangerously-skip-permissions` on your host machine means Claude can execute arbitrary commands, install packages, modify system files, and access everything on your disk. That's powerful for autonomous coding but risky on a machine with your SSH keys, credentials, and personal files. + +This script creates throwaway VMs where Claude can run unrestricted: + +- **Isolated filesystem** — Claude can't touch your macOS files, keys, or configs +- **Isolated network** — services run on their own IP, no port conflicts with your host +- **Disposable** — `orb delete my-sandbox` wipes everything; recreate in one command +- **Multiple VMs** — run separate sandboxes per project with shared git credentials +- **Full access from Mac** — edit files in your editor, browse databases, view running apps + +```bash +# Create a sandbox, SSH in, run Claude unrestricted +./setup_env.sh my-project +ssh my-project@orb +claude --dangerously-skip-permissions +``` + +If anything goes wrong: `orb delete my-project && ./setup_env.sh my-project` + +## Quick Start + +```bash +# Create and provision a VM (one command from macOS) +./setup_env.sh my-sandbox +``` + +On first run, you'll be prompted for git name, email, and a VNC password. These are saved to `config.env` and reused for all future VMs. You'll also get an interactive checklist to select which components to install. + +```bash +# Create additional VMs — reuses config.env, shows component picker +./setup_env.sh my-other-project +./setup_env.sh elixir-playground +``` + +When run manually inside a VM, you're prompted for each component individually: + +```bash +# Inside a VM — interactive, prompts per component +./setup_env.sh + +# Inside a VM — accept all without prompting +./setup_env.sh -y +./setup_env.sh --yes +``` + +## Requirements + +- macOS with Apple Silicon (ARM64) +- [OrbStack](https://orbstack.dev) installed (`brew install orbstack`) + +## What Gets Installed + +All components are optional — deselect what you don't need in the interactive picker. + +| Tool | Version | Purpose | +|------|---------|---------| +| mise | latest | Version manager for runtimes | +| Node.js | LTS | JavaScript runtime | +| Erlang | 28.3.1 | BEAM VM | +| Elixir | 1.19.5-otp-28 | Elixir language | +| Chromium | system | Browser automation target | +| Playwright | latest | Browser testing framework | +| PostgreSQL | system default | Database | +| Ollama | latest | Local LLM inference | +| Claude Code | latest | AI coding assistant | +| TigerVNC + XFCE | system | VNC access for browser login flows | + +## Connecting from macOS + +### SSH + +```bash +# Shell into the VM (no key setup required) +ssh my-sandbox@orb + +# Run a command directly +ssh my-sandbox@orb -- ls ~/projects + +# Run Claude unrestricted +ssh my-sandbox@orb -- claude --dangerously-skip-permissions +``` + +OrbStack handles SSH key configuration automatically. + +### Editing Files + +Your editor connects to the VM over SSH. You edit files as if they were local — full LSP support, syntax highlighting, file tree, integrated terminal. + +**Zed:** +1. `Cmd+Shift+P` -> "Open Remote Folder" +2. Enter: `my-sandbox@orb:~/projects` + +**VS Code / Cursor:** +1. Install the "Remote - SSH" extension +2. `Cmd+Shift+P` -> "Remote-SSH: Connect to Host" +3. Enter: `my-sandbox@orb` +4. Open folder: `~/projects` + +**Finder (direct filesystem access):** +``` +/Volumes/OrbStack/my-sandbox/home// +``` + +### Viewing Running Apps + +Services running in the VM are accessible from your Mac via `.orb.local`: + +```bash +# Phoenix/Rails/Next.js dev server running on port 4000 inside the VM +open http://my-sandbox.orb.local:4000 + +# Or use port forwarding if the app only binds to localhost +ssh -L 4000:localhost:4000 my-sandbox@orb +``` + +PostgreSQL, Redis, or any service listening on the VM's interfaces is reachable at `my-sandbox.orb.local:` from your Mac — no extra configuration. + +## VNC (Browser Access) + +For tasks requiring a visible browser (e.g., Claude Code OAuth login): + +```bash +# Start VNC server inside the VM +ssh my-sandbox@orb -- vnc-start + +# Connect from macOS (opens Screen Sharing.app) +open vnc://my-sandbox.orb.local:5901 + +# Stop when done (saves resources) +ssh my-sandbox@orb -- vnc-stop +``` + +### macOS Screen Sharing (built-in) + +```bash +open vnc://my-sandbox.orb.local:5901 +``` + +Enter your VNC password when prompted. + +### RealVNC Viewer + +1. Download from https://www.realvnc.com/en/connect/download/viewer/ +2. Enter address: `my-sandbox.orb.local:5901` +3. When prompted for credentials, enter your VNC password (username can be left blank) + +### TigerVNC Viewer + +```bash +# Install via Homebrew +brew install tiger-vnc + +# Connect +vncviewer my-sandbox.orb.local:5901 +``` + +Enter your VNC password when prompted. + +### Connection details for any VNC client + +- **Host**: `my-sandbox.orb.local` +- **Port**: `5901` (display `:1`) +- **Password**: the VNC password from `config.env` +- **Resolution**: 1280x800 (configurable in `~/bin/vnc-start`) + +## PostgreSQL + +A superuser matching your Linux username and a `dev` database are created automatically. + +**Auth model:** +- Local socket (`psql dev`): peer auth (OS username must match PG role) +- Localhost TCP (127.0.0.1): trust (passwordless) +- Host network (192.168.0.0/16, i.e., from macOS): scram-sha-256 + +### From inside the VM + +```bash +psql dev # Connect to dev database +psql -l # List databases +createdb myapp_dev # Create a new database +``` + +### From macOS + +```bash +# Direct (requires: brew install libpq) +psql -h my-sandbox.orb.local -U -d dev +``` + +### Connection strings + +``` +# Elixir/Phoenix +postgres://@my-sandbox.orb.local/myapp_dev + +# Generic +host=my-sandbox.orb.local port=5432 dbname=dev user= +``` + +### DataGrip + +1. New Data Source -> PostgreSQL +2. **SSH/SSL** tab: Check "Use SSH tunnel", Host: `my-sandbox@orb`, Auth: Key pair +3. **General** tab: Host: `localhost`, Port: `5432`, User: your VM username, Database: `dev`, No password +4. Test Connection -> Apply + +### DBeaver + +1. New Database Connection -> PostgreSQL +2. **SSH** tab: Check "Use SSH Tunnel", Host: `my-sandbox.orb.local`, Port: `22`, Auth: Public Key +3. **Main** tab: Host: `localhost`, Port: `5432`, Database: `dev`, Username: your VM username, no password +4. Test Connection -> Finish + +### Creating additional databases + +```bash +# From inside the VM +createdb myapp_dev +createdb myapp_test + +# From macOS +ssh my-sandbox@orb -- createdb myapp_dev +``` + +## Claude Code Plugins + +The script installs these plugins at user scope: + +**Anthropic marketplace** (`anthropics/claude-code`): +- code-review, code-simplifier, feature-dev, pr-review-toolkit, security-guidance, frontend-design + +**Superpowers marketplace** (`obra/superpowers`): +- double-shot-latte, elements-of-style, superpowers, superpowers-chrome, superpowers-lab + +**MCP Servers**: +- `playwright` - Browser automation and screenshots +- `superpowers-chrome` - Direct Chrome/Chromium control (headless) + +## Configuration + +Edit the version variables at the top of `setup_env.sh`: + +```bash +ERLANG_VERSION="28.3.1" +ELIXIR_VERSION="1.19.5-otp-28" +``` + +### Shared credentials + +Credentials are stored in `config.env` (gitignored). To reset: + +```bash +rm config.env +./setup_env.sh my-sandbox # will prompt again +``` + +Or copy the example and edit: + +```bash +cp config.env.example config.env +# Edit config.env with your values +``` + +## Managing VMs + +```bash +# List all VMs +orb list + +# Delete a VM (instant cleanup) +orb delete my-sandbox + +# Stop a VM (preserves state, frees resources) +orb stop my-sandbox + +# Start a stopped VM +orb start my-sandbox + +# Nuclear option — delete and recreate +orb delete my-sandbox && ./setup_env.sh my-sandbox +``` + +## Idempotency + +The VM provisioning script is safe to run multiple times. It checks for existing installations before re-installing and avoids appending duplicate configuration lines. + +## Logs + +Each provisioning run creates a log file at `/tmp/setup_env_.log` inside the VM with detailed output from package installations and any errors. diff --git a/config.env.example b/config.env.example new file mode 100644 index 0000000..e22417b --- /dev/null +++ b/config.env.example @@ -0,0 +1,6 @@ +# Copy this file to config.env and fill in your values. +# config.env is gitignored and shared across all VMs you create. + +GIT_NAME="guessthepw" +GIT_EMAIL="admin@guessthe.pw" +VNC_PASSWORD="changeme123" diff --git a/setup_env.sh b/setup_env.sh old mode 100644 new mode 100755 index e6936fa..d227b34 --- a/setup_env.sh +++ b/setup_env.sh @@ -1,126 +1,480 @@ #!/bin/bash -# orbstack-dev-sandbox-setup.sh +# setup_env.sh # ============================================================================ # OrbStack Development Sandbox Setup Script # ============================================================================ # -# Creates a fully configured development environment for Claude Code with -# Elixir/Erlang, browser automation, and PostgreSQL. +# Dual-mode script: +# - On macOS: Orchestrates VM creation via OrbStack +# - On Linux: Provisions the VM with all development tools # -# REQUIREMENTS: -# - macOS with Apple Silicon (ARM64) -# - OrbStack installed (https://orbstack.dev) +# USAGE (from macOS): +# ./setup_env.sh [vm-name] # Creates and provisions a new VM +# ./setup_env.sh my-project # Custom VM name # -# USAGE: -# 1. Create the VM: orb create ubuntu dev-sandbox -# 2. Shell into it: orb shell dev-sandbox -# 3. Run this script: ./orbstack-dev-sandbox-setup.sh +# On first run, prompts for git credentials and VNC password, saves to config.env. +# Subsequent runs reuse config.env (shared across all VMs). # -# WHAT THIS INSTALLS: -# - mise (version manager) -# - Node.js (latest LTS) -# - Erlang 28.3.1 -# - Elixir 1.19.5-otp-28 -# - Claude Code + plugins -# - Chromium (ARM64 native, for browser automation) -# - Playwright -# - PostgreSQL 16 -# -# CONNECTING FROM macOS: -# SSH: ssh dev-sandbox@orb -# Zed: Cmd+Shift+P → "Open Remote Folder" → dev-sandbox@orb:~/projects -# Ghostty: ssh dev-sandbox@orb -# VS Code: Remote-SSH → dev-sandbox@orb -# Files: /Volumes/OrbStack/dev-sandbox/home// -# -# POSTGRESQL: -# The script creates a superuser matching your Linux username and a 'dev' database. -# -# From inside VM: -# psql dev # Connect to dev database -# psql -l # List databases -# createdb myapp_dev # Create new database -# -# From macOS: -# psql -h dev-sandbox.orb.local -U -d dev -#` -# Connection string for Elixir/Phoenix: -# postgres://@dev-sandbox.orb.local/myapp_dev -# -# DBeaver connection (from macOS): -# Host: dev-sandbox.orb.local -# Port: 5432 -# Database: dev -# User: -# Password: (leave blank, uses peer auth over SSH) -# -# Or use SSH tunnel in DBeaver: -# SSH Host: dev-sandbox@orb -# SSH Auth: Use your macOS SSH key -# -# CLAUDE CODE PLUGINS INSTALLED: -# From anthropics/claude-code marketplace: -# - code-review Multi-agent PR review with confidence scoring -# - code-simplifier Cleans up verbose AI-generated code -# - feature-dev 7-phase feature development workflow -# - pr-review-toolkit Specialized review agents -# - security-guidance Security monitoring hook -# - frontend-design UI/UX design guidance skill -# -# From obra/superpowers marketplace: -# - double-shot-latte Stops "Would you like me to continue?" interruptions -# - elements-of-style Writing guidance based on Strunk's classic -# - superpowers Core superpowers functionality -# - superpowers-chrome Browser automation via Chrome DevTools Protocol -# - superpowers-lab Experimental/in-development skills -# -# MCP SERVERS: -# - playwright Browser automation and screenshots -# - superpowers-chrome Direct Chrome/Chromium control (headless) +# 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 -e +set -euo pipefail -# Colors for output +# ============================================================================ +# 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' # No Color +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" < 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/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_=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" +echo " OrbStack Development Sandbox Setup (VM Provisioning)" echo "============================================================================" echo -e "${NC}" +echo "Log file: $LOG_FILE" +echo "" -# Prompt for name and email -read -p "Enter your name for git config: " GIT_NAME -read -p "Enter your email for git config: " GIT_EMAIL +# ============================================================================ +# 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 - echo -e "${RED}Error: Name and email are required.${NC}" - exit 1 + 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 -echo "" -echo -e "${YELLOW}Setting up with:${NC}" -echo " Name: $GIT_NAME" -echo " Email: $GIT_EMAIL" -echo "" -read -p "Continue? (y/n) " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 -fi +# ============================================================================ +# System update (always runs) +# ============================================================================ +log_info "Updating system" +sudo apt-get update -qq && sudo apt-get upgrade -y -qq >> "$LOG_FILE" 2>&1 -echo "" -echo -e "${GREEN}=== Updating system ===${NC}" -sudo apt update && sudo apt upgrade -y - -echo "" -echo -e "${GREEN}=== Installing base dependencies ===${NC}" -sudo apt install -y \ +# ============================================================================ +# Base dependencies (always runs) +# ============================================================================ +log_info "Installing base dependencies" +sudo apt-get install -y -qq \ curl \ wget \ git \ @@ -139,137 +493,387 @@ sudo apt install -y \ libxml2-utils \ unzip \ inotify-tools \ - jq + jq \ + ripgrep \ + fd-find \ + direnv \ + tmux \ + python3 \ + python3-pip \ + python3-venv >> "$LOG_FILE" 2>&1 -echo "" -echo -e "${GREEN}=== Installing PostgreSQL ===${NC}" -sudo apt install -y postgresql postgresql-contrib libpq-dev +# 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 -# Start and enable PostgreSQL -sudo systemctl enable postgresql -sudo systemctl start postgresql +# 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 -# Create user and dev database -sudo -u postgres createuser -s $USER 2>/dev/null || echo "PostgreSQL user already exists" -createdb dev 2>/dev/null || echo "Database 'dev' already exists" +# Enable direnv hook in bashrc +if ! grep -q 'direnv hook bash' ~/.bashrc; then + echo 'eval "$(direnv hook bash)"' >> ~/.bashrc +fi -# Allow password-less local connections -sudo sed -i "s/local all all peer/local all all trust/" /etc/postgresql/*/main/pg_hba.conf -sudo sed -i "s/host all all 127.0.0.1\/32 scram-sha-256/host all all 127.0.0.1\/32 trust/" /etc/postgresql/*/main/pg_hba.conf +# ============================================================================ +# 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 -# Listen on all interfaces (for connections from macOS host) -echo "listen_addresses = '*'" | sudo tee -a /etc/postgresql/*/main/postgresql.conf -echo "host all all 0.0.0.0/0 trust" | sudo tee -a /etc/postgresql/*/main/pg_hba.conf + sudo systemctl enable postgresql + sudo systemctl start postgresql -sudo systemctl restart 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 -echo "" -echo -e "${GREEN}=== Installing mise ===${NC}" -curl https://mise.run | sh -echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc -export PATH="$HOME/.local/bin:$PATH" -eval "$(~/.local/bin/mise activate bash)" + # 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 -echo "" -echo -e "${GREEN}=== Installing Node.js (latest LTS) via mise ===${NC}" -mise use -g node@lts + # 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) -echo "" -echo -e "${GREEN}=== Installing Erlang 28.3.1 & Elixir 1.19.5-otp-28 via mise ===${NC}" -mise use -g erlang@28.3.1 -mise use -g elixir@1.19.5-otp-28 + if [ -z "$PG_HBA" ] || [ -z "$PG_CONF" ]; then + log_error "Could not find PostgreSQL configuration files" + exit 1 + fi -echo "" -echo -e "${GREEN}=== Installing Chromium (ARM64 native) ===${NC}" -# Note: Google Chrome doesn't have ARM64 Linux builds, so we use Chromium -sudo apt install -y chromium-browser + # 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" -# Create symlink so tools expecting 'google-chrome' work -sudo ln -sf /usr/bin/chromium-browser /usr/bin/google-chrome + # 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 -echo "" -echo -e "${GREEN}=== Installing Claude Code ===${NC}" -npm install -g @anthropic-ai/claude-code + # 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 -echo "" -echo -e "${GREEN}=== Installing Playwright ===${NC}" -npx playwright install --with-deps chromium + # 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 -echo "" -echo -e "${GREEN}=== Configuring Git ===${NC}" + 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 -echo "" -echo -e "${GREEN}=== Setting up Claude Code plugins (system-wide) ===${NC}" +# ============================================================================ +# 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 -# Create system-wide Claude Code settings directory -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 -# Add marketplaces -echo "Adding marketplaces..." -claude plugin marketplace add anthropics/claude-code -claude plugin marketplace add obra/superpowers + # 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" + ) -# Install plugins from Anthropic official marketplace -echo "Installing Anthropic plugins..." -claude plugin install code-review@claude-code-plugins --scope user -claude plugin install code-simplifier@claude-code-plugins --scope user -claude plugin install feature-dev@claude-code-plugins --scope user -claude plugin install pr-review-toolkit@claude-code-plugins --scope user -claude plugin install security-guidance@claude-code-plugins --scope user -claude plugin install frontend-design@claude-code-plugins --scope user + 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..." -claude plugin install double-shot-latte@superpowers-marketplace --scope user -claude plugin install elements-of-style@superpowers-marketplace --scope user -claude plugin install superpowers@superpowers-marketplace --scope user -claude plugin install superpowers-chrome@superpowers-marketplace --scope user -claude plugin install superpowers-lab@superpowers-marketplace --scope user + # 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" + ) -echo "" -echo -e "${GREEN}=== Adding MCP servers ===${NC}" -claude mcp add playwright --scope user -- npx @anthropic-ai/mcp-server-playwright -claude mcp add superpowers-chrome --scope user -- npx github:obra/superpowers-chrome --headless + for plugin in "${SUPERPOWERS_PLUGINS[@]}"; do + claude plugin install "$plugin" --scope user 2>> "$LOG_FILE" || log_warn "Failed to install $plugin" + done -echo "" -echo -e "${GREEN}=== Creating projects directory ===${NC}" + # 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 -echo "" -echo -e "${GREEN}=== Verifying installations ===${NC}" +# ============================================================================ +# Verification +# ============================================================================ +log_info "Verifying installations" echo "---" -echo "Node: $(node --version)" -echo "npm: $(npm --version)" -echo "Erlang: $(erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell)" -echo "Elixir: $(elixir --version | head -1)" -echo "Chromium: $(chromium-browser --version)" -echo "Claude: $(claude --version)" -echo "PostgreSQL: $(psql --version)" -echo "mise: $(mise --version)" +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}CONNECT FROM macOS:${NC}" -echo " SSH: ssh dev-sandbox@orb" -echo " Zed: Open Remote Folder → dev-sandbox@orb:~/projects" -echo " Files: /Volumes/OrbStack/dev-sandbox/home/$USER/" +echo -e "${YELLOW}VNC:${NC}" +echo " Start: vnc-start" +echo " Stop: vnc-stop" echo "" echo -e "${YELLOW}POSTGRESQL:${NC}" -echo " From VM: psql dev" -echo " From macOS: psql -h dev-sandbox.orb.local -d dev" -echo " DBeaver host: dev-sandbox.orb.local:5432" +echo " psql dev" echo "" echo -e "${YELLOW}CLAUDE CODE:${NC}" -echo " Run: claude" -echo " Plugins: claude plugin list" +echo " claude" echo ""