Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fb83ada34 | ||
|
|
ca12925111 | ||
|
|
65790ee3e2 | ||
|
|
cc1277cd98 | ||
|
|
77093a0ce6 | ||
|
|
70c2559d40 | ||
|
|
2861664a03 | ||
|
|
63bcc0aea3 | ||
|
|
26501daa4e | ||
|
|
cbc379c0cc | ||
|
|
b3ed5e66a5 | ||
|
|
9ee89df424 | ||
|
|
20fa7fa3c5 |
6 changed files with 2186 additions and 113 deletions
169
CHANGELOG.md
Normal file
169
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.14.1] - 2025-01-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated README.md with Windows skip parameters and VNC password info
|
||||||
|
- Updated CLAUDE.md with Windows script documentation and security patterns
|
||||||
|
- Fixed VNC password minimum documentation (6 → 8 chars)
|
||||||
|
|
||||||
|
## [0.14.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Add SHA256 checksum verification for Ubuntu cloud image downloads (prevents MITM attacks)
|
||||||
|
- Add strict input validation for git name, email, VM password, and VNC password
|
||||||
|
- Validate loaded config.env values to detect tampering
|
||||||
|
- VNC password minimum increased from 6 to 8 characters
|
||||||
|
- Block shell metacharacters in all user inputs to prevent injection
|
||||||
|
- Config file created with restricted ACL from the start (no race condition window)
|
||||||
|
- Add security reminder to remove cloud-init ISO after first boot (contains passwords)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- ARM64 architecture detection for Windows on ARM devices
|
||||||
|
- Log file creation at `$env:TEMP\setup_env_windows_<timestamp>.log`
|
||||||
|
- Cleanup on failure: automatically removes partial VM, disk, and ISO on error
|
||||||
|
- Hosts file backup before modification (removed on success, kept on failure)
|
||||||
|
- Input validation functions: `Test-GitName`, `Test-GitEmail`, `Test-VMPassword`, `Test-VNCPassword`
|
||||||
|
- Checksum caching to avoid re-downloading verification data
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Ubuntu image URL now uses detected architecture instead of hardcoded amd64
|
||||||
|
- All major operations now log to file for troubleshooting
|
||||||
|
- VM creation wrapped in try/catch with automatic cleanup on failure
|
||||||
|
|
||||||
|
## [0.13.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rewrote `setup_env_windows.ps1` to fully implement WINDOWS_PLAN.md
|
||||||
|
- Password handling uses cloud-init `chpasswd` with plaintext (type: text) instead of broken hash generation
|
||||||
|
- Multiple ISO creation methods with fallback chain: oscdimg → WSL genisoimage → IMAPI2 COM
|
||||||
|
- Downloads use BITS transfer for reliability with progress reporting
|
||||||
|
- SSH readiness checking with timeout before displaying connection info
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Component skip parameters: `-SkipVNC`, `-SkipPostgreSQL`, `-SkipOllama`, `-SkipPlaywright`
|
||||||
|
- VNC password support via base64 encoding in cloud-init
|
||||||
|
- Automatic hosts file cleanup when VM is deleted with `-Force`
|
||||||
|
- Proper prerequisite checking for Hyper-V, Windows edition, and admin privileges
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Cloud-init password configuration (was using bash syntax in PowerShell)
|
||||||
|
- ISO creation now works without Windows ADK by using WSL or IMAPI2 fallbacks
|
||||||
|
- Hosts file handling with proper admin privilege elevation
|
||||||
|
|
||||||
|
## [0.12.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Python runtime management via mise (alongside Node.js, Erlang, Elixir)
|
||||||
|
- `WINDOWS_PLAN.md` documenting Hyper-V implementation strategy and security rationale
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Tidewave CLI download URL (now uses correct `tidewave_app` repo with musl binaries)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Python is now a selectable component managed by mise instead of system apt
|
||||||
|
|
||||||
|
## [0.11.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Windows support via Hyper-V for maximum security isolation
|
||||||
|
- `setup_env_windows.ps1` PowerShell script with full VM provisioning
|
||||||
|
- Ubuntu cloud image support with cloud-init automation
|
||||||
|
- SSH key generation for passwordless VM access on Windows
|
||||||
|
- Hosts file integration for easy `<vmname>.local` access
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Hyper-V provides stronger isolation than WSL2 (separate kernel, network, filesystem)
|
||||||
|
- No host integration services enabled by default
|
||||||
|
|
||||||
|
## [0.10.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- OpenCode: Open-source AI coding assistant with multi-provider support
|
||||||
|
- Tidewave CLI: Elixir/Phoenix MCP server for AI-powered development
|
||||||
|
- New component selection options for OpenCode and Tidewave
|
||||||
|
|
||||||
|
## [0.9.1] - 2025-01-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add error checking for base64 decode operations in VM provisioning
|
||||||
|
- Add `set -e` to VM bootstrap script for early failure detection
|
||||||
|
|
||||||
|
## [0.9.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fix rustup pipe-to-shell vulnerability: now downloads to temp file with validation before execution
|
||||||
|
- Fix SKIP_EXPORTS command injection risk: refactored to use base64-encoded list instead of shell command string
|
||||||
|
- Fix Playwright symlink path validation: validates executable and path prefix before creating symlinks
|
||||||
|
|
||||||
|
## [0.8.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Project memory system using CLAUDE.md, CHANGELOG.md, and README.md
|
||||||
|
- Versioning rules and documentation update guidelines in CLAUDE.md
|
||||||
|
|
||||||
|
## [0.7.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- CHANGELOG.md with version history following Keep a Changelog format
|
||||||
|
|
||||||
|
## [0.6.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All tools now use latest versions by default instead of pinning specific versions
|
||||||
|
- PostgreSQL authentication uses scram-sha-256 for all TCP connections
|
||||||
|
- Simplified tool installation by removing version pinning constraints
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- VNC passwords are never stored and must be entered each time
|
||||||
|
- Added documentation for input validation patterns and safe config loading
|
||||||
|
|
||||||
|
## [0.5.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Prevents shell injection through input validation and safe parameter passing
|
||||||
|
- Replaces direct sourcing with manual config parsing to avoid code execution
|
||||||
|
- Downloads and validates install scripts before execution instead of piping
|
||||||
|
- Uses base64 encoding for secure VM parameter transmission
|
||||||
|
- Adds checksum verification for binary downloads
|
||||||
|
- Creates secure temporary directories and files with proper permissions
|
||||||
|
|
||||||
|
## [0.4.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Significantly reduces total setup time by running independent steps concurrently
|
||||||
|
|
||||||
|
## [0.3.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Dual-mode operation: orchestration on macOS, provisioning on Linux
|
||||||
|
- Interactive component selection with visual menu interface
|
||||||
|
- VNC desktop access for OAuth workflows and browser-based tasks
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Secure VM creation with disabled host filesystem access
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- OrbStack development sandbox setup script
|
||||||
|
- mise version manager with Node.js, Erlang, and Elixir support
|
||||||
|
- PostgreSQL 16 with remote access configuration
|
||||||
|
- Claude Code integration with multiple plugin marketplaces
|
||||||
|
- Chromium browser and Playwright for automation tasks
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project structure
|
||||||
85
CLAUDE.md
85
CLAUDE.md
|
|
@ -1,17 +1,42 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Project Memory
|
||||||
|
|
||||||
|
This project uses three documentation files as persistent memory. **You must keep these files up to date when making changes:**
|
||||||
|
|
||||||
|
| File | Purpose | Update When |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `CLAUDE.md` | Technical context for Claude Code sessions | Adding new patterns, conventions, or implementation details |
|
||||||
|
| `CHANGELOG.md` | Version history following Keep a Changelog format | Every commit (add entry, bump version) |
|
||||||
|
| `README.md` | User-facing documentation | Changing user-visible behavior, adding features, or modifying usage |
|
||||||
|
|
||||||
|
### Versioning Rules
|
||||||
|
|
||||||
|
- Use semantic versioning (MAJOR.MINOR.PATCH)
|
||||||
|
- Increment PATCH for bug fixes and minor updates
|
||||||
|
- Increment MINOR for new features and enhancements
|
||||||
|
- Increment MAJOR for breaking changes
|
||||||
|
- Tag every commit with its version: `git tag -a vX.Y.Z -m "Description"`
|
||||||
|
|
||||||
## Project Overview
|
## 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.
|
This repository provides scripts for creating isolated development sandboxes for Claude Code on macOS (OrbStack) and Windows (Hyper-V). Both platforms offer full VM isolation for safely running AI coding assistants with elevated permissions.
|
||||||
|
|
||||||
|
**Supported platforms:**
|
||||||
|
- **macOS**: OrbStack VMs (ARM64 Apple Silicon)
|
||||||
|
- **Windows**: Hyper-V VMs (maximum security, stronger than WSL2)
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
setup_env.sh - Main script (macOS host mode + Linux VM provisioning mode)
|
setup_env.sh - macOS script (OrbStack host mode + Linux VM provisioning)
|
||||||
|
setup_env_windows.ps1 - Windows script (Hyper-V with cloud-init)
|
||||||
|
WINDOWS_PLAN.md - Windows implementation plan and security rationale
|
||||||
config.env.example - Example credentials file
|
config.env.example - Example credentials file
|
||||||
config.env - User credentials (gitignored, created on first run)
|
config.env - User credentials (gitignored, created on first run)
|
||||||
.gitignore - Ignores config.env
|
.gitignore - Ignores config.env
|
||||||
README.md - User-facing documentation
|
README.md - User-facing documentation
|
||||||
|
CHANGELOG.md - Version history (Keep a Changelog format)
|
||||||
CLAUDE.md - This file (context for Claude Code sessions)
|
CLAUDE.md - This file (context for Claude Code sessions)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -28,11 +53,11 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end.
|
||||||
|
|
||||||
- **Target environment**: OrbStack Ubuntu VM on macOS Apple Silicon (ARM64)
|
- **Target environment**: OrbStack Ubuntu VM on macOS Apple Silicon (ARM64)
|
||||||
- **Version manager**: mise (manages Node.js, Erlang, Elixir)
|
- **Version manager**: mise (manages Node.js, Erlang, Elixir)
|
||||||
- **Language versions**: Configured at the top of `setup_env.sh` as variables (`ERLANG_VERSION`, `ELIXIR_VERSION`)
|
- **Versions**: All tools use latest by default. Erlang/Elixir versions can be configured at the top of `setup_env.sh` (`ERLANG_VERSION`, `ELIXIR_VERSION`) - set to "latest" or pin to specific versions
|
||||||
- **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)
|
- **PostgreSQL auth**: Peer for local socket, scram-sha-256 for all TCP connections (localhost and network)
|
||||||
- **Browser**: Chromium (no Chrome ARM64 Linux builds exist), symlinked to `google-chrome`
|
- **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`
|
- **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
|
- **Shared credentials**: `config.env` stores git name/email only; VNC password is prompted each time (never stored)
|
||||||
|
|
||||||
## Working on This Project
|
## Working on This Project
|
||||||
|
|
||||||
|
|
@ -61,9 +86,40 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end.
|
||||||
- `--yes`/`-y` accepts all components without prompting but still allows interactive credential entry
|
- `--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
|
- `MISE_GLOBAL_CONFIG_FILE` and `MISE_CONFIG_DIR` are set to prevent OrbStack host-mount config pollution
|
||||||
|
|
||||||
|
### Security Patterns (macOS/Linux)
|
||||||
|
|
||||||
|
- **Input validation**: Use `validate_vm_name()`, `validate_vnc_password()`, `validate_safe_input()` for all user inputs
|
||||||
|
- **Safe config loading**: Use `load_config_safely()` which parses key=value pairs without shell execution (never `source`)
|
||||||
|
- **Credential passing**: Base64-encode values before passing to VM via `orb run` to prevent shell injection
|
||||||
|
- **Download helper**: Use `download_verified_binary()` for binary downloads (supports optional checksum verification)
|
||||||
|
- **Temp files**: Always use `mktemp` with restrictive permissions (`chmod 600`/`700`)
|
||||||
|
- **Symlinks**: Check with `[ ! -e path ]` (not `[ ! -L path ]`) to avoid overwriting existing files
|
||||||
|
- **Architecture detection**: Use `detect_architecture()` for portable binary downloads
|
||||||
|
|
||||||
|
### Editing setup_env_windows.ps1
|
||||||
|
|
||||||
|
- PowerShell script for Windows Hyper-V VM creation
|
||||||
|
- Uses Ubuntu cloud images with cloud-init for automated provisioning
|
||||||
|
- Supports ARM64 architecture detection for Windows on ARM
|
||||||
|
- Skip parameters: `-SkipVNC`, `-SkipPostgreSQL`, `-SkipOllama`, `-SkipPlaywright`
|
||||||
|
- ISO creation fallback chain: oscdimg → WSL genisoimage → IMAPI2 COM
|
||||||
|
- Log file created at `$env:TEMP\setup_env_windows_<timestamp>.log`
|
||||||
|
|
||||||
|
### Security Patterns (Windows)
|
||||||
|
|
||||||
|
- **Input validation**: Use `Test-GitName`, `Test-GitEmail`, `Test-VMPassword`, `Test-VNCPassword` for all user inputs
|
||||||
|
- **Config file security**: ACL set to owner-only before writing content (no race condition)
|
||||||
|
- **Config validation**: Loaded config.env values are re-validated to detect tampering
|
||||||
|
- **Checksum verification**: Ubuntu image verified against SHA256 checksums from Canonical
|
||||||
|
- **Cleanup on failure**: `Invoke-Cleanup` removes partial VM, disk, and ISO on errors
|
||||||
|
- **Hosts file backup**: Created before modification, removed on success, kept on failure
|
||||||
|
- **Cloud-init ISO**: Contains passwords in plaintext; user reminded to remove after first boot
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
There are no automated tests. To test changes:
|
There are no automated tests. To test changes:
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
1. Remove an existing test VM: `orb delete test-sandbox`
|
1. Remove an existing test VM: `orb delete test-sandbox`
|
||||||
2. Run: `./setup_env.sh test-sandbox`
|
2. Run: `./setup_env.sh test-sandbox`
|
||||||
3. Verify provisioning completes
|
3. Verify provisioning completes
|
||||||
|
|
@ -71,10 +127,21 @@ There are no automated tests. To test changes:
|
||||||
5. Test VNC: `ssh test-sandbox@orb -- vnc-start`, then `open vnc://test-sandbox.orb.local:5901`
|
5. Test VNC: `ssh test-sandbox@orb -- vnc-start`, then `open vnc://test-sandbox.orb.local:5901`
|
||||||
6. Clean up: `orb delete test-sandbox`
|
6. Clean up: `orb delete test-sandbox`
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
1. Remove existing test VM: `Stop-VM -Name test-sandbox -Force; Remove-VM -Name test-sandbox -Force`
|
||||||
|
2. Run: `.\setup_env_windows.ps1 -VMName test-sandbox`
|
||||||
|
3. Verify provisioning completes (check `/var/log/provision.log` in VM)
|
||||||
|
4. Test SSH: `ssh -i $env:USERPROFILE\.ssh\id_ed25519_test-sandbox dev@test-sandbox.local`
|
||||||
|
5. Clean up: `Stop-VM -Name test-sandbox -Force; Remove-VM -Name test-sandbox -Force`
|
||||||
|
|
||||||
### Security Considerations
|
### Security Considerations
|
||||||
|
|
||||||
- `config.env` is chmod 600 and gitignored
|
- `config.env` is chmod 600 / owner-only ACL and gitignored (stores git name/email only, never passwords)
|
||||||
- PostgreSQL remote access uses scram-sha-256 (not trust)
|
- PostgreSQL uses scram-sha-256 for all TCP connections (peer auth for local socket)
|
||||||
- The script refuses to run as root inside the VM
|
- The script refuses to run as root inside the VM
|
||||||
- VNC password is required (min 6 chars)
|
- VNC password is required (min 8 chars), validated to block shell metacharacters, never stored
|
||||||
- VNC binds to all interfaces (`-localhost no`) to allow connections from the macOS host — this is intentional for the OrbStack use case
|
- VNC binds to all interfaces (`-localhost no`) to allow connections from the host — this is intentional and documented
|
||||||
|
- All user inputs are validated before use to prevent command injection
|
||||||
|
- External scripts (mise, ollama) are downloaded to temp files and validated before execution
|
||||||
|
- Host filesystem access is disabled inside the VM for isolation
|
||||||
|
- Windows: Ubuntu image downloads are verified against SHA256 checksums
|
||||||
|
|
|
||||||
115
README.md
115
README.md
|
|
@ -1,20 +1,27 @@
|
||||||
# OrbStack Development Sandbox
|
# Secure AI Coding Sandboxes
|
||||||
|
|
||||||
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.
|
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.
|
The VM is a real Linux machine with its own filesystem, network, and process space — complete isolation from your host with easy access for development.
|
||||||
|
|
||||||
|
| Platform | Script | Isolation Level |
|
||||||
|
|----------|--------|-----------------|
|
||||||
|
| **macOS** | `setup_env.sh` (OrbStack) | Full VM isolation |
|
||||||
|
| **Windows** | `setup_env_windows.ps1` (Hyper-V) | Full VM isolation |
|
||||||
|
|
||||||
## Why This Exists
|
## 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.
|
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:
|
These scripts create throwaway VMs where Claude can run unrestricted:
|
||||||
|
|
||||||
- **Isolated filesystem** — Claude can't touch your macOS files, keys, or configs
|
- **Isolated filesystem** — Claude can't touch your host files, keys, or configs
|
||||||
- **Isolated network** — services run on their own IP, no port conflicts with your host
|
- **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
|
- **Disposable** — delete and recreate in one command
|
||||||
- **Multiple VMs** — run separate sandboxes per project with shared git credentials
|
- **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
|
- **Full access from host** — edit files in your editor, browse databases, view running apps
|
||||||
|
|
||||||
|
## Quick Start (macOS)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a sandbox, SSH in, run Claude unrestricted
|
# Create a sandbox, SSH in, run Claude unrestricted
|
||||||
|
|
@ -25,14 +32,14 @@ claude --dangerously-skip-permissions
|
||||||
|
|
||||||
If anything goes wrong: `orb delete my-project && ./setup_env.sh my-project`
|
If anything goes wrong: `orb delete my-project && ./setup_env.sh my-project`
|
||||||
|
|
||||||
## Quick Start
|
### macOS Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create and provision a VM (one command from macOS)
|
# Create and provision a VM (one command from macOS)
|
||||||
./setup_env.sh my-sandbox
|
./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.
|
On first run, you'll be prompted for git commit author name and email. 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. If you select VNC, you'll be prompted for a VNC password (this is never stored and must be entered each time).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create additional VMs — reuses config.env, shows component picker
|
# Create additional VMs — reuses config.env, shows component picker
|
||||||
|
|
@ -51,11 +58,65 @@ When run manually inside a VM, you're prompted for each component individually:
|
||||||
./setup_env.sh --yes
|
./setup_env.sh --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
### macOS Requirements
|
||||||
|
|
||||||
- macOS with Apple Silicon (ARM64)
|
- macOS with Apple Silicon (ARM64)
|
||||||
- [OrbStack](https://orbstack.dev) installed (`brew install orbstack`)
|
- [OrbStack](https://orbstack.dev) installed (`brew install orbstack`)
|
||||||
|
|
||||||
|
## Quick Start (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run as Administrator
|
||||||
|
.\setup_env_windows.ps1 -VMName my-project
|
||||||
|
|
||||||
|
# Connect via SSH (after provisioning)
|
||||||
|
ssh -i $env:USERPROFILE\.ssh\id_ed25519_my-project dev@my-project.local
|
||||||
|
|
||||||
|
# Run Claude unrestricted
|
||||||
|
claude --dangerously-skip-permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
If anything goes wrong: `Remove-VM -Name my-project -Force` then run the script again.
|
||||||
|
|
||||||
|
### Windows Setup
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Create and provision a VM (run as Administrator)
|
||||||
|
.\setup_env_windows.ps1 -VMName my-sandbox
|
||||||
|
|
||||||
|
# Customize resources
|
||||||
|
.\setup_env_windows.ps1 -VMName my-sandbox -MemoryGB 16 -DiskGB 100 -CPUs 8
|
||||||
|
|
||||||
|
# Skip optional components
|
||||||
|
.\setup_env_windows.ps1 -VMName my-sandbox -SkipVNC -SkipOllama
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, you'll be prompted for:
|
||||||
|
- Git commit author name and email (saved to `config.env`)
|
||||||
|
- VM user password (min 8 chars, not stored)
|
||||||
|
- VNC password if VNC is enabled (min 8 chars, not stored)
|
||||||
|
|
||||||
|
### Windows Requirements
|
||||||
|
|
||||||
|
- Windows 10/11 Pro, Enterprise, or Education (Hyper-V not available on Home)
|
||||||
|
- Hyper-V enabled: `Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All`
|
||||||
|
- Administrator privileges
|
||||||
|
- One of: Windows ADK, WSL with genisoimage, or Windows 10+ (IMAPI2 fallback)
|
||||||
|
|
||||||
|
### Why Hyper-V (not WSL2)?
|
||||||
|
|
||||||
|
WSL2 is convenient but provides weaker isolation:
|
||||||
|
- Shares kernel with all WSL2 instances
|
||||||
|
- Host filesystem mounted by default
|
||||||
|
- Can launch Windows executables from Linux
|
||||||
|
- Network traffic bypasses Windows firewall
|
||||||
|
|
||||||
|
Hyper-V provides **maximum security**:
|
||||||
|
- Separate kernel per VM
|
||||||
|
- Complete filesystem isolation
|
||||||
|
- Own network stack
|
||||||
|
- No Windows integration by default
|
||||||
|
|
||||||
## What Gets Installed
|
## What Gets Installed
|
||||||
|
|
||||||
All components are optional — deselect what you don't need in the interactive picker.
|
All components are optional — deselect what you don't need in the interactive picker.
|
||||||
|
|
@ -64,13 +125,17 @@ All components are optional — deselect what you don't need in the interactive
|
||||||
|------|---------|---------|
|
|------|---------|---------|
|
||||||
| mise | latest | Version manager for runtimes |
|
| mise | latest | Version manager for runtimes |
|
||||||
| Node.js | LTS | JavaScript runtime |
|
| Node.js | LTS | JavaScript runtime |
|
||||||
| Erlang | 28.3.1 | BEAM VM |
|
| Erlang | latest | BEAM VM |
|
||||||
| Elixir | 1.19.5-otp-28 | Elixir language |
|
| Elixir | latest | Elixir language |
|
||||||
| Chromium | system | Browser automation target |
|
| Chromium | system | Browser automation target |
|
||||||
| Playwright | latest | Browser testing framework |
|
| Playwright | latest | Browser testing framework |
|
||||||
| PostgreSQL | system default | Database |
|
| PostgreSQL | system default | Database |
|
||||||
| Ollama | latest | Local LLM inference |
|
| Ollama | latest | Local LLM inference |
|
||||||
| Claude Code | latest | AI coding assistant |
|
| Claude Code | latest | AI coding assistant (Anthropic) |
|
||||||
|
| OpenCode | latest | Open-source AI coding assistant (multi-provider) |
|
||||||
|
| Tidewave | latest | Elixir/Phoenix MCP server for AI tools |
|
||||||
|
| yq | latest | YAML processor |
|
||||||
|
| watchexec | latest | File watcher (via cargo) |
|
||||||
| TigerVNC + XFCE | system | VNC access for browser login flows |
|
| TigerVNC + XFCE | system | VNC access for browser login flows |
|
||||||
|
|
||||||
## Connecting from macOS
|
## Connecting from macOS
|
||||||
|
|
@ -177,7 +242,7 @@ A superuser matching your Linux username and a `dev` database are created automa
|
||||||
|
|
||||||
**Auth model:**
|
**Auth model:**
|
||||||
- Local socket (`psql dev`): peer auth (OS username must match PG role)
|
- Local socket (`psql dev`): peer auth (OS username must match PG role)
|
||||||
- Localhost TCP (127.0.0.1): trust (passwordless)
|
- Localhost TCP (127.0.0.1): scram-sha-256 (password required)
|
||||||
- Host network (192.168.0.0/16, i.e., from macOS): scram-sha-256
|
- Host network (192.168.0.0/16, i.e., from macOS): scram-sha-256
|
||||||
|
|
||||||
### From inside the VM
|
### From inside the VM
|
||||||
|
|
@ -246,16 +311,18 @@ The script installs these plugins at user scope:
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit the version variables at the top of `setup_env.sh`:
|
All tools are configured to use the latest versions by default. Erlang and Elixir versions can be customized in `setup_env.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ERLANG_VERSION="28.3.1"
|
ERLANG_VERSION="latest" # or pin to specific version like "28.3.1"
|
||||||
ELIXIR_VERSION="1.19.5-otp-28"
|
ELIXIR_VERSION="latest" # or pin to specific version like "1.19.5-otp-28"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shared credentials
|
### Shared credentials
|
||||||
|
|
||||||
Credentials are stored in `config.env` (gitignored). To reset:
|
Git credentials (name, email) are stored in `config.env` (gitignored). VNC passwords are never stored and must be entered each time you create a VM with VNC enabled.
|
||||||
|
|
||||||
|
To reset git credentials:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm config.env
|
rm config.env
|
||||||
|
|
@ -294,4 +361,16 @@ The VM provisioning script is safe to run multiple times. It checks for existing
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
Each provisioning run creates a log file at `/tmp/setup_env_<timestamp>.log` inside the VM with detailed output from package installations and any errors.
|
Each provisioning run creates a log file at `/tmp/setup_env_<timestamp>.log` inside the VM with detailed output from package installations and any errors. Log files are created with mode 600 (owner-only access).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The script includes several security hardening measures:
|
||||||
|
|
||||||
|
- **Input validation**: VM names, VNC passwords, and git credentials are validated against strict patterns
|
||||||
|
- **No credential storage**: VNC passwords are prompted each time and never written to disk
|
||||||
|
- **Safe credential passing**: Values are base64-encoded when passed to the VM to prevent shell injection
|
||||||
|
- **Config file security**: `config.env` is created with mode 600 and parsed safely (not sourced)
|
||||||
|
- **Secure temp files**: Uses `mktemp` with restrictive permissions for all temporary files
|
||||||
|
- **Host filesystem isolation**: macOS home directory access is disabled inside the VM
|
||||||
|
- **PostgreSQL hardening**: Uses peer auth for local sockets, scram-sha-256 for network connections
|
||||||
|
|
|
||||||
257
WINDOWS_PLAN.md
Normal file
257
WINDOWS_PLAN.md
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
# Windows Hyper-V Implementation Plan
|
||||||
|
|
||||||
|
This document describes the plan for implementing secure, isolated development sandboxes on Windows using Hyper-V instead of WSL2.
|
||||||
|
|
||||||
|
## Why Hyper-V Over WSL2
|
||||||
|
|
||||||
|
### WSL2 Security Limitations
|
||||||
|
|
||||||
|
| Issue | Impact |
|
||||||
|
|-------|--------|
|
||||||
|
| **Shared kernel** | All WSL2 instances share Microsoft's Linux kernel |
|
||||||
|
| **Host filesystem access** | `/mnt/c` mounted by default, can access Windows files |
|
||||||
|
| **Bidirectional execution** | Linux can launch Windows executables |
|
||||||
|
| **Firewall bypass** | WSL2 outbound traffic bypasses Windows firewall rules |
|
||||||
|
| **Process visibility** | Windows has limited visibility into WSL2 processes |
|
||||||
|
| **Credential exposure** | Linux processes can potentially access Windows credentials |
|
||||||
|
|
||||||
|
### CVE History
|
||||||
|
|
||||||
|
- **CVE-2024-20681**: Local privilege escalation to SYSTEM via WSL
|
||||||
|
- **CVE-2025-9074**: Container escape via WSL2 filesystem sharing
|
||||||
|
- **CVE-2025-53788**: Undisclosed flaw in WSL/host communication
|
||||||
|
|
||||||
|
### Hyper-V Isolation Model
|
||||||
|
|
||||||
|
| Feature | Benefit |
|
||||||
|
|---------|---------|
|
||||||
|
| **Separate kernel** | Each VM runs its own Linux kernel |
|
||||||
|
| **Complete filesystem isolation** | No access to Windows files by default |
|
||||||
|
| **Own network stack** | Separate IP, no firewall bypass |
|
||||||
|
| **No Windows integration** | Cannot launch Windows programs |
|
||||||
|
| **Snapshot support** | Can checkpoint and restore VM state |
|
||||||
|
| **Hardware isolation** | Uses VT-x/AMD-V virtualization |
|
||||||
|
|
||||||
|
## Implementation Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Windows Host │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ setup_env_windows.ps1│ │ Hyper-V Manager │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ • Check prereqs │───▶│ ┌──────────────────────────┐ │ │
|
||||||
|
│ │ • Download image │ │ │ Ubuntu VM │ │ │
|
||||||
|
│ │ • Create cloud-init │ │ │ │ │ │
|
||||||
|
│ │ • Provision VM │ │ │ • Isolated filesystem │ │ │
|
||||||
|
│ │ • Configure network │ │ │ • Own network (NAT) │ │ │
|
||||||
|
│ └─────────────────────┘ │ │ • Claude Code + tools │ │ │
|
||||||
|
│ │ │ • No Windows access │ │ │
|
||||||
|
│ │ └──────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Workflow
|
||||||
|
|
||||||
|
### Phase 1: Prerequisites Check
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Verify environment
|
||||||
|
1. Check Administrator privileges
|
||||||
|
2. Check Windows edition (Pro/Enterprise/Education required)
|
||||||
|
3. Check Hyper-V enabled
|
||||||
|
4. Check for existing VM (handle -Force flag)
|
||||||
|
5. Find suitable network switch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Configuration
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Collect credentials
|
||||||
|
1. Load config.env if exists (git name/email)
|
||||||
|
2. Prompt for missing values
|
||||||
|
3. Prompt for VM password (not stored)
|
||||||
|
4. Generate SSH key pair for VM access
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Image Preparation
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Prepare Ubuntu cloud image
|
||||||
|
1. Download Ubuntu cloud image (VHDX format)
|
||||||
|
2. Cache in $env:ProgramData\HyperV-DevSandbox\Images
|
||||||
|
3. Copy and resize for new VM
|
||||||
|
4. Generate cloud-init ISO with:
|
||||||
|
- User account configuration
|
||||||
|
- SSH key injection
|
||||||
|
- Package installation
|
||||||
|
- Setup script embedding
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: VM Creation
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Create Hyper-V VM
|
||||||
|
1. Create Generation 2 VM (UEFI)
|
||||||
|
2. Configure resources (CPU, RAM, disk)
|
||||||
|
3. Disable Secure Boot (for Ubuntu)
|
||||||
|
4. Attach cloud-init ISO
|
||||||
|
5. Disable integration services for isolation
|
||||||
|
6. Enable nested virtualization (for Docker)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Provisioning
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Start and provision
|
||||||
|
1. Start VM
|
||||||
|
2. Wait for IP assignment
|
||||||
|
3. Add to hosts file (<vmname>.local)
|
||||||
|
4. Cloud-init runs setup_env.sh internally
|
||||||
|
5. Display connection instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloud-Init Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#cloud-config
|
||||||
|
hostname: <vmname>
|
||||||
|
users:
|
||||||
|
- name: dev
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- <generated-public-key>
|
||||||
|
packages:
|
||||||
|
- openssh-server
|
||||||
|
- curl
|
||||||
|
- git
|
||||||
|
write_files:
|
||||||
|
- path: /tmp/setup_env.sh
|
||||||
|
content: <base64-encoded-script>
|
||||||
|
runcmd:
|
||||||
|
- bash /tmp/setup_env.sh --non-interactive --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Hardening
|
||||||
|
|
||||||
|
### VM Configuration
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Disable integration services for maximum isolation
|
||||||
|
Disable-VMIntegrationService -Name "Guest Service Interface"
|
||||||
|
Disable-VMIntegrationService -Name "Heartbeat"
|
||||||
|
# Keep Time Synchronization and Shutdown for usability
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Isolation Options
|
||||||
|
|
||||||
|
1. **NAT (Default)**: VM can access internet, isolated from LAN
|
||||||
|
2. **Internal Only**: VM can only communicate with host
|
||||||
|
3. **Private**: Completely isolated network
|
||||||
|
|
||||||
|
### Firewall Rules (Optional)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Allow SSH access only
|
||||||
|
New-NetFirewallRule -Name "HyperV-SSH-$VMName" `
|
||||||
|
-Direction Inbound -LocalPort 22 -Protocol TCP `
|
||||||
|
-RemoteAddress <VM-IP> -Action Allow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Windows Edition
|
||||||
|
|
||||||
|
- Windows 10/11 Pro
|
||||||
|
- Windows 10/11 Enterprise
|
||||||
|
- Windows 10/11 Education
|
||||||
|
- Windows Server 2016+
|
||||||
|
|
||||||
|
**Not supported**: Windows 10/11 Home (no Hyper-V)
|
||||||
|
|
||||||
|
### Hardware
|
||||||
|
|
||||||
|
- 64-bit processor with SLAT (Second Level Address Translation)
|
||||||
|
- VM Monitor Mode extensions (VT-x on Intel, AMD-V on AMD)
|
||||||
|
- Minimum 4 GB RAM (8+ GB recommended)
|
||||||
|
- BIOS/UEFI virtualization enabled
|
||||||
|
|
||||||
|
### Software
|
||||||
|
|
||||||
|
- Hyper-V enabled: `Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All`
|
||||||
|
- Windows ADK (for cloud-init ISO creation): [Download](https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Create Sandbox
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Basic usage (run as Administrator)
|
||||||
|
.\setup_env_windows.ps1 -VMName my-project
|
||||||
|
|
||||||
|
# With custom resources
|
||||||
|
.\setup_env_windows.ps1 -VMName my-project -MemoryGB 16 -DiskGB 100 -CPUs 8
|
||||||
|
|
||||||
|
# Replace existing VM
|
||||||
|
.\setup_env_windows.ps1 -VMName my-project -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connect to Sandbox
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Via SSH (recommended)
|
||||||
|
ssh -i $env:USERPROFILE\.ssh\id_ed25519_<vmname> dev@<vmname>.local
|
||||||
|
|
||||||
|
# Via Hyper-V console
|
||||||
|
vmconnect localhost <vmname>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage Sandbox
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Stop VM
|
||||||
|
Stop-VM -Name <vmname>
|
||||||
|
|
||||||
|
# Start VM
|
||||||
|
Start-VM -Name <vmname>
|
||||||
|
|
||||||
|
# Get VM IP
|
||||||
|
(Get-VM -Name <vmname> | Get-VMNetworkAdapter).IPAddresses
|
||||||
|
|
||||||
|
# Delete VM completely
|
||||||
|
Stop-VM -Name <vmname> -Force
|
||||||
|
Remove-VM -Name <vmname> -Force
|
||||||
|
Remove-Item -Path "$env:ProgramData\HyperV-DevSandbox\VMs\<vmname>" -Recurse -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison: OrbStack vs Hyper-V
|
||||||
|
|
||||||
|
| Feature | OrbStack (macOS) | Hyper-V (Windows) |
|
||||||
|
|---------|------------------|-------------------|
|
||||||
|
| Host OS | macOS only | Windows only |
|
||||||
|
| Setup complexity | Low | Medium |
|
||||||
|
| Isolation | Full VM | Full VM |
|
||||||
|
| DNS | `*.orb.local` auto | Manual hosts file |
|
||||||
|
| SSH | Automatic | Key-based |
|
||||||
|
| Filesystem sharing | Configurable | Disabled by default |
|
||||||
|
| Performance | Near-native | Near-native |
|
||||||
|
| Nested virtualization | Yes | Yes |
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
1. **Windows ADK Required**: Cloud-init ISO creation requires `oscdimg` from Windows ADK
|
||||||
|
2. **Manual DNS**: No automatic DNS like OrbStack's `*.orb.local`
|
||||||
|
3. **Key Management**: SSH keys generated per-VM, stored in user's .ssh folder
|
||||||
|
4. **No Clipboard Sharing**: Disabled for security (can be enabled if needed)
|
||||||
|
5. **Generation 2 Only**: Uses UEFI, no legacy BIOS support
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
- [ ] Add option for WSL2 (for users who prefer convenience over security)
|
||||||
|
- [ ] Implement automatic DNS via Windows hosts file or local DNS server
|
||||||
|
- [ ] Add PowerShell ISO creation without Windows ADK dependency
|
||||||
|
- [ ] Support for VM templates/checkpoints
|
||||||
|
- [ ] Integration with Windows Terminal for easy access
|
||||||
|
- [ ] Optional WinRM configuration for PowerShell remoting
|
||||||
577
setup_env.sh
577
setup_env.sh
|
|
@ -25,10 +25,11 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Configuration - edit versions here
|
# Configuration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
ERLANG_VERSION="28.3.1"
|
# Use 'latest' for mise-managed tools to always get the newest version
|
||||||
ELIXIR_VERSION="1.19.5-otp-28"
|
ERLANG_VERSION="latest"
|
||||||
|
ELIXIR_VERSION="latest"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
@ -63,6 +64,113 @@ command_exists() {
|
||||||
command -v "$1" &>/dev/null
|
command -v "$1" &>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Security: Validate input contains only safe characters for config storage
|
||||||
|
# Allows alphanumeric, spaces, common punctuation, but blocks shell metacharacters
|
||||||
|
validate_safe_input() {
|
||||||
|
local input="$1"
|
||||||
|
local field_name="$2"
|
||||||
|
# Block dangerous shell metacharacters: backticks, $, (), {}, ;, |, &, <, >, newlines
|
||||||
|
if [[ "$input" =~ [\`\$\(\)\{\}\;\|\&\<\>\"] ]] || [[ "$input" == *$'\n'* ]]; then
|
||||||
|
log_error "$field_name contains invalid characters (shell metacharacters not allowed)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: Validate VM name contains only safe characters
|
||||||
|
validate_vm_name() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ ! "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
|
||||||
|
log_error "VM name must start with a letter and contain only alphanumeric characters, hyphens, and underscores"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ ${#name} -gt 64 ]; then
|
||||||
|
log_error "VM name must be 64 characters or less"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: Validate VNC password
|
||||||
|
validate_vnc_password() {
|
||||||
|
local password="$1"
|
||||||
|
if [ ${#password} -lt 6 ]; then
|
||||||
|
echo "Password must be at least 6 characters."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ ${#password} -gt 64 ]; then
|
||||||
|
echo "Password must be 64 characters or less."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Block characters that could cause shell injection
|
||||||
|
if [[ "$password" =~ [\`\$\(\)\{\}\;\|\&\<\>\'\"] ]]; then
|
||||||
|
echo "Password contains invalid characters."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: Detect system architecture and return normalized name
|
||||||
|
# Returns: amd64, arm64, or exits with error for unsupported architectures
|
||||||
|
detect_architecture() {
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m)
|
||||||
|
case "$arch" in
|
||||||
|
x86_64|amd64)
|
||||||
|
echo "amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unsupported architecture: $arch"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security: Download and verify a binary with optional checksum
|
||||||
|
# Args: $1=url, $2=output_path, $3=expected_checksum (optional, sha256)
|
||||||
|
download_verified_binary() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
local expected_checksum="${3:-}"
|
||||||
|
|
||||||
|
local temp_file
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
|
||||||
|
# Download to temp file first
|
||||||
|
if ! curl -fsSL "$url" -o "$temp_file"; then
|
||||||
|
log_error "Failed to download: $url"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify checksum if provided
|
||||||
|
if [ -n "$expected_checksum" ]; then
|
||||||
|
local actual_checksum
|
||||||
|
actual_checksum=$(sha256sum "$temp_file" | awk '{print $1}')
|
||||||
|
if [ "$actual_checksum" != "$expected_checksum" ]; then
|
||||||
|
log_error "Checksum mismatch for $url"
|
||||||
|
log_error "Expected: $expected_checksum"
|
||||||
|
log_error "Got: $actual_checksum"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Move to final location
|
||||||
|
if [[ "$output" == /usr/* ]]; then
|
||||||
|
sudo mv "$temp_file" "$output"
|
||||||
|
sudo chmod +x "$output"
|
||||||
|
else
|
||||||
|
mv "$temp_file" "$output"
|
||||||
|
chmod +x "$output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# macOS Host Mode
|
# macOS Host Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -87,42 +195,87 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
# VM name from argument or default
|
# VM name from argument or default
|
||||||
VM_NAME="${1:-dev-sandbox}"
|
VM_NAME="${1:-dev-sandbox}"
|
||||||
|
|
||||||
|
# Security: Validate VM name (Issue #6)
|
||||||
|
if ! validate_vm_name "$VM_NAME"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Locate config.env relative to this script
|
# Locate config.env relative to this script
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
CONFIG_FILE="$SCRIPT_DIR/config.env"
|
CONFIG_FILE="$SCRIPT_DIR/config.env"
|
||||||
|
|
||||||
|
# Security: Load config safely without executing code (Issue #2)
|
||||||
|
# Instead of sourcing, parse key=value pairs manually
|
||||||
|
load_config_safely() {
|
||||||
|
local config_file="$1"
|
||||||
|
while IFS='=' read -r key value || [ -n "$key" ]; do
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
# Remove leading/trailing whitespace from key
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
# Remove surrounding quotes from value
|
||||||
|
value=$(echo "$value" | sed 's/^"//;s/"$//;s/^'"'"'//;s/'"'"'$//')
|
||||||
|
case "$key" in
|
||||||
|
GIT_NAME) GIT_NAME="$value" ;;
|
||||||
|
GIT_EMAIL) GIT_EMAIL="$value" ;;
|
||||||
|
# VNC_PASSWORD intentionally not stored - prompted each time for security
|
||||||
|
esac
|
||||||
|
done < "$config_file"
|
||||||
|
}
|
||||||
|
|
||||||
# Create or load config
|
# Create or load config
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
source "$CONFIG_FILE"
|
load_config_safely "$CONFIG_FILE"
|
||||||
echo -e "${YELLOW}Using saved config from: $CONFIG_FILE${NC}"
|
echo -e "${YELLOW}Using saved config from: $CONFIG_FILE${NC}"
|
||||||
echo " Name: $GIT_NAME"
|
echo " Name: $GIT_NAME"
|
||||||
echo " Email: $GIT_EMAIL"
|
echo " Email: $GIT_EMAIL"
|
||||||
echo " VNC Pass: ******"
|
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
echo "First run — creating config file for shared credentials."
|
echo "First run — creating config file for shared credentials."
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Enter your name for git config: " GIT_NAME
|
|
||||||
read -rp "Enter your email for git config: " GIT_EMAIL
|
# Security: Validate git name (Issue #5, #10, #11)
|
||||||
while true; do
|
while true; do
|
||||||
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
|
read -rp "Git commit author name: " GIT_NAME
|
||||||
echo
|
if [ -z "$GIT_NAME" ]; then
|
||||||
if [ ${#VNC_PASSWORD} -ge 6 ]; then
|
echo "Name is required."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ ${#GIT_NAME} -gt 256 ]; then
|
||||||
|
echo "Name must be 256 characters or less."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if validate_safe_input "$GIT_NAME" "Name"; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
echo "Password must be at least 6 characters."
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
|
# Security: Validate git email (Issue #5, #10, #11)
|
||||||
log_error "Name and email are required."
|
while true; do
|
||||||
exit 1
|
read -rp "Git commit author email: " GIT_EMAIL
|
||||||
|
if [ -z "$GIT_EMAIL" ]; then
|
||||||
|
echo "Email is required."
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
|
if [ ${#GIT_EMAIL} -gt 256 ]; then
|
||||||
|
echo "Email must be 256 characters or less."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! [[ "$GIT_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
|
||||||
|
echo "Please enter a valid email address."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if validate_safe_input "$GIT_EMAIL" "Email"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
cat > "$CONFIG_FILE" <<EOF
|
# Security: Write config with proper escaping (Issue #10)
|
||||||
GIT_NAME="$GIT_NAME"
|
# VNC password is NOT stored - will be prompted when VNC is selected
|
||||||
GIT_EMAIL="$GIT_EMAIL"
|
{
|
||||||
VNC_PASSWORD="$VNC_PASSWORD"
|
printf 'GIT_NAME="%s"\n' "$GIT_NAME"
|
||||||
EOF
|
printf 'GIT_EMAIL="%s"\n' "$GIT_EMAIL"
|
||||||
|
} > "$CONFIG_FILE"
|
||||||
chmod 600 "$CONFIG_FILE"
|
chmod 600 "$CONFIG_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Config saved to: $CONFIG_FILE"
|
echo "Config saved to: $CONFIG_FILE"
|
||||||
|
|
@ -131,30 +284,34 @@ EOF
|
||||||
|
|
||||||
# ---- Interactive component selection ----
|
# ---- Interactive component selection ----
|
||||||
# Component IDs and descriptions
|
# Component IDs and descriptions
|
||||||
COMP_IDS=(postgresql mise node erlang chromium vnc ollama claude playwright plugins)
|
COMP_IDS=(postgresql mise node python erlang chromium vnc ollama claude opencode playwright plugins)
|
||||||
COMP_NAMES=(
|
COMP_NAMES=(
|
||||||
"PostgreSQL"
|
"PostgreSQL"
|
||||||
"mise"
|
"mise"
|
||||||
"Node.js (LTS)"
|
"Node.js (LTS)"
|
||||||
|
"Python (latest)"
|
||||||
"Erlang/Elixir"
|
"Erlang/Elixir"
|
||||||
"Chromium"
|
"Chromium"
|
||||||
"VNC + XFCE"
|
"VNC + XFCE"
|
||||||
"Ollama"
|
"Ollama"
|
||||||
"Claude Code"
|
"Claude Code"
|
||||||
|
"OpenCode"
|
||||||
"Playwright"
|
"Playwright"
|
||||||
"Claude Plugins"
|
"Claude Plugins + Tidewave"
|
||||||
)
|
)
|
||||||
COMP_DESCS=(
|
COMP_DESCS=(
|
||||||
"Database server for local development"
|
"Database server for local development"
|
||||||
"Version manager for Node.js, Erlang, Elixir"
|
"Version manager for Node.js, Python, Erlang, Elixir"
|
||||||
"JavaScript runtime (required for Claude Code)"
|
"JavaScript runtime (required for Claude/OpenCode)"
|
||||||
|
"Python runtime managed by mise"
|
||||||
"BEAM VM + Elixir functional language"
|
"BEAM VM + Elixir functional language"
|
||||||
"Browser for automation and testing"
|
"Browser for automation and testing"
|
||||||
"Remote desktop for browser-based login flows"
|
"Remote desktop for browser-based login flows"
|
||||||
"Local LLM runner for offline inference"
|
"Local LLM runner for offline inference"
|
||||||
"AI coding assistant CLI from Anthropic"
|
"AI coding assistant CLI from Anthropic"
|
||||||
|
"Open-source AI coding assistant (multi-provider)"
|
||||||
"Browser testing and automation framework"
|
"Browser testing and automation framework"
|
||||||
"Code review, feature dev, browser automation plugins"
|
"Code review, Tidewave MCP, browser automation"
|
||||||
)
|
)
|
||||||
|
|
||||||
# All selected by default
|
# All selected by default
|
||||||
|
|
@ -237,12 +394,12 @@ EOF
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Build SKIP env var exports for unselected components
|
# Build list of skipped component IDs (space-separated, safe characters only)
|
||||||
SKIP_EXPORTS=""
|
# Security: Only hardcoded component IDs from COMP_IDS are used, no user input
|
||||||
|
SKIP_LIST=""
|
||||||
for ((i=0; i<COMP_COUNT; i++)); do
|
for ((i=0; i<COMP_COUNT; i++)); do
|
||||||
if [ "${COMP_SELECTED[$i]}" = "0" ]; then
|
if [ "${COMP_SELECTED[$i]}" = "0" ]; then
|
||||||
UPPER_ID=$(echo "${COMP_IDS[$i]}" | tr '[:lower:]' '[:upper:]')
|
SKIP_LIST="${SKIP_LIST}${COMP_IDS[$i]} "
|
||||||
SKIP_EXPORTS="${SKIP_EXPORTS}export SKIP_${UPPER_ID}=1; "
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -260,6 +417,27 @@ EOF
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Security: Prompt for VNC password only when VNC is selected (Issue #4)
|
||||||
|
# Password is never stored to disk - prompted fresh each time
|
||||||
|
VNC_PASSWORD=""
|
||||||
|
VNC_INDEX=-1
|
||||||
|
for ((i=0; i<COMP_COUNT; i++)); do
|
||||||
|
if [ "${COMP_IDS[$i]}" = "vnc" ]; then
|
||||||
|
VNC_INDEX=$i
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$VNC_INDEX" -ge 0 ] && [ "${COMP_SELECTED[$VNC_INDEX]}" = "1" ]; then
|
||||||
|
while true; do
|
||||||
|
read -rsp "Enter VNC password (6-64 chars): " VNC_PASSWORD
|
||||||
|
echo
|
||||||
|
if validate_vnc_password "$VNC_PASSWORD"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if VM already exists
|
# Check if VM already exists
|
||||||
if orb list 2>/dev/null | grep -qw "$VM_NAME"; then
|
if orb list 2>/dev/null | grep -qw "$VM_NAME"; then
|
||||||
log_error "VM '$VM_NAME' already exists."
|
log_error "VM '$VM_NAME' already exists."
|
||||||
|
|
@ -289,12 +467,32 @@ EOF
|
||||||
orb push -m "$VM_NAME" "$SCRIPT_DIR/setup_env.sh" /tmp/setup_env.sh
|
orb push -m "$VM_NAME" "$SCRIPT_DIR/setup_env.sh" /tmp/setup_env.sh
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [ -n "$SKIP_EXPORTS" ]; then
|
if [ -n "$SKIP_LIST" ]; then
|
||||||
echo -e "${YELLOW}Skipping:${NC} $SKIP_EXPORTS"
|
echo -e "${YELLOW}Skipping:${NC} $SKIP_LIST"
|
||||||
fi
|
fi
|
||||||
echo -e "${YELLOW}Running provisioning inside VM (this will take a while)...${NC}"
|
echo -e "${YELLOW}Running provisioning inside VM (this will take a while)...${NC}"
|
||||||
echo ""
|
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"
|
|
||||||
|
# Security: Use base64 encoding to safely pass all values to VM
|
||||||
|
# This prevents any shell injection through special characters
|
||||||
|
GIT_NAME_B64=$(printf '%s' "$GIT_NAME" | base64)
|
||||||
|
GIT_EMAIL_B64=$(printf '%s' "$GIT_EMAIL" | base64)
|
||||||
|
VNC_PASSWORD_B64=$(printf '%s' "$VNC_PASSWORD" | base64)
|
||||||
|
SKIP_LIST_B64=$(printf '%s' "$SKIP_LIST" | base64)
|
||||||
|
|
||||||
|
# Security: All user-controlled values are base64-encoded before passing to VM
|
||||||
|
# The decode script sets SKIP_* env vars from the safe SKIP_LIST
|
||||||
|
orb run -m "$VM_NAME" bash -c "
|
||||||
|
set -e
|
||||||
|
export GIT_NAME=\$(echo '$GIT_NAME_B64' | base64 -d) || { echo 'Failed to decode GIT_NAME'; exit 1; }
|
||||||
|
export GIT_EMAIL=\$(echo '$GIT_EMAIL_B64' | base64 -d) || { echo 'Failed to decode GIT_EMAIL'; exit 1; }
|
||||||
|
export VNC_PASSWORD=\$(echo '$VNC_PASSWORD_B64' | base64 -d) || { echo 'Failed to decode VNC_PASSWORD'; exit 1; }
|
||||||
|
for comp in \$(echo '$SKIP_LIST_B64' | base64 -d); do
|
||||||
|
upper=\$(echo \"\$comp\" | tr '[:lower:]' '[:upper:]')
|
||||||
|
export \"SKIP_\${upper}=1\"
|
||||||
|
done
|
||||||
|
bash /tmp/setup_env.sh --non-interactive
|
||||||
|
"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}============================================================================${NC}"
|
echo -e "${GREEN}============================================================================${NC}"
|
||||||
|
|
@ -326,9 +524,18 @@ fi
|
||||||
# Linux VM Mode (everything below runs inside the VM)
|
# Linux VM Mode (everything below runs inside the VM)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
# Security: Create log file with restrictive permissions (Issue #13)
|
||||||
LOG_FILE="/tmp/setup_env_$(date +%Y%m%d_%H%M%S).log"
|
LOG_FILE="/tmp/setup_env_$(date +%Y%m%d_%H%M%S).log"
|
||||||
LOG_DIR="/tmp/setup_env_steps_$$"
|
touch "$LOG_FILE"
|
||||||
mkdir -p "$LOG_DIR"
|
chmod 600 "$LOG_FILE"
|
||||||
|
|
||||||
|
# Security: Use mktemp for unpredictable temp directory (Issue #8, #9)
|
||||||
|
LOG_DIR=$(mktemp -d "/tmp/setup_env_steps.XXXXXXXXXX")
|
||||||
|
if [ ! -d "$LOG_DIR" ]; then
|
||||||
|
log_error "Failed to create secure temp directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod 700 "$LOG_DIR"
|
||||||
|
|
||||||
# Prevent mise from reading host config via OrbStack mount (/Users/john/.mise.toml)
|
# 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_GLOBAL_CONFIG_FILE="$HOME/.config/mise/config.toml"
|
||||||
|
|
@ -629,7 +836,7 @@ cleanup() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
# Note: trap is set later to include both cleanup and stop_sudo_keepalive
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Step functions (each is a self-contained install)
|
# Step functions (each is a self-contained install)
|
||||||
|
|
@ -658,7 +865,12 @@ install_postgresql() {
|
||||||
|
|
||||||
[ -z "$PG_HBA" ] || [ -z "$PG_CONF" ] && return 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"
|
# Security: Use 'peer' for local connections (authenticates via OS user)
|
||||||
|
# This is more secure than 'trust' while still convenient for development
|
||||||
|
# Users connect via: psql -U $USER dbname (no password needed if OS user matches DB user)
|
||||||
|
sudo sed -i 's/^local\s\+all\s\+all\s\+peer$/local all all peer/' "$PG_HBA"
|
||||||
|
# For TCP localhost, keep scram-sha-256 (requires password) instead of trust
|
||||||
|
# This prevents any local process from accessing the DB without credentials
|
||||||
|
|
||||||
if ! sudo grep -q "# OrbStack host access" "$PG_HBA"; then
|
if ! sudo grep -q "# OrbStack host access" "$PG_HBA"; then
|
||||||
echo "# OrbStack host access" | sudo tee -a "$PG_HBA" > /dev/null
|
echo "# OrbStack host access" | sudo tee -a "$PG_HBA" > /dev/null
|
||||||
|
|
@ -678,7 +890,30 @@ install_mise() {
|
||||||
echo "mise already installed"
|
echo "mise already installed"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
curl -fsSL https://mise.run | sh
|
|
||||||
|
# Security: Download script first, validate, then execute (Issue #3)
|
||||||
|
# This allows for inspection and avoids direct pipe to shell
|
||||||
|
local mise_script
|
||||||
|
mise_script=$(mktemp)
|
||||||
|
curl -fsSL https://mise.run -o "$mise_script"
|
||||||
|
|
||||||
|
# Verify it's a shell script with multiple checks
|
||||||
|
if ! head -1 "$mise_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
|
||||||
|
log_error "Downloaded mise script doesn't have a valid shell shebang"
|
||||||
|
rm -f "$mise_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Check file size is reasonable (not empty, not huge)
|
||||||
|
local script_size
|
||||||
|
script_size=$(wc -c < "$mise_script")
|
||||||
|
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
|
||||||
|
log_error "Downloaded mise script has suspicious size: $script_size bytes"
|
||||||
|
rm -f "$mise_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sh "$mise_script"
|
||||||
|
rm -f "$mise_script"
|
||||||
|
|
||||||
# Add to bashrc
|
# Add to bashrc
|
||||||
if ! grep -q 'mise activate bash' ~/.bashrc; then
|
if ! grep -q 'mise activate bash' ~/.bashrc; then
|
||||||
|
|
@ -698,6 +933,15 @@ install_node() {
|
||||||
export PATH="$HOME/.local/share/mise/shims:$PATH"
|
export PATH="$HOME/.local/share/mise/shims:$PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_python() {
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
eval "$(~/.local/bin/mise activate bash)"
|
||||||
|
mise use -g python@latest
|
||||||
|
export PATH="$HOME/.local/share/mise/shims:$PATH"
|
||||||
|
# Ensure pip is available via mise python
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
}
|
||||||
|
|
||||||
install_erlang() {
|
install_erlang() {
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
eval "$(~/.local/bin/mise activate bash)"
|
eval "$(~/.local/bin/mise activate bash)"
|
||||||
|
|
@ -726,11 +970,11 @@ install_chromium() {
|
||||||
sudo apt-get install -y -q chromium --no-install-recommends 2>/dev/null || true
|
sudo apt-get install -y -q chromium --no-install-recommends 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Symlink if installed
|
# Symlink if installed (check for existing file, not just symlink)
|
||||||
local bin=""
|
local bin=""
|
||||||
command_exists chromium-browser && bin=$(which chromium-browser)
|
command_exists chromium-browser && bin=$(which chromium-browser)
|
||||||
command_exists chromium && bin=$(which chromium)
|
command_exists chromium && bin=$(which chromium)
|
||||||
if [ -n "$bin" ] && [ ! -L /usr/bin/google-chrome ]; then
|
if [ -n "$bin" ] && [ ! -e /usr/bin/google-chrome ]; then
|
||||||
sudo ln -sf "$bin" /usr/bin/google-chrome
|
sudo ln -sf "$bin" /usr/bin/google-chrome
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -738,8 +982,17 @@ install_chromium() {
|
||||||
install_vnc() {
|
install_vnc() {
|
||||||
sudo apt-get install -y -q tigervnc-standalone-server xfce4 xfce4-terminal dbus-x11
|
sudo apt-get install -y -q tigervnc-standalone-server xfce4 xfce4-terminal dbus-x11
|
||||||
|
|
||||||
|
# Create .vnc directory with secure permissions
|
||||||
mkdir -p ~/.vnc
|
mkdir -p ~/.vnc
|
||||||
|
chmod 700 ~/.vnc
|
||||||
|
|
||||||
|
# Security: Create password file atomically with correct permissions
|
||||||
|
# Use umask to ensure file is created with 600 permissions from the start
|
||||||
|
(
|
||||||
|
umask 077
|
||||||
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
|
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
|
||||||
|
)
|
||||||
|
# Ensure permissions are correct (belt and suspenders)
|
||||||
chmod 600 ~/.vnc/passwd
|
chmod 600 ~/.vnc/passwd
|
||||||
|
|
||||||
cat > ~/.vnc/xstartup <<'XEOF'
|
cat > ~/.vnc/xstartup <<'XEOF'
|
||||||
|
|
@ -754,6 +1007,10 @@ XEOF
|
||||||
|
|
||||||
cat > ~/bin/vnc-start <<'VEOF'
|
cat > ~/bin/vnc-start <<'VEOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Security note: VNC listens on all interfaces (-localhost no) to allow
|
||||||
|
# connections from the macOS host via OrbStack's network. The VM is only
|
||||||
|
# accessible from the host machine, not external networks.
|
||||||
|
# VNC is password-protected (set during setup).
|
||||||
VNC_HOST="$(hostname -s).orb.local"
|
VNC_HOST="$(hostname -s).orb.local"
|
||||||
if pgrep -f "Xtigervnc :1" > /dev/null 2>&1; then
|
if pgrep -f "Xtigervnc :1" > /dev/null 2>&1; then
|
||||||
echo "VNC is already running on display :1"
|
echo "VNC is already running on display :1"
|
||||||
|
|
@ -782,7 +1039,29 @@ install_ollama() {
|
||||||
echo "Ollama already installed"
|
echo "Ollama already installed"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
curl -fsSL https://ollama.com/install.sh | sudo sh
|
|
||||||
|
# Security: Download script first, validate, then execute with sudo (Issue #3)
|
||||||
|
local ollama_script
|
||||||
|
ollama_script=$(mktemp)
|
||||||
|
curl -fsSL https://ollama.com/install.sh -o "$ollama_script"
|
||||||
|
|
||||||
|
# Verify it's a shell script with multiple checks
|
||||||
|
if ! head -1 "$ollama_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
|
||||||
|
log_error "Downloaded Ollama script doesn't have a valid shell shebang"
|
||||||
|
rm -f "$ollama_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Check file size is reasonable (not empty, not huge)
|
||||||
|
local script_size
|
||||||
|
script_size=$(wc -c < "$ollama_script")
|
||||||
|
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
|
||||||
|
log_error "Downloaded Ollama script has suspicious size: $script_size bytes"
|
||||||
|
rm -f "$ollama_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo sh "$ollama_script"
|
||||||
|
rm -f "$ollama_script"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_claude() {
|
install_claude() {
|
||||||
|
|
@ -792,19 +1071,78 @@ install_claude() {
|
||||||
|
|
||||||
install_playwright() {
|
install_playwright() {
|
||||||
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
||||||
npx --yes playwright install --with-deps chromium
|
npm install -g playwright
|
||||||
|
playwright install --with-deps chromium
|
||||||
|
|
||||||
# Symlink Playwright's Chromium if no system one
|
# Symlink Playwright's Chromium if no system one (check for existing files)
|
||||||
if ! command_exists chromium-browser && ! command_exists chromium; then
|
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)
|
local pw_chrome
|
||||||
if [ -n "$pw_chrome" ]; then
|
pw_chrome=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1)
|
||||||
sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
|
# Security: Validate the found path before creating symlinks
|
||||||
sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
|
# - Must not be empty
|
||||||
|
# - Must be an executable file
|
||||||
|
# - Must be within the expected playwright cache directory
|
||||||
|
if [ -n "$pw_chrome" ] && [ -x "$pw_chrome" ] && [[ "$pw_chrome" == "$HOME/.cache/ms-playwright"* ]]; then
|
||||||
|
[ ! -e /usr/local/bin/chromium ] && sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
|
||||||
|
[ ! -e /usr/local/bin/google-chrome ] && sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
|
||||||
echo "Using Playwright's bundled Chromium"
|
echo "Using Playwright's bundled Chromium"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_opencode() {
|
||||||
|
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
||||||
|
if command_exists opencode; then
|
||||||
|
echo "OpenCode already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
npm install -g opencode-ai
|
||||||
|
}
|
||||||
|
|
||||||
|
install_tidewave() {
|
||||||
|
if command_exists tidewave; then
|
||||||
|
echo "Tidewave already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download Tidewave CLI binary from tidewave_app releases
|
||||||
|
# Binary naming: tidewave-cli-<arch>-unknown-linux-musl
|
||||||
|
local machine_arch
|
||||||
|
machine_arch=$(uname -m)
|
||||||
|
local tidewave_arch=""
|
||||||
|
case "$machine_arch" in
|
||||||
|
x86_64) tidewave_arch="x86_64" ;;
|
||||||
|
aarch64) tidewave_arch="aarch64" ;;
|
||||||
|
*)
|
||||||
|
log_error "Unsupported architecture for Tidewave: $machine_arch"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Get latest release URL from GitHub (tidewave_app repo)
|
||||||
|
local release_url="https://github.com/tidewave-ai/tidewave_app/releases/latest/download/tidewave-cli-${tidewave_arch}-unknown-linux-musl"
|
||||||
|
|
||||||
|
# Security: Download to temp file first, validate, then install
|
||||||
|
local temp_file
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
if curl -fsSL "$release_url" -o "$temp_file"; then
|
||||||
|
# Verify it's an executable (ELF binary)
|
||||||
|
if file "$temp_file" | grep -q "ELF"; then
|
||||||
|
sudo mv "$temp_file" /usr/local/bin/tidewave
|
||||||
|
sudo chmod +x /usr/local/bin/tidewave
|
||||||
|
echo "Tidewave CLI installed"
|
||||||
|
else
|
||||||
|
log_error "Downloaded tidewave is not a valid binary"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Failed to download Tidewave CLI"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
install_plugins() {
|
install_plugins() {
|
||||||
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
||||||
mkdir -p ~/.config/claude
|
mkdir -p ~/.config/claude
|
||||||
|
|
@ -824,6 +1162,12 @@ install_plugins() {
|
||||||
echo "Adding MCP servers..."
|
echo "Adding MCP servers..."
|
||||||
claude mcp add playwright --scope user -- npx @anthropic-ai/mcp-server-playwright 2>/dev/null || true
|
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
|
claude mcp add superpowers-chrome --scope user -- npx github:obra/superpowers-chrome --headless 2>/dev/null || true
|
||||||
|
|
||||||
|
# Note: Tidewave MCP is configured per-project when running a Phoenix app
|
||||||
|
# Use: claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp
|
||||||
|
echo ""
|
||||||
|
echo "Tidewave MCP: Add per-project with your Phoenix app running:"
|
||||||
|
echo " claude mcp add --transport http tidewave http://localhost:PORT/tidewave/mcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_base_deps() {
|
install_base_deps() {
|
||||||
|
|
@ -838,20 +1182,55 @@ install_base_deps() {
|
||||||
python3 python3-pip python3-venv
|
python3 python3-pip python3-venv
|
||||||
|
|
||||||
# fd symlink (fd-find installs as 'fdfind', symlink to 'fd')
|
# fd symlink (fd-find installs as 'fdfind', symlink to 'fd')
|
||||||
if [ ! -L /usr/local/bin/fd ] && command_exists fdfind; then
|
# Check for existing file, not just symlink
|
||||||
|
if [ ! -e /usr/local/bin/fd ] && command_exists fdfind; then
|
||||||
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
|
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# yq
|
# yq - with architecture detection (always latest)
|
||||||
if ! command_exists yq; then
|
if ! command_exists yq; then
|
||||||
sudo curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" -o /usr/local/bin/yq
|
local yq_arch
|
||||||
sudo chmod +x /usr/local/bin/yq
|
local arch
|
||||||
|
arch=$(detect_architecture)
|
||||||
|
case "$arch" in
|
||||||
|
amd64) yq_arch="amd64" ;;
|
||||||
|
arm64) yq_arch="arm64" ;;
|
||||||
|
esac
|
||||||
|
local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${yq_arch}"
|
||||||
|
download_verified_binary "$yq_url" "/usr/local/bin/yq"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# watchexec
|
# watchexec - install via cargo (always latest)
|
||||||
if ! command_exists watchexec; then
|
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" | \
|
if command_exists cargo; then
|
||||||
sudo tar -xJ --strip-components=1 -C /usr/local/bin/ --wildcards '*/watchexec'
|
cargo install watchexec-cli
|
||||||
|
else
|
||||||
|
# Security: Download rustup script first, validate, then execute (Issue #3)
|
||||||
|
# Same pattern as mise/ollama - never pipe directly to shell
|
||||||
|
local rustup_script
|
||||||
|
rustup_script=$(mktemp)
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o "$rustup_script"
|
||||||
|
|
||||||
|
# Verify it's a shell script
|
||||||
|
if ! head -1 "$rustup_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
|
||||||
|
log_error "Downloaded rustup script doesn't have a valid shell shebang"
|
||||||
|
rm -f "$rustup_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Check file size is reasonable (not empty, not huge)
|
||||||
|
local script_size
|
||||||
|
script_size=$(wc -c < "$rustup_script")
|
||||||
|
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 2000000 ]; then
|
||||||
|
log_error "Downloaded rustup script has suspicious size: $script_size bytes"
|
||||||
|
rm -f "$rustup_script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sh "$rustup_script" -y
|
||||||
|
rm -f "$rustup_script"
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
cargo install watchexec-cli
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# direnv hook
|
# direnv hook
|
||||||
|
|
@ -875,35 +1254,55 @@ if ! command_exists sudo; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Security: Sudo keepalive - prevent sudo timeout during long operations
|
||||||
|
# Runs in background and refreshes sudo timestamp every 50 seconds
|
||||||
|
SUDO_KEEPALIVE_PID=""
|
||||||
|
start_sudo_keepalive() {
|
||||||
|
# In non-interactive mode (VM), sudo should be passwordless - skip keepalive
|
||||||
|
if [ "$NON_INTERACTIVE" = true ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate sudo access first (allow password prompt to show)
|
||||||
|
if ! sudo -v; then
|
||||||
|
log_error "Failed to obtain sudo privileges"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start background process to refresh sudo timestamp
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
sudo -n true 2>/dev/null
|
||||||
|
sleep 50
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
SUDO_KEEPALIVE_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_sudo_keepalive() {
|
||||||
|
if [ -n "$SUDO_KEEPALIVE_PID" ] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then
|
||||||
|
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||||
|
wait "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure keepalive is stopped on exit
|
||||||
|
# Update trap to include both cleanup functions
|
||||||
|
trap 'stop_sudo_keepalive; cleanup' EXIT
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Git configuration prompts
|
# Git configuration prompts (VM-side, non-interactive mode only)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
if [ "$NON_INTERACTIVE" = true ]; then
|
if [ "$NON_INTERACTIVE" = true ]; then
|
||||||
if [ -z "${GIT_NAME:-}" ] || [ -z "${GIT_EMAIL:-}" ]; then
|
if [ -z "${GIT_NAME:-}" ] || [ -z "${GIT_EMAIL:-}" ]; then
|
||||||
log_error "In non-interactive mode, GIT_NAME and GIT_EMAIL env vars are required."
|
log_error "In non-interactive mode, GIT_NAME and GIT_EMAIL env vars are required."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -z "${VNC_PASSWORD:-}" ]; then
|
# Only require VNC_PASSWORD if VNC is being installed
|
||||||
log_error "In non-interactive mode, VNC_PASSWORD env var is required (min 6 chars)."
|
if [ -z "${SKIP_VNC:-}" ] && [ -z "${VNC_PASSWORD:-}" ]; then
|
||||||
|
log_error "In non-interactive mode, VNC_PASSWORD env var is required when VNC is selected (min 6 chars)."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -913,12 +1312,15 @@ register_step "base" "System packages (git, build tools, curl)"
|
||||||
register_step "postgresql" "PostgreSQL database server"
|
register_step "postgresql" "PostgreSQL database server"
|
||||||
register_step "mise" "mise version manager"
|
register_step "mise" "mise version manager"
|
||||||
register_step "node" "Node.js LTS runtime"
|
register_step "node" "Node.js LTS runtime"
|
||||||
|
register_step "python" "Python (mise-managed)"
|
||||||
register_step "erlang" "Erlang/OTP VM"
|
register_step "erlang" "Erlang/OTP VM"
|
||||||
register_step "elixir" "Elixir language"
|
register_step "elixir" "Elixir language"
|
||||||
register_step "chromium" "Chromium browser"
|
register_step "chromium" "Chromium browser"
|
||||||
register_step "vnc" "VNC server + XFCE desktop"
|
register_step "vnc" "VNC server + XFCE desktop"
|
||||||
register_step "ollama" "Ollama local LLM runner"
|
register_step "ollama" "Ollama local LLM runner"
|
||||||
register_step "claude" "Claude Code CLI"
|
register_step "claude" "Claude Code CLI"
|
||||||
|
register_step "opencode" "OpenCode CLI"
|
||||||
|
register_step "tidewave" "Tidewave CLI (Elixir MCP)"
|
||||||
register_step "playwright" "Playwright browser automation"
|
register_step "playwright" "Playwright browser automation"
|
||||||
register_step "plugins" "Claude Code plugins"
|
register_step "plugins" "Claude Code plugins"
|
||||||
|
|
||||||
|
|
@ -926,21 +1328,29 @@ register_step "plugins" "Claude Code plugins"
|
||||||
should_skip "postgresql" && skip_step "postgresql"
|
should_skip "postgresql" && skip_step "postgresql"
|
||||||
should_skip "mise" && skip_step "mise"
|
should_skip "mise" && skip_step "mise"
|
||||||
should_skip "node" && skip_step "node"
|
should_skip "node" && skip_step "node"
|
||||||
|
should_skip "python" && skip_step "python"
|
||||||
should_skip "erlang" && skip_step "erlang"
|
should_skip "erlang" && skip_step "erlang"
|
||||||
should_skip "chromium" && skip_step "chromium"
|
should_skip "chromium" && skip_step "chromium"
|
||||||
should_skip "vnc" && skip_step "vnc"
|
should_skip "vnc" && skip_step "vnc"
|
||||||
should_skip "ollama" && skip_step "ollama"
|
should_skip "ollama" && skip_step "ollama"
|
||||||
should_skip "claude" && skip_step "claude"
|
should_skip "claude" && skip_step "claude"
|
||||||
|
should_skip "opencode" && skip_step "opencode"
|
||||||
should_skip "playwright" && skip_step "playwright"
|
should_skip "playwright" && skip_step "playwright"
|
||||||
should_skip "plugins" && skip_step "plugins"
|
should_skip "plugins" && skip_step "plugins"
|
||||||
|
|
||||||
# Elixir skipped if Erlang skipped
|
# Elixir skipped if Erlang skipped
|
||||||
[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "elixir"
|
[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "elixir"
|
||||||
|
|
||||||
# Claude/Playwright skipped if Node skipped
|
# Tidewave skipped if Erlang skipped (it's for Phoenix/Elixir projects)
|
||||||
|
[ "${STEP_STATUS[erlang]}" = "skipped" ] && skip_step "tidewave"
|
||||||
|
|
||||||
|
# Python skipped if mise skipped
|
||||||
|
[ "${STEP_STATUS[mise]}" = "skipped" ] && skip_step "python"
|
||||||
|
|
||||||
|
# Claude/OpenCode/Playwright skipped if Node skipped
|
||||||
if [ "${STEP_STATUS[node]}" = "skipped" ]; then
|
if [ "${STEP_STATUS[node]}" = "skipped" ]; then
|
||||||
[ "${STEP_STATUS[mise]}" = "skipped" ] || true # mise can still install
|
|
||||||
skip_step "claude"
|
skip_step "claude"
|
||||||
|
skip_step "opencode"
|
||||||
skip_step "playwright"
|
skip_step "playwright"
|
||||||
skip_step "plugins"
|
skip_step "plugins"
|
||||||
fi
|
fi
|
||||||
|
|
@ -956,6 +1366,11 @@ echo ""
|
||||||
draw_dashboard
|
draw_dashboard
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Start sudo keepalive to prevent timeout during long operations
|
||||||
|
# ============================================================================
|
||||||
|
start_sudo_keepalive
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Phase 1: Base dependencies (sequential, everything depends on this)
|
# Phase 1: Base dependencies (sequential, everything depends on this)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -974,19 +1389,23 @@ run_step_sync "base" install_base_deps || true
|
||||||
wait_for_running_steps
|
wait_for_running_steps
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Phase 3: Node + Erlang (parallel, both need mise)
|
# Phase 3: Node + Python + Erlang (parallel, all need mise)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
if [ "${STEP_STATUS[mise]}" = "done" ]; then
|
if [ "${STEP_STATUS[mise]}" = "done" ]; then
|
||||||
[ "${STEP_STATUS[node]}" != "skipped" ] && start_step "node" install_node
|
[ "${STEP_STATUS[node]}" != "skipped" ] && start_step "node" install_node
|
||||||
|
[ "${STEP_STATUS[python]}" != "skipped" ] && start_step "python" install_python
|
||||||
[ "${STEP_STATUS[erlang]}" != "skipped" ] && start_step "erlang" install_erlang
|
[ "${STEP_STATUS[erlang]}" != "skipped" ] && start_step "erlang" install_erlang
|
||||||
wait_for_running_steps
|
wait_for_running_steps
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Phase 4: Elixir + Claude + Playwright (Elixir needs Erlang, others need Node)
|
# Phase 4: Elixir + Claude + OpenCode + Playwright + Tidewave
|
||||||
|
# (Elixir/Tidewave need Erlang, others need Node)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[elixir]}" != "skipped" ] && start_step "elixir" install_elixir
|
[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[elixir]}" != "skipped" ] && start_step "elixir" install_elixir
|
||||||
|
[ "${STEP_STATUS[erlang]}" = "done" ] && [ "${STEP_STATUS[tidewave]}" != "skipped" ] && start_step "tidewave" install_tidewave
|
||||||
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[claude]}" != "skipped" ] && start_step "claude" install_claude
|
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[claude]}" != "skipped" ] && start_step "claude" install_claude
|
||||||
|
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[opencode]}" != "skipped" ] && start_step "opencode" install_opencode
|
||||||
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[playwright]}" != "skipped" ] && start_step "playwright" install_playwright
|
[ "${STEP_STATUS[node]}" = "done" ] && [ "${STEP_STATUS[playwright]}" != "skipped" ] && start_step "playwright" install_playwright
|
||||||
|
|
||||||
wait_for_running_steps
|
wait_for_running_steps
|
||||||
|
|
@ -1061,6 +1480,12 @@ fi
|
||||||
if [ "${STEP_STATUS[claude]}" = "done" ]; then
|
if [ "${STEP_STATUS[claude]}" = "done" ]; then
|
||||||
echo -e "${CYAN}Claude:${NC} claude"
|
echo -e "${CYAN}Claude:${NC} claude"
|
||||||
fi
|
fi
|
||||||
|
if [ "${STEP_STATUS[opencode]}" = "done" ]; then
|
||||||
|
echo -e "${CYAN}OpenCode:${NC} opencode"
|
||||||
|
fi
|
||||||
|
if [ "${STEP_STATUS[tidewave]}" = "done" ]; then
|
||||||
|
echo -e "${CYAN}Tidewave:${NC} tidewave (run in Phoenix project dir)"
|
||||||
|
fi
|
||||||
if [ "${STEP_STATUS[ollama]}" = "done" ]; then
|
if [ "${STEP_STATUS[ollama]}" = "done" ]; then
|
||||||
echo -e "${CYAN}Ollama:${NC} ollama run llama3.2"
|
echo -e "${CYAN}Ollama:${NC} ollama run llama3.2"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
1076
setup_env_windows.ps1
Normal file
1076
setup_env_windows.ps1
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue