"20 minute script using bash" - A simple, powerful development environment setup that replaces complex configuration management with straightforward bash scripts.
This repository provides a modular, extensible development environment setup specifically adapted for macOS. It uses your personal dotfiles and Homebrew to create a complete development workspace in minutes.
- π― Philosophy & Goals
- π§ How The System Works
- π Repository Structure
- π Getting Started
- π οΈ Available Components
- βοΈ The Run System Explained
- π Dotfiles Integration
- ποΈ Customization Guide
- π Advanced Usage
- π Troubleshooting
- π€ Contributing
- Complex Setup Tools: No more Ansible, Chef, or overcomplicated configuration management
- One-Size-Fits-All: Generic setups that don't respect your personal workflow
- Platform Incompatibility: Linux-focused setups that don't work well on macOS
- Opaque Processes: Black-box installers where you don't know what's happening
- Simple Bash Scripts: Each component is a readable, modifiable bash script
- Modular Design: Install only what you need, when you need it
- Your Dotfiles: Uses YOUR configurations, not someone else's
- Transparent Process: Every action is visible and customizable
- macOS Native: Homebrew-based, respects macOS conventions
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β ./run βββββΆβ runs/* βββββΆβ Your System β
β (Orchestrator) β β (Components) β β (Configured) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
./runis the main orchestrator that discovers and executes component scriptsruns/*directory contains individual setup scripts for different tools- Your dotfiles are integrated seamlessly through the
dotfilescomponent - Everything is installed via Homebrew for consistency and macOS compatibility
Each script in runs/ is:
- Self-contained: Can be run independently
- Idempotent: Safe to run multiple times
- Conditional: Only installs if not already present
- Focused: Does one thing well
dev/
βββ README.md # This comprehensive guide
βββ SETUP.md # Quick start instructions
βββ main.go # Web server for remote setup
βββ go.mod/go.sum # Go dependencies
βββ Dockerfile # Container setup
βββ fly.toml # Deployment configuration
βββ run # π― Main orchestrator script
βββ init # Project initialization
βββ resources/
β βββ setup # Web-served setup script
β βββ luarocks # Lua package manager
βββ runs/ # π Component scripts directory
β βββ dotfiles # Your personal dotfiles setup
β βββ dev # Core development tools
β βββ neovim # Neovim editor setup
β βββ tmux # Terminal multiplexer
β βββ zsh # Shell configuration
β βββ docker # Container platform
β βββ node # JavaScript runtime
β βββ libs # Development libraries
β βββ i3 # Window management (yabai/skhd)
β βββ rofi # Application launcher (Raycast)
β βββ rust # Rust toolchain
β βββ eleven-tools # Additional utilities
β βββ emoji # Emoji support
β βββ gdb # Debugger setup
β βββ ghostty # Terminal emulator
β βββ keyboard # Keyboard configuration
β βββ odin # Odin programming language
β βββ opencode # VS Code setup
β βββ php # PHP development
β βββ zed # Zed editor
βββ tmux-sessionizer/ # Project session management
βββ env/ # Legacy environment files (unused)
- macOS (this setup is macOS-specific)
- Internet connection for downloading packages
- Your dotfiles repository ready
Ensure your dotfiles are properly structured:
~/.dotfilesmacos/
βββ scripts/bin/setup # Main setup script
βββ zsh/ # Zsh configuration
βββ tmux/ # Tmux configuration
βββ nvim/ # Neovim configuration
βββ git/ # Git configuration
βββ ... # Other tool configurationsClone your dotfiles if not already done:
git clone https://github.com/YOUR_USERNAME/dotfiles.git ~/.dotfilesmacosgit clone https://github.com/YOUR_USERNAME/dev ~/personal/dev
cd ~/personal/dev# Set the environment variable (required)
export DEV_ENV="$(pwd)"
# Run everything
./run
# Or run specific components
./run dotfiles neovim tmux
# Or see what would happen
./run --dryStart the web server:
go run main.goThen from any terminal:
curl -sSL http://localhost:8080 | bash./run dotfiles- Executes your
~/.dotfilesmacos/scripts/bin/setup - Stows your configurations using your existing system
- Installs your custom scripts to
~/.local/bin - This is where YOUR personality comes into the setup
./run devInstalls:
go- Go programming languagenode&npm- JavaScript runtime and package managerfzf- Fuzzy finder for terminalbtop- System monitorbat- Better cat with syntax highlightingtree- Directory structure visualizationripgrep- Fast text searchtldr- Simplified man pagesstylua- Lua code formatterffmpeg- Media processingghostty- Modern terminal emulatorraycast- Application launcherfont-jetbrains-mono-nerd-font- Developer font
./run neovimInstalls:
neovim- Modern Vim forklua- Lua scripting languageluarocks- Lua package manager- Creates
~/personal/projectsdirectory for your code
./run tmuxInstalls:
tmux- Terminal multiplexertpm- Tmux Plugin Manager- Sets up plugin directory at
~/.tmux/plugins/tpm
./run zshInstalls:
zshshell (if not present)- Changes default shell to zsh
- Installs Oh My Zsh framework
- Note: Your dotfiles will override with your zsh configuration
./run dockerInstalls:
- Docker Desktop (GUI option)
- docker-compose
- Alternative: Uncomment Colima lines for CLI-only Docker
./run nodeInstalls:
- Node.js and npm
- Sets up npm prefix to
~/.local/npm - Installs
n(Node version manager) - Installs latest LTS Node
- Deno runtime
./run rustInstalls:
- Rust toolchain via rustup
- Common Rust development tools
./run i3Installs:
yabai- Tiling window manager for macOSskhd- Simple hotkey daemon- Note: Replaces Linux i3 with macOS equivalents
./run rofiInstalls:
raycast- Modern application launcher for macOS- Note: Replaces Linux rofi with macOS equivalent
./run libsInstalls:
- Core development libraries
- Python 3.13
- Git, ripgrep, jq, tldr
- Raycast for screenshots
./run ghosttyInstalls:
- Ghostty terminal emulator
- Modern, fast terminal alternative
The ./run script is the heart of the system. Here's how it works:
#!/usr/bin/env bash
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Environment variable check
if [ -z "$DEV_ENV" ]; then
echo "env var DEV_ENV needs to be present"
exit 1
fi
# Command line argument parsing
grep=""
dry_run="0"
while [[ $# -gt 0 ]]; do
if [[ "$1" == "--dry" ]]; then
dry_run="1"
else
grep="$1"
fi
shift
done
# Find all executable scripts in runs/
runs_dir=`find $script_dir/runs -mindepth 1 -maxdepth 1 -executable`
# Execute matching scripts
for s in $runs_dir; do
if basename $s | grep -vq "$grep"; then
# Skip if doesn't match filter
continue
fi
# Execute the script
if [[ $dry_run == "0" ]]; then
$s
fi
doneexport DEV_ENV="$(pwd)"Why? This ensures scripts know where the repository is located and prevents accidental execution.
./run neovim # Only run neovim-related scripts
./run docker node # Run docker and node scripts
./run # Run all scriptsHow it works: Uses grep to match script names containing your filter terms.
./run --dry
./run --dry neovimPurpose: See what would be executed without actually running anything.
The system automatically finds all executable files in runs/ directory:
find $script_dir/runs -mindepth 1 -maxdepth 1 -executable#!/usr/bin/env bash
# Check if tool is already installed
if ! command -v my-tool &> /dev/null; then
echo "Installing my-tool..."
brew install my-tool
else
echo "my-tool already installed"
fi
# Additional configuration
echo "Configuring my-tool..."
# Your configuration here- Make it executable:
chmod +x runs/my-script - Check before installing: Use
command -vorbrew list - Provide feedback: Echo what's happening
- Handle errors: Exit gracefully on failures
- Be idempotent: Safe to run multiple times
#!/usr/bin/env bash
# runs/python-dev
echo "Setting up Python development environment..."
# Install Python via Homebrew
if ! brew list python@3.13 &> /dev/null; then
echo "Installing Python 3.13..."
brew install python@3.13
fi
# Install pipenv for dependency management
if ! command -v pipenv &> /dev/null; then
echo "Installing pipenv..."
brew install pipenv
fi
# Install common development packages
echo "Installing Python development packages..."
pip3 install --user black flake8 mypy pytest
# Create virtual environment directory
mkdir -p "$HOME/python-envs"
echo "Python development setup complete!"
echo "Create new projects with: pipenv install"The dotfiles run script acts as a bridge between this setup system and your personal configuration:
#!/usr/bin/env bash
# runs/dotfiles
# Ensure stow is installed
if ! command -v stow &> /dev/null; then
brew install stow
fi
# Navigate to your dotfiles and run setup
if [[ -d "$HOME/.dotfilesmacos" ]]; then
cd "$HOME/.dotfilesmacos"
# Use your existing setup script
if [[ -f "scripts/bin/setup" ]]; then
./scripts/bin/setup
else
# Fallback: manual stowing
for dir in zsh tmux nvim git alacritty kitty ghostty bat bpytop lsd wm zen; do
if [[ -d "$dir" ]]; then
stow -v "$dir"
fi
done
fi
fiYour existing dotfiles structure is respected:
~/.dotfilesmacos/
βββ scripts/bin/setup # β Called automatically
βββ zsh/.zshrc # β Stowed to ~/.zshrc
βββ tmux/.tmux.conf # β Stowed to ~/.tmux.conf
βββ nvim/.config/nvim/ # β Stowed to ~/.config/nvim/
βββ git/.gitconfig # β Stowed to ~/.gitconfig
βββ scripts/bin/ # β Copied to ~/.local/bin/
If your dotfiles have a different structure, modify runs/dotfiles:
#!/usr/bin/env bash
# Custom dotfiles integration
DOTFILES_DIR="$HOME/.config/dotfiles" # Your custom location
if [[ -d "$DOTFILES_DIR" ]]; then
cd "$DOTFILES_DIR"
# Your custom setup command
make install
# Or custom stowing
stow --target="$HOME" zsh tmux vim
fitouch runs/my-new-tool
chmod +x runs/my-new-tool#!/usr/bin/env bash
# runs/my-new-tool
echo "Setting up my-new-tool..."
# Install via Homebrew
if ! brew list my-new-tool &> /dev/null; then
brew install my-new-tool
fi
# Configuration
mkdir -p "$HOME/.config/my-new-tool"
cat > "$HOME/.config/my-new-tool/config.yaml" << EOF
setting1: value1
setting2: value2
EOF
echo "my-new-tool setup complete!"export DEV_ENV="$(pwd)"
./run --dry my-new-tool # Test without executing
./run my-new-tool # Actually run it# Edit runs/dev
vim runs/dev
# Add your favorite tools
brew install your-favorite-tool
brew install another-tool
# Remove tools you don't want
# (just comment out or delete the lines)You can create scripts that install related tools:
#!/usr/bin/env bash
# runs/web-dev
echo "Setting up web development tools..."
# Frontend tools
brew install node npm yarn
npm install -g create-react-app vue-cli @angular/cli
# CSS tools
npm install -g sass postcss-cli
# Build tools
npm install -g webpack webpack-cli parcel
echo "Web development setup complete!"Create different configurations for different contexts:
#!/usr/bin/env bash
# runs/work-setup
echo "Setting up work-specific tools..."
# Work-specific applications
brew install --cask slack microsoft-teams zoom
# Work development tools
brew install awscli terraform
# Company-specific configurations
git config --global user.email "work@company.com"
echo "Work setup complete!"Run different setups based on conditions:
# Only run if on work laptop
if [[ $(hostname) == *"work"* ]]; then
export DEV_ENV="$(pwd)"
./run work-setup
fi
# Only run if home directory exists
if [[ -d "$HOME/personal" ]]; then
export DEV_ENV="$(pwd)"
./run personal-setup
fi# .git/hooks/post-merge
#!/bin/bash
cd "$(git rev-parse --show-toplevel)"
export DEV_ENV="$(pwd)"
./run dotfiles # Re-sync dotfiles after pulling changes# Add to crontab for weekly updates
0 9 * * 1 cd ~/personal/dev && export DEV_ENV="$(pwd)" && ./run --dry# Add to any run script for debugging
set -x # Show commands as they execute
set -e # Exit on any error# Capture logs
export DEV_ENV="$(pwd)"
./run 2>&1 | tee setup.logFor independent tools, you can run them in parallel:
# Custom run script with parallel execution
export DEV_ENV="$(pwd)"
./run node &
./run rust &
./run docker &
wait # Wait for all background jobs to completeSome tools can cache downloads for faster re-runs:
# In your run script
CACHE_DIR="$HOME/.cache/dev-setup"
mkdir -p "$CACHE_DIR"
if [[ ! -f "$CACHE_DIR/my-tool.tar.gz" ]]; then
curl -o "$CACHE_DIR/my-tool.tar.gz" "https://example.com/my-tool.tar.gz"
fi# Error: Permission denied
# Solution: Ensure scripts are executable
chmod +x runs/*# Error: env var DEV_ENV needs to be present
# Solution: Export the variable
export DEV_ENV="$(pwd)"# Error: brew: command not found
# Solution: Install Homebrew first
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"# Error: Dotfiles not found at $HOME/.dotfilesmacos
# Solution: Clone your dotfiles
git clone https://github.com/YOUR_USERNAME/dotfiles.git ~/.dotfilesmacosexport DEV_ENV="$(pwd)"
./run --dryexport DEV_ENV="$(pwd)"
./run dotfiles # Test just dotfiles# Add debugging to any script
#!/usr/bin/env bash
set -x # Show all commands
echo "Starting script: $(basename "$0")"
# ... rest of script
echo "Script completed: $(basename "$0")"# Check if tools were installed correctly
brew list
which nvim tmux git nodecd ~/.dotfilesmacos
stow -D zsh tmux nvim # Unstow configurations
stow zsh tmux nvim # Re-stow configurationsbrew cleanup
brew doctor# Remove all Homebrew packages (nuclear option)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
# Then re-run setupWhen contributing new components to this repository:
- Follow Naming Convention: Use lowercase, descriptive names
- Make It Executable:
chmod +x runs/your-component - Test Thoroughly: Test on clean macOS installation
- Document: Add clear comments explaining what it does
- Be Idempotent: Safe to run multiple times
#!/usr/bin/env bash
# runs/data-science
# Sets up Python data science environment with Jupyter, pandas, etc.
echo "Setting up data science environment..."
# Install Python data science tools
if ! brew list jupyter &> /dev/null; then
brew install jupyter
fi
# Install via pip
pip3 install --user pandas numpy matplotlib seaborn scikit-learn
# Create data projects directory
mkdir -p "$HOME/data-projects"
# Create sample Jupyter config
jupyter --generate-config
echo "Data science environment ready!"
echo "Start Jupyter with: jupyter notebook"# Test your new component
export DEV_ENV="$(pwd)"
./run --dry your-new-component # Dry run first
./run your-new-component # Then real run