From e118a4eb441e6bee70a8b37cabcbc202fb6824d4 Mon Sep 17 00:00:00 2001 From: Colin Henry Date: Sun, 23 Nov 2025 09:07:39 -0800 Subject: [PATCH] initial commit --- .gitignore | 16 +++ QUICKSTART.md | 51 +++++++++ README.md | 76 +++++++++++++ SUMMARY.md | 65 +++++++++++ config/Xresources | 43 +++++++ config/bashrc | 81 +++++++++++++ config/gitconfig | 28 +++++ config/gitignore_global | 31 +++++ config/link-dotfiles.sh | 101 +++++++++++++++++ config/starship.toml | 29 +++++ config/tmux.conf | 38 +++++++ config/twmrc | 120 ++++++++++++++++++++ config/vscode-extensions.txt | 12 ++ config/xinitrc | 22 ++++ lib/common.sh | 118 +++++++++++++++++++ lib/package.sh | 214 +++++++++++++++++++++++++++++++++++ provision | 138 ++++++++++++++++++++++ scripts/apps.sh | 134 ++++++++++++++++++++++ scripts/golang.sh | 66 +++++++++++ scripts/packages.sh | 108 ++++++++++++++++++ scripts/plan9port.sh | 60 ++++++++++ 21 files changed, 1551 insertions(+) create mode 100644 .gitignore create mode 100644 QUICKSTART.md create mode 100644 README.md create mode 100644 SUMMARY.md create mode 100644 config/Xresources create mode 100644 config/bashrc create mode 100644 config/gitconfig create mode 100644 config/gitignore_global create mode 100755 config/link-dotfiles.sh create mode 100644 config/starship.toml create mode 100644 config/tmux.conf create mode 100644 config/twmrc create mode 100644 config/vscode-extensions.txt create mode 100755 config/xinitrc create mode 100755 lib/common.sh create mode 100755 lib/package.sh create mode 100755 provision create mode 100755 scripts/apps.sh create mode 100755 scripts/golang.sh create mode 100755 scripts/packages.sh create mode 100755 scripts/plan9port.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c801590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary files +*.log +*.tmp +.cache/ diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..c2723cb --- /dev/null +++ b/QUICKSTART.md @@ -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! diff --git a/README.md b/README.md new file mode 100644 index 0000000..8488211 --- /dev/null +++ b/README.md @@ -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/ diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..ba2c1d0 --- /dev/null +++ b/SUMMARY.md @@ -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 diff --git a/config/Xresources b/config/Xresources new file mode 100644 index 0000000..3108b0a --- /dev/null +++ b/config/Xresources @@ -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-*-*-*-*-*-* + + diff --git a/config/bashrc b/config/bashrc new file mode 100644 index 0000000..a827d82 --- /dev/null +++ b/config/bashrc @@ -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 diff --git a/config/gitconfig b/config/gitconfig new file mode 100644 index 0000000..432a982 --- /dev/null +++ b/config/gitconfig @@ -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 diff --git a/config/gitignore_global b/config/gitignore_global new file mode 100644 index 0000000..850c230 --- /dev/null +++ b/config/gitignore_global @@ -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/ diff --git a/config/link-dotfiles.sh b/config/link-dotfiles.sh new file mode 100755 index 0000000..10a5bc5 --- /dev/null +++ b/config/link-dotfiles.sh @@ -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" diff --git a/config/starship.toml b/config/starship.toml new file mode 100644 index 0000000..f8ea31b --- /dev/null +++ b/config/starship.toml @@ -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}" diff --git a/config/tmux.conf b/config/tmux.conf new file mode 100644 index 0000000..32df0fe --- /dev/null +++ b/config/tmux.conf @@ -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' diff --git a/config/twmrc b/config/twmrc new file mode 100644 index 0000000..c89d75c --- /dev/null +++ b/config/twmrc @@ -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) diff --git a/config/vscode-extensions.txt b/config/vscode-extensions.txt new file mode 100644 index 0000000..ebf8841 --- /dev/null +++ b/config/vscode-extensions.txt @@ -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 diff --git a/config/xinitrc b/config/xinitrc new file mode 100755 index 0000000..9a42ff5 --- /dev/null +++ b/config/xinitrc @@ -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 diff --git a/lib/common.sh b/lib/common.sh new file mode 100755 index 0000000..3cefda6 --- /dev/null +++ b/lib/common.sh @@ -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) diff --git a/lib/package.sh b/lib/package.sh new file mode 100755 index 0000000..9bfc71a --- /dev/null +++ b/lib/package.sh @@ -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" +} diff --git a/provision b/provision new file mode 100755 index 0000000..84e6561 --- /dev/null +++ b/provision @@ -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" diff --git a/scripts/apps.sh b/scripts/apps.sh new file mode 100755 index 0000000..25f736d --- /dev/null +++ b/scripts/apps.sh @@ -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" diff --git a/scripts/golang.sh b/scripts/golang.sh new file mode 100755 index 0000000..119203b --- /dev/null +++ b/scripts/golang.sh @@ -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" diff --git a/scripts/packages.sh b/scripts/packages.sh new file mode 100755 index 0000000..895bd51 --- /dev/null +++ b/scripts/packages.sh @@ -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" diff --git a/scripts/plan9port.sh b/scripts/plan9port.sh new file mode 100755 index 0000000..f190819 --- /dev/null +++ b/scripts/plan9port.sh @@ -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"