initial commit

This commit is contained in:
2025-11-23 09:07:39 -08:00
commit e118a4eb44
21 changed files with 1551 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.log
*.tmp
.cache/

51
QUICKSTART.md Normal file
View File

@@ -0,0 +1,51 @@
# Quick Start
## One Command
```bash
# macOS
xcode-select --install && \
git clone https://git.sdf.org/jchenry/provision ~/.provision && \
~/.provision/provision
# Linux
git clone https://git.sdf.org/jchenry/provision ~/.provision && \
~/.provision/provision
```
## What Happens
1. Installs package manager (Homebrew/apt/pacman)
2. Installs CLI tools (tmux, fzf, ripgrep, starship, etc.)
3. Installs apps (VSCodium, 1Password, Obsidian, Chrome, Todoist)
4. Installs Go to `/usr/local/go`
5. Installs Plan9 to `/usr/local/plan9`
6. Links config files
## After Installation
```bash
# Restart shell
exec $SHELL
# Edit git config
nano ~/.gitconfig
# Install tmux plugins
# In tmux: Ctrl+a then I
```
## Skip Options
```bash
# Skip apps (faster, CLI-only setup)
provision --skip-apps
# Skip Go
provision --skip-go
# Skip Plan9
provision --skip-p9
```
Done!

76
README.md Normal file
View File

@@ -0,0 +1,76 @@
# Provision
Simple, idempotent provisioning for macOS and Linux.
## Quick Start
```bash
git clone https://git.sdf.org/jchenry/provision ~/.provision
~/.provision/provision
exec $SHELL
```
## What Gets Installed
- **Packages**: git, curl, tmux, fzf, ripgrep, starship, zoxide, eza, fd, gh, jq
- **Apps**: VSCodium, 1Password, Obsidian, Chrome, Todoist
- **Go**: Latest version to `/usr/local/go`
- **Plan9**: Installed to `/usr/local/plan9`
- **Config**: bash, tmux, starship, git configs
## Options
```bash
provision # Full provision
provision --skip-apps # Skip GUI apps
provision --skip-go # Skip Go installation
provision --skip-p9 # Skip Plan9 installation
provision --help # Show help
```
## Structure
```
provision/
├── provision # Main script
├── lib/ # Utilities
├── scripts/ # Install scripts
│ ├── packages.sh # CLI tools
│ ├── apps.sh # GUI applications
│ ├── golang.sh # Go installation
│ └── plan9port.sh # Plan9 installation
└── config/ # Config files
├── bashrc
├── tmux.conf
├── starship.toml
├── gitconfig
├── gitignore_global
├── vscode-extensions.txt
├── xinitrc # X11 init (TWM)
├── Xresources # X11 resources
└── twmrc # TWM window manager config
```
## Supported Platforms
- macOS (Homebrew)
- Debian/Ubuntu (apt)
- Arch Linux (pacman + AUR)
## Customization
Edit files in `config/` directory, then re-run:
```bash
~/.provision/config/link-dotfiles.sh
```
## After Installation
1. Edit `~/.gitconfig` - set your name and email
2. In tmux, press `Ctrl+a` then `I` to install plugins
3. Customize `~/.provision/config/` files as needed
## References
- Blog: https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/

65
SUMMARY.md Normal file
View File

@@ -0,0 +1,65 @@
# Provision System - Final Summary
## What It Does
One-command provisioning for macOS and Linux with:
- CLI tools (tmux, fzf, ripgrep, starship, zoxide, etc.)
- GUI apps (VSCodium, 1Password, Obsidian, Chrome)
- Go (tar.gz → /usr/local/go)
- Plan9port (built → /usr/local/plan9)
- Config files (bash, tmux, git, starship)
## Structure
```
provision/
├── provision # Main script (140 lines)
├── README.md # 69 lines
├── QUICKSTART.md # 52 lines
├── lib/ # Utilities
│ ├── common.sh # Platform detection, logging
│ └── package.sh # Package manager abstraction
├── scripts/ # 4 install scripts
│ ├── packages.sh # All CLI tools
│ ├── apps.sh # All GUI apps
│ ├── golang.sh # Go from tar.gz
│ └── plan9port.sh # Plan9 from git
└── config/ # Config files
├── link-dotfiles.sh
├── bashrc # Includes Go & Plan9 paths
├── tmux.conf
├── starship.toml
├── gitconfig
└── gitignore_global
```
## Usage
```bash
# Full provision
~/.provision/provision
# Skip components
provision --skip-apps # No GUI apps
provision --skip-go # No Go
provision --skip-p9 # No Plan9
```
## Platforms
- macOS (Homebrew + casks)
- Debian/Ubuntu (apt + .deb downloads)
- Arch Linux (pacman + AUR)
## Key Features
1. **Simple**: 4 scripts instead of 20+
2. **Fast**: Parallel installs where possible
3. **Idempotent**: Safe to re-run
4. **Self-contained**: All config files included
5. **Flexible**: Skip any component
Total: ~500 lines of shell script

43
config/Xresources Normal file
View File

@@ -0,0 +1,43 @@
! X11 Resources Configuration
! Dracula Xresources palette
*.foreground: #F8F8F2
*.background: #282A36
*.color0: #000000
*.color8: #4D4D4D
*.color1: #FF5555
*.color9: #FF6E67
*.color2: #50FA7B
*.color10: #5AF78E
*.color3: #F1FA8C
*.color11: #F4F99D
*.color4: #BD93F9
*.color12: #CAA9FA
*.color5: #FF79C6
*.color13: #FF92D0
*.color6: #8BE9FD
*.color14: #9AEDFE
*.color7: #BFBFBF
*.color15: #E6E6E6
! XTerm settings
XTerm*termName: xterm-256color
XTerm*locale: true
XTerm*utf8: 1
XTerm*saveLines: 10000
XTerm*scrollBar: false
XTerm*rightScrollBar: false
! XTerm fonts
XTerm*faceName: Monospace
XTerm*faceSize: 11
! TWM settings
Twm*BorderWidth: 2
Twm*TitleFont: -adobe-helvetica-bold-r-normal--*-120-*-*-*-*-*-*
Twm*MenuFont: -adobe-helvetica-bold-r-normal--*-120-*-*-*-*-*-*
Twm*IconFont: -adobe-helvetica-bold-r-normal--*-100-*-*-*-*-*-*

81
config/bashrc Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Bash configuration
export BASH_SILENCE_DEPRECATION_WARNING=1
# XDG Base Directory
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
# Homebrew (macOS)
if [[ "$OSTYPE" == "darwin"* ]]; then
if [ -f "/opt/homebrew/bin/brew" ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [ -f "/usr/local/bin/brew" ]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
fi
# Go
if [ -d "/usr/local/go" ]; then
export PATH="$PATH:/usr/local/go/bin"
export PATH="$PATH:$HOME/go/bin"
fi
# Plan 9
if [ -d "/usr/local/plan9" ]; then
export PLAN9="/usr/local/plan9"
export PATH="$PATH:$PLAN9/bin"
fi
# History settings
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
shopt -s histappend
# Aliases
alias ll='ls -lah'
alias la='ls -A'
alias l='ls -CF'
# Modern replacements
if command -v eza &> /dev/null; then
alias ls='eza'
alias ll='eza -la'
alias lt='eza --tree'
fi
if command -v bat &> /dev/null; then
alias cat='bat'
fi
# fd for Debian (named fd-find)
if command -v fdfind &> /dev/null && ! command -v fd &> /dev/null; then
alias fd='fdfind'
fi
# Git completions
if [ -f /usr/share/bash-completion/completions/git ]; then
. /usr/share/bash-completion/completions/git
fi
# Starship prompt
if command -v starship &> /dev/null; then
eval "$(starship init bash)"
fi
# Zoxide (smarter cd)
if command -v zoxide &> /dev/null; then
eval "$(zoxide init bash)"
fi
# fzf
if command -v fzf &> /dev/null; then
if [ -f ~/.fzf.bash ]; then
source ~/.fzf.bash
elif [ -f /usr/share/doc/fzf/examples/key-bindings.bash ]; then
source /usr/share/doc/fzf/examples/key-bindings.bash
fi
fi

28
config/gitconfig Normal file
View File

@@ -0,0 +1,28 @@
[user]
name = YOUR_NAME
email = YOUR_EMAIL
[core]
excludesfile = ~/.gitignore_global
editor = nano
[init]
defaultBranch = main
[pull]
rebase = false
[push]
default = simple
[alias]
st = status
co = checkout
br = branch
ci = commit
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
last = log -1 HEAD
unstage = reset HEAD --
[color]
ui = auto

31
config/gitignore_global Normal file
View File

@@ -0,0 +1,31 @@
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
# Linux
*~
.directory
# Editors
.vscode/
.idea/
*.swp
*.swo
*.sublime-project
*.sublime-workspace
# Compiled
*.class
*.pyc
*.o
*.so
# Logs
*.log
# Temporary
tmp/
temp/
.cache/

101
config/link-dotfiles.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
log_info "Linking configuration files"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
# Link bash configuration
log_info "Setting up bash configuration"
if ! grep -q "source $SCRIPT_DIR/bashrc" "$HOME/.bashrc" 2>/dev/null; then
echo "" >> "$HOME/.bashrc"
echo "# Added by provision" >> "$HOME/.bashrc"
echo "source $SCRIPT_DIR/bashrc" >> "$HOME/.bashrc"
log_success "Added bash configuration to ~/.bashrc"
else
log_info "Bash configuration already in ~/.bashrc"
fi
if [ -f "$HOME/.bash_profile" ]; then
if ! grep -q "source $SCRIPT_DIR/bashrc" "$HOME/.bash_profile" 2>/dev/null; then
echo "" >> "$HOME/.bash_profile"
echo "# Added by provision" >> "$HOME/.bash_profile"
echo "source $SCRIPT_DIR/bashrc" >> "$HOME/.bash_profile"
log_success "Added bash configuration to ~/.bash_profile"
else
log_info "Bash configuration already in ~/.bash_profile"
fi
fi
# Link tmux configuration
log_info "Setting up tmux configuration"
ensure_dir "$XDG_CONFIG_HOME/tmux"
safe_symlink "$SCRIPT_DIR/tmux.conf" "$XDG_CONFIG_HOME/tmux/tmux.conf"
# Link starship configuration
log_info "Setting up starship configuration"
safe_symlink "$SCRIPT_DIR/starship.toml" "$XDG_CONFIG_HOME/starship.toml"
# Link git configuration
log_info "Setting up git configuration"
if [ -f "$HOME/.gitconfig" ]; then
log_warn "~/.gitconfig already exists, skipping (edit $SCRIPT_DIR/gitconfig and copy manually)"
else
safe_symlink "$SCRIPT_DIR/gitconfig" "$HOME/.gitconfig"
log_warn "Remember to edit ~/.gitconfig and set your name and email!"
fi
safe_symlink "$SCRIPT_DIR/gitignore_global" "$HOME/.gitignore_global"
# Link X11 configuration (Linux only)
if is_linux; then
log_info "Setting up X11 configuration"
if [ -f "$SCRIPT_DIR/xinitrc" ]; then
safe_symlink "$SCRIPT_DIR/xinitrc" "$HOME/.xinitrc"
chmod +x "$HOME/.xinitrc"
fi
if [ -f "$SCRIPT_DIR/Xresources" ]; then
safe_symlink "$SCRIPT_DIR/Xresources" "$HOME/.Xresources"
fi
if [ -f "$SCRIPT_DIR/twmrc" ]; then
safe_symlink "$SCRIPT_DIR/twmrc" "$HOME/.twmrc"
fi
fi
# Install VSCode extensions
if command_exists codium || command_exists code; then
if [ -f "$SCRIPT_DIR/vscode-extensions.txt" ]; then
log_info "Installing VSCode/VSCodium extensions"
CODE_CMD="codium"
if ! command_exists codium && command_exists code; then
CODE_CMD="code"
fi
while IFS= read -r extension; do
# Skip empty lines and comments
[[ -z "$extension" || "$extension" =~ ^[[:space:]]*# ]] && continue
if $CODE_CMD --list-extensions 2>/dev/null | grep -qi "^$extension$"; then
log_info "Extension already installed: $extension"
else
log_info "Installing extension: $extension"
$CODE_CMD --install-extension "$extension" --force 2>/dev/null || log_warn "Failed to install: $extension"
fi
done < "$SCRIPT_DIR/vscode-extensions.txt"
fi
else
log_info "VSCode/VSCodium not installed, skipping extensions"
fi
log_success "Configuration files linked successfully"
log_info ""
log_info "Next steps:"
log_info " 1. Edit ~/.gitconfig to set your name and email"
log_info " 2. Restart your shell: exec \$SHELL"
log_info " 3. Install tmux plugins: press Ctrl+a then I in tmux"

29
config/starship.toml Normal file
View File

@@ -0,0 +1,29 @@
# Starship prompt configuration
add_newline = false
[username]
style_user = "white bold"
format = "$user($style)"
show_always = true
[hostname]
ssh_only = false
format = "@$hostname:"
disabled = false
[directory]
truncation_length = 3
truncate_to_repo = true
[character]
success_symbol = "[\\$](green)"
error_symbol = "[\\$](red)"
[git_branch]
symbol = " "
[git_status]
ahead = "⇡${count}"
diverged = "⇕⇡${ahead_count}⇣${behind_count}"
behind = "⇣${count}"

38
config/tmux.conf Normal file
View File

@@ -0,0 +1,38 @@
# Tmux configuration
# Based on: https://www.youtube.com/watch?v=B-1wGwvUwm8
# Remap prefix from 'C-b' to 'C-a'
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# Reload config
unbind r
bind r source-file ~/.config/tmux/tmux.conf \; display "Reloaded!"
# Split panes
unbind h
bind h split-window -v
unbind v
bind v split-window -h
# Enable mouse mode (optional)
# set -g mouse on
# Start windows and panes at 1, not 0
set -g base-index 1
setw -g pane-base-index 1
# TPM plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
# Dracula theme
set -g @plugin 'dracula/tmux'
set -g @dracula-show-powerline true
set -g @dracula-show-left-icon session
set -g @dracula-plugins "cpu-usage ram-usage"
# Initialize TPM (keep this line at the very bottom)
run '~/.config/tmux/plugins/tpm/tpm'

120
config/twmrc Normal file
View File

@@ -0,0 +1,120 @@
# TWM Configuration File
# Color settings
Color
{
BorderColor "slategrey"
DefaultBackground "rgb:2/a/9"
DefaultForeground "gray85"
TitleBackground "rgb:2/a/9"
TitleForeground "gray85"
MenuBackground "rgb:2/a/9"
MenuForeground "gray85"
MenuTitleBackground "gray70"
MenuTitleForeground "rgb:2/a/9"
IconBackground "rgb:2/a/9"
IconForeground "gray85"
IconBorderColor "gray85"
IconManagerBackground "rgb:2/a/9"
IconManagerForeground "gray85"
}
# Fonts
TitleFont "-adobe-helvetica-bold-r-normal--*-120-*-*-*-*-*-*"
ResizeFont "-adobe-helvetica-bold-r-normal--*-120-*-*-*-*-*-*"
MenuFont "-adobe-helvetica-bold-r-normal--*-120-*-*-*-*-*-*"
IconFont "-adobe-helvetica-bold-r-normal--*-100-*-*-*-*-*-*"
IconManagerFont "-adobe-helvetica-bold-r-normal--*-100-*-*-*"
# Border and title settings
BorderWidth 2
TitleButtonBorderWidth 0
ButtonIndent 0
FramePadding 2
TitlePadding 8
# Behavior
DecorateTransients
NoDefaults
NoGrabServer
RestartPreviousState
RandomPlacement
NoTitle
{
"TWM"
}
# Icon Manager
ShowIconManager
IconManagerGeometry "200x-1-1+0" 1
IconManagerDontShow
{
"xclock"
"xload"
}
# Auto raise
AutoRaise
{
"XTerm"
}
# Functions
Function "move-or-lower" { f.move f.deltastop f.lower }
Function "move-or-raise" { f.move f.deltastop f.raise }
Function "move-or-iconify" { f.move f.deltastop f.iconify }
# Mouse Button Bindings
Button1 = : root : f.menu "defops"
Button2 = : root : f.menu "windowops"
Button3 = : root : f.menu "TwmWindows"
Button1 = m : window|icon : f.function "move-or-lower"
Button2 = m : window|icon : f.iconify
Button3 = m : window|icon : f.function "move-or-raise"
Button1 = : title : f.function "move-or-raise"
Button2 = : title : f.raiselower
Button3 = : title : f.menu "windowops"
Button1 = : icon : f.function "move-or-iconify"
Button2 = : icon : f.iconify
Button3 = : icon : f.menu "windowops"
Button1 = : iconmgr : f.iconify
Button2 = : iconmgr : f.iconify
Button3 = : iconmgr : f.raiselower
# Menus
menu "defops"
{
"TWM" f.title
"XTerm" f.exec "xterm &"
"" f.nop
"Restart" f.restart
"Exit" f.quit
}
menu "windowops"
{
"Window Ops" f.title
"Raise" f.raise
"Lower" f.lower
"Iconify" f.iconify
"Resize" f.resize
"Move" f.move
"" f.nop
"Focus" f.focus
"Unfocus" f.unfocus
"" f.nop
"Delete" f.delete
"Destroy" f.destroy
}
# Key Bindings
"F1" = : all : f.iconify
"F2" = : all : f.raiselower
"F3" = : all : f.fullzoom
"F4" = : all : f.delete
# Startup commands (via .xinitrc instead)

View File

@@ -0,0 +1,12 @@
# VSCode/VSCodium Extensions
# One extension ID per line
# Lines starting with # are ignored
# Themes
dracula-theme.theme-dracula
# Languages
golang.go
# AI/Assistant
anthropic.claude-code

22
config/xinitrc Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
# X11 initialization script
# Load X resources
if [ -f "$HOME/.Xresources" ]; then
xrdb -merge "$HOME/.Xresources"
fi
# Set keyboard repeat rate
xset r rate 200 30
# Disable screen blanking
xset s off
xset -dpms
# Start terminal emulator in background
if command -v xterm >/dev/null 2>&1; then
xterm &
fi
# Start TWM window manager
exec twm

118
lib/common.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Common utility functions for provisioning scripts
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# Platform detection
detect_os() {
case "$OSTYPE" in
darwin*) echo "macos" ;;
linux*)
if [ -f /etc/arch-release ]; then
echo "arch"
elif [ -f /etc/debian_version ]; then
echo "debian"
else
echo "linux"
fi
;;
*) echo "unknown" ;;
esac
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if running on macOS
is_macos() {
[ "$(detect_os)" = "macos" ]
}
# Check if running on Debian/Ubuntu
is_debian() {
[ "$(detect_os)" = "debian" ]
}
# Check if running on Arch
is_arch() {
[ "$(detect_os)" = "arch" ]
}
# Check if running on Linux
is_linux() {
[[ "$OSTYPE" == "linux"* ]]
}
# Create directory if it doesn't exist
ensure_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
log_info "Created directory: $1"
fi
}
# Create symlink, backing up existing file if needed
safe_symlink() {
local source="$1"
local target="$2"
if [ -L "$target" ]; then
# It's already a symlink
if [ "$(readlink "$target")" = "$source" ]; then
log_info "Symlink already exists: $target -> $source"
return 0
else
log_warn "Replacing existing symlink: $target"
rm "$target"
fi
elif [ -f "$target" ] || [ -d "$target" ]; then
# File or directory exists, back it up
local backup="${target}.backup.$(date +%Y%m%d_%H%M%S)"
log_warn "Backing up existing file to: $backup"
mv "$target" "$backup"
fi
ln -sf "$source" "$target"
log_success "Created symlink: $target -> $source"
}
# Execute command with sudo if not running as root
maybe_sudo() {
if [ "$EUID" -ne 0 ]; then
sudo "$@"
else
"$@"
fi
}
# Check if script is being sourced or executed
is_sourced() {
[ "${BASH_SOURCE[0]}" != "${0}" ]
}
# Export OS detection for use in other scripts
export OS_TYPE=$(detect_os)

214
lib/package.sh Executable file
View File

@@ -0,0 +1,214 @@
#!/usr/bin/env bash
# Package manager abstraction functions
# Source common utilities if not already sourced
if [ -z "$OS_TYPE" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
fi
# Update package manager cache
update_package_cache() {
log_info "Updating package cache"
case "$OS_TYPE" in
macos)
if command_exists brew; then
brew update
else
log_error "Homebrew not installed"
return 1
fi
;;
debian)
maybe_sudo apt-get update -qq
;;
arch)
maybe_sudo pacman -Sy --noconfirm
;;
*)
log_error "Unsupported OS: $OS_TYPE"
return 1
;;
esac
}
# Install a package using the appropriate package manager
install_package() {
local package="$1"
local package_debian="${2:-$package}"
local package_arch="${3:-$package}"
log_info "Installing package: $package"
case "$OS_TYPE" in
macos)
if ! command_exists brew; then
log_error "Homebrew not installed. Run system/macos-setup.sh first"
return 1
fi
brew install "$package"
;;
debian)
maybe_sudo apt-get install -y -qq "$package_debian"
;;
arch)
maybe_sudo pacman -S --noconfirm --needed "$package_arch"
;;
*)
log_error "Unsupported OS: $OS_TYPE"
return 1
;;
esac
}
# Install a cask (macOS only)
install_cask() {
local cask="$1"
if ! is_macos; then
log_warn "Cask installation only supported on macOS"
return 1
fi
log_info "Installing cask: $cask"
if ! command_exists brew; then
log_error "Homebrew not installed. Run system/macos-setup.sh first"
return 1
fi
brew install --cask "$cask"
}
# Install from AUR (Arch only)
install_aur() {
local package="$1"
if ! is_arch; then
log_warn "AUR installation only supported on Arch Linux"
return 1
fi
log_info "Installing AUR package: $package"
# Check for yay first, then paru
if command_exists yay; then
yay -S --noconfirm --needed "$package"
elif command_exists paru; then
paru -S --noconfirm --needed "$package"
else
log_error "No AUR helper found. Install yay or paru first"
return 1
fi
}
# Install cargo package
install_cargo() {
local package="$1"
if ! command_exists cargo; then
log_info "Installing Rust toolchain"
install_package rustup rust cargo
if is_debian || is_arch; then
rustup default stable
fi
fi
log_info "Installing cargo package: $package"
cargo install "$package"
}
# Install Python package with pip
install_pip() {
local package="$1"
if ! command_exists pip3; then
log_info "Installing pip"
install_package python3-pip python3-pip python-pip
fi
log_info "Installing pip package: $package"
pip3 install --user "$package"
}
# Install npm package globally
install_npm() {
local package="$1"
if ! command_exists npm; then
log_error "npm not installed. Install Node.js first"
return 1
fi
log_info "Installing npm package: $package"
npm install -g "$package"
}
# Check if package is installed
package_installed() {
local package="$1"
case "$OS_TYPE" in
macos)
brew list "$package" >/dev/null 2>&1
;;
debian)
dpkg -l "$package" 2>/dev/null | grep -q "^ii"
;;
arch)
pacman -Q "$package" >/dev/null 2>&1
;;
*)
return 1
;;
esac
}
# Ensure Homebrew is installed (macOS only)
ensure_homebrew() {
if ! is_macos; then
return 0
fi
if command_exists brew; then
log_success "Homebrew already installed"
return 0
fi
log_info "Installing Homebrew"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for Apple Silicon Macs
if [ -f "/opt/homebrew/bin/brew" ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
}
# Ensure AUR helper is installed (Arch only)
ensure_aur_helper() {
if ! is_arch; then
return 0
fi
if command_exists yay || command_exists paru; then
log_success "AUR helper already installed"
return 0
fi
log_info "Installing yay AUR helper"
# Install dependencies
maybe_sudo pacman -S --noconfirm --needed git base-devel
# Clone and build yay
local temp_dir=$(mktemp -d)
git clone https://aur.archlinux.org/yay.git "$temp_dir/yay"
cd "$temp_dir/yay"
makepkg -si --noconfirm
cd -
rm -rf "$temp_dir"
log_success "yay installed"
}

138
provision Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
usage() {
cat << 'EOF'
Usage: provision [OPTIONS]
Provision macOS or Linux desktop computers.
OPTIONS:
-h, --help Show this help
-s, --skip-go Skip Go installation
-p, --skip-p9 Skip Plan9 installation
-a, --skip-apps Skip GUI applications
EXAMPLES:
provision # Full provision
provision --skip-go # Skip Go
provision --skip-p9 # Skip Plan9
provision --skip-apps # Skip apps (VSCode, 1Password, etc.)
EOF
exit 0
}
# Parse arguments
SKIP_GO=false
SKIP_P9=false
SKIP_APPS=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
;;
-s|--skip-go)
SKIP_GO=true
shift
;;
-p|--skip-p9)
SKIP_P9=true
shift
;;
-a|--skip-apps)
SKIP_APPS=true
shift
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
log_info "Starting provision for $OS_TYPE"
echo ""
# System setup
log_info "=== System Setup ==="
source "$SCRIPT_DIR/lib/package.sh"
case "$OS_TYPE" in
macos)
# Install Xcode Command Line Tools
if ! xcode-select -p >/dev/null 2>&1; then
log_info "Installing Xcode Command Line Tools"
xcode-select --install
log_warn "Please complete Xcode CLI tools installation and re-run this script"
exit 0
fi
# Install Homebrew
ensure_homebrew
brew update
;;
debian)
update_package_cache
maybe_sudo apt-get upgrade -y
maybe_sudo apt-get install -y build-essential curl wget git
;;
arch)
update_package_cache
maybe_sudo pacman -Syu --noconfirm
maybe_sudo pacman -S --noconfirm --needed base-devel git curl wget
ensure_aur_helper
;;
*)
log_error "Unsupported OS: $OS_TYPE"
exit 1
;;
esac
log_success "System setup complete"
echo ""
# Install packages
log_info "=== Installing Packages ==="
"$SCRIPT_DIR/scripts/packages.sh"
echo ""
# Install apps
if [ "$SKIP_APPS" = false ]; then
log_info "=== Installing Applications ==="
"$SCRIPT_DIR/scripts/apps.sh"
echo ""
fi
# Install Go
if [ "$SKIP_GO" = false ]; then
log_info "=== Installing Go ==="
"$SCRIPT_DIR/scripts/golang.sh"
echo ""
fi
# Install Plan9
if [ "$SKIP_P9" = false ]; then
log_info "=== Installing Plan9 ==="
"$SCRIPT_DIR/scripts/plan9port.sh"
echo ""
fi
# Link config files
log_info "=== Linking Config Files ==="
"$SCRIPT_DIR/config/link-dotfiles.sh"
echo ""
log_success "Provision complete!"
echo ""
log_info "Next steps:"
log_info " 1. Restart your shell: exec \$SHELL"
log_info " 2. Edit ~/.gitconfig to set your name and email"
log_info " 3. In tmux, press Ctrl+a then I to install plugins"

134
scripts/apps.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
source "$SCRIPT_DIR/../lib/package.sh"
log_info "Installing applications"
# VSCodium
if command_exists codium || command_exists code; then
log_success "VSCodium/VSCode already installed"
else
log_info "Installing VSCodium"
case "$OS_TYPE" in
macos)
install_cask vscodium
;;
debian)
wget -qO - https://gitlab.com/paulcarroty/vscodium-deb-rpm-repo/raw/master/pub.gpg \
| gpg --dearmor \
| maybe_sudo dd of=/usr/share/keyrings/vscodium-archive-keyring.gpg
echo 'deb [ signed-by=/usr/share/keyrings/vscodium-archive-keyring.gpg ] https://download.vscodium.com/debs vscodium main' \
| maybe_sudo tee /etc/apt/sources.list.d/vscodium.list
update_package_cache
install_package codium
;;
arch)
install_aur vscodium-bin
;;
esac
fi
# 1Password
if command_exists 1password || [ -d "/Applications/1Password.app" ] 2>/dev/null; then
log_success "1Password already installed"
else
log_info "Installing 1Password"
case "$OS_TYPE" in
macos)
install_cask 1password
;;
debian)
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
maybe_sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | \
maybe_sudo tee /etc/apt/sources.list.d/1password.list
maybe_sudo mkdir -p /etc/debsig/policies/AC2D62742012EA22/
curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol | \
maybe_sudo tee /etc/debsig/policies/AC2D62742012EA22/1password.pol
maybe_sudo mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
maybe_sudo gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg
update_package_cache
install_package 1password
;;
arch)
install_aur 1password
;;
esac
fi
# Obsidian
if command_exists obsidian || [ -d "/Applications/Obsidian.app" ] 2>/dev/null; then
log_success "Obsidian already installed"
else
log_info "Installing Obsidian"
case "$OS_TYPE" in
macos)
install_cask obsidian
;;
debian)
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
wget -q "https://github.com/obsidianmd/obsidian-releases/releases/download/v1.5.3/obsidian_1.5.3_amd64.deb"
maybe_sudo dpkg -i obsidian_*.deb
maybe_sudo apt-get install -f -y
cd -
rm -rf "$TEMP_DIR"
;;
arch)
install_aur obsidian
;;
esac
fi
# Google Chrome
if command_exists google-chrome || command_exists google-chrome-stable || [ -d "/Applications/Google Chrome.app" ] 2>/dev/null; then
log_success "Google Chrome already installed"
else
log_info "Installing Google Chrome"
case "$OS_TYPE" in
macos)
install_cask google-chrome
;;
debian)
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
maybe_sudo dpkg -i google-chrome-stable_current_amd64.deb
maybe_sudo apt-get install -f -y
cd -
rm -rf "$TEMP_DIR"
;;
arch)
install_aur google-chrome
;;
esac
fi
# Todoist
if command_exists todoist || [ -d "/Applications/Todoist.app" ] 2>/dev/null; then
log_success "Todoist already installed"
else
log_info "Installing Todoist"
case "$OS_TYPE" in
macos)
install_cask todoist
;;
debian)
# Install via snap
if ! command_exists snap; then
log_info "Installing snapd"
maybe_sudo apt-get install -y snapd
fi
maybe_sudo snap install todoist
;;
arch)
install_aur todoist-appimage
;;
esac
fi
log_success "Applications installed"

66
scripts/golang.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
GO_VERSION="1.23.3"
GO_INSTALL_DIR="/usr/local/go"
log_info "Installing Go ${GO_VERSION}"
if [ -d "$GO_INSTALL_DIR" ] && [ -f "$GO_INSTALL_DIR/bin/go" ]; then
CURRENT_VERSION=$($GO_INSTALL_DIR/bin/go version | awk '{print $3}' | sed 's/go//')
if [ "$CURRENT_VERSION" = "$GO_VERSION" ]; then
log_success "Go ${GO_VERSION} already installed"
exit 0
else
log_info "Upgrading Go from ${CURRENT_VERSION} to ${GO_VERSION}"
maybe_sudo rm -rf "$GO_INSTALL_DIR"
fi
fi
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
GO_ARCH="amd64"
;;
aarch64|arm64)
GO_ARCH="arm64"
;;
*)
log_error "Unsupported architecture: $ARCH"
exit 1
;;
esac
# Detect OS
if is_macos; then
GO_OS="darwin"
else
GO_OS="linux"
fi
GO_TARBALL="go${GO_VERSION}.${GO_OS}-${GO_ARCH}.tar.gz"
GO_URL="https://go.dev/dl/${GO_TARBALL}"
log_info "Downloading Go from ${GO_URL}"
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
if ! curl -fsSL -o "$GO_TARBALL" "$GO_URL"; then
log_error "Failed to download Go"
rm -rf "$TEMP_DIR"
exit 1
fi
log_info "Extracting Go to ${GO_INSTALL_DIR}"
maybe_sudo tar -C /usr/local -xzf "$GO_TARBALL"
cd -
rm -rf "$TEMP_DIR"
log_success "Go ${GO_VERSION} installed to ${GO_INSTALL_DIR}"
log_info "Add to PATH: export PATH=\$PATH:/usr/local/go/bin"

108
scripts/packages.sh Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
source "$SCRIPT_DIR/../lib/package.sh"
log_info "Installing packages"
# Essential packages
PACKAGES=(
"git"
"curl"
"wget"
"tmux"
"jq"
"fzf"
"ripgrep"
)
# Modern CLI tools
MODERN_TOOLS=(
"starship"
"zoxide"
"eza"
"fd"
"gh"
)
# Install essential packages
for pkg in "${PACKAGES[@]}"; do
if command_exists "$pkg" || command_exists "${pkg}find" 2>/dev/null; then
log_success "$pkg already installed"
else
log_info "Installing $pkg"
case "$pkg" in
fd)
install_package fd fd-find fd
;;
*)
install_package "$pkg"
;;
esac
fi
done
# Install modern tools
for tool in "${MODERN_TOOLS[@]}"; do
if command_exists "$tool"; then
log_success "$tool already installed"
continue
fi
log_info "Installing $tool"
case "$tool" in
starship)
if is_debian; then
curl -sS https://starship.rs/install.sh | sh -s -- -y
else
install_package starship
fi
;;
zoxide)
if is_debian && ! package_installed zoxide; then
install_cargo zoxide
else
install_package zoxide
fi
;;
eza)
if is_debian && ! package_installed eza; then
if command_exists exa; then
log_success "exa (predecessor) already installed"
else
install_cargo eza
fi
else
install_package eza
fi
;;
gh)
if is_debian && ! package_installed gh; then
maybe_sudo mkdir -p -m 755 /etc/apt/keyrings
wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | maybe_sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | maybe_sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
update_package_cache
fi
install_package gh gh github-cli
;;
*)
install_package "$tool"
;;
esac
done
# Install TPM (Tmux Plugin Manager)
TPM_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/tmux/plugins/tpm"
if [ ! -d "$TPM_DIR" ]; then
log_info "Installing TPM (Tmux Plugin Manager)"
ensure_dir "$(dirname "$TPM_DIR")"
git clone https://github.com/tmux-plugins/tpm "$TPM_DIR"
log_success "TPM installed"
else
log_success "TPM already installed"
fi
log_success "All packages installed"

60
scripts/plan9port.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
PLAN9_INSTALL_DIR="/usr/local/plan9"
log_info "Installing plan9port"
if [ -d "$PLAN9_INSTALL_DIR" ] && [ -f "$PLAN9_INSTALL_DIR/bin/9" ]; then
log_success "plan9port already installed at ${PLAN9_INSTALL_DIR}"
exit 0
fi
# Install build dependencies
log_info "Installing build dependencies"
if is_macos; then
# macOS needs Xcode Command Line Tools
if ! xcode-select -p >/dev/null 2>&1; then
log_error "Xcode Command Line Tools required. Run: xcode-select --install"
exit 1
fi
elif is_debian; then
maybe_sudo apt-get install -y build-essential libx11-dev libxt-dev libxext-dev libfontconfig1-dev
elif is_arch; then
maybe_sudo pacman -S --noconfirm --needed base-devel xorg-server-devel fontconfig
fi
log_info "Cloning plan9port repository"
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
if ! git clone https://github.com/9fans/plan9port.git plan9; then
log_error "Failed to clone plan9port"
rm -rf "$TEMP_DIR"
exit 1
fi
cd plan9
log_info "Building plan9port (this may take a while)"
if ! ./INSTALL; then
log_error "Failed to build plan9port"
cd -
rm -rf "$TEMP_DIR"
exit 1
fi
log_info "Installing to ${PLAN9_INSTALL_DIR}"
cd ..
maybe_sudo mv plan9 "$PLAN9_INSTALL_DIR"
cd -
rm -rf "$TEMP_DIR"
log_success "plan9port installed to ${PLAN9_INSTALL_DIR}"
log_info "Add to environment:"
log_info " export PLAN9=${PLAN9_INSTALL_DIR}"
log_info " export PATH=\$PATH:\$PLAN9/bin"